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
As mentioned in part 1, we can hide x86 instructions within ASM.JS constant assignments such as
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:
1 2 3 4 5 6 7 8 9 10 11 12
The corresponding x86 code prepares the five integer parameters (0xa9909090) for the function ffi_func() before calling it:
1 2 3 4 5
Again, if we jump into the middle of the third instruction at offset 0x52, we hit our injected code (NOP-sled):
1 2 3 4 5 6 7 8 9
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:
1 2 3
When we start executing from offset 7, our two NOPs and the subsequent JMP is hit to get the injected code running:
1 2 3 4 5 6 7 8 9 10 11
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
2) The amount of executable ASM.JS/WASM code per process was cut down to 128MB on 32-bit:
1 2 3 4 5 6 7 8
So far, so good. The first thing which caught my attention was the fallback code in the first change:
1 2 3 4
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:
- 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.
- 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().
- 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.