JIT与浏览器Pwn入门
JIT与浏览器Pwn入门
本文适合
已经理解 堆基础、ROP基础,并能读基础 JavaScript 的高阶 Pwn 学习者。学完你能:把浏览器/JIT 题拆成触发 bug、制造越界读写、泄露地址、构造任意读写、执行 payload 这几个阶段
一句话判断
题目给 V8、d8、SpiderMonkey、JavaScriptCore、浏览器补丁 diff、JIT 优化代码,或者 PoC 是一段 JavaScript 导致引擎崩溃,就按 JIT/浏览器 Pwn 处理。
题目中常见信号
题目材料:
d8
v8.patch
diff.patch
build/x64.release/d8
js shell
poc.js关键词:
TurboFan
Ignition
JIT
Map
ElementsKind
ArrayBuffer
TypedArray
WebAssembly
OOB
addrof
fakeobj崩溃信号:
Segmentation fault
Access violation
CHECK failed
heap-use-after-free
type confusion核心概念
浏览器/JIT 利用常见链条:
触发 JIT 优化错误
-> 类型混淆或数组越界
-> 获得 OOB 读写
-> 构造 addrof / fakeobj
-> 泄露地址
-> 任意读写
-> 写入或劫持可执行区域
-> 执行 payload几个核心词:
- JIT:把热点 JavaScript 编译成本地代码。
- Map / Hidden Class:描述对象布局。
- ElementsKind:描述数组元素类型和存储方式。
- OOB:越界读写。
- addrof:得到对象地址。
- fakeobj:伪造对象。
- Wasm code page:Wasm 编译产生的可执行内存页,CTF 中常用来承载 payload。
入门阶段不要试图一次看懂整个浏览器。先把“bug 如何变成读写原语”讲清楚。
最小分析流程
1. 确认版本和运行方式
./d8 --version
./d8 poc.js如果题目给 patch:
git diff
cat v8.patch重点看 patch 改了:
- 数组长度。
- 类型检查。
- Map 检查。
- bounds check。
- JIT 优化假设。
2. 稳定触发
浏览器题经常需要 warm-up:
for (let i = 0; i < 10000; i++) {
trigger(1.1);
}
trigger(bad_input);如果不 warm-up,JIT 不优化,bug 可能不触发。
3. 判断崩溃是否可控
只崩溃不等于可利用。
要问:
- 能不能控制数组长度。
- 能不能越界读写相邻对象。
- 能不能读出对象地址。
- 能不能写入 ArrayBuffer backing store。
- 能不能稳定多次触发。
4. 构造原语
常见目标:
addrof(obj)
fakeobj(addr)
read64(addr)
write64(addr, value)如果能做到任意读写,后续才进入传统 Pwn 阶段。
最小验证示例
验证 OOB 数组
一个简化验证思路:
let victim = [1.1, 2.2, 3.3];
let marker = {x: 0x41414141};
// 触发漏洞后,如果 victim.length 变得异常大
console.log(victim.length);
console.log(victim[100]);验证点:
length是否异常。- 越界读取是否能读到相邻对象数据。
- 越界写入是否会改变相邻对象行为。
这不是完整 exploit,只是证明“崩溃”正在变成“可控读写”。
验证 Wasm 可执行页
CTF 中常用 Wasm 创建可执行代码页:
let wasmCode = new Uint8Array([
0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00
// 省略完整模块
]);实际题解里通常会:
- 创建 Wasm 实例。
- 泄露 Wasm 函数地址。
- 找到 code page。
- 用任意写写入 payload。
- 调用 Wasm 函数触发执行。
如果题目只要求读 flag,可能不需要走到系统命令执行。
常见利用 / 解题路线
路线总览:
路线一:补丁 diff 驱动
适合比赛给源码 diff 的题。
步骤:
- 看 patch 删除了什么检查。
- 写 JS 触发被删检查对应的异常状态。
- 让 JIT 基于错误假设优化。
- 构造 OOB 或类型混淆。
路线二:OOB 数组到任意读写
步骤:
- 构造 float array 和 object array。
- 用 OOB 改相邻数组 length 或元素指针。
- 实现
addrof和fakeobj。 - 通过 ArrayBuffer backing store 实现
read64/write64。
路线三:任意写到 Wasm code page
步骤:
- 创建 Wasm 实例。
- 泄露 Wasm 函数对象。
- 找到可执行页地址。
- 写入 payload。
- 调用导出函数。
路线四:只做 JS shell 题
很多 CTF 入门浏览器题只要求在 d8 或 JS shell 中控制 RIP,不要求完整沙箱逃逸。
这时 WP 要明确写:
- 是否只是 renderer / shell 代码执行。
- 是否需要 sandbox escape。
- 题目环境是否禁用系统调用。
常见失败原因
- 没有 warm-up:JIT 没触发,漏洞状态不出现。
- 只追崩溃:崩溃地址不可控,不能变成 exploit。
- 版本不一致:V8 偏移和对象布局高度依赖版本。
- 压缩指针没处理:新版本 V8 常见 pointer compression,地址计算会错。
- 对象被 GC 移动:没有稳定引用,调试时对象位置变化。
- 把 XSS 当浏览器 Pwn:二者攻击层级完全不同。
- 忽略沙箱:在浏览器里拿到 renderer RCE 不等于拿到系统权限。
迷你案例
题目给:
d8
bug.patch
poc.jsbug.patch 中看到某个数组 bounds check 被错误移除。
第一步,运行 PoC:
./d8 poc.js输出:
victim.length = 1073741824说明数组长度异常。
第二步,布置相邻对象:
let oob = [1.1, 2.2, 3.3];
let obj = [{mark: 0x1337}];
let buf = new ArrayBuffer(0x100);第三步,用 OOB 观察相邻对象变化。如果改写某个索引后 buf.byteLength 或 backing store 表现异常,就可能继续构造任意读写。
第四步,创建 Wasm 实例并泄露 code page。最终路线:
OOB
-> addrof/fakeobj
-> read64/write64
-> 找 Wasm code page
-> 写入 payload
-> 调用 Wasm 导出函数WP 不需要贴一大段不可解释的 exploit。更重要的是写清楚每个阶段的证据:
- 哪个 diff 导致错误优化。
- 怎么证明 OOB。
- 怎么从 OOB 变成 addrof/fakeobj。
- 怎么定位执行页。