PAC and JIT Hardening Bypass in WebKit on iOS
As per discussions with product-security@apple.com, Apple would like to treat the PAC bypass described here as a security vulnerability by itself. The bypass was initially reported without a deadline on May 6. After receiving the reply that they will treat it as a separate vulnerability, this issue has been created to start the 90-day deadline (from today).
On macOS, gaining shellcode execution from arbitrary memory read/write in a WebKit renderer only requires finding and writing to the JIT region. However, on iOS a combination of APRR [1] and PAC [2] protect the JIT region from an attacker with arbitrary read/write.
WebKit has support for in-process signal handling. This is for example used by some JIT optimizations in JSC [3]. The main signal handler is `catch_mach_exception_raise_state` in Signals.cpp [4], which will traverse a linked list of handlers and call each one of them. If any of the handlers returns success, the signal is treated as handled and the thread will continue.
This enables the following attack:
1. The linked list of handlers is turned into a cycle, causing `catch_mach_exception_raise_state` to loop infinitely upon catching a signal
2. A crash is triggered in another thread, for example in a WebWorker. A GCD thread is now \"stuck\" in `catch_mach_exception_raise_state`
3. The main thread searches the stacks for the stackframe of `catch_mach_exception_raise_state`. Once found, it has access to the reply mach message of `catch_mach_exception_raise_state` and with that to the context (registers + stack) of the crashed thread. It can modify them arbitrarily except for PC which is protected by PAC. After modifying the state and marking the exception as property handled in the reply message, it fixes the linked list of handlers, causing `catch_mach_exception_raise_state` in the other thread to finish
4. The crashed thread now resumes execution with attacker-controlled registers and/or stack content
It should also be possible to catch multiple signals following each other by first making a copy of the handlers list/cycle, then swapping the \"active\" and \"inactive\" exception hander lists before repairing the now inactive handler list. The current exception handler will then return, but if a new exception is immediately raised, the handling thread will again be stuck in `catch_mach_exception_raise_state` as it uses the other list which is still a cycle. It is also worth noting that it should be possible to modify the global `activeExceptions` variable in Signals.cpp prior to the installation of signal handlers, thus allowing the attacker to control which exceptions are handled.
This \"debugger\" now immediately allows brute-forcing PACs as PAC mostly relies on conventional access violations when failing. Moreover, it allows PAC to be bypassed trivially for some pointers, namely in cases where the authentication and use are two separate instructions, with the second instruction triggering a crash. The PoC demonstrates this by bypassing the PAC protecting a TypedArray's backing storage pointer: first, a TypedArray's backing storage pointer in a worker is corrupted, then accessed. This will cause the AUTDB instruction to fail, leaving the pointer clobbered and causing a crash when the pointer is subsequently accessed. Next, this crash is \"handled\" with the debugger and the register containing the
clobbered pointer is replaced with an arbitrary pointer. The worker then continues and re-executes the access instruction which now succeeds and thus accesses an address of the attacker's choosing.
With this, it should now be possible to achieve arbitrary native code execution (i.e. bypassing the JIT hardening). Possible ideas for that are:
- Corrupt the AssemblerBuffer so arbitrary instructions are copied into the JIT region by the LinkBuffer. This will cause the computed hashes to mismatch and the linker to crash, but that only happens after the instructions have been copied and the crash can then simply be caught
- Crash during one of the writes into the JIT region in LinkBuffer::copyCompactAndLinkCode (by corrupting the destination pointer prior to that) and change the content of the source register so that an arbitrary instruction is written into the JIT region while the original instruction is used for the hash computation
- Crash during LinkBuffer::copyCompactAndLinkCode and resume execution somewhere else. This should leave the JIT region writable (although not executable) for that thread
- Brute-force a PAC code (e.g. by repeatedly accessing, crashing, and then changing a PAC protected pointer), then JOP into one of the functions into which performJITMemcpy is inlined
[1] https://siguza.github.io/APRR/
[2] https://github.com/apple/llvm-project/blob/apple/master/clang/docs/PointerAuthentication.rst
[3] https://github.com/WebKit/webkit/blob/015fb86d51851fc3e13f05898c85d62d0b1bae8f/Source/JavaScriptCore/runtime/OptionsList.h#L466
[4] https://github.com/WebKit/webkit/blob/4ceb36e525b55b9d49aed0b400507d522953e025/Source/WTF/wtf/threads/Signals.cpp#L137
This bug is subject to a 90 day disclosure deadline. After 90 days elapse,
the bug report will become visible to the public. The scheduled disclosure
date is 2020-08-13. Disclosure at an earlier date is possible if
agreed upon by all parties.
Related CVE Numbers: CVE-2020-9910
Found by: saelo@google.com