篇文章是一篇关于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 * auxSlotPtr