17th May 2024 - Alisa Esage
Emergency security updates were recently released by Google for a two-bug exploit chain under active exploitation targeting Chrome browser. The bugs were patched on 9th May (sandbox bypass) and 13th May (remote code execution). This quick technical note looks at the bug chain from a cutting edge vulnerability research perspective, placing root cause analysis in the context of both system internals and offensive research trends. Disclaimer: due to theoretical analysis approach based on reverse-engineering security patches, some details about the bugs may be off.
From the general attack & defense perspective, the exploit chain is rather common. It is based on two bugs, first to compromise the renderer (remote code execution in v8), second to escape browser sandbox. There might be a third bug involved to elevate privileges to kernel, which is also a common arrangement "in the wild", that wasn't publicly disclosed. However, both bugs are slightly interesting from vulnerability research perspective, which is the main topic of this technical note. Based on my evaluation, the exploit can be recreated in about a week by someone with good knowledge and experience in Chrome exploitation, so it's definitely worth updating Chrome-based infrastructures as soon as possible.
Browser renderer process was compromised with a type confusion bug in v8, patch is here (CVE-2024-4761).
Missing check in JSReceiver::SetOrCopyDataProperties (CVE-2024-4761)
SetOrCopyDataProperties
is a core engine function which "Reads all enumerable own properties of source [object] and adds them to target [object], using either Set or CreateDataProperty depending on the use_set argument." (js-objects.h). Rather than being exposed directly to JavaScript, this function is used internally in a variety of runtime scenarios where slow-path copying of object properties is involved.
SetOrCopyDataProperties to runtime
The easiest way to trigger the bug from JavaScript code in release configuration seems to be with a call to Object.assign (via Builtin::kSetDataProperties
):
Object.assign to JSReceiver::SetOrCopyDataProperties
Root cause of the issue lies in the branch within the function which performs normalization of object properties (with JSObject::NormalizeProperties
). The code wrongly assumes that, after a few initial checks, the target can only be a JSObject type, which leads to memory corruption during target object map normalization, as it attempts to low-level copy source object contents to incompatible structure type. In practice this assumption can be broken with a target which is a WebAssembly object for instance, as demonstrated in this proof of concept (courtesy of buptdsb). The patch adds a minimal check to ensure that the target is a JSObject.
This bug represents a recently new trend in JavaScript engine vulnerabilities related to WebAssembly. Wasm was leveraged in JavaScript engine exploits for a long time, but previously, mostly for exploitation primitives.
Sandbox escape bug is a Use-after-free in Visuals, patched here (CVE-2024-4671).
Visuals is a privileged subsystem in Chrome which serves as a backend for various operations related to rendering graphics with GPU. This extract from documentation gives an idea how it's used:
The Viz (Visuals) service is a collection of subservices: compositing, gl, hit testing, and media. Viz bugs are tracked with the Internals>Viz component if no more specific component (e.g. Internals>Compositing, Internals>GPU) would serve. Viz has two types of clients: a single privileged client and one or more unprivileged clients. The privileged client is responsible for starting and restarting Viz after a crash and for facilitating connections to Viz from unprivileged clients. The privileged client is trusted by all other clients, and is expected to be long-lived and not prone to crashes. Unprivileged clients request connections to Viz through the privileged client such as the browser process or the window server. Furthermore, unprivileged clients may be malicious or may crash at any time. Unprivileged clients are expected to be mutually distrusting of one another. Thus, an unprivileged client cannot be provided interfaces by which it can impact the operation of another client. For example, a channel to the GL service can only be dispensed by the privileged client, but can be used by unprivileged clients. GL commands are exposed as a stable public API to the command buffer by the client library whereas the underlying IPC messages and their semantics are constantly changing and meaningless without deep knowledge of implementation details. -- viz/README.md
Viz is built on top of Mojo, Chrome's modern IPC/RPC which enables communication between depriveleged renderer process and various privileged backends, such as browser process and GPU process. Generally in Mojo, clients (such as renderer process) are free to implement their own protocols of calling into the backend service, while Mojo provides a universal low-level communication framework. How it seems to work in viz specifically is client-server data is split into 'frames' in blink, that are bundled in 'bundles', to be submitted through the endpoints named 'sinks' on a flush event.
Visuals client submission (Blink side)
On the service side, which is where the bug was, those bundles would be deserialized back into separate frames and further dispatched to associated handlers for processing in the privileged GPU process.
Here is the most relevant snippet of the patch that pretty much explains the bug:
Patched FrameSinkBundleImpl::Submit (CVE-2024-4671)
In this code, submitted frame bundles from blink side are preprocessed by grouping them about the associated frame sinks (viz::FrameSinkBundleImpl::SinkGroup *
). A local set named groups
holds pointers to sink groups in current bundle, while active sink groups are independently stored and managed elsewhere on class scope. If a sink group would be freed in the middle of this code - which seems likely as there is a bunch of far-fetching and potentially asynchronous calls here, then a dangling pointer to it in groups
set would point to attacker-controlled memory, a use-after-free situation. With a virtual call on that pointer coming next in group->DidFinishFrame();
and group->FlushMessages();
the use-after-free can be exploited to execute attacker's code in the context of the privileged GPU process.
The buggy function FrameSinkBundleImpl::Submit
is directly accessible from the renderer process, as suggested by this mojo interface definition - but not from webpage JavaScript in release configuration:
Snippet of frame_sink_bundle.mojom
The logic of the patch may elude those who are not familiar with specialized Chrome exploit mitigations, so let's take another look at it. While the algorithm in the affected code is mostly unchanged (groups
variable changed from set to map?), security is achieved by strengthening the pointers. Especially, SinkGroup
pointers are locally wrapped in raw_ptr<>
type, which is a smart pointer designed to protect the code against Use-after-free bugs; also known as MiraclePtr exploit mitigation. In addition, a WeakPtr<>
smart pointer type applied underneath to null out freed pointers (although Chrome developer documentation claims that WeakPtr
is not smart, I'll just leave it here).
Why I find this bug interesting is that MiraclePtr mitigation was applied to this subsystem since 2022, suggesting that finding another exploitable Use-after-free here wasn't an easy dang.
Full-chain exploits for Google Chrome keep following largely same trends over years, despite numerous initiatives of hardening code. Bypassing relevant exploit mitigations is an essential part of 0-day exploit development workflow. While it can be argued here that MiraclePtr mitigation wasn't technically "bypassed", as it wasn't there in the first place, relevant code hardening announcements definitely suggest an impression that this attack surface is supposed to be covered by it. Researchers are quick with reverse-engineering patches, too. It is important to be aware that, aside from the Chrome browser from Google, Chromium open source project and v8 open source JavaScript engine are both widely used underneath a huge amount of independently developed apps, such as those built on Electron framework. Patching Chromium embeddings in these scenarios remains at the discretion of app developers. So, if you suspect that Chromium code is used in your software, it's worth checking with app developers about their patching procedures, especially if the software is not widespread or well known.
Vendor advisory - Stable Channel Update for Desktop (13th May, CVE-2024-4761, v8) Stable Channel Update for Desktop (9th May, CVE-2024-4671, viz)
Masterclasses: Browser Security Nightly Training course: Zero Day Vulnerability Research