Sergei Glazunov & Maddie Stone, Project Zero (Originally posted on Project Zero blog 2020-07-27)

The Basics

Disclosure or Patch Date: 31 October 2019

Product: Google Chrome

Advisory: https://chromereleases.googleblog.com/2019/10/stable-channel-update-for-desktop_31.html

Affected Versions: Chrome 76 - 78.0.3904.70

First Patched Version: Chrome 78.0.3904.87

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

Patch CL: https://chromium-review.googlesource.com/c/chromium/src/+/1888103

Bug-Introducing CL: https://chromium-review.googlesource.com/c/chromium/src/+/1077713/

Reporter(s): Anton Ivanov and Alexey Kulaev of Kaspersky Lab (Thanks to Kaspersky Lab for sharing a public detailed analysis!)

The Code

Proof-of-concept: Kaspersky provided a proof-of-concept (POC) in their bug report to Chrome. Note that this is a proof-of-concept and not the original exploit sample.

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:

The bug is a use-after-free in the webaudio component (convolver_node.cc) of the Blink renderer. This UaF enables renderer remote code execution. The UaF is caused by only using a mutex on one thread that accesses the members, not both.

The UaF is caused by two different threads, the main thread and the audio rendering thread, operating on ConvolverNode members at the same time. One thread, the audio rendering thread, is triggered by startRendering in POC and calls the Process method shown below. The main thread (the while (!finished) clause at line 184 in the POC) changes the convolver.buffer value. The convolver.buffer assignments calls ConvolverHandler::SetBuffer.

void ConvolverHandler::Process(uint32_t frames_to_process) {
  AudioBus* output_bus = Output(0).Bus();
  DCHECK(output_bus);

  // Synchronize with possible dynamic changes to the impulse response.
  MutexTryLocker try_locker(process_lock_);
  if (try_locker.Locked()) {
    if (!IsInitialized() || !reverb_) {
      output_bus->Zero();
    } else {
      // Process using the convolution engine.
      // Note that we can handle the case where nothing is connected to the
      // input, in which case we'll just feed silence into the convolver.
      // FIXME:  If we wanted to get fancy we could try to factor in the 'tail
      // time' and stop processing once the tail dies down if
      // we keep getting fed silence.
      reverb_->Process(Input(0).Bus(), output_bus, frames_to_process);
    }
  } else {
    // Too bad - the tryLock() failed.  We must be in the middle of setting a
    // new impulse response.
    output_bus->Zero();
  }
}

In ConvolverHandler::SetBuffer, if the value to set the buffer to is null (like in line 185 of the POC), then the reverb_ member is freed. However, it is still in use by the Process() thread/function.

void ConvolverHandler::SetBuffer(AudioBuffer* buffer,
                                 ExceptionState& exception_state) {
  DCHECK(IsMainThread());

  if (!buffer) {
    reverb_.reset();
    shared_buffer_ = nullptr;
    return;
  }

  if (buffer->sampleRate() != Context()->sampleRate()) {
    exception_state.ThrowDOMException(
        DOMExceptionCode::kNotSupportedError,
        "The buffer sample rate of " + String::Number(buffer->sampleRate()) +
            " does not match the context rate of " +
            String::Number(Context()->sampleRate()) + " Hz.");
    return;
  }

The bug is caused by the lack of MutexLocker locker(process_lock_); prior to freeing reverb_ and shared_buffer_.

Patch analysis: N/A

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

Based solely on the vulnerability, it seems equally reasonable that it could have been found through fuzzing or manual code auditing. This is due to the bug being rather shallow, meaning that the pattern that can trigger the use-after-free is not complex. It’d likely be faster to find manually if you’ve already looked at the previous bugs in webaudio and thus know what to look for, but you could probably find it using static analysis tools too

(Historical/present/future) context of bug:

In their POC, at line 87, Kaspersky commented // Exploit (I bet it should be similar to unreleased CVE-2019-5851?). Google Chrome disclosed and patched CVE-2019-5851, a use-after-poison in webaudio, in July 2019. The bug report was made public in October 2019, 10 days before Kaspersky discovered and reported this vulnerability. The POC was deleted from the bug report, but this vulnerability, either in July or October, could have influenced the attackers to look at the webaudio for lifetime management issues. (Note: Chrome recently restored the POC.)

This Chrome vulnerability was paired with the Windows escalation of privilege CVE-2019-1458.

The Exploit

Is the exploit method known? Yes

Exploit method:

Based on Kaspersky’s May 2020 blog post, the exploit modifies the PartitionPage metadata to exploit the UAF and get arbitrary read/write. To get code execution, the exploit abuses WebAssembly to map RWX pages into the process.

The Next Steps

Variant analysis

Areas/approach for variant analysis (and why):

We performed variant analysis on this vulnerability using a Semmle/CodeQL query. We ran the query over the webaudio and webdatabases modules in the renderer and the core/workers folder of the renderer.

In April 2020, Man Yue Mo of the Github Security Lab (previously Semmle) published on finding more use-after-free bugs in the webaudio module of Chrome. They do not explicitly state how they performed the variant analysis, but it's almost certain that they used Semmle to find use-after-free memory corruption bugs within the webaudio module.

Found variants:

  • P0 1963 (CVE-2019-13732, CVE-2020-6406): Heap use-after-free in PannerHandler::TailTime. First patch was incomplete and thus the 2nd CVE was issued to fully fix the issue.
  • GHSL-2020-035 (CVE-2020-6427) discovered by Man Yue Mo of Github Security Lab: Use-after-poison in IIRFilterHandler and BiquadFilterHandler.
  • GHSL-2020-037 (CVE-2020-6428) discovered by Man Yue Mo of Github Security Lab: Use-after-free in DeferredTaskHandler::BreakConnections.
  • GHSL-2020-038 (CVE-2020-6429) discovered by Man Yue Mo of Github Security Lab: Use-after-poison in AudioScheduledSourceHandler::NotifyEnded.
  • GHSL-2020-040 (CVE-2020-6449) discovered by Man Yue Mo of Github Security Lab: Use-after-free in DeferredTaskHandler::BreakConnections(2).
  • GHSL-2020-041 (CVE-2020-6451) discovered by Man Yue Mo of Github Security Lab: Use-after-free in DeferredTaskHandler::ProcessAutomaticPullNodes.
  • GHSL-2020-053 (CVE-2020-6450) discovered by Man Yue Mo of Github Security Lab: Incomplete fix of the vulnerabilities reported in GHSL-2020-035 and GHSL-2020-038.

Structural improvements

  • Refactoring webaudio: The webaudio module has a large number of security vulnerabilities within it likely due to the fact that it’s one of the few modules that has multithreading, relies on shared memory, and has a large amount of JavaScript interaction. It needs that shared memory architecture because it’s especially latency critical in order for users to have a smooth audio experience. There is an ongoing discussion here.
  • Prevent the PartitionAlloc exploitation method by hardening the ArrayBuffer partition. This could be done by moving the freelist out of line or validating freelist pointers.

0-day detection methods

  • Look for creation of a large number of IIRFilterNode objects
  • Would it be possible to detect functions that exploits use to often force garbage collection?

Other References