Analysis of a Chrome Zero Day: CVE-2019-5786

1. Introduction

On March 1st, Google published an advisory [1] for a use-after-free in the Chrome implementation of the FileReader API (CVE 2019-5786). Clement Lecigne from Google Threat Analysis Group reported the bug as being exploited in the wild and targeting Windows 7, 32-bit platforms. The exploit leads to code execution in the Renderer process, and a second exploit was used to fully compromise the host system [2]. This blog is a technical write-up detailing the first bug and how to find more information about it. At the time of writing, the bug report [2b] is still sealed. Default installation of Chrome will install updates automatically, and users running the latest version of Chrome are already protected against that bug. To make sure you’re running the patched version, visit chrome://version, the version number displayed on the page should be 72.0.3626.121 or greater.

2. Information gathering

2.1 The bug fix

Most of the Chrome codebase is based on the Chromium open source project. The bug we are looking at is contained inside the open source code, so we can directly look at what was fixed in the new release pertaining to the FileReader API. Conveniently, Google shares the changelog for its new release [3].

The message hints that having multiple references to the same underlying ArrayBuffer is a bad thing. It is not clear what it means right now, but the following paragraphs will work on figuring out what wisdom lies hidden in this message.

For starters, we can look at the commit diff [3b] and see what changed. For ease of reading, here is a comparison of the function before and after the patch.

The two versions can be found on GitHub at [4a] and [4b]. This change modifies the behavior of the ArrayBufferResult function that is responsible for returning data when a user wants to access the FileReader.result member.
The behavior of the function is as follows: if the result is already ‘cached,’ return that. If not, there are two cases; if the data has finished loading, create a DOMArrayBuffer, cache the result, and returns it. If not, it creates a temporary DOMArrayBuffer and returns that instead. The difference between the unpatched and patched version is how that temporary DOMArrayBuffer is handled, in case of a partial load. In one case, we can see a call to:

 

This prompted us to go down a few more rabbit holes. Let us compare what is going on in both the unpatched and patched situation.

We can start with the patched version, as it is the simplest to understand. We can see a call to ArrayBuffer::Create that takes two arguments, a pointer to the data and its length (the function is defined in the source tree at /third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer.h)

This basically creates a new ArrayBuffer, wraps it into a scoped_refptr<ArrayBuffer> and then copies the data into it. The scoped_refptr is a way for Chromium to handle reference counting [5]. For readers unfamiliar with the notion, the idea is to keep track of how many times an object is being referenced. When creating a new instance of a scoped_refptr, the reference count for the underlying object is incremented; when the object exits its scope, the reference count is decremented. When that reference count reaches 0, the object is deleted (and for the curious, Chrome will kill a process if the reference count overflows….). As we’re looking for a potential use-after-free, knowing that the buffer is ref-counted closes some avenues of exploitation.

In the unpatched version, instead of calling ArrayBuffer::Create, the code uses the return value of ArrayBufferBuilder::ToArrayBuffer() (from third_party/blink/renderer/platform/wtf/typed_arrays/array_buffer_builder.cc):

Here is yet another rabbit hole to dive into (but we will keep it high level).  Depending on the value of bytes_used_), the function will either return its buffer, or a Sliced version of it (i.e. a new ArrayBuffer of a smaller size, that contains a copy of the data)

To sum up what we have so far, in all the code paths we have looked at, they all return a copy of the data instead of the actual buffer, unless we run the unpatched code, and the buffer we try to access is `fully used` (per the comment in ArrayBufferBuilder::ToArrayBuffer()).
Because of the implementation of the FileReaderLoader object, the buffer_->ByteLength() is the pre-allocated size of the buffer, which correspond to the size of the data we want to load (this will be relevant later on).
If we now remember the commit message and what the bad scenario was, it looks like the only situation to exploit the bug is to access multiple times the ArrayBufferBuilder::ToArrayBuffer(), before the finished_loading is set to true, but after the data is fully loaded.

To wrap up this part of the code review, let us look at the behavior of the DOMArrayBuffer::Create function that is being called in both patched/unpatched cases, the case interesting to us is when we have the following call DOMArrayBuffer::Create(raw_data_->ToArrayBuffer());

then `b` takes ownership of what belonged to `a` (`b` now contains “hello”) and `a` is now in a somewhat undefined state (C++11 specs explain that in more precise terms)).

