Exploits & Vulnerabilities
CVE-2020-1380: Analysis of Recently Fixed IE Zero-Day
As part of August’s Patch Tuesday, Microsoft patched one zero-day vulnerability that targeted Internet Explorer 11, specifically CVE-2020-1380. It is a use-after-free bug in Internet Explorer's JavaScript engine, jscript9.dll.
As part of August’s Patch Tuesday, Microsoft patched one zero-day vulnerability that targeted Internet Explorer 11, specifically CVE-2020-1380. It is a use-after-free (UAF) bug in Internet Explorer's JavaScript engine, jscript9.dll. Over the past few years, we’ve observed that zero-day attacks against Internet Explorer usually exploit vbscript.dll and jscript.dll to run shellcode. This time, the target changed to jscript9.dll and used the modern JavaScript engine's Just-In-Time (JIT) engine to trigger the bug, so I decided to dive into the jscrtip9.dll JIT engine to try to figure out the root cause of CVE-2020-1380.
Overview of Jscirpt9.dll Execution Pipeline
Jscript9.dll is the default JavaScript engine that is used to replace jscript.dll starting from Internet Explorer 9. Figure 1 shows the execution pipeline of jscript9.dll.
Generally, there are five major steps to execute JavaScript source code in jscript9.dll:
- The parser parses the JavaScript source code to get the abstract syntax tree (AST).
- The ByteCodeGenerator traverses the AST and generates ByteCode.
- The Interpreter is a virtual machine that executes ByteCode. Profile data, such as type information, is collected when ByteCode is executed.
- When some code snippets are invoked multiple times, like in a for-loop, the Interpreter sends the ByteCode and profile data to the backend Just-In-Time (JIT) engine to generate machine code and then replaces the ByteCode entry point with the generated machine code.
- When the machine code is executed, if some status breaks the profile assumption, the machine code will bail out to the Interpreter to execute the ByteCode again to avoid any safety issues.
Locating the Machine Code
Before talking about the vulnerability, we first need to locate the machine code generated by the JIT engine. A for-loop usually invokes the JIT engine in the Interpreter. Figure 2 shows a simple JavaScript code that can trigger the JIT:
When the loop count is greater than some threshold values (0x32 in the code snippet in Figure 3), the loop body and the inner call function, opt, will be sent to the backend JIT engine job queue to generate optimized machine code:
The backend JIT engine thread gets the job from the queue and finally calls jscript9!Func::Codegen to generate optimized machine code:
The backend JIT engine goes through several steps to generate optimized machine code, such as: build intermediate representation (IR), inlining, build control flow graph (CFG), data flow analysis, lower, allocate register, layout, encode, etc.:
When optimized machine code is generated, it will be used to replace the ByteCode loop body. When the for-loop is called next, the machine code will be invoked instead in function Js::InterpreterStackFrame::CallLoopBody:
Finally, the loop body machine code will invoke the inner call function opt’s machine code, which we can see here:
Root Cause Analysis of CVE-2020-1380
The PoC of CVE-2020-1380 is shown in Figure 8:
The following steps can trigger the bug:
- The for-loop sends the opt function to the JIT engine.
- In function opt, the three lines of “arguments operation” can set value2 to value1, then the Float32Array first element arr[0] is set by value1.
- After function opt is sent to the JIT engine, it changes the arguments value2 from an integer 0x1337 to an object that has a valueOf callback function.
- In the final call of function opt, because the argument 'flag' is set to 0, the basic block of “if (flag == 1)” is not executed, which sets value2 to arr[0]. Because an object replaces value2, implicit type transform happens, then the valueOf callback function may be invoked in machine code.
JavaScript is a dynamic language in which type or property can be transformed implicitly. Generated machine code that makes JavaScript implicit calls directly without any check are not trustworthy. Jscript9.dll uses the function ExecuteImplicitCall to make the JavaScript implicit call safe.
First, we change the three lines of “arguments operation” to “arguments[0] = value2”, which has the same effect. Figure 9 shows the generated JIT code snippet.
Before the type conversion function jscript9!Js::JavascriptConversion::ToFloat_Helper is called, some values are set to flags stored in address 0x140F3F68 and 0x140F3E86 separately. The flag stored in 0x140F3F68 is ImplicitCallFlags and the other flag stored in 0x140F3E86 is DisableImplicitFlags. DisableImplicitFlags is set to 3 (DisableImplicitCallFlag | DisableImplicitExceptionFlag), which means the type conversion to float is not allowed on an implicit call from JavaScript code to be invoked, such as valueOf.
Function Js::JavascriptConversion::ToFloat_Helper checks the input type to decide which type conversion path to choose. Because value2 is an object, Js::DynamicObject::ToPrimitive is called, then finally ExecuteImplicitCall is called:
ExecuteImplicitCall checks the DisableImplicitFlags; if the value is not equal to 0, the JavaScript implicit call will not be called and return undefined directly. Finally, the machine code will bail out to Interpreter and call the implicit call in Interpreter safely:
However, when the three lines of “arguments operation” are used to replace “arguments[0] = value2”, we can see that the generated machine code calls Js::JavascriptConversion::ToFloat_Helper directly without setting the DisableImplicitFlags:
Finally, the implicit call valueOf will be called directly from machine code. An attacker can use this no-check callback opportunity to trigger a UAF vulnerability, like neutering the TypedArray's ArrayBuffer memory by Worker thread.
Why the three lines of “arguments operation” can eliminate the DisableImplicitFlags setting machine code piqued my curiosity. I think the type inference error of arguments[0] in the backend JIT GlobOpt phase is the root cause. The JIT engine doesn’t know the side effect of Array.prototype.push, which can be used to change the type of arguments[0]. The type of arguments[0] should be killed after Array.prototype.push operation to avoid this type inference error issue.
Other methods can trigger this bug, like using Array.prototype.splice or using Float64Array to invoke a conversion path of Js::JavascriptConversion::ToNumber_Helper. This bug was also fixed in the August patch.
Conclusion
Over the past few years, zero-day attacks that target Internet Explorer usually exploit vbscrpt.dll and jscript.dll vulnerabilities. CVE-2020-1380 is special because it targets the JIT engine of jscript9.dll. JIT vulnerabilities are a common issue in modern JavaScript engines like V8, JavascriptCore, Spidermonkey, and Chakra. Perhaps attackers are now choosing to target the JIT engine of Internet Explorer.
Trend Micro™ Deep Security™ and Vulnerability Protection protect users from exploits that target this vulnerability via the following rule:
- 1010441 – Microsoft Internet Explorer Scripting Engine Memory Corruption Vulnerability (CVE-2020-1380)
Trend Micro™ TippingPoint® protects customers through the following rules:
- 37955: HTTP: Microsoft Internet Explorer Use-After-Free