RCTF 2025 - no_check_WASM
RCTF 2025 - no_check_WASM
目录
前言
pwn2own 24过后,wasm子系统一直是v8漏洞挖掘的热点,这次出到wasm模块相关的内容,因此想着过来看一下,但是比赛的时候笔者有一个步骤卡住了,导致没构造出最后的逃逸原语,可惜可惜
赛后与@Tplus@Qanux师傅交流了下,解决了自己卡住的地方,感谢两位师傅。
基本信息
编译参数是,默认开启沙箱
1 | |
题目下发的commit hash是42f5ff65d12f0ef9294fa7d3875feba938a81904

提交比较新,所以nday bypass的概率很低,就直接分析patch文件
看下文件上传逻辑,算一个hash,然后直接上传,文件大小<102400,应该是考虑到了wasm-module-builder.js的因素,所以这么大
注意这里的启动参数加了一个这个–no-memory-protection-keys,其实是为了防止intel cpu的pkey内存保护,简单的说就是构造出AAR和AAW之后,写一些内存页的时候会crash,详细的内容见这个issue
1 | |
环境搭建一下,方便本地调试
1 | |
修改编译参数为如下
1 | |
接着autoninja -C out/x64.release d8 编译
漏洞分析
触发漏洞路径
提取下patch内容
1 | |
主要patch的位置在WasmFullDecoder类里,这个类主要负责解析wasm函数体的内容,追下函数TypeCheckStackAgainstMerge_Slow,发现是一个slow path的函数,所以大概率还有fast path判断的逻辑
找到了上层函数,想要进入这slow path的话需要让栈值数量与函数签名不匹配,或者是一些复杂的函数
1 | |
同时也需要注意到这个check是针对于merge point的检查,所以构造某些参数数量正常的函数的时候,需要采用特殊的branch
1 | |
那么这里就可以简单构建一个poc,下一个断点来测试一下是否能抵达这个位置
1 | |
其实是可以的

当前的栈回溯(部分
1 | |
那我们的推测是没问题的,构造一个wasm stack的值数量与实际函数签名不符合的情况/或者是一个比较复杂的函数,可以进入到被patch的函数,接着来看patch掉了什么逻辑
WASM函数签名数量混淆
首先被patch掉的是对于wasm stack的值数量与实际函数签名不相等,意味着可以去传递多个参数,也就是说可以通过传递多个返回值的方法去泄漏wasm stack上的内容,泄漏出的值大概率是v8沙箱之外的,这个也可以算上是沙箱逃逸的第一步了
1 | |
WASM子系统类型混淆
下发会调用IsSubtypeOf对于stack_values进行类型检查,如果不符合则会报错,详细的实现位于v8/src/wasm/wasm-subtyping.cc:L421
1 | |
这里的意思是wasm子类型检查被删去,因此可以实现类似于这样的类型混淆i64→struct。wasm的设计文档其实有很多,这里只是举了一个简单的例子,因此不难想到一些类型混淆的思路,下方的extern可以理解为js中的对象,struct可以通过get和set方式进行取值,也就是类似于指针解引的过程
1 | |
其中AAR和AAW笔者比赛的时候犯了一个错误,在一个函数体中,直接将i64 cast为ref struct,然后直接解引用,这就造成了问题。
当对于一个struct类型进行kExprStructSet或者kExprStructGet操作的时候,会去检查底层的heaptype,因此当直接传入一个i64的值,不进行先进行类型混淆,而是直接cast使用就会造成问题。本质上就是触发了struct.get/set的检查,也并不是对于patch的利用,铸币了
1 | |
而如果提前进行混淆,采用将i64转换成ref struct的方式,这会进入到这个函数
1 | |
产生如下的调用链,将原本的i64标记为ref struct,因此后续利用的时候可以将这个i64接引,造成沙箱逃逸
1 | |
漏洞利用
沙箱内任意读写
这里用到上面提到的思路,当然下方也可以采用i64,这样也方便泄漏出cage_base,也可以通过泄漏indirectcall的函数ref,思路很多
1 | |
下方就是addressOf和fakeObj的原语,然后leak_cage_base的逻辑其实和addressOf是一致的,leak_cage_base主要是这里没有截断高32位,所以可以泄漏出沙箱的基地址
1 | |
泄漏raw_pointer
通过上方的分析,其实我们可以通过参数不匹配的方式去泄漏wasm stack的值,所以这里可以尝试一下能泄漏出什么东西
1 | |

这里泄漏出了 raw pointer(64-bit C++ 栈地址),通过扫描这个栈区域,可以找到
- JIT 代码的返回地址
- 指向 trusted space 的指针
- …………
AAR && AAW && RCE
与上方分析的一致,这里需要将i64 → struct → i64,这样就可以实现沙箱逃逸,需要注意的是i64 → struct最好单独拆开,写成一个wasm函数供另外的wasm函数调用,代码如下。
1 | |
接着就可以对于泄漏出的栈地址进行扫描,经过测试,可以发现第一个泄露出来的地址位于jit code所在的段,后续计算也发现与trusted_data中的jump_table偏移固定,因此我们可以得到jump_table的具体地址,并修改为我们的shellcode,出于稳定性的考虑,可以在前面加上一个nop指令即可
这里泄漏出来的值与栈偏移处的值并不相等,原因应该是当i64被cast成struct的时候,底层的heaptype等字段被修改,因此实际转换成struct进行取值的时候,会略微有偏差(通过以前阅读src/wasm/*的推测,这次并没有通过源码验证)

exploit
wasm-module-builder版本
1 | |
ByteArray版本
1 | |
上传脚本
ai搓一个就行(
1 | |
