Introduction
If someone wants to view a web page with his phone or computer, he will use a web browser. You also might be reading this article with a web browser. All the commodity web browsers in the world (i.e., Microsoft Edge, Apple Safari, Google Chrome, and Mozilla Firefox) implement their own JavaScript engines to support client-side scripting language for dynamic interaction with a client. That is the reason why the JavaScript engine is one of the most popular targets of both hackers and security researchers.
In this post, we introduce an interesting security vulnerability in ChakraCore (JS engine for Edge) we found during our research, CVE-2019-0609. Although the root cause of this bug is not so complicated, it was located quiet deeply unlike other interpreter bugs. Thus, this bug was hard to catch and long-lasting. We guess this is the reason why Microsoft paid the maximum amount of payment to us for the bug bounty.
Analyzing CVE-2019-0609, a use-after-unmap vulnerability
The first insight
Let's start with the assertion hit by the PoC in debug build.
ASSERTION 264714: (/home/soyeon/jsfuzz/js-static/engines/chakracore-1.11.3/lib/Runtime/Library/StackScriptFunction.cpp, line 249) stackFunction->boxedScriptFunction == boxedFunction
Failure: (stackFunction->boxedScriptFunction == boxedFunction)
[1] 264714 illegal hardware instruction ~/jsfuzz/js-static/engines/chakracore-1.11.3/out/Debug/ch 35977387.js
From the assertion, we can infer that the PoC has hit an issue while boxing a JavaScript function. No doubt you will be curious about what boxing is, as I did. So, what is boxing in JavaScript?
Boxing in JavaScript
If you declare an object such as a Number or a Function inside the global code or a function, they will be allocated on the stack normally. However, they should be located on the heap for some case, such as referencing same objects with different pointers or scope escaping of an object. For example, if a function allocated on the stack is returned, the function should be boxed to avoid scope escaping. This can happen in JavaScript because a function is a first-class object in JavaScript.
function test() {
function stackFun(){}
function heapFun(){print("hi!");}
return heapFun;
}
test()();
// output : hi!
In the above code, function stackFun
and heapFun
would be located on the stack when they are declared. Further, the function heapFun
should be moved to the heap to avoid pointing the address on the stack of function test
, as it is returned to the outside of function test
. The behavior that the JS engine moves the object to heap from stack, is called boxing. It is similar to the concept of boxing in Java.
What makes the assertion
Based on the concept of boxing, we can infer that a scriptFunction
needs boxing but it failed for some reason. For details, let's take a look at the code around the assertion on StackScriptFunction::BoxState::Box
in lib/Runtime/Library/StackScriptFunction.cpp
.
247 StackScriptFunction *stackFunction = interpreterFrame->GetStackNestedFunction(i);
248 ScriptFunction *boxedFunction = this->BoxStackFunction(stackFunction);
249 Assert(stackFunction->boxedScriptFunction == boxedFunction);
250 this->UpdateFrameDisplay(stackFunction);
In the above code, it tries to box a stackFunction
for some reason, and check whether or not the function is boxed through the assertion. However, the reached assertion shows that it is not really boxed. With gdb, we checked the boxedFunction
still indicates stackFunction
on the stack, and boxedScriptFunction
is nullptr. In normal case, it should points theboxedFunction
.
Stopped reason: SIGILL
0x0000555558a9be2f in Js::StackScriptFunction::BoxState::Box (this=0x7ffffffe2540) at /home/soyeon/jsfuzz/js-static/engines/chakracore-1.11.5/lib/Runtime/Library/StackScriptFunction.cpp:249
249 Assert(stackFunction->boxedScriptFunction == boxedFunction);
$ print boxedFunction
$1 = (Js::ScriptFunction *) 0x7ff7f024fff8
$ print stackFunction
$2 = (Js::StackScriptFunction *) 0x7ff7f024fff8
$ print stackFunction->boxedScriptFunction
$3 = (Js::ScriptFunction *) 0x0
Something went wrong! We should check what really happened in BoxStackFunction
.
Why boxing stack function is failed
This is the code snippet of the StackScriptFunction::BoxState::BoxStackFunction
.
710 ScriptFunction * StackScriptFunction::BoxState::BoxStackFunction(ScriptFunction * scriptFunction)
711 {
712 // Box the frame display first, which may in turn box the function
713 FrameDisplay * frameDisplay = scriptFunction->GetEnvironment();
714 FrameDisplay * boxedFrameDisplay = BoxFrameDisplay(frameDisplay);
715
716 if (!ThreadContext::IsOnStack(scriptFunction))
717 {
718 return scriptFunction;
719 }
...
748 boxedFunction = ScriptFunction::OP_NewScFunc(boxedFrameDisplay,
749 reinterpret_cast<FunctionInfoPtrPtr>(&functionInfo));
750 stackFunction->boxedScriptFunction = boxedFunction;
If the scriptFunction
is not on the stack, the function does not box the scriptFunction
, but just returns the scriptFunction
. Probably, this is because the BoxStackFunction
wants to avoid boxing a scriptFunction
again, which is already boxed and does not exist on the stack. However, it should be located on the stack, as it was StackScriptFunction
. This made us suspect the process of the stack variable allocation.
We found a hint in lib/Runtime/Language/InterpreterStackFrame.cpp: Var InterpreterStackFrame::InterpreterHelper
.
if (varAllocCount > InterpreterStackFrame::LocalsThreshold)
While allocating the stack for a function, the engine first checks if the space of the local variables exceeds the threshold (InterpreterStackFrame::LocalsThreshold
). If so, the engine will allocate a private arena as the stack instead of using the existing native stack. However, the scope analysis which is done by ThreadContext::IsOnStack
afterward, forgets to treat this private arena as a stack frame as well. Therefore, the stack function on the private arena will not be boxed and escape the original scope.
After the function, which has a private arena as its stack, is destructed, the stack will be also un-mapped. However, the non-boxed function still points to the stale stack space, which eventually results in a use-after-unmap vulnerability.
Patch analysis
This is the patch for CVE-2019-0609 released in ChakraCore 1.11.7.
+ if (stackVarAllocCount != 0)
+ {
+ size_t stackVarSizeInBytes = stackVarAllocCount * sizeof(Var);
+ PROBE_STACK_PARTIAL_INITIALIZED_INTERPRETER_FRAME(GetScriptContext(), Js::Constants::MinStackInterpreter + stackVarSizeInBytes);
+ stackAllocation = (Var*)_alloca(stackVarSizeInBytes);
+ }
In the patch, the engine first calculates stackVarAllocCount
as the number of stackScriptFunction
whether it is necessary to box or not. Then, it moves the stackScriptFunctions
to the heap through _alloca
.
Proof-of-Concept
Here is the PoC of CVE-2019-0609. The [big-size object] should be a large object literal that has an enough number of initialized members to exceed the threshold to allocate a private arena as the function stack.
function test() {
function a() {
function d() {
let e = function() {};
return e;
}
function b() {
let fun_d = [d];
return fun_d;
}
var obj = [big-size object]
return b();
}
return a();
}
var f = test();
function test1() {
var obj = [big-size object] // reallocate for use-after-unmap.
print(f[0]); // function d still points the address on stack as it is not boxed.
}
test1();