TL;DR: This is the story about ASM.JS JIT-Spray in Mozilla Firefox (x86 32-bit) on Windows tracked as CVE-2017-5375 and CVE-2017-5400. It allows to fully bypass DEP and ASLR.
I always liked the idea of JIT-Spray since the first time I saw it being used for Flash in 2010. Just to name a few, JIT-Spray has been used to exploit bugs in Apple Safari, create info leak gadgets in Flash, attack various other client software, and has even been abusing Microsoft’s WARP Shader JIT Engine
@asintsov wrote in 2010:
No JIT-SPRAY in Flash 10.1. Pages with code are crypted )) But idea will never die, that i show on HITB in AMS)
— Alyosha Sintsov (@asintsov) June 11, 2010
Yes, the idea will never die, and from time to time JIT-Spray reappears…
JIT-Spray
It greatly simplifies exploiting a memory corruption bug such as an use-after-free, because the attacker only needs to hijack the intruction pointer and jump to JIT-Sprayed shellcode. There is no need to disclose code locations or base addresses of DLLs, and there is no need for any code-reuse.
JIT-Spray is usually possible when:
- Machine code can be hidden within constants of a high-level language such as JavaScript: This bypasses DEP.
- The attacker is able to force the JIT compiler to emit the constants into many execuable code regions whose addresses are predictable: This bypasses ASLR.
For example to achieve (1), we can inject NOPS (0x90) in ASM.JS code with:
1 2 |
|
Firefox’ ASM.JS compiler generates the following x86 machine code:
1 2 |
|
When we jump into to offset 01 (the middle of the first instruction) we can execute our hidden code:
1 2 3 4 5 6 7 8 |
|
Thus, in our four-byte constants, we have three bytes to hide our code and one byte (0xA8) to wrap the ADD EAX, … instruction into the NOP-like instruction TEST AL, 05.
To achieve condition (2), i.e., to create many executable regions containing our code we request the ASM.JS module many times:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Technically, ASM.JS is an ahead-of-time (AOT) compiler and not a just-in-time (JIT) compiler. Hence, the function asm_js_function() doesn’t need to be called to get your machine code injected into memory at predictable addresses. It is sufficient to load a web page containing the ASM.JS script.
The Flaw
Each time an ASM.JS module is requested, CodeSegment::create() is called which in turn calls AllocateCodeSegment() in firefox-50.1.0/js/src/asmjs/WasmCode.cpp line #206 (based on the source of Firefox 50.1.0):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
AllocateCodeSegment() further calls AllocateExecutableMemory() in line #67:
1 2 3 4 5 6 7 8 9 10 11 |
|
Finally, AllocateExecutableMemory() invokes VirtualAlloc() which returns a new RW (PAGE_READWRITE) region aligned to a 64KB boundary (0xXXXX0000) (firefox-50.1.0/js/src/jit/ExecutableAllocatorWin.cpp, line #190).
1 2 3 4 5 6 7 8 9 10 11 12 |
|
If we set a breakpoint on VirtualAlloc() in WinDbg, we get the following call stack during runtime (Firefox 50.1.0):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
After returning into method CodeSegment::create(), the ASM.JS compiled/native code is copied to the RW region (firefox-50.1.0/js/src/asmjs/WasmCode.cpp, line #223). And in line #229 the RW region is made executable (PAGE_EXECUTE_READ) with ExecutableAllocator::makeExecutable() invoking VirtualProtect().
1 2 3 4 5 6 7 |
|
Requesting one ASM.JS module many times leads to the creation of many RX regions.
Due to the allocation granularity of VirtualAlloc (64KB) we can then choose a
fixed address (such as 0x1c1c0000) and can be certain that the
emitted machine code is located there (containing our hidden payload).
The astute reader might have noticed that constant blinding is missing and allows to emit ASM.JS constants as x86 code in the first place.
Show me a PoC!
Let’s see how a proof of concept looks in practice: we hide our payload within ASM.JS constants and request the ASM.JS module many times. Hence, we spray many executable code regions to occupy predictable addresses.
The payload consists of two parts:
- Very large NOP-sled (line #35 to #74): to hit it, we can choose a predictable address, such as 0x1c1c0053, and set EIP to it.
- Shellcode (line #75 to #152): it resolves kernel32!WinExec()and executes cmd.exe.
The payload strictly contains at most three-byte long instructions excepts MOVs, which are handled differently. It was automatically generated by a custom transformation tool shellcode2asmjs which uses the Nasm assembler and Distorm3 disassembler. The payload is strongly inspired by Writing JIT-Spray-Shellcode.
As no memory corruption is abused in this PoC, you have to set EIP in your favorite debugger when you are prompted to ;)
Exploiting a former Tor-Browser 0day with ASM.JS JIT-Spray
Let’s take a real memory corruption (CVE-2016-9079) and see how super easy exploitation becomes when using ASM.JS JIT-Spray. This use-after-free has been analyzed thoroughly, so most of the hard work to write a custom exploit was already done. Note: We target Firefox 50.0.1 and not 50.1.0 as above.
Despite JIT-Spraying executable regions, following steps are conducted:
- We use the bug-trigger from the bug report (line #296 to #372).
- We heap-spray a fake object (line #258 to #281).
- During runtime, the chosen values in our fake object drive the execution to a program path with an indirect call. There, EIP is set with the address of one JIT-Sprayed region (0x1c1c0054).
- As soon as the bug is triggered, the JIT-sprayed payload is executed and cmd.exe should pop up.
That’s all. The full exploit targets Mozilla Firefox 50.0.1, and we don’t need any information-leaks and code-reuse. Note that the Tor-Browser has ASM.JS disabled by default, and hence, ASM.JS JIT-Spray won’t work unless the user enables it.
I wonder if Endgames HA-CFI catches this exploit?
Dynamic Payloads
Above exploits contain “hardcoded” payloads within constants. That makes it kind of cumbersome to use different shellcodes. However, we can generate ASM.JS scripts on the fly and invoke them during runtime. A PoC where payloads are exchangeable uses the following:
- JavaScript code creates ASM.JS script-code dynamically. The ASM.JS script is included with the Blob JavaScript API (line #88 to #137).
- A custom VirtualAlloc stage0. It allocates RWX pages and copies the actual stage1 payload (i.e. metasploit shellcode) to it. Afterwards, stage0 jumps to stage1 (line #53 to #69).
This way, you can replace the payload with your favorite shellcode of choice (line #33). The PoC and especially the stage0 payload were also auto-generated with the custom shellcode2asmjs tool.
The Incomplete Fix
Mozilla fixed this issue in Firefox 51 on Jan. 24, 2017. However, the fix can be bypassed which resulted in CVE-2017-5400. This will be explained in part 2.