読者です 読者をやめる 読者になる 読者になる

Windowsで電卓を起動するシェルコードを書いてみる

「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_u32x86ror命令を実行する組み込み(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関数を呼び出すものである。 簡単に処理の内容をまとめると次のようになる。

  1. PEBからInMemoryOrderModuleListの最初にあるモジュールを得る
  2. モジュールのDllBaseからEXPORT_DIRECTORY_TABLEをたどり、AddressOfNamesにある各関数名についてハッシュ値比較を行う
  3. 一致するものが見つかれば、AddressOfFunctionsから対応する関数アドレスを取得し、リターンアドレスと関数引数の間にあるハッシュ値をスタックから抜き取った後ジャンプする
  4. 一致するものが見つからなければ、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バイトである。

関連リンク