CVE-2020-1380: Internet Explorer JScript9 Use-after-Free
Maddie Stone & Samuel Groß, Project Zero (Originally posted on Project Zero blog 2020-08-24)
The Basics
Disclosure or Patch Date: 11 August 2020
Product: Microsoft Internet Explorer
Advisory: https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2020-1380
Affected Versions: For Windows 10 2004, KB4565503 and previous
First Patched Version: For Windows 10 2004, KB4566782
Issue/Bug Report: N/A
Patch CL: N/A
Bug-Introducing CL: N/A
Reporter(s): Boris Larin (@oct0xor) of Kaspersky Lab (Thanks to Kaspersky Lab for sharing their detailed analysis!)
The Code
Proof-of-concept:
- https://securelist.com/ie-and-windows-zero-day-operation-powerfall/97976/
- https://www.trendmicro.com/en_us/research/20/h/cve-2020-1380-analysis-of-recently-fixed-ie-zero-day.html
Exploit sample: N/A
Did you have access to the exploit sample when doing the analysis? No
The Vulnerability
Bug class: use-after-free
Vulnerability details:
Proof of concept from Trend Micro:
function opt(value1, value2, arr, flag) {
// value1 = int, value2 = int when compiling, Object to trigger
// arr = Float32Array, flag = int
value1 = 1;
arguments.push = Array.prototype.push;
arguments.length = 0;
// Sets value1 = value2
arguments.push(value2);
if (flag == 1) {
value1 = 2;
}
// arr is a Float32Array. When value1 has become
// an Object, valueOf callback is
// executed due to incorrect type inference.
// The backing array buffer can be detached in
// the valueOf callback, causing the UAF.
arr[1] = value1;
};
var arr = new Float32Array(0x100);
//Compile opt such that it believes value2 is always an int
for (var i = 0; i < 10000; i++) {
opt(0x1337, 0x1337, arr, 1);
}
var obj = new Object();
obj.valueOf = function () {
alert("callback");
//Free backing array buffer
worker = new Worker("worker.js");
worker.postMessage(0, [arr.buffer]);
worker.terminate();
worker = null;
var start = Date.now();
while(Date.now() - start < 200) {}
return 1;
};
// Call opt with value2 as an object, not an int.
// JIT has incorrectly modeled that obj.valueOf
// will be called.
opt(0x1337, obj, arr, 0);
This vulnerability is an use-after-free due to two JIT mismodellings by JScript9:
- The JIT compiler fails to predicut that calling
Array.prototype.push
witharguments
as thethis
object can modify the function argument values on the stack, and - The JIT compiler then assumes that
value1
is still a primitive value (an int) and thus fails to model that doingToPrimitive
on it (for storing in the Float32Array) can involve callbacks and thus execute arbitrary code.
The arguments
object is "Array-like", but not a true Array. This means that it doesn't have all of the usual Array properties except for length
. Usually the function arguments.push
doesn't and shouldn't exist. To trigger this bug, we set the push
method for arguments
to be the same as Array.push
. When push
is used to modify arguments
, the JIT compiler fails to model the modifications of the function arguments correct and thus infers the types incorrectly.
The incorrect type inference allows triggering the valueOf
callback when assigning an Object to the Float32Array (arr[1] = value1
). In the callback, we can detach the Float32Array leading to the UaF.
Patch analysis:
Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):
It seems unlikely that the bug was uncovered purely through reverse-engineering of the JIT compiler. That leaves three options:
-
Fuzzing, e.g. with a generative fuzzer or something like Fuzzilli. Generic JS fuzzers will likely have a bit of a hard time generating trigger code for these callback/reentrancy/side-effect-mismodelling related issues (possibly because they require specifically crafted dataflow that a coverage-guided fuzzer isn't rewarded for). It's definitely plausible that a fuzzer like Fuzzilli would find this (in fact, Fuzzilli somewhat tries to generate such code), but if someone did set up something like Fuzzilli for Jscript9, then it seems surprising if this was the bug they found with that. On the other hand, a mutation-based fuzzer started with a corpus of old bug triggers might find something like this fairly quickly - see also (3) below - and it's also plausible that a generative fuzzer tuned to finding these kinds of issues would find this fairly quickly. It’s possible someone already had such a fuzzer from previous JIT work and could reuse it here.
-
Manual experimentation/analysis If there is a way to inspect the JIT's intermediate code representation - or something roughly equivalent - in Jscript9, then it's very plausible that someone did a bit of grey-box analysis and manually experimented with how the JIT optimized different types of code. Then they would look at how it behaved for some of the typical edge-cases that a JS JIT has to deal with, in this case side-effect modelling, and would probably find this bug fairly quickly (trying to detach an ArrayBuffer during an indexed access through a type conversion seems like a pretty obvious thing to try). Since the bug is quite simple, this seems plausible. If the IR is not available, then it could still be possible to use this approach by using the assembly output and some tooling to simplify and find patterns.
-
From public regression tests The public test suites for e.g. v8 or JSC contain a multitude of bug triggers for old JS engine bugs. It’s possible that this exact bug was even covered by one of those tests. As such, it could also be that someone ported those tests to Jscript9, or even used them as basis for a mutation based fuzzer.
(Historical/present/future) context of bug:
There have been many recent 0-day exploits targeting JScript in Internet Explorer (CVE-2018-8653, CVE-2019-1367, CVE-2019-1429, CVE-2020-0674). This exploit differs from those in that it targets jscript9.dll, the default Javascript engine in IE9-11, rather than jscript.dll which was the default in IE8 and earlier.
This vulnerability was chained with CVE-2020-0986 where CVE-2020-0986 was the elevation of privilege. CVE-2020-0986 was patched 2 months prior (June 2020) to CVE-2020-1380.
The Exploit
Is the exploit method known? Yes
Exploit method:
This section is wholly based on Kaspersky’s blog post since we did not have a copy of the exploit.
Each step of the exploitation method is well known and commonly used.
To force the JIT to compile the function, the function is called many times with only an integer being stored to the Float32Array. This is a common way to force the JIT to compile. To free the underlying ArrayBuffer, the exploit uses the web workers postMessage
function, which is a well known and often used technique to trigger UAFs on typed arrays. To trigger the garbage collection, the exploit creates a “Sleep” method, which causes GC to occur based on exhausting its timeout. The exploit re-allocates the freed memory with a LargeHeapBlock. The exploit wants a memory layout where the call to store an object at an index the Float32Array will overwrite the Allocated Block Count (+0x14) in the LargeHeapBlock with 0. The exploit then does more allocating and freeing of arrays to ultimately end up with two JavascriptNativeIntArray objects whose ‘head’ members point to the same address. The OOB r/w afford by the JavascriptNativeIntArrays is then used to create new DataView objects to get arbitrary read/write primitives. These are commonly used techniques for getting the r/w primitives in Internet Explorer.
Once the exploit has arbitrary r/w, it needs to get code execution. The exploit finds its stack address and use a ROP chain to execute its own shellcode. It gets its stack address using the function LinktoBeginning
The Next Steps
Variant analysis
Areas/approach for variant analysis (and why):
- Perform fuzzing, similar to Fuzzilli, on Internet Explorer as long as it will still be in Windows builds.
- To find very close variants, manually review other potential callback issues.
- A less resource intensive option may be to port regression tests from other engines to JScript9.
Found variants:
- CVE-2020-17053: Discovered by Elliot Cao (@iamelli0t). [Blog Post]
Structural improvements
- Internet Explorer is now considered “legacy” software. Remove it.
- Attempt to break the JavascriptNative arrays/LargeHeapBlocks exploit method. Webkit attempts to do it with Gigacage. A similar approach implemented differently could likely have good results.
0-day detection methods
- Look for JScript scripts that run the same function hundreds of times in a loop in order to trigger JIT compilation.
- Look for scripts that attempt to trigger garbage collection through “sleep” behavior.
Other References
- Aug 2020: "Internet Explorer and Windows zero-day exploits used in Operation PowerFall" by Boris Larin of Kaspersky
- Very detailed write-up about the vulnerability and exploit methodology! Also includes POC.
- Aug 2020: "CVE-2020-1380: Analysis of Recently Fixed IE Zero-Day" by Elliot Cao of Trend Micro