Memory corruption vulnerabilities have been well known for a long time and programmers have developed various methods to prevent them. One type of memory corruption that is very hard to prevent is the use-after-free and the reason is that it has too many faces! Since it cannot be associated with any specific pattern in source code, it is not trivial to eliminate this vulnerability class. In this blog, a use-after-free vulnerability in Mozilla Firefox will be explained which has been assigned CVE-2022-26381. The Mozilla bug entry 1756793 is still closed to the public as of this writing, but the Zero Day Initiative advisory page ZDI-22-502 can provide a bit more information.
What Is a Use-After-Free Vulnerability?
A use-after-free (UAF) vulnerability happens when a pointer to a freed object is accessed. It does not make sense! Why would a programmer free an object and afterward access it again?
It happens due to the complexity of today’s software. A browser, for example, has many components and each of them may allocate different objects. They may even pass these objects to each other for processing. A component may free an object when it is done using it, while other components still have a pointer to that object. Any dereference of that pointer can lead to a use-after-free vulnerability.
Proof-of-Concept
Let’s start quickly by having a look at the minimized proof-of-concept:
When running this on the latest vulnerable release version of Mozilla Firefox, which is 97.0.1, it gives a very promising crash:
This is what the crash point looks like in IDA. It happens inside a loop:
It dereferences a value from memory and then makes an indirect call (a virtual function call) using the fetched value. Thus, this is rated as a remote code execution vulnerability. The value of the “rax” register which is used during dereferencing is particularly interesting: 0xE5E5E5E5E5E5E5E5. This is a magic value that Firefox uses to “poison” the memory of a freed object so that a dereference of a value fetched from that freed object will cause a crash, as this value is never a valid memory address. This helps greatly to catch use-after-free conditions.
To analyze a use-after-free vulnerability, it is always desired to have more information about the freed object: its type, size, where it is allocated, where it is freed, and where it is subsequently used. On Windows, this is usually done by enabling advanced debugging features using the GFlags tool to enable various global flags. Specifically, it can be used to enable pageheap and create a user-mode stack trace to capture the stack trace at the time a particular object is allocated. Unfortunately, this will not help us on Mozilla Firefox, because Firefox has its own memory management mechanism called jemalloc. The way we can get more information about the object is to run the PoC on an ASAN version of Firefox. You can see the result below:
We got lots of information. Let’s break it down a bit by checking where the object is allocated:
Let’s further check this by looking at the source code (line 1164 of /builds/worker/checkouts/gecko/layout/svg/SVGObserverUtils.cpp). You can download the source code of Firefox 97.0.1 or use the online version (note that line numbers of the online version may not match, as it gets updated constantly):
And this is how it looks in the compiled release version:
So the object size is 0x70 (112) bytes and it is used to store and track properties of frames during reflow triggered by scrolling.
Then we want to know where it is freed and reused. ASAN provides a long stack trace. A closer look gives a good hint. Let’s first check the stack trace when the object is freed:
And now the stack trace when the object is subsequently used:
We can see the “mozilla::SVGRenderingObserverSet::InvalidateAll” function in the stack trace when the crash happens and when the object free is initiated. This also matches the crash point of the release version which is inside the OnNonDOMMutationRenderingChange
function (it says it is inlined in xul!mozilla::SVGRenderingObserverSet::InvalidateAll
). We can now make an initial educated guess: while an object was being processed in a loop in the “mozilla::SVGRenderingObserverSet::InvalidateAll” function, a code path was reached that freed the object being processed, leading to a use-after-free vulnerability.
Now that we have all the details, we can validate this hypothesis step-by-step by running the PoC on the released version of Firefox.
First, we want to know the address of an allocated object so we can monitor it. This can easily be achieved by setting a breakpoint that prints the address of the object upon allocation:
Then, let’s see how the objects are processed in the loop we saw in IDA inside the “mozilla::SVGRenderingObserverSet::InvalidateAll” function. We will print the address of the object that is going to be processed. We also set a breakpoint on the subsequent virtual function call:
We run the PoC, and the debugger stops before calling the virtual function. As you can see, two objects are allocated and these two are going to be processed in the loop. First, one object is processed and a call to the “SVGTextPathObserver::OnRenderingChange” function is made, which eventually frees various allocated objects including the second object which is awaiting processing!
We can see this clearly in the picture below, which is taken immediately after the return from the call. As you can see, the second object has been freed (and poisoned with 0xe5) during the processing of the first object:
In the second iteration, the freed object is loaded for processing, leading to a load of the poison value and resulting in a crash:
Release Versus ASAN Behavior
When running the PoC against the release version, we got a crash during a dereference of 0xE5E5E5E5E5E5E5E5. However, in the ASAN version, it crashed when writing to memory. Why is there a difference? The reason is as follows:
In a release (non-ASAN) build, when freeing an object, its memory remains accessible (not unmapped), and thus any read and write to that memory will still succeed without triggering an immediate crash. That is why the instruction “mov byte ptr [rcx+8], 0” in the above picture executed without error. A crash is likely to occur further along, though. As in our case, if a value is fetched value from a freed object and then dereferenced, the dereference may cause a crash. This is especially true if the freed object content is overwritten by “poison” values as seen above. Note that there is a chance that there will be no crash at all, for example, if there are only reads and writes to the freed object without any dereference operations on fetched values, or if the poison value becomes overwritten with unrelated data. This means that if we fuzz a release version, there is a chance we could miss a vulnerability.
ASAN, on the other hand, monitors all read, write, and dereferences on memory and can catch such vulnerabilities as soon as possible. That is why it is recommended to use an ASAN version for fuzzing.
The Patch
Use-after-free vulnerabilities are often fixed by converting raw pointers to smart pointers or by correcting the management of the object reference count. Here, it was fixed by changing how continuations frame reflows are handled in the engine:
Final Notes
Developers have expended a great deal of effort to eliminate vulnerabilities associated with known patterns in source code, and they have mostly succeeded in decreasing their prevalence. However, there are some classes of vulnerabilities that are harder to prevent, and use-after-free is one of them. Assuring perfect management of object lifecycles in software with a million lines of code is extremely difficult. This is one of the main motivations behind languages like Rust that enforce proper object ownership and lifetime management.
You can find me on Twitter at @hosselot and follow the team for the latest in exploit techniques and security patches.