Thursday, August 29, 2019

JSC Exploits

Posted by Samuel Groß, Project Zero

In this post, we will take a look at the WebKit exploits used to gain an initial foothold onto the iOS device and stage the privilege escalation exploits. All exploits here achieve shellcode execution inside the sandboxed renderer process (WebContent) on iOS. Although Chrome on iOS would have also been vulnerable to these initial browser exploits, they were only used by the attacker to target Safari and iPhones.  

After some general discussion, this post first provides a short walkthrough of each of the exploited WebKit bugs and how the attackers construct a memory read/write primitive from them, followed by an overview of the techniques used to gain shellcode execution and how they bypassed existing JIT code injection mitigations, namely the “bulletproof JIT”.  

It is worth noting that none of the exploits bypassed the new, PAC-based JIT hardenings that are enabled on A12 devices. The exploit writeups are sorted by the most recent iOS version the exploit supports as indicated by a version check in the exploit code itself. If that version check was missing from the exploit, the supported version range was guessed based on the date of the fix and the previous exploits.

The renderer exploits follow common practice and first gain memory read/write capabilities, then inject shellcode into the JIT region to gain native code execution. In general it seems that every time a new bug was necessary/available, the new bug was exploited for read/write and then plugged into the existing exploit framework. The exploits for the different bugs also appear to generally use common exploit techniques, e.g. by first creating the addrof and fakeobj primitives, then faking JS objects to achieve read/write.

For many of the exploits it is unclear whether they were originally exploited as 0day or as 1day after a fix had already shipped. It is also unknown how the attackers obtained knowledge of the vulnerabilities in the first place. Generally they could have discovered the vulnerabilities themselves or used public exploits released after a fix had shipped. Furthermore, at least for WebKit, it is often possible to extract details of a vulnerability from the public source code repository before the fix has been shipped to users. CVE-2019-8518 can be used to highlight this problem (as can many other recent vulnerabilities). The vulnerability was publicly fixed in WebKit HEAD on Feb 9 2019 with commit 4a23c92e6883. This commit contains a testcase that triggers the issue and causes an out-of-bounds access into a JSArray - a scenario that is usually easy to exploit. However, the fix only shipped to users with the release of iOS 12.2 on March 25 2019, roughly one and a half months after details about the vulnerability were public. An attacker in possession of a working exploit for an older WebKit vulnerability would likely only need a few days to replace the underlying vulnerability and thus gain the capability to exploit up-to-date devices without the need to find new vulnerabilities themselves. It is likely that this happened for at least some of the following exploits.

For comparison, here is how other browser vendors deal with this “patch-gap” or vulnerability window problem:
  • Google has this same problem with Chromium (e.g. commit 52a9e67a477b fixing CVE-2018-17463 and including a PoC trigger). However, it appears that some recent bugfixes no longer include the JavaScript test cases commits. For example the following two fixes for vulnerabilities reported by our team member Sergey Glazunov: aa00ee22f8f7 (for issue 1784) and 4edcc8605461 (for issue 1793). In the latter case, only a C++ test was added that tested the new behaviour without indication of how the vulnerable code could be reached.
  • Microsoft keeps security fixes in the open source Chakra engine private until the fixes have been shipped to users. The security fixes are then released and marked as such with a CVE identifier. See commit 7f0d390ad77d for an example of this. However, it should be noted that Chakra will soon be replaced by V8 (Chromium’s JavaScript engine) in Edge.
  • Mozilla appears to hold back security fixes from the public repository until somewhat close to the next release. Furthermore, the commits usually do not include the JavaScript testcases used to trigger the vulnerability.

However, it is worth noting that even if no JavaScript testcase is attached to the commit, it is often still possible to reconstruct a trigger (and ultimately an exploit) for the vulnerability from the code changes and/or commit message with moderate effort.

Exploit 1: iOS 10.0 until 10.3.2

This exploit targets CVE-2017-2505 which was originally reported by lokihardt as Project Zero issue 1137 and fixed in WebKit HEAD with commit 4a23c92e6883 on Mar 11th 2017. The fix was then shipped to users with the release of iOS 10.3.2 on May 15th 2017, over two months later.

Of interest, the exploit trigger is almost exactly the same as in the bug report and the regression test file in the WebKit repository. This can be seen in the following two images, the left one showing the testcase published in the WebKit code repository as part of the bugfix and the right showing the part of the in-the-wild exploit code that triggered the bug.
This image shows a line-by-line, side-by-side comparison between the vulnerability test case from the webkit tracker on the left, and the vulnerability trigger code used in the exploit on the right. They are very similar, with matching variable names and code structure. The only difference is that one value which had the fixed value 1.45 in the test case has been parameterised out to a variable named ow in the exploit, and the trigger has been wrapped inside a function definition which takes ow as an argument. The bug causes an out-of-bounds write to the JSC heap with controlled data. The attackers exploit this by corrupting the first QWord of a controlled JSObject, changing its Structure ID (which associates runtime type information with a JSCell) to make it appear as a Uint32Array instead. This way, they essentially create a fake TypedArray which directly allows them to construct a memory read/write primitive.

Exploit 2: iOS 10.3 until 10.3.3

This exploit seems to target CVE-2017-7064 (or a variant thereof), which was originally discovered by lokihardt and reported as issue 1236. The bug was fixed in WebKit HEAD with commit ad6d74945b13 on Apr 18th 2017 and shipped to users with the release of iOS 10.3.3 on Jul 19th 2017, over three months later. 

The bug causes uninitialized memory to be treated as the content of a JS Array. Through standard heap manipulation techniques it is possible to control the uninitialized data, at which point it becomes possible to construct the well-known addrof and fakeobj primitives through a type confusion between doubles and JSValues and thus gain memory read/write by constructing a fake TypedArray.