In our current situation, what is going on here is somewhat confusing [6a] [6b]. The object returned by ArrayBufferBuilder::ToArrayBuffer() is already a scoped_refptr<ArrayBuffer>. I believe the meaning of all this, is that when calling ToArrayBuffer(), the refcount on the ArrayBuffer is increased by one, and the std::move takes ownership of that instance of the refcounted object (as opposed to the one owned by the ArrayBufferBuilder). Calling ToArrayBuffer() 10 times will increase the refcount by 10, but all the return values will be valid (as opposed to the toy example with the strings `a` and `b` mentioned above where operating on `a` would result in unexpected behavior).
This closes an obvious case of use-after-free where the buffer_ object from the ArrayBufferBuilder would get corrupted if we would call ToArrayBuffer() multiple times during the sweet spot described above.

2.2 FileReader API

Another angle of approach for figuring out how to exploit this bug is to look at the API that is available to us from JavaScript and see if we can come up with a way to reach the sweet spot we were looking at.

We can get all the information we want from Mozilla web docs [7]. Our options are fairly terse; we can call readAsXXX functions on either Blob or File, we can abort the read, and finally there are a couple of events to which we can register callbacks (onloadstart, onprogress, onloadend, …).

The onprogress events sounds like the most interesting one, as it is being called while data is loading, but before the loading is finished. If we look at the FileReader.cc source file, we can see that the logic behind the invocation of this event is to fire every 50ms (or so) when data is received. Let us have a look at how this behaves in a real system…

3. Testing in a web-browser

3.1 Getting started

The first thing we want to do is download a vulnerable version of the code. There are some pretty useful resources out there [8] where one can download older builds rather than having to build them yourself.

Something interesting to note is that there is also a separate zip file that has `syms` in its name. You can also download to get debug symbols for the build (in the form of .pdb files). Debuggers and disassemblers can import those symbols which will make your life way easier as every function will be renamed by its actual name in the source code.

3.2 Attaching a debugger

Chromium is a complex software and multiple processes communicate together which makes debugging harder. The most efficient way to debug it is to start Chromium normally and then attach the debugger to the process you want to exploit. The code we are debugging is running in the renderer process, and the functions we were looking at are exposed by chrome_child.dll (those details were found by trial and error, attaching to any Chrome process, and looking for function names of interest).

If you want to import symbols in x64dbg, a possible solution is to go in the Symbol pane, right click on the .dll/.exe you want to import the symbols for and select Download symbols. It may fail if the symbol server setting is not configured properly, but it will still create the directory structure in x64dbg’s `symbols` directory, where you can put the .pdb files you’ve previously downloaded.

3.3 Looking for the exploitable code path

Not that we have downloaded an unpatched version of Chromium, and we know how to attach a debugger, let us write some JavaScript to see if we can hit the code path we care about.

To sum up what is going on here, we create a Blob that we pass to the FileReader. We register a callback to the progress event and, when the event is invoked, we try to access multiple times the result from the reader. We have seen previously that the data needs to be fully loaded (that is why we check the size of the buffer) and if we get multiple DOMArrayBuffer with the same backing ArrayBuffer, they should appear to be to separate objects to JavaScript (hence the equality test). Finally, to double check we have indeed two different objects backed by the same buffer, we create views to modify the underlying data and we verify that modify one modifies the other as well.

There is an unfortunate issue that we had not foreseen: the progress event is not called frequently, so we have to load a really large array in order to force the process to take some time and trigger the event multiple times. There might be better ways of doing so (maybe the Google bug report will reveal one!) but all the attempts to create a slow loading object were a failure (using a Proxy, extending the Blob class…). The loading is tied to a Mojo Pipe, so exposing MojoJS could be a way of having more control as well but it seems unrealistic in an attacker scenario as this is the entry point of the attack. See [9] for an example for that approach.

3.4 Causing a crash

So, now that we have figured out how to get into the code path that is vulnerable, how do we exploit it? This was definitely the hardest question to answer, and this paragraph is meant to share the process to find an answer to that question.

We have seen that the underlying ArrayBuffer is refcounted, so it is unlikely we’ll be able to magically free it by just getting garbage collected from some of the DOMArrayBuffer we’ve obtained. Overflowing the refcount sounds like a fun idea, but if we try by hand to modify the refcount value to be near its maximum value (via x64dbg) and see what happens… well, the process crashes. Finally, we cannot do much on those ArrayBuffers; we can change their content but not their size, nor can we manually free them…
Not being familiar enough with the codebase, the best approach then is to pour through various bug reports that mention use-after-free, ArrayBuffer, etc., and see what people did or talked about. There must be some assumption somewhere that a DOMArrayBuffer owns its underlying memory, and that is an assumption we know we are breaking.
After some searching, we started to find some interesting comments like this one [10a] and this one [10b]. Those two links talk about various situation where DOMArrayBuffer gets externalized, transferred and neutered. We are not familiar with those terms, but from the context it sounds like when this happens, the ownership of the memory is transferred to somebody else. That sounds pretty perfect for us as we want the underlying buffer to be freed (as we are hunting for a use-after-free).

