CVE-2024-7965 Vulnerability Analysis

Introduction

On August 21, the Chrome browser received an update that fixed 37 security-related bugs. The attention of researchers around the world was drawn to the vulnerability CVE-2024-7965, described as an incorrect implementation in V8. In practice, this means the possibility of RCE (remote code execution) in the browser renderer, which opens up space for subsequent exploitation. The interest of researchers increased even more when on August 26, Google reported the exploitation of the vulnerability “in the wild”.

We have analyzed this vulnerability, so you don't have to.

Initial analysis of the patch

Unlike our previous research, where it was necessary to compare executable files, now there is no need to do anything like that: the entire V8 source code is public. However, some analysis is still necessary to find the necessary commit. After some searching, we see the following:

Here we immediately draw attention to an important detail: the patch is included in the V8 TurboFan component – an optimizing compiler for JS code. TurboFan works on the principle sea ​​of ​​nodes: first it builds a graph, performs optimizations on it, and then selects instructions for a specific architecture and generates machine code.

The function has been fixed ZeroExtendsWord32ToWord64with which the compiler checks whether a value coming from different paths (the so-called phi-node chain, phi nodes), has the upper 32 bits equal to 0. If the compiler cannot prove that the upper 32 bits of the number are equal to 0, then it adds an additional check – compares the number with the maximum value unsigned int (0xffffffff).

The potentially vulnerable function works as follows: it recursively traverses a graph of phi nodes in depth, marking the nodes it has traversed as kUpperBitsGuaranteedZero. Marking of nodes occurs by writing to a custom vector phi_states_. If among the descendants there is at least one node whose upper bits are guaranteed not to be zero, then a recursive return occurs, marking all nodes on the path to the given one as kNoGuaranteeIf the node has already been marked, the compiler does not pass it.

Incorrect graph

If we look closely at the patch, we will notice that when we first enter a node, it resets all previously saved values ​​for previous nodes. This leads us to think that the graph traversal makes incorrect marks. But how does this happen? After all, if we try to traverse a regular graph, we will notice that everything works correctly:

Graph of phi nodes

Graph of phi nodes

In the x86-64 processor architecture, the compiler assumes that a positive constant has its upper 32 bits set to 0, while a negative constant does not. When traversing such a graph, phi_states_ will be in a completely correct state.

The first stage of the graph traversal. In phi_states_ all nodes that have already been traversed are marked in green

The first stage of graph traversal. phi_states_ All nodes that have already been visited are marked in green

The second stage of the graph traversal. When a node that needs to be marked red is encountered, the traversal begins backtracking, marking all parent nodes red recursively

The second stage of the graph traversal. When a node that needs to be marked red is encountered, the traversal begins backtracking, marking all parent nodes red recursively

The final graph after the traversal (everything is correct)

The final graph after the traversal (everything is correct)

In the graph traversal pictures, the vertices that are defined as having their upper 32 bits equal to zero are marked in green, and those whose upper 32 bits are guaranteed not to be equal to zero are marked in red. As we remember, if at least one descendant of a vertex does not have its upper 32 bits equal to zero, then the vertex itself is marked in red.

However, if the graph is cyclic, then an incorrect state can be obtained. phi_states_: for example, provided that the traversal will go along the cycle earlier than along other neighboring nodes, in phi_states_ an incorrect state will be stored.

First stage of graph traversal

First stage of graph traversal

The second stage of the graph traversal. All nodes have been marked, a recursive return occurs

The second stage of the graph traversal. All nodes have been marked, a recursive return occurs

Final graph (incorrect)

Final graph (incorrect)

It is possible to obtain a graph of phi nodes in which a certain node is marked in phi_states_ as green, but the upper 32 bits of its descendant may not be zero. So there is a way for the compiler to “believe” our value, and we fool it. This is what the patch fixes. Each time the function is called again, ZeroExtendsWord32ToWord64 it resets the state phi_states_.

What critical things can happen in such a situation? If we look at how the operation of accessing an array by index is compiled, we will notice that in 64-bit architectures the compiler first tries to make sure that the index we are accessing has the upper 32 bits equal to zero, then puts it into a 64-bit register and performs the operation on the memory. If the compiler “believes” that the upper 32 bits are equal to zero, then it removes the check that the number is less than 0xffffffff. Accordingly, we get a situation in which if the upper bits of a 64-bit register are not equal to zero at the time of access by index, then an out-of-bounds access will occur.

Unfortunately, on the x86-64 architecture, it was not possible to obtain memory corruption PoC due to the fact that when trying to convert a 64-bit number to a 32-bit number, compilation occurs in such a way as to guarantee zeroing the upper 32 bits, so we cannot get a register that will contain undefined values. The comment at the beginning of the function describes this quite well:

However, in the ARM64 architecture, a similar function leaves undefined values ​​in the upper bits, which allows us to get them to be non-zero when accessing the array by index:

Just as a serious chess player, without calculating the variation to the end, can understand that it is necessary to play exactly this way, at this stage we were sure that this was the very correct way of exploitation and began developing PoC.

Exploitation, Impact and Conclusion

So, our plan for exploiting the vulnerability:

  1. We obtain the required cyclic graph using cycles and conditions.

  2. Initiate an invalid state in phi_states_. Our PoC uses for this BigInt.

  3. Add a vertex to the graph TruncateInt64ToWord32 using a combination Math.min with operator >>> 0.

  4. We initiate the subsequent call so that the index is the number into which we placed the upper bits.

These steps allow you to get memory corruption and catch a segmentation fault in V8.

What does exploiting this vulnerability give? Since it allows attacking only devices with ARM64 processor architecture, it mostly affects Android smartphones and Apple laptops released after November 2020. If hackers have an exploit that allows escaping from the browser sandbox, they can gain full control over the browser application: read passwords, steal user sessions.

At the same time, separate exploitation of the vulnerability in question is also dangerous: if there is XSS on any of the site's subdomains, you can obtain passwords and cookies from the main domain and all subdomains. In this case, you cannot obtain confidential data from other sites, because Site Isolation technology, which separates renderer processes, protects against this. However, even in this form, the vulnerability is critical, and if the Chrome version on your devices is still lower than 128.0.6613.84, you need to update your browser as soon as possible.

Fun fact: since we control the upper 32 bits when accessing an array by index, it is likely that a situation will arise where no additional vulnerability will be needed to bypass the V8 sandbox and we can immediately begin exploiting the browser's sandbox.

Link to PoC.

Appendix. Graph obtained during operation

Author of the article:

Yuri Pazdnikov, Junior Vulnerability Researcher

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *