Introduction
On August 21, Google released an update for Chrome, fixing a total of 37 security flaws. Researchers across the globe paid their attention to the CVE-2024–7965 vulnerability described as an inappropriate implementation in the browser’s V8 engine. The vulnerability can lead to remote code execution (RCE) in the Chrome renderer and thus become a starting point for further exploitation. The researchers’ curiosity got piqued when on August 26 Google mentioned that exploits for CVE-2024–7965 exist “in the wild.”
We have examined the vulnerability.
Initial analysis of the patch
Unlike in our previous research, this time we did not have to compare executable files as all the V8 source code is publicly available. Still, at least some analysis was necessary to find the required commit. Our search revealed the following:
Here we immediately pay attention to an important detail: the patch is included into TurboFan, an optimizing JS code compiler for V8. TurboFan uses a sea-of-nodes representation: the compiler first builds a graph, performs optimizations on it, and then selects instructions for a particular architecture and generates machine code.
The ZeroExtendsWord32ToWord64
function is fixed. The compiler uses the function to check whether a value arriving via different paths (the so-called chain of phi nodes) always has the upper 32 bits equal to 0. If the compiler cannot prove that the upper 32 bits are equal to 0, it adds an additional check by comparing the value with the maximum value of unsigned int (0xffffffff)
.
The potentially vulnerable function works as follows: it recursively traverses the depth of the phi node graph and marks the nodes passed as kUpperBitsGuaranteedZero
. Nodes are marked via writing to the custom vector phi_states_
. If, among the descendants, there is at least one node whose upper bits are not guaranteed zero, a recursion is performed with all the nodes on the path to this one marked as kNoGuarantee
. If the node has already been marked, the compiler skips it.
Incorrect graph
If we take a closer look at the patch, we will see that the first time it enters a node, it resets all earlier stored values for previous nodes to zero. This suggests that the graph traversal is doing incorrect markings. But how does it happen? After all, if we try traversing a regular graph, we will see that everything works correctly:
In the x86–64 processor architecture, the compiler interprets the positive constant as having the upper 32 bits equal to 0, and the negative constant as having the upper 32 bits not equal to 0. When traversing such a graph, phi_states_
will be in a fully valid state.
In the graph traversal pictures, the green color indicates those nodes which are defined as having the upper 32 bits equal to zero; the red color indicates the nodes whose upper 32 bits are not guaranteed zero. As we remember, if at least one descendant of a node does not have the upper 32 bits equal to zero, the node itself is marked red.
However, if the graph is cyclic, it is possible to get invalid phi_states_
. For example, if the traversal goes through the cycle before going through other neighboring nodes, phi_states_
will store an invalid state.
We can get a graph of phi_nodes where a particular node is marked green in phi_states_
, but the upper 32 bits of its descendant may be other than zero. Thus, there is a path in which the compiler will “trust” our value and we will deceive the compiler. This is exactly what the patch fixes. Every subsequent call of the ZeroExtendsWord32ToWord64
function resets phi_states_
to zero.
What can be critical in such a situation? If we look at how array indexing is compiled, we will notice that in 64-bit architectures the compiler first tries to make sure that the index we are addressing has the upper 32 bits equal to zero, then places it into the 64-bit register and performs an operation on memory. If the compiler “believes” that the upper 32 bits are equal to zero, it removes the check that the value is less than 0xffffffff
(4 bytes). Accordingly, what we end up with: if the upper bits in the 64-bit register turn out to be other than zero at the moment of array indexing, an out-of-bounds access will occur.
Unfortunately, we were unable to get memory corruption PoC on the x86–64 architecture because, when trying to convert a 64-bit value to a 32-bit one, the compilation is done in such a way that the upper 32 bits are guaranteed to be zeroed, so we cannot get a register with undefined values. The comment at the beginning of the function describes this situation quite well:
However, in the ARM64 architecture, a similar function leaves undefined values in the upper bits, which allows us to get them not equal to zero when indexing an array:
Just like a serious chess player who can understand exactly how to continue the game without going through all the details of a variation, we were sure that this was the right way to exploit the vulnerability and started developing our PoC.
Exploitation, impact, and conclusion
Here is our plan to exploit the vulnerability:
- Get the required cyclic graph by using loops and conditions.
- Initiate incorrect values in
phi_states_
. To do so, our PoC usesBigInt
. - Add the
TruncateInt64ToWord32
node to the graph by combiningMath.min
and>>> 0
. - Initiate the next call in such a way that the index is the number we placed the upper bits in.
These steps enable us to get memory corruption and catch segmentation fault in V8.
What can be achieved by exploiting this vulnerability? Since it only allows attacks on ARM64 devices, the vulnerability affects mostly Android smartphones and Apple laptops released after November 2020. If hackers have an exploit to escape from the browser sandbox, they can gain full control over the browser application: read passwords and hijack user sessions.
At the same time, a single exploitation of this vulnerability is also dangerous. If XSS is present in any of the website subdomains, it is possible to retrieve passwords and cookies from the main domain and all subdomains. In this case, it is impossible to get sensitive data from other websites because Google’s Site Isolation (a security feature that separates renderer processes) protects against this. However, even like this, the vulnerability is critical, and if your Chrome version is still pre-128.0.6613.84, you should update your browser as soon as possible.
We conduct this and similar research to raise technical specialists’ awareness of software vulnerabilities and improve the quality of our pentest and red team services.
See our PoC on GitHub.