#navi_header|技術| PEファイルの再配置情報を取得、表示してみる。 さらにその発展として、PythonによりDLLをメモリ上にロード、再配置情報とIATを手動で書き換え実行可能にし、実際にエクスポート関数をPythonから呼んでみる。 Pythonは 2.6 を使用。 OSはWindows XP SP3, VCはVC++ 2008 Express Edition, コンパイラとリンカのバージョンは次の通り: > cl Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. > link Microsoft (R) Incremental Linker Version 9.00.30729.01 Copyright (C) Microsoft Corporation. All rights reserved. #more|| #outline|| ---- * サンプルコード 今回は、特に再配置情報を操作するPythonスクリプトが巨大化した為、C言語のソースファイルと合わせてzipファイルにまとめた。 以下のURLからダウンロード出来る。 [[yb:///medias/manual_dll_relocation.zip]] - dlltest.c : 再配置情報で遊ぶ為のDLL - main.c : dlltest.dll動作確認用の簡単なEXE - main1.py : Pythonのctypesモジュールで簡単にdlltest.dllを利用出来ますよ~というサンプル - main2.py : ctypesモジュール経由でLoadLibrary() + GetProcAddress()で明示的にdlltest.dllをロードするサンプル - show_relocations.py : 再配置情報を表示するPythonスクリプト - test_pseudo_load.py : DLLを手動でメモリ上に展開し、再配置情報を更新し、インポート情報を基にIATを更新し、実行可能なメモリイメージとして構築するPythonスクリプト show_relocations.py, test_pseudo_load.py の解説は後に回し、まずはdlltest.c, main.c, main1/2.py を紹介する。 ** DLL側:dlltest.c 再配置情報を弄って遊ぶためのDLLで、"/NOENTRY"を指定してCRTのエントリ関数を削り、余計なインポートや再配置情報を省いてみた。 dlltest.c: #code|c|> #include int __declspec(dllexport) funcA(int a, int b) { MessageBox(GetDesktopWindow(), "funcA", "captionA", MB_OK); a = funcC(b); return a + b + 1; } int __declspec(dllexport) funcB(int a, int b) { MessageBox(GetDesktopWindow(), "funcB", "captionB", MB_OK); b = funcC(a); return a * b + 1; } int funcC(int c) { return c * 2; } ||< GetDesktopWindow(), MessageBox(), funcC()のアドレスが再配置の対象になると予想される。 ビルド: > cl /LD dlltest.c user32.lib /link /noentry *** dumpbinでインポート情報を確認 #pre||> > dumpbin /imports dlltest.dll ... USER32.dll 10002000 Import Address Table 10002034 Import Name Table 0 time date stamp 0 Index of first forwarder reference 11C GetDesktopWindow 1F8 MessageBoxA ||< *** dumpbinで再配置情報を確認 #pre||> > dumpbin /relocations dlltest.dll ... BASE RELOCATIONS #4 1000 RVA, 18 SizeOfBlock 6 HIGHLOW 10003000 B HIGHLOW 1000300C 11 HIGHLOW 10002000 18 HIGHLOW 10002004 46 HIGHLOW 10003014 4B HIGHLOW 10003020 51 HIGHLOW 10002000 58 HIGHLOW 10002004 # offset type ファイル上の値 ||< *** dumpbinで逆アセンブラ結果と付き合わせてみる 再配置情報と付き合わせてみると、見事に各offsetとその値が逆アセンブラ結果と対応していることが分かる。 また、GetDesktopWindow()/MessageBoxA()がそれぞれ call dword ptr ds:[10002000h] # GetDesktopWindow() call dword ptr ds:[10002004h] # MessageBoxA() という命令になっているが、"10002000h"は丁度IATのアドレスを指すようになっている。PEヘッダー上のIATアドレスはRVAなのでローダが処理してくれるが、実行コード内のIATアドレスについては、再配置情報としてローダに処理して貰う。 #pre||> > dumpbin /disasm dlltest.dll ... ## funcA() 10001000: 55 push ebp 10001001: 8B EC mov ebp,esp 10001003: 6A 00 push 0 ## -> "captionA" 10001005: 68 00 30 00 10 push 10003000h ## -> "funcA" 1000100A: 68 0C 30 00 10 push 1000300Ch ## -> GetDesktopWindow() 1000100F: FF 15 00 20 00 10 call dword ptr ds:[10002000h] 10001015: 50 push eax ## -> MessageBoxA() 10001016: FF 15 04 20 00 10 call dword ptr ds:[10002004h] 1000101C: 8B 45 0C mov eax,dword ptr [ebp+0Ch] 1000101F: 50 push eax ## -> funcC() 10001020: E8 5B 00 00 00 call 10001080 10001025: 83 C4 04 add esp,4 10001028: 89 45 08 mov dword ptr [ebp+8],eax 1000102B: 8B 4D 0C mov ecx,dword ptr [ebp+0Ch] 1000102E: 8B 55 08 mov edx,dword ptr [ebp+8] 10001031: 8D 44 0A 01 lea eax,[edx+ecx+1] 10001035: 5D pop ebp 10001036: C3 ret ... ## funcB() 10001040: 55 push ebp 10001041: 8B EC mov ebp,esp 10001043: 6A 00 push 0 ## -> "captionB" 10001045: 68 14 30 00 10 push 10003014h ## -> "funcB" 1000104A: 68 20 30 00 10 push 10003020h ## -> GetDesktopWindow() 1000104F: FF 15 00 20 00 10 call dword ptr ds:[10002000h] 10001055: 50 push eax ## -> MessageBoxA() 10001056: FF 15 04 20 00 10 call dword ptr ds:[10002004h] 1000105C: 8B 45 08 mov eax,dword ptr [ebp+8] 1000105F: 50 push eax ## -> funcC() 10001060: E8 1B 00 00 00 call 10001080 10001065: 83 C4 04 add esp,4 10001068: 89 45 0C mov dword ptr [ebp+0Ch],eax 1000106B: 8B 45 08 mov eax,dword ptr [ebp+8] 1000106E: 0F AF 45 0C imul eax,dword ptr [ebp+0Ch] 10001072: 83 C0 01 add eax,1 10001075: 5D pop ebp 10001076: C3 ret ... ## funcC() 10001080: 55 push ebp 10001081: 8B EC mov ebp,esp 10001083: 8B 45 08 mov eax,dword ptr [ebp+8] 10001086: D1 E0 shl eax,1 10001088: 5D pop ebp 10001089: C3 ret ||< ** DLLをロードするサンプル:C言語版 main.c main.c: #code|c|> #include int __declspec(dllimport) funcA(int a, int b); int __declspec(dllimport) funcB(int a, int b); int main(int argc, char *argv[]) { printf("funcA(2, 3) = %d\n", funcA(2, 3)); printf("funcB(2, 3) = %d\n", funcB(2, 3)); return 0; } ||< ビルド・実行: > cl main.c dlltest.lib > main.exe (メッセージボックス表示) funcA(2, 3) = 10 (メッセージボックス表示) funcB(2, 3) = 9 ** DLLをロードするサンプル:Python版 main1.py(ctypesで自動ロード) main1.py: #code|python|> import ctypes dlltest = ctypes.cdll.dlltest print "funcA(2, 3) = %d" % dlltest.funcA(2, 3) print "funcB(2, 3) = %d" % dlltest.funcB(2, 3) ||< 実行: > python main1.py (メッセージボックス表示) funcA(2, 3) = 10 (メッセージボックス表示) funcB(2, 3) = 9 ** DLLをロードするサンプル:Python版 main2.py(LoadLibrary(), GetProcAddress()で手動ロード) main2.py: #code|python|> import ctypes # LoadLibrary(), GetProcAddress()についてはctypes標準機能で自動ロード hModule = ctypes.windll.kernel32.LoadLibraryA('dlltest.dll') print "hModule = 0x%08X" % hModule addr_funcA = ctypes.windll.kernel32.GetProcAddress(hModule, 'funcA') print "Address of 'funcA' = 0x%08X" % addr_funcA addr_funcB = ctypes.windll.kernel32.GetProcAddress(hModule, 'funcB') print "Address of 'funcB' = 0x%08X" % addr_funcB # アドレス値から関数を取得する為の下準備として、prototypeを作成する。 # 今回はcdecl呼び出し規約の関数をインポートするので"CFUNCTYPE"を使う。 # stdcallの場合は"WINFUNCTYPE"を使うことになる。 prototypes = { 'funcA' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int), 'funcB' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int), } # prototypeとアドレスから、ctypesの関数インスタンスを作成する。 funcA = prototypes['funcA'](addr_funcA) funcB = prototypes['funcB'](addr_funcB) # 実際に呼んでみる print "funcA(2, 3) = %d" % funcA(2, 3) print "funcB(2, 3) = %d" % funcB(2, 3) ||< 実行: > pythonn main2.py hModule = 0x10000000 Address of 'funcA' = 0x10001000 Address of 'funcB' = 0x10001040 (メッセージボックス表示) funcA(2, 3) = 10 (メッセージボックス表示) funcB(2, 3) = 9 * show_relocations.py : 再配置情報を表示するPythonスクリプト IMAGE_OPTIONAL_HEADERのDataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]はIMAGE_BASE_RELOCATION構造体の配列の先頭アドレスを指しているので、そこから辿ることが出来る。 一つのIMAGE_BASE_RELOCATION構造体の後ろには、実際に書き換えるアドレスの種別とOFFSETを記録したWORD配列が並んでいる。 WORD値の上位4bitが再配置のタイプ、下位12bitがOFFSETとなる。OFFSETはIMAGE_BASE_RELOCATION構造体のVirtualAddressからのOFFSETとなる。 ** show_relocations.py : ソース全体 show_relocations.py: (ソース巨大化のため省略) ** show_relocations.py : 解説 途中までは [[677]] と同じで、構造体の定義とメモリ上にヘッダーとセグメントをロードする。 再配置情報の取得は以下のコードから始まる: #code|python|> # Base Relocations br_entry = opt_header.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC] br_rva = br_entry.VirtualAddress br_head = image_buf_ptr + br_rva br_tail = br_head + br_entry.Size if 0 == br_rva: print "NO BASE RELOCATIONS" quit() ||< br_tailでわざわざ再配置情報のメモリブロック終端アドレスを保存しているのは、ループ脱出条件に使う為。IMAGE_BASE_RELOCATION構造体には要素数を保持するメンバが無い為、br_tailと比べることでループ脱出を判定している。 EXEなど再配置情報が存在しない場合は、メッセージを表示して終了。 続くコードで、IMAGE_BASE_RELOCATION構造体の配列を順次取得していく: #code|python|> br_sz = ctypes.sizeof(IMAGE_BASE_RELOCATION) br_type_sz = ctypes.sizeof(WORD) offset = br_head brs = [] while 1: br = IMAGE_BASE_RELOCATION() ctypes.memmove( ctypes.addressof(br), offset, br_sz) br.types = [] offset = offset + br_sz ||< 次のコードは、IMAGE_BASE_RELOCATION構造体のSizeOfBlockメンバを元に、WORD配列の要素数を逆算している: br_types_num = (br.SizeOfBlock - br_sz) / br_type_sz SizeOfBlockはWORD配列のサイズ + IMAGE_BASE_RELOCATION構造体自身のサイズになっているため、自分自身を引いた後、WORD型のサイズで割れば配列のよう素数が得られる。 以降、要素数を元にWORD配列を取得する: #code|python|> for i in xrange(br_types_num): br_type = WORD() ctypes.memmove( ctypes.addressof(br_type), offset, br_type_sz) offset = offset + br_type_sz br.types.append(br_type) brs.append(br) if offset + 1 > br_tail: break ||< 以下は取得出来たIMAGE_BASE_RELOCATION構造体の内容を出力する: #code|python|> for br in brs: print "-----------------" print "VirtualAddress = 0x%08X" % br.VirtualAddress print "SizeOfBlock = 0x%08X" % br.SizeOfBlock print "\t[TYPE],\t[OFFSET],\t[FILE VALUE]" for br_type in br.types: # 上位4bit rel_base_type = (0xF000 & br_type.value) >> 12 # 下位12bit rel_base_offset = 0xFFF & br_type.value # 実際にファイルに書き込まれている値を取得 file_value = DWORD() ctypes.memmove( ctypes.addressof(file_value), image_buf_ptr + br.VirtualAddress + rel_base_offset, ctypes.sizeof(DWORD)) print "\t%s,\t%X h,\t%X" % ( IMAGE_REL_BAESD_TYPES[rel_base_type], rel_base_offset, file_value.value ) ||< ** show_relocations.py : 実行例 #pre||> > show_relocations.py dlltest.dll ----------------- VirtualAddress = 0x00001000 SizeOfBlock = 0x00000018 [TYPE], [OFFSET], [FILE VALUE] HIGHLOW, 6 h, 10003000 HIGHLOW, B h, 1000300C HIGHLOW, 11 h, 10002000 HIGHLOW, 18 h, 10002004 HIGHLOW, 46 h, 10003014 HIGHLOW, 4B h, 10003020 HIGHLOW, 51 h, 10002000 HIGHLOW, 58 h, 10002004 ||< dumpbin: #pre||> > dumpbin /relocations dlltest.dll ... BASE RELOCATIONS #4 1000 RVA, 18 SizeOfBlock 6 HIGHLOW 10003000 B HIGHLOW 1000300C 11 HIGHLOW 10002000 18 HIGHLOW 10002004 46 HIGHLOW 10003014 4B HIGHLOW 10003020 51 HIGHLOW 10002000 58 HIGHLOW 10002004 ||< dumpbinと同様に再配置情報を取得出来ている。 * test_pseudo_load.py : メモリ上に展開した後、手動で再配置情報とIATを書き換えて実行可能にしてみる つまり、OSのローダがしている処理を自前で実装してみる実験。 簡単にするため、以下のように機能をそぎ落としている。 - DLLしかロードしない - 序数でエクスポートしているシンボルは無視 - 序数でインポートしているシンボルも無視 - インポートで転送しているのが見つかっても無視 - 事前バインドも無視 - 遅延ロードは考慮しない とりあえず今回のdlltest.dllをロードし、funcA(), funcB()を実行するにはこれでも問題ない。 ** test_pseudo_load.py : ソース全体 test_pseudo_load.py: (ソース巨大化のため省略) ** test_pseudo_load.py : 解説 クラス定義に加え、インポート情報/エクスポート情報/再配置情報それぞれの構築とダンプ表示、再配置情報とIATの更新を関数にまとめた。 本体の、実験の要となる処理について見ていく。 まず、ファイルイメージをロードするメモリ領域を VirtualAlloc() を使って読み書き・実行可能なページに確保するようにしている。 これまでの実験ではctypesのcreate_string_buffer()で確保していたが、それだと実行可能なページには確保されない。 #code|python|> # {{{ load to memory f.seek(0, 0) file_bytes = f.read() # read all file contents MEM_COMMIT = 0x1000 PAGE_EXECUTE_READWRITE = 0x40 # WinXP SP2 や Win2k 等では無効かも。MSDNを確認して下さい。 image_buf_ptr = ctypes.windll.kernel32.VirtualAlloc( None, opt_header.SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE ) # copy headers ctypes.memmove( image_buf_ptr, file_bytes, opt_header.SizeOfHeaders ) ||< 続いて再配置情報を取得・表示する。 #code|python|> base_relocations = build_base_relocations(opt_header, image_buf_ptr) if 0 == len(base_relocations): print "NO BASE RELOCATIONS" quit() print "-------[ RELOCATION INFO : BEFORE RELOCATION ]-------" dump_base_relocations(base_relocations, image_buf_ptr) print "-----------------------------------------------------" print ||< その後、ヘッダー上のImageBaseと実際にロードされた先頭アドレスの差分を取得し、再配置情報を更新する。 #code|python|> print "File ImageBase = 0x%08X" % opt_header.ImageBase print "Real ImageBase = 0x%08X" % image_buf_ptr diff = image_buf_ptr - opt_header.ImageBase print "Differencial = 0x%08X" % diff print relocate_base_relocations(base_relocations, image_buf_ptr, diff) raw_input('Pausing ... Hit Return for Continue :') ||< ここまでの実行結果: #pre||> > test_pseudo_load.py dlltest.dll -------[ RELOCATION INFO : BEFORE RELOCATION ]------- ----------------- VirtualAddress = 0x00001000 SizeOfBlock = 0x00000018 [TYPE], [OFFSET], [FILE VALUE] HIGHLOW, 6 h, 10003000 HIGHLOW, B h, 1000300C HIGHLOW, 11 h, 10002000 HIGHLOW, 18 h, 10002004 HIGHLOW, 46 h, 10003014 HIGHLOW, 4B h, 10003020 HIGHLOW, 51 h, 10002000 HIGHLOW, 58 h, 10002004 ----------------------------------------------------- File ImageBase = 0x10000000 Real ImageBase = 0x00A00000 Differencial = 0x-F600000 Pausing ... Hit Return for Continue : ||< ENTERキーを押せば、更新された再配置情報を表示する。 #code|python|> print "-------[ RELOCATION INFO : AFTER RELOCATION ]-------" dump_base_relocations(base_relocations, image_buf_ptr) print "----------------------------------------------------" print ||< 実行結果を見ると、実際にアドレスが調整されたことを確認出来る: #pre||> -------[ RELOCATION INFO : AFTER RELOCATION ]------- ----------------- VirtualAddress = 0x00001000 SizeOfBlock = 0x00000018 [TYPE], [OFFSET], [FILE VALUE] HIGHLOW, 6 h, A03000 HIGHLOW, B h, A0300C HIGHLOW, 11 h, A02000 HIGHLOW, 18 h, A02004 HIGHLOW, 46 h, A03014 HIGHLOW, 4B h, A03020 HIGHLOW, 51 h, A02000 HIGHLOW, 58 h, A02004 ---------------------------------------------------- ||< 続いてインポート情報をダンプし、表示する。 #code|python|> ids1 = build_import_descriptors(opt_header, image_buf_ptr) print "-------[ IMPORT INFO : BEFORE IMPORTING ]-------" dump_import_descriptors(ids1, image_buf_ptr) print "------------------------------------------------" print ||< この時点でのダンプ表示: #pre||> -------[ IMPORT INFO : BEFORE IMPORTING ]------- [USER32.dll] FirstThunk: 0000204E (Original VirtualAddress = 00A02000) FirstThunk: 00002040 (Original VirtualAddress = 00A02004) OriginalFirstThunk: 0000204E (Original VirtualAddress = 00A02034) Hint: 011C Name: GetDesktopWindow OriginalFirstThunk: 00002040 (Original VirtualAddress = 00A02038) Hint: 01F8 Name: MessageBoxA ------------------------------------------------ ||< 実際にインポート情報を基にアドレス解決を行い、IATを書き換えた後の情報を表示する。 #code|python|> import_dlls(ids1, image_buf_ptr) raw_input('Pausing ... Hit Return for Continue :') ids2 = build_import_descriptors(opt_header, image_buf_ptr) print "-------[ IMPORT INFO : AFTER IMPORTING ]-------" dump_import_descriptors(ids2, image_buf_ptr) print "-----------------------------------------------" print ||< 実行結果を見ると、FirstThunk(=IAT)の内容が実際のアドレスに変更されていることを確認出来る: #pre||> Pausing ... Hit Return for Continue :(ENTERキーを押す) -------[ IMPORT INFO : AFTER IMPORTING ]------- [USER32.dll] FirstThunk: 77D0D1D2 (Original VirtualAddress = 00A02000) FirstThunk: 77D307EA (Original VirtualAddress = 00A02004) OriginalFirstThunk: 0000204E (Original VirtualAddress = 00A02034) Hint: 011C Name: GetDesktopWindow OriginalFirstThunk: 00002040 (Original VirtualAddress = 00A02038) Hint: 01F8 Name: MessageBoxA ----------------------------------------------- ||< 最後にロードしたDLL自身のエクスポート情報をダンプ表示する: #code|python|> export_directory = build_export_directory( opt_header, section_headers, image_buf_ptr) print "-------[ EXPORT INFO ]-------" dump_export_directory(export_directory) print "-----------------------------" print raw_input('O.K, Here we go ... Hit Return for Continue :') ||< 実際のダンプ表示: #pre||> -------[ EXPORT INFO ]------- >>> IMAGE_EXPORT_DIRECTORY <<< Characteristics = 0000 TimeDateStamp = 4C108036 MajorVersion = 00 MinorVersion = 00 Name = dlltest.dll [RVA=20AC] Base = 0001 NumberOfFunctions = 0002 NumberOfNames = 0002 AddressOfFunctions RVA = 2098 AddressOfNames RVA = 20A0 AddressOfNameOrdinals RVA = 20A8 ----------------- Functions: Function[0] RVA = 00001000 Function[1] RVA = 00001040 ----------------- Names: Name[0] = funcA Name[1] = funcB ----------------- Ordinals: Ordinal[0] = 0 Ordinal[1] = 1 ----------------------------- O.K, Here we go ... Hit Return for Continue : ||< この時点で、手動で展開したメモリイメージの実行準備が整った。 funcA, funcBのアドレスをエクスポート情報から取得してみる: #code|python|> addr_funcA = image_buf_ptr + get_export_address(export_directory, 'funcA') print "funcA = %08X" % addr_funcA addr_funcB = image_buf_ptr + get_export_address(export_directory, 'funcB') print "funcB = %08X" % addr_funcB ||< get_export_address()は次のようになっており、エクスポート名→序数→アドレスへの変換を行う: #code|python|> def get_export_address(d, name): # 名前 → 序数テーブル上のインデックス番号 i = 0 found = False for v in d._Names_: if v == name: found = True break i = i + 1 if not found: return 0 # 序数テーブル上のインデックス番号 → 序数 o = d._Ordinals_[i] # 序数 = EAT上のインデックス番号 → 関数アドレス return d._Functions_[o] ||< 以降は main2.py と同様にCFUNCTYPEでプロトタイプを用意し、取得したアドレス値を使ってctypesの関数オブジェクトを作成し、呼ぶ。 #code|python|> prototypes = { 'funcA' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int), 'funcB' : ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int), } funcA = prototypes['funcA'](addr_funcA) funcB = prototypes['funcB'](addr_funcB) print "funcA(2, 3) = %d" % funcA(2, 3) print "funcB(2, 3) = %d" % funcB(2, 3) ||< 実行結果: #pre||> O.K, Here we go ... Hit Return for Continue : (ENTERを押す) funcA = 00A01000 funcB = 00A01040 (メッセージボックス表示) funcA(2, 3) = 10 (メッセージボックス表示) funcB(2, 3) = 9 ||< 以上、PythonスクリプトによるDLLの手動ロードと実行を簡単なサンプルで確認出来た。 * 参考 - アレ用の何か : PEファイルフォーマット その5 ~ ベース再配置情報 ~ -- http://hp.vector.co.jp/authors/VA050396/tech_11.html PEフォーマットの各情報を取得・表示するこれまでの実験: - [[660]] - [[661]] - [[662]] - [[663]] - [[665]] - [[666]] - [[674]] - [[675]] - [[676]] - [[677]] #navi_footer|技術|