Samuel Groß and Sergei Glazunov, Project Zero (Originally posted on Project Zero blog 2020-08-05)

The Basics

Disclosure or Patch Date: 24 February 2020

Product: Google Chrome

Advisory: https://chromereleases.googleblog.com/2020/02/stable-channel-update-for-desktop_24.html

Affected Versions: Google Chrome 60 - 80

First Patched Version: 80.0.3987.122

Issue/Bug Report: https://bugs.chromium.org/p/chromium/issues/detail?id=1053604

Patch CL: https://chromium.googlesource.com/v8/v8.git/+/fb0a60e15695466621cf65932f9152935d859447

Bug-Introducing CL: https://chromium.googlesource.com/v8/v8.git/+/0f716acadaed1d9e194593543dbe1340d600d6fc

Reporter(s): Clément Lecigne of Google's Threat Analysis Group

The Code

Proof-of-concept:

'use strict';
(function() {
  var popped;

  function trigger(new_target) {
    function inner(new_target) {
      function constructor() {
        popped = Array.prototype.pop.call(array);
      }
      var temp = array[0];
      return Reflect.construct(constructor, arguments, new_target);
    }

    inner(new_target);
  }

  var array = new Array(0, 0, 0, 0, 0);

  for (var i = 0; i < 20000; i++) {
    trigger(function() { });
    array.push(0);
  }

  var proxy = new Proxy(Object, {
    get: () => (array[4] = 1.1, Object.prototype)
  });
 
  trigger(proxy);
  print(popped);
}());

Exploit sample: N/A

Did you have access to the exploit sample when doing the analysis? Yes

The Vulnerability

Bug class: Incorrect side-effect modelling in JIT Compiler

Vulnerability details:

The function NodeProperties::InferReceiverMapsUnsafe is responsible for inferring the Map of an object. From the source code documentation: "This information can be either 'reliable', meaning that the object is guaranteed to have one of these maps at runtime, or 'unreliable', meaning that the object is guaranteed to have HAD one of these maps". In the latter case, the caller has to ensure that the object has the correct type, either by using CheckMap nodes or CodeDependencies.

On a high level, the InferReceiverMapsUnsafe function traverses the effect chain until it finds the node creating the object in question and, at the same time, marks the result as unreliable if it encounters a node without the kNoWrite flag, indicating that executing the node could have side-effects such as changing the Maps of an object. There is a mistake in the handling of kJSCreate: if the object in question is not the output of kJSCreate, then the loop continues without marking the result as unreliable. This is incorrect because kJSCreate can have side-effects, for example by using a Proxy as third argument to Reflect.construct. The bug can then for example be triggered by inlining Array.pop and changing the elements' kind from SMIs to Doubles during the unexpected side effect.

Normally, this bug class occurs due to mis-modelling of an operation in the JIT compiler. However, this time the modelling was correct (the JSCreate function was correctly marked as having side effects), however, the specific piece of code relying on this data was incorrect as it had special-cased the JSCreate operation and then forgot to perform the side-effect modelling.

Patch analysis: N/A

Thoughts on how this vuln might have been found (fuzzing, code auditing, variant analysis, etc.):

JIT side-effect related bugs are likely hard to discover through generic JavaScript engine fuzzing approaches, as, without specific compiler instrumentation, they don’t normally result in observable misbehaviour at runtime (i.e., a crash). Given that the vulnerable function is clearly security-critical, it is thus more likely that the bug was discovered through manual code analysis while looking for type inference related issues.

(Historical/present/future) context of bug:

JIT side effect modelling issues have been found and exploited in all major engines in the past years.

This vulnerability was chained with both Windows 0-days (CVE-2020-0938, CVE-2020-1020, CVE-2020-1027 and Android n-days as a part of a watering hole attack.

The Exploit

Is the exploit method known? Yes

Exploit method:

The attacker abused the unexpected side-effect to cause type confusion between the floating-point and tagged pointer array types, which allowed them to implement the fakeobj primitive. fakeobj makes the JavaScript engine treat an arbitrary value as a pointer to a JavaScript object. The attacker constructed a “fake” typed array and used it to overwrite the machine code of a compiled WebAssembly function with their shellcode.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why):

Manual auditing for similar issues in the same function and related ones. A more generic approach would be to build compiler instrumentation to detect unexpected side effects at runtime and abort early.

Found variants: None

Structural improvements

N/A. This bug seems to be somewhat unique (at least it’s quite different from all the other side-effect mis-modelling bugs we can think of) and thus probably not much can be done in terms of generic/structural improvements.

0-day detection methods

These types of exploits are likely hard to detect generically.

Other References