commit
通过附件中的build.dockerfile
我们可以看到v8的实际回滚版本:
1 | ... |
对应commit:f603d5769be54dcd24308ff9aec041486ea54f5b的父节点。另外,提供的patch并没增添漏洞功能,只是删除了一些全局obj,那么关键点就在于分析f603d5这个commit的提交内容了。
通过检索,可以得到以下基本信息:
- https://github.com/v8/v8/commit/f603d5769be54dcd24308ff9aec041486ea54f5b
- https://issues.chromium.org/issues/336507783
在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文件相关链接:
- 查看链接:https://issues.chromium.org/action/issues/336507783/attachments/56286498?download=false
- 下载链接:https://issues.chromium.org/action/issues/336507783/attachments/56286498?download=true
那么这个题目就转变为了,已知POC完成EXP编写的过程。
Proof of Concept
基本逻辑
运行POC,可以看到如下报错
可以证明的确发生了沙盒外的内存错误,且期望hash与实际hash值不相符。紧接着看看POC中内存错误的触发点,相关代码大致如下:
1 |
|
可以看构建了两个函数func1
和boom
func1()
的调用形式为:param(i64) –> result(i32),类似C/C++中传入一个自定义的 $struct结构体,返回voidboom()
的调用形式为: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 | ... |
直观上看,也很容易理解,从func0
中取出存储的特定内容,并覆盖给func1
的对应位置,使得func1
的调用时出现错误。
具体来说,详细的逻辑链路应当如下:
- 从特定位置获取func0的签名 :
let f0_int = getField(f0, kWasmInternalFunctionOffset);
- 用func0的签名覆盖func1的签名:
setField(f1, kWasmInternalFunctionOffset, f0_int);
- 调用boom函数,target为i64格式:
instance.exports.boom(target);
- boom调用func1的引用,并期望签名为$sig_i_l:
kExprCallRef, $sig_i_l,
- 检查被调用函数签名与期望签名是否相符:此时,由于
func1
的签名已经被覆盖为func0
的makeSig([wasmRefType($struct)], [])
,与$sig_i_l
不相符 - 调用失败,程序崩溃:The following harmless error was encountered: Check failed: expected_hash == internal_function->signature_hash().
漏洞利用
从POC到原语
首先我们需要清楚这个POC为我们提供了什么原语。漏洞提供的POC本身是为了证明函数的签名可以被混淆,那我们可以猜想:
能不能篡改签名,使得函数的参数与定义不符的情况下,依然可以被正常调用呢?
我们构造一个新的POC:
1 | function hex(i) { |
简单来讲我们做了如下操作:
- 更改table中需要存储的函数func_l_0签名
- 将func放入table中func_l_0的位置
- 调用leak,引用table中索引0的函数,期望类型为$sig_l_0
- 返回根本没有传入的参数,从而造成内存泄露
即我们将一个类型为param(i64) –> result(i64)的函数转变为param(NULL) –> result(i64),由于实际执行代码时根本没有传入参数(类似未初始化漏洞),则可能导致泄露沙盒外的指针。
更多利用原语
有了沙箱外的地址泄露,下一步就是考虑如何构造AAW以及AAR
AAW
假设有如下函数,可以将argv[1]
写入$struct
结构体的对应域
1 | let $struct = builder.addStruct([makeField(kWasmI64, true)]); |
如混淆其调用类型为param(i64, i64) –> result(NULL) ,不就是类似[argv[0] -1 + 8 ]= argv[1]
的任意地址写:
因为argv[0]在实际代码执行时,被当作指针,因此需要计算偏移,前面为map和hash的部分
AAR
类似的,有如下函数可以获取$struct
的对应域
1 | let $sig_l_s = builder.addType(makeSig([wasmRefType($struct)], [kWasmI64])); |
如混淆其调用类型为param(i64) –> result(i64) ,即类似于reslut = [argv[0]-1+8]
开始利用
有了这些基础,我们就可以开始编写exp了,首先我们需要看看leak能够得到什么
1 | let leakAddr = instance.exports.leak; |
我们能够发现leak得到的是一个WasmDispatchTableTable对象,如下:
并且能够看到,在特定偏移出存放rwx段的原始指针。继续查看指针附近的内容:
相信看到这个内容,足够敏感的pwner已经能够注意到这是一个类似GOT - PLT表的结构,0x0 - 0x9对应了类似libc中runtime_resolve()
所需的函数索引值。
想要具体了解这些结构运行逻辑的同学可以自己调试,并查阅资料
那么我们只需要做类似GOT Hijacking的操作:
- 在一个空闲的区域写入shellcode
- 然后将某个函数的执行流更改到shellcode上即可
- 这里我选择的是新创建的函数
boom
,这样也不会和其他构造的函数冲突
1 | builder.addFunction("boom", $sig_l_l) |
因为是最后一个构造的函数,对应就是索引为0x9,偏移为0x2d的位置。写入时需要注意参数1的形式
1 | anyAddrWrite(target, 0x622fbf4856f63148n); |
关于shellcode的构造,借助pwntools库即可。这里我用deepseek帮我生成了一个脚本,用于生成小端序的i64格式shellcode:
1 | from pwn import * |
运行结果:
Expolit
1 |
|