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:

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:

  1. The JIT compiler fails to predicut that calling Array.prototype.push with arguments as the this object can modify the function argument values on the stack, and
  2. The JIT compiler then assumes that value1 is still a primitive value (an int) and thus fails to model that doing ToPrimitive 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:

  1. 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.

  2. 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.

  3. 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 and traversing the linked list of ThreadContext objects for its own. To find the address of VirtualProtect, in order to set its shellcode buffer to executable, the exploit calculates the base address for the jscript9.dll module by reading the vftable pointer and then gets other modules’ base address from the Import Directory Table in the jscript9 PE header. The shellcode loads the elevation of privilege exploit, CVE-2020-0986.

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:

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