The Left Branch Less Travelled: A Story of a Mozilla Firefox Use-After-Free Vulnerability
July 01, 2019 | Hossein LotfiIn December 2018, Mozilla released Firefox version 64 via mfsa2018-29, which was originally discovered and reported by Nils. This release fixed several security issues, among them CVE-2018-18492, which is a use-after-free (UAF) vulnerability related to the select
element. We’ve discussed UAF bugs before, and we’ve seen vendors implement sweeping protections [PDF] in an attempt to eliminate them. Even today, it’s not unusual to discover UAF-related vulnerabilities in web browsers, so understanding them is essential in finding and fixing these bugs. In this blog, I provide you with more details regarding this particular UAF vulnerability and the patch released to address it.
Triggering the vulnerability
The following proof of concept can be used to trigger this issue:
Running this proof of concept on an affected version of Firefox gives you the following crash and stack trace:
As you can see, a read access violation is produced when dereferencing a memory address filled with 0xe5e5e5e5. This is a value used by jemalloc to poison freed memory. By “poisoning”, we mean filling freed memory with a recognizable pattern for diagnostic purposes. Preferably, the fill pattern does not correspond to accessible addresses, so that any attempt to dereference values loaded from the filled memory – for example, in the event of a use-after-free – will result in an immediate and distinctive crash.
Root cause analysis
The PoC consists of 6 lines. Let’s break it down line by line:
1) A div
element is created.
2) An option
element is created.
3) The option
element is appended to the div
element. The div
is now the parent of the option
element.
4) A DOMNodeRemoved
event listener is added to the div
element. This means that if the option
node is removed, the function we put here will be called.
5) A select
element is created.
We go a bit deeper here:
When a select
element is created in JavaScript, the function xul.dll!NS_NewHTMLSelectElement
receives control. It allocates an object of 0x118 bytes for this select element:
As you can see, at the end, a jump to mozilla::dom::HTMLSelectElement::HTMLSelectElement
function is done.
Within this function, various fields of the newly allocated object are initialized. Note that another object of 0x38 bytes is also allocated, and it is initialized as an HTMLOptionsCollection
object. As a result, every select
element will have an options collection by default. Let`s move to last line.
6) The option
element created at Step 2 is moved to the options collection of the select
element. Doing this in JavaScript will cause the mozilla::dom::HTMLOptionsCollection::IndexedSetter
function to be called (you can see this function is called in the stack trace shown in Figure 2).
Here some checks are done by the browser. For example, if the option index is larger than the current length of the options collection, the options collection is enlarged via a call to the mozilla::dom::HTMLSelectElement::SetLength
function. In our PoC, it is zero due to [0] in line 6 (see Figure 1). Then a check is done at the blue block in Figure 5. If the index to be set is not equal to the options count of the options collection, the right branch is taken. In our PoC, the desired index value is 0, and the options count is also zero, so the left branch is taken. Thus, execution reaches the nsINode::ReplaceOrInsertBefore
function, as you can see below in the red block:
Within the nsINode::ReplaceOrInsertBefore
function, a call to the nsContentUtils::MaybeFireNodeRemoved
function is done to notify parent about child removal if it is listening for such an event.
As we set a DOMNodeRemoved
event listener on the div
element at line 4 of the PoC (see Figure 1), the function we put there is fired. In this function, first the sel
variable is set to 0. This removes the last reference to the select
element. Next, the function creates a huge array buffer. This produces memory pressure, causing the garbage collector to kick in. At this point the select
element object is freed as there are no longer any references to it. This freed memory will be poisoned by 0xe5e5e5e5. Finally, the function calls alert
to flush pending asynchronous tasks. Upon return from the nsContentUtils::MaybeFireNodeRemoved
function, the freed select
object is used to read a pointer that triggers a read access violation:
One interesting note here is that if right branch was taken, the exact same function (nsINode::ReplaceOrInsertBefore
) will be called, but just before this call, the AddRef
function will be used to increase the reference count of the select
object. Consequently, no use-after-free will occur:
The Patch
Mozilla patched this vulnerability via changeset d4f3e119ae841008c1be59e72ee0a058e3803cf3. The main change is that the weak reference to the select
element within the options collection is replaced by a strong reference:
Conclusion
Despite being a well-known issue, UAF bugs continue to be a problem for multiple browsers. Just a couple of months ago, active attacks targeting Google Chrome used a UAF vulnerability. UAFs exist beyond the browser, too. The Linux kernel released a patch to address a denial-of-service condition that was caused by a UAF. Understanding how UAFs occur is a key to detecting them. Similar to buffer overflows, we’re unlikely to ever see the end of UAFs in software. However, proper coding and secure development practices can help eliminate, or at least lessen, the impact from UAFs in the future.
You can find me on Twitter at @hosselot and follow the team for the latest in exploit techniques and security patches.