Exploit 3: likely iOS 11.0 until 11.3

This exploit targets the WebKit bug 181867 which might be CVE-2018-4122. It was fixed in WebKit HEAD on Jan 19, 2018 and presumably shipped to users with the release of iOS 11.3 on Mar 29th 2018.

The bug is a classic (by 2019 standards) JIT side-effect modelling issue. It remains unclear whether the attackers knew about this bug class before it started to be widely known around the beginning of 2018. The exploit again constructs the addrof and fakeobj primitives by confusing unboxed double and JSValue arrays, then gains memory read/write by again faking a typed array object.

Exploit 4: likely iOS 11.3 until 11.4.1

This exploit targets the bug fixed in commit b4e567d371fd on May 16th 2018 and corresponding to WebKit issue 185694. Unfortunately, we were unable to determine the CVE assigned to this issue, but it seems likely that the fix shipped to users with the release of iOS 11.4.1 on Jul 9th 2018.

This is another JIT side-effect modelling bug with similar exploit to the previous one, again constructing the fakeobj primitive to fake JS object. However, by now the Gigacage mitigation had shipped. As such it was no longer useful to construct fake ArrayBuffers/TypedArrays. Instead, the exploit constructs a fake unboxed double Array and with that gains an initial, somewhat limited memory read/write primitive. It then appears to use that initial primitive to disable the Gigacage mitigation and then continues to abuse TypedArrays to perform the rest of the exploit work.

Exploit 5: iOS 11.4.1

This exploit targets CVE-2018-4438, which was first reported by lokihardt as issue 1649. The bug was fixed with commit 8deb8bd96f4a on Oct 26th 2018 and shipped to users with the release of iOS 12.1.1 on Dec 5th 2018.

Due to the bug, it was possible to construct an Array with a Proxy prototype that wasn’t expected by the engine. It is then possible to turn this bug into an incorrect side-effect modelling issue by performing effectful changes during a proxy trap triggered (unexpectedly) in JIT compiled code. The exploit is then very similar to the previous one, first disabling the Gigacage with the limited JS Array read/write, then performing the shellcode injection with a full read/write via TypedArrays.

Exploit 6: likely iOS 12.0 until 12.1.1

This exploit targets CVE-2018-4442, which was originally discovered by lokihardt and reported as issue 1699 and fixed in HEAD with commit 1f1683cea15c on Oct 17th 2018. The fix then shipped to users with the release of iOS 12.1.1 on Dec 5th 2018.

In contrast to the other bugs, this bug yields a use-after-free in the JavaScriptEngine. Similar to the PoC in the WebKit tracker, the attackers abuse the UaF by freeing the property backing storage of an object (the butterfly), then reclaim that storage with a JSBoundFunction’s m_boundArgs array by repeatedly calling func.bind(). If that is successful, the attackers are now able to get access to an internal object, m_boundArgs, by loading a property from the freed object’s butterfly. With that, it becomes possible to construct an OOB access by making the m_boundArgs array sparse, then calling the bound function. This will invoke JSBoundFunction::boundArgsCopy which assumes that m_boundArgs is dense and otherwise reads JSValues past the end of a buffer which it passes as argument to a controlled function (that was bound() previously).

This fact has been exploited in the past, which is why there is now a comment next to the definition of m_boundArgs: `// DO NOT allow this array to be mutated!`. From there, the attackers again construct the addrof and fakeobj primitives and reuse the rest of the exploit from before.

Exploit 7: iOS 12.1.1 until 12.1.3

The final exploit targets the same bug as exploited by Linus Henze here:, which is again a JIT side-effect modelling issue. The WebKit bugtracker id for it appears to be 191731. It is unclear whether a CVE number was assigned to it, but it could be CVE-2019-6217 which was disclosed during mobile Pwn2Own that year by Team flouroacetate. The bug seems to have been fixed on Nov 16th 2018 and shipped to users with the release of iOS 12.1.3 on Jan 22nd 2019.

Instead of using WASM objects to gain memory read/write as Linus does, the attackers appear to instead have plugged the new bug into their old exploit and again create a fake JS Array to gain initial memory read/write capabilities, then continue the same way they did before.

Shellcode Execution

After gaining memory read/write capabilities, the renderer exploit pivots to shellcode execution, which then performs the privilege escalation exploits. The way they achieve shellcode execution is the same in all exploits: by bypassing the JIT mitigations to overwrite an existing function’s JIT code and then invoking that function.

For some time now (first announced by Apple at BlackHat 2016 and then shipped with iOS 10), iOS features a JIT hardening measure that aims to make it more difficult for an attacker to write code directly into the RWX JIT region. It basically achieves that by creating a second, “hidden” mapping of the JIT region that is writable and keeping the first mapping of the region non-writable. However, one weakness of this approach, and acknowledged in the presentation by Apple, is that there has to be a “jit_memcpy” function that is called to copy the generated code into the JIT region. As such, it remains viable to perform a ROP or JOP style attack to execute this function with controlled shellcode as argument. This is what the attackers do as well. This problem now appears to be somewhat mitigated on PAC enabled devices by signing the JIT code during code generation and verifying the signature later on. The exploits we found did not include a bypass for PAC enabled devices and instead bailed out if they ran on an A12 device.

In more detail, the attackers construct a JOP chain, consisting of three different gadgets that allow them to perform a function call of an arbitrary function with controlled arguments. To kick off the chain, they replace the native function pointer of the `escape` JS function with the first gadget of the chain. The chain then performs a call to the ”jit_memcpy” function to overwrite the JIT code of a previously compiled function with the shellcode. Finally they replace the function pointer of `escape` one last time and point it to the shellcode inside the JIT region.

No comments:

Post a Comment