The memory being dereferenced is in ECX (we also have EAX == 0 but that’s because we’re looking at the first item in the view). The address looks valid, but it isn’t. ECX contains the address where the raw data of our buffer was stored (the AAAAA…) but because it got freed, the system unmapped the pages that held it, causing the access violation (we’re trying to access an unmapped memory address). We reached the use-after-free we were looking for!

4. Exploit considerations and next steps

4.1 Exploit

It is not the point of this document to illustrate how to push beyond the use-after-free to get full code execution (in fact Exodus have released a blog and a working exploit roughly coinciding with the timing of this publication). However, there are some interesting comments to be made.
Due to the way we are triggering the use-after-free, we are ending up with a very large buffer unallocated. The usual way to exploit a use-after-free is to get a new object allocated on top of the freed region to create some sort of confusion. Here, we are freeing the raw memory that is used to back the data of our ArrayBuffer. That is great because we can read/write over a large region. Yet, a problem in this approach is that because the memory region is really large, there is no one object that would just fit in. If we had a small buffer, we could create lots of objects that have that specific size and hope one would be allocated there. Here it is harder because we need to wait that until that memory is reclaimed by the heap for unrelated objects. On Windows 10 64-bit, it is hard because of how random allocations are, and the entropy available for random addresses. On Windows 7 32-bit, it is much easier as the address space is much smaller, and the heap allocation is more deterministic. Allocating a 10k object might be enough to have some metadata land within the address space we can control.
The second interesting aspect is that because we are going to dereference a region that has been unmapped, if the 10k allocation mentioned above fails to allocate at least one object in that area we control, then we are out of luck; we will get an access violation and the process will die. There are ways to make this step more reliable, such as the iframe method described here [11]
An example on how to move on if one can corrupt the metadata of a JavaScript object can be found here [12].

4.2 Next step

Once an attacker has gained code execution inside the renderer process they are still limited by the sandbox. In the exploit found in the wild, the attacker used a second 0-day that targeted the Windows Kernel to escape the sandbox. A write up describing that exploit was recently released by the 360CoreSec here [13].

5. Conclusion

By looking at the commit that fixed the bug and hunting down hints and similar fixes we were able to recover the likely path towards exploitation. Once again, we can see that modern mitigations introduced in the later version of Windows makes life way harder on attackers and we should celebrate those wins from the defensive side. Also, Google is extremely efficient and aggressive in its patching strategy, and most of its user base will have already seamlessly updated to the latest version of Chrome.

 

Links

[1] https://chromereleases.googleblog.com/2019/03/stable-channel-update-for-desktop.html
[2] https://security.googleblog.com/2019/03/disclosing-vulnerabilities-to-protect.html
[2b] https://bugs.chromium.org/p/chromium/issues/detail?id=936448
[3] https://chromium.googlesource.com/chromium/src/+log/72.0.3626.119..72.0.3626.121?pretty=fuller
[3b] https://github.com/chromium/chromium/commit/ba9748e78ec7e9c0d594e7edf7b2c07ea2a90449
[4a] https://github.com/chromium/chromium/blob/17cc212565230c962c1f5d036bab27fe800909f9/third_party/blink/renderer/core/fileapi/file_reader_loader.cc
[4b] https://github.com/chromium/chromium/blob/75ab588a6055a19d23564ef27532349797ad454d/third_party/blink/renderer/core/fileapi/file_reader_loader.cc
[5] https://www.chromium.org/developers/smart-pointer-guidelines
[6a] https://chromium.googlesource.com/chromium/src/+/lkgr/styleguide/c++/c++.md#object-ownership-and-calling-conventions
[6b] https://www.chromium.org/rvalue-references
[7] https://developer.mozilla.org/en-US/docs/Web/API/FileReader
[8] https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Win_x64/612439/
[9] https://www.exploit-db.com/exploits/46475
[10a] https://bugs.chromium.org/p/v8/issues/detail?id=2802
[10b] https://bugs.chromium.org/p/chromium/issues/detail?id=761801
[11] https://blog.exodusintel.com/2019/01/22/exploiting-the-magellan-bug-on-64-bit-chrome-desktop/
[12] https://halbecaf.com/2017/05/24/exploiting-a-v8-oob-write/
[13] http://blogs.360.cn/post/RootCause_CVE-2019-0808_EN.html

Introducing McAfee+

Identity theft protection and privacy for your digital life

FacebookLinkedInTwitterEmailCopy Link

Stay Updated

Follow us to stay updated on all things McAfee and on top of the latest consumer and mobile security threats.

FacebookTwitterInstagramLinkedINYouTubeRSS

More from McAfee Labs

Back to top