注:題目來自於如下連接地址:http://www.pediy.com/kssd/html
目錄:第13篇 論壇活動 \ 金山杯2007逆向分析挑戰賽 \ 第一階段 \ 第二題 \ 題目 \ [第一階段 第二題]node
題目描述:編程
己知是一個 PE 格式 EXE 文件,其三個(section)區塊的數據文件依次以下:(詳見附件)
_text,_rdata,_datawindows
1. 將 _text, _rdata, _data 合併成一個 EXE 文件,重建一個 PE 頭,一些關鍵參數,如 EntryPoint,ImportTable 的 RVA,請本身分析文件得到。合併成功後,程序便可運行。
2. 請在第1步得到的EXE文件基礎上,增長菜單。具體見圖:數組
3. 執行菜單 Help / About 彈出以下圖所示的 MessageBox 窗口:網絡
題目分析和解答:數據結構
(一)拼接可執行文件:編輯器
首先下載題目的附件,附件中已經有三個文件,分別是 PE 文件的三個 section,能夠看到三個 section 文件已經按照 0x1000 大小對齊。這樣咱們只須要把這三個文件依次鏈接在一塊兒,接在一個正確的 PE 文件頭後面就能夠了。ide
能夠先用 VC (我採用 VS2005)建立一個 Windows 窗口程序(它將提供一些主要樣本,因此稱這個程序爲樣本程序),把程序寫的儘量和題目中的程序相似,而後編譯,即首先獲得了一個 PE 文件頭的原型,再次基礎上進行修改,也就是根據題目給出的 section,適當調整 PE 文件頭中的須要修改的字段。函數
在本題求解過程當中,我嚴重依賴於我從前寫的一個展現 PE 文件格式的應用程序,此程序最近通過個人調整和改進,它的優勢是因爲此程序基於擴展 TreeView 控件,所以幫助快速理解 PE 文件頭的結構,其效果見如下截圖:
關於此程序的更多信息,請參見個人博客文章:《[VC6] 圖像文件格式數據查看器》。
BmpFileView 的可執行文件的下載連接(不敢說它是最好的,但做爲幫助學習PE文件格式的輔助工具而強烈推薦):
http://files.cnblogs.com/hoodlum1980/BmpFileView_V2_Bin.zip
觀察題目給出的三個 section 文件,能夠給出這三個 section 的基本信息以下:
SectionName | VirtualAddress | RawDataSize | VirtualSize |
---|---|---|---|
.text | 1000h | 6000h | 5B73h |
.rdata | 7000h | 1000h | 0C6Eh |
.data | 8000h | 3000h | 4000h |
.rsrc | B000h |
其中,.rsrc 是須要在稍後插入的資源 section,將在稍後講解。
這裏須要特別注意的是,.data 的虛擬內存尺寸,必需要比文件尺寸(RawDataSize)更大一些,關於這一點我還暫時不能給出詳細的解釋,有待於在未來作進一步研究。若是把 .data 的 VirtualSize 設置爲和 RawDataSize 同樣大(3000h),則程序沒法運行,會彈出一個消息框提示這不是一個有效的 Win32 程序。因此這一步我也是反覆嘗試是不是其餘字段的問題,糾結了半天才發現原來問題卡在這個地方。
對於 PE 文件頭的 IMAGE_OPTINAL_HEADER.CheckSum,Windows 看起來徹底忽略這個字段的值,因此這個字段能夠不用管。
明確了以上問題,如今能夠把這三個 section 和文件頭連接成一個新的 PE 文件了,把樣本程序 pediy02.exe 和三個 section 文件放在同一個目錄下,經過一個輔助的 Console 項目(pediy02_helper 項目)來完成這些工做,生成的新的 PE 文件名爲 pediy02_new.exe,使用的輔助函數以下(爲了簡單明瞭起見,代碼中並無插入繁瑣的檢測性代碼,例如申請的緩衝區大小,已經根據須要,在編碼時被靜態的肯定了):
Code 1.1 將三個 Section 拼接成 PE 文件的 C++ 代碼:
void WriteToFile(FILE *fp, void* pBuf, DWORD nSize); int CreateNewPe() { //PIMAGE_IMPORT_DESCRIPTOR pImportTable = NULL; PIMAGE_DOS_HEADER pDosHdr = NULL; PIMAGE_NT_HEADERS pNtHdrs = NULL; PIMAGE_SECTION_HEADER pSectionHdr = NULL; FILE *fp1, *fp2, *fp3; TCHAR szPath[MAX_PATH]; LPCTSTR szNames[3] = { _T("_text"), _T("_rdata"), _T("_data") }; _stprintf_s(szPath, _T("%s\\pediy02.exe"), THE_DIR); _tfopen_s(&fp1, szPath, _T("rb")); _stprintf_s(szPath, _T("%s\\pediy02_new.exe"), THE_DIR); _tfopen_s(&fp2, szPath, _T("wb")); //讀取文件頭部 void* buf = malloc(0xD000); fread(buf, 1, 0x1000, fp1); pDosHdr = (PIMAGE_DOS_HEADER)buf; pNtHdrs = (PIMAGE_NT_HEADERS)((DWORD)buf + pDosHdr->e_lfanew); pSectionHdr = (PIMAGE_SECTION_HEADER)((DWORD)pNtHdrs + sizeof(IMAGE_NT_HEADERS)); /* ---------------------------------------------- | section | addr | RawDataSize | VirtualSize | |---------+-------+--------------+-------------| | .text | 1000h | 6000h | 5B73h | | .rdata | 7000h | 1000h | 0C6Eh | | .data | 8000h | 3000h | 4000h | | .rsrc | B000h | 1000h | 1000h | ---------------------------------------------- */ pNtHdrs->FileHeader.NumberOfSections = 4; pNtHdrs->OptionalHeader.BaseOfCode = 0x1000; pNtHdrs->OptionalHeader.BaseOfData = 0x8000; //+1000h 的 .rsrc pNtHdrs->OptionalHeader.SizeOfCode = 0x6000; pNtHdrs->OptionalHeader.SizeOfImage = 0xD000; pNtHdrs->OptionalHeader.SizeOfInitializedData = 0x5000; pNtHdrs->OptionalHeader.SizeOfUninitializedData = 0; pNtHdrs->OptionalHeader.AddressOfEntryPoint = 0x1527; //入口點 //IMAGE_DIRECTORY_ENTRY_IMPORT 須要進一步調整, kernel32.dll, gdi32.dll, user32.dll 加上一個結尾 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = 0x7618; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size = sizeof(IMAGE_IMPORT_DESCRIPTOR) * (3 + 1); //資源表 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].VirtualAddress = 0xC000; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE].Size = 0x011C; // IMAGE_DIRECTORY_ENTRY_DEBUG 6 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].VirtualAddress = 0; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG].Size = 0; // IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].VirtualAddress = 0; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG].Size = 0; //IMAGE_DIRECTORY_ENTRY_IAT 12; (import address table), IMAGE_IMPORT_DESCRIPTOR.FirstTrunk 中的最小值 //IAT 地址須要在修改後找,須要進一步調整 //IAT 的地址一般就是 .rdata 的起始地址 //Size 是 FirstTrunk 中的最大地址 - IAT 起始地址) + 8; //(其中 +4 是最後一個元素佔用的空間,再 +4 是一個NULL元素,表示結尾) pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].VirtualAddress = 0x7000; pNtHdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IAT].Size = 0x012C; //section_headers //.text pSectionHdr[0].VirtualAddress = 0x1000; pSectionHdr[0].SizeOfRawData = 0x6000; pSectionHdr[0].PointerToRawData = 0x1000; pSectionHdr[0].Misc.VirtualSize = 0x5B73; //.rdata pSectionHdr[1].VirtualAddress = 0x7000; pSectionHdr[1].SizeOfRawData = 0x1000; pSectionHdr[1].PointerToRawData = 0x7000; pSectionHdr[1].Misc.VirtualSize = 0x1000; //.data //.data 的虛擬內存大小(VirtualSize)必須比文件中更大,不然沒法啓動,如今我也不知道爲何 pSectionHdr[2].VirtualAddress = 0x8000; pSectionHdr[2].SizeOfRawData = 0x3000; pSectionHdr[2].PointerToRawData = 0x8000; pSectionHdr[2].Misc.VirtualSize = 0x4000; //【重要!】必須比 SizeofRawData 大一些 //.rsrc (resource) 由於.data 比文件中大,因此.rsrc 相應的要像高地址移動 pSectionHdr[3].VirtualAddress = 0xC000; pSectionHdr[3].SizeOfRawData = 0x1000; pSectionHdr[3].PointerToRawData = 0xB000; //文件中的地址仍是緊靠.data pSectionHdr[3].Misc.VirtualSize = 0x011C; //從範本文件中獲得該值 fwrite(buf, 1, 0x1000, fp2); fflush(fp2); int i; DWORD dwFileSize; for(i = 0; i < 3; i++) { _stprintf_s(szPath, _T("%s\\%s"), THE_DIR, szNames[i]); _tfopen_s(&fp3, szPath, _T("rb")); fseek(fp3, 0, SEEK_END); dwFileSize = ftell(fp3); fseek(fp3, 0, SEEK_SET); fread(buf, 1, dwFileSize, fp3); fclose(fp3); WriteToFile(fp2, buf, dwFileSize); } //從已有的範本複製 .rsrc 節 fseek(fp1, 0xB000, SEEK_SET); fread(buf, 1, 0x1000, fp1); WriteToFile(fp2, buf, 0x1000); fclose(fp1); fclose(fp2); free(buf); return 0; }
//寫入文件,以 1KB 爲單位 void WriteToFile(FILE *fp, void* pBuf, DWORD nSize) { //以1KB爲基本單位,逐次寫入 char* pos = (char*)pBuf; size_t BytesToWrite; while(nSize > 0) { BytesToWrite = min(nSize, 0x400); fwrite(pos, 1, BytesToWrite, fp); fflush(fp); nSize -= BytesToWrite; pos += BytesToWrite; } }
上面的函數已是最終版本的函數,它已經完成了如下工做:
(1)肯定 AddressOfEntryPoint 的地址。
(2)肯定 DataDirectory[1]: ImportTable (導入表)的地址和尺寸。
(3)肯定 DataDirectory[12]: Import Address Table (綁定導入函數地址表)的地址和尺寸。
(4)從樣本程序 pediy02.exe 中插入資源 (.rsrc) section,並肯定 DataDirectory[2]: resource Table (資源表)的地址和尺寸。
固然很顯然上面的工做並非一步到位完成的,下面簡要介紹上面的工做是如何完成的:
(1)肯定入口點地址:
該工做相對簡單容易,先把 EntryPoint 設置爲 .text (代碼段)的起始地址:0x1000,而後生成文件後,加載到 IDA 中分析代碼段的內容,就能夠很容易的找到如下函數的地址(如下地址爲 VA,即加上了 ImageBase 後的地址):
0x00401527: __tmainCRTStartup,是 PE 文件的實際入口點。
0x004011EC: WinMain,高級語言編程時的程序入口點。
0x004012D5: WndProc, 當前的窗口過程(稍後將會被子類化)
0x004059C4: sub_4059C4,基本等價於 MessageBoxA,很重要,稱它爲 ___crtMessageBoxA。
如今只要知道,在文件頭中把入口地址設置到 __tmainCRTStartup 函數便可,文件頭要求的是 RVA,所以在代碼中設置入口點:
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint = 0x1527;
這樣入口點地址就肯定好了。
(2)肯定 DataDirectory [1] 導入表的地址和大小:
這一步也相對比較簡單,導入表位於 .rdata 中(位於中部)。在此以前,必須瞭解導入表的結構,導入表是一個由多個 IMAGE_IMPORT_DESCRIPTOR 元素組成的數組,以 NULL 元素(內容所有是 0 )標識結尾(IMAGE_IMPORT_DESCRIPTOR 的數據結構定義參見 winnt.h)。每一個元素由 5 個 DWORD 組成,其中倒數第二個 DWORD 是 Name 字段(字符串指針),它的值是一個 RVA(即相對於 ImageBase 的偏移),指向了 Dll 名字(ASCII)字符串(該字符串一樣位於 .rdata 中)。
導入表的示意結構以下圖所示(圖中展現的是兩個 Thunk 數組並行狀況,所以 FirstThunk 也是字符串指針的大多數狀況,圖中的字符串雖然位於整齊的矩形格子以內,這只是爲了圖形外觀,應該強調的是這些字符串的長度是不固定的,長度有長有短,因此它們在空間中的分佈是良莠不齊的):
上圖表示了 pediy2_new.exe 的實際導入表,共導入了 3 個 DLL,每一個導入 DLL 是導入表中的一個元素,在這個數組中的每一個元素大小爲 20 Bytes,若是引用了 3 個 DLL,則這個數組一共爲 (3 + 1) * 20 = 80 Bytes (最後有一個 null terminator element)。下面是單個元素 descriptor 大小:
sizeof ( IMAGE_IMPORT_DESCRIPTOR ) = sizeof ( DWORD ) * 5 = 20 Bytes;
每一個元素的 OriginalFirstTrunk 和 FirstTrunk 是兩個指針,指向了兩個 並行的指針數組,一般狀況下(即沒有在連接時事先綁定)這兩個數組的內容是相同的(即兩個數組的全部元素的值相同),在靜態 PE 文件中,都指向相同的長度不固定的函數名稱字符串(或者是被導入函數的 Ordinal)。
補充說明:在沒有通過事先綁定時,OriginalFirstTrunk 和 FirstTrunk 指向的數組內容在加載以前都指向 .rdata 中的一些長度不固定的 Ascii 編碼的字符串,在加載時 FirstTrunk 指向的數組被系統綁定成映射到本進程的 DLL 的實際函數地址(所以該數組稱爲 IAT),因此這些元素稱爲 Trunk (意味着其身份的可變性,這些元素在加載後其身份發生了變化),由於指向的是數組頭部,因此稱之爲 First(IMAGE_IMPORT_DESCRIPTOR.(Original)FirstTrunk 表示某個 DLL 被本模塊導入的首個函數的 Trunk 的位置,後面還有更多的函數 Trunk,以 NULL 表徵結束)。OriginalFirstTrunk 在加載後保持不變(因此稱爲 Original),因此至關於存儲着導入函數名稱的一份副本。在模塊被加載後,能夠經過 OriginalFirstTrunk 數組瞭解到該模塊導入了哪些函數(名稱),經過 FirstTrunk 數組的內容可瞭解到導入函數的運行時虛擬地址。導入函數的實際地址是在加載時綁定的(沒法在編譯時肯定),編譯器可能爲每一個 dll 函數調用生成一個很小的函數體,稱爲 j_XXX, 該函數體負責 jmp 到 FirstTrunk 數組中的元素給出的運行時函數地址,也能夠直接調用 IAT 元素內容指向的 VA 地址。
雖然應用程序能夠經過序號導入函數,並具備極高效率,可是這樣會致使看不到導入函數的名字,對程序和系統的維護形成障礙。因此除非成本過高(例如 MFC 類庫的導出函數過多,且面向對象的 C++ 函數名稱也很長,因此 MFC 類庫的函數以 Ordinal 方式被導入),按名稱導入是廣泛作法,顯然按名稱導入,須要線性搜索模塊的導出函數表,這就會消耗必定的加載時間成本。爲了提升程序加載時效率,應用程序能夠經過 「事先 Rebase」 (將程序須要導入的模塊自身建議的 ImageBase 進行精心調整,從而避免在加載時重定向) 和 「事先綁定」 提升程序在客戶運行環境的加載速度,系統經過時間戳斷定綁定信息是否有效,若是時間戳不一致,或者發生重定向,系統則必須再次進行加載時綁定。
OriginalFirstTrunk 和 FirstTrunk 指向的這兩個指針數組位於 .rdata 的不一樣位置,其中 FirstTrunk 指向的數組位於 .rdata 的起始位置(稍後能夠看到這就是 IAT),OriginalFirstTrunk 指向的數組位於稍微靠後的位置。兩個 Trunk 在 PE 文件中的值都指向相同的 IMAGE_IMPORT_BY_NAME (由 Hint 和 函數名稱字符串 組成的數據結構)。IAT 所在的頁面將在加載時被臨時設定爲可寫,綁定以後再恢復爲只讀。有關這部分的細節請參考個人博客文章:《讀取PE文件的導入表》。
關於導入表和 IAT 的在內存空間中的位置佈局,請參考本文的補充討論(2)。
瞭解了導入表結構,就能夠很快找到導入表的位置了,首先在 .rdata 中查找 DLL 名稱字符串,能夠找到以下的字符串:
FA: 0x000077AC: "KERNEL32.dll"; (這裏使用的是文件地址 FA,或者說是 RVA)
找到附近指向該位置的指針,即在附近的文件內容中搜索 "AC 77 00 00" 片斷,能夠找到文件地址:
FA: 0x00007624: AC 77 00 00
這裏就是一個 IMAGE_IMPORT_DESCRIPTOR 元素,把該地址減去 3 個 DWORD 值,即獲得該元素的起始地址爲 0x00007618。因爲導入表元素內容很是有特色,很容易就能夠判斷導入表的兩端邊界,所以能夠很快肯定導入表的起始地址(RVA)和 Size 以下:
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].VirtualAddress = 0x7618;
IMAGE_NT_HEADERS.IMAGE_OPTIONAL_HEADER.DataDirectory[2].Size = sizeof ( IMAGE_IMPORT_DESCRIPTOR ) * 4;
(3)肯定 DataDirectory [12],IAT的地址和大小:
IAT 的地址比較簡單,它就是全部 DLL 的 FirstTrunk 字段的最小值,一般就是 .rdata 的起始位置(那些常量字符串位於 IAT 和 ImportTable 的後面),也就是 0x7000 (能夠看到這裏是從 Gdi32.dll 的導入的第一個函數 DeleteObject)。
要計算 IAT 的大小,須要遍歷導入表,找到導入的全部 Dll 的 FirstTrunk 的最後一個元素的位置,同時還要考慮到結尾還須要一個 NULL 指針做爲結束標誌,因此:
IAT.Size = max ( 全部 DLL 的 FirstTrunk 數組元素所在的地址(RVA) ) - IAT.VirtualAddress (RVA) + 8 。
有關如何遍歷導入表的更多內容,請參考個人博客文章(在此就再也不詳細敘述了):《讀取PE文件的導入表》。
本題目中全部的 Trunk 的最大地址(RVA)是 0x7124(從 USER32.dll 導入的 DispatchMessageA),可得:
DataDirectory[12].VirualAddress = 0x7000; // RVA (Relative to ImageBase )
DataDirectory[12].Size = 0x012C;
通過以上修改,能夠經過 CreateNewPe 函數,生成一個能夠執行的 PE 文件了。題目的前半部分要求此時完成。接下來考慮後半部分要求,爲程序添加菜單和相關的命令處理函數。
(二)添加菜單 和 處理函數。
(1)添加 .rsrc section (菜單資源)
添加資源,一樣經過在樣本程序中實現。在樣本程序中,添加題目要求同樣的資源(只保留菜單,刪除全部其餘種類資源,這樣可使 .rsrc 最小,僅佔用 1000h 大小),而後能夠從樣本程序中拷貝 .rsrc 段,追加到咱們已經獲得的 PE 文件的尾部。同時調整 PE 文件頭中的相關字段。
注意:因爲 .data 節在加載到虛擬內存中時被擴大了 1000h,因此位於最後的 .rsrc 的文件地址(FA)和虛擬地址(VA)將會誤差 1000h。即:
VA = FA + 1000h;
衆所周知,窗口的菜單一般是在註冊窗口類時指定的。所以爲了添加菜單,在 IDA 中觀察 WinMain 函數的代碼:
Code 2.1 由 .text 提供的 WinMain 函數的彙編代碼:
.text:004011EC ; int __stdcall WinMain(int,int,int,int nCmdShow) .text:004011EC WinMain proc near ; CODE XREF: start+C9p .text:004011EC .text:004011EC WndClass = WNDCLASSA ptr -50h .text:004011EC Msg = MSG ptr -28h .text:004011EC var_C = dword ptr -0Ch .text:004011EC arg_0 = dword ptr 8 .text:004011EC nCmdShow = dword ptr 14h .text:004011EC .text:004011EC push ebp .text:004011ED mov ebp, esp .text:004011EF sub esp, 50h .text:004011F2 push ebx .text:004011F3 push esi .text:004011F4 push edi .text:004011F5 mov esi, offset aPediy_com ; "pediy.com" .text:004011FA lea edi, [ebp+var_C] .text:004011FD mov ebx, [ebp+arg_0] .text:00401200 movsd ; char var_C[] = "pediy.com"; 【重要暗示!!!】 .text:00401201 movsd .text:00401202 movsw .text:00401204 mov edi, 7F00h .text:00401209 xor esi, esi .text:0040120B push edi ; lpIconName .text:0040120C push esi ; hInstance .text:0040120D mov dword_40ABAC, ebx .text:00401213 mov [ebp+WndClass.style], 3 .text:0040121A mov [ebp+WndClass.lpfnWndProc], offset sub_406B80 .text:00401221 mov [ebp+WndClass.cbClsExtra], esi .text:00401224 mov [ebp+WndClass.cbWndExtra], esi .text:00401227 mov [ebp+WndClass.hInstance], ebx .text:0040122A call ds:LoadIconA .text:00401230 push edi ; lpCursorName .text:00401231 push esi ; hInstance .text:00401232 mov [ebp+WndClass.hIcon], eax .text:00401235 call ds:LoadCursorA .text:0040123B push esi ; int .text:0040123C mov [ebp+WndClass.hCursor], eax .text:0040123F call ds:GetStockObject .text:00401245 mov [ebp+WndClass.hbrBackground], eax .text:00401248 lea eax, [ebp+var_C] .text:0040124B mov [ebp+WndClass.lpszMenuName], eax ; lpszMenuName = var_C; .text:0040124E lea eax, [ebp+WndClass] .text:00401251 mov edi, offset aPediy_com_0 ; "pediy.com" .text:00401256 push eax ; lpWndClass .text:00401257 mov [ebp+WndClass.lpszClassName], edi .text:0040125A call ds:RegisterClassA .text:00401260 test ax, ax .text:00401263 jnz short loc_401269 .text:00401265 xor eax, eax 。。。
菜單資源能夠採用數字來標識,也能夠採用字符串標識。若是在 VC 中添加菜單,默認爲以數字標識。若是要以數字標識菜單,第一個想法是須要 hack 上面的代碼。
但上面的代碼實際上不須要作任何改動,由於它給了咱們一個強烈暗示,上面的彙編代碼翻譯到 C 語言以下:
Code 2.2 將 WinMain 從彙編代碼翻譯到 C++ 的代碼(獲得 Menu Name):
int APIENTRY WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { char var_C[] = "pediy.com"; // ---- 重要暗示!!!---- MSG msg; WNDCLASSA wndCls; //保存到全局變量 hInst = hInstance; wndCls.style = CS_HREDRAW | CS_VREDRAW; wndCls.lpfnWndProc = WndProc; wndCls.cbClsExtra = 0; wndCls.cbWndExtra = 0; wndCls.hInstance = hInst; wndCls.hIcon = LoadIconA(NULL, IDI_APPLICATION); wndCls.hCursor = LoadCursorA(NULL, IDC_ARROW); wndCls.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndCls.lpszMenuName = var_C; // ---- 重要暗示!!!---- wndCls.lpszClassName = "pediy.com"; if (!RegisterClassA(&wndCls)) return FALSE; HWND hWnd = CreateWindowExA( 0, // EXStyle "pediy.com", // wndClass WS_BORDER | WS_DLGFRAME | WS_SYSMENU | WS_THICKFRAME | WS_GROUP | WS_TABSTOP,
//style CW_USEDEFAULT, // X CW_USEDEFAULT, // Y CW_USEDEFAULT, // nWidth CW_USEDEFAULT, // nHeight NULL, // hWndParent NULL, // hMenu hInst, // hInstance 0); // lParam ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while(GetMessageA(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; }
就是窗口類的菜單是由 var_C 指定的,var_C 是棧上的臨時變量,內容被加載爲」pediy.com「。即菜單的字符串標識是 」pediy.com「。因此任務就簡單了,在樣本程序中,把菜單的 ID 改成字符串」pediy.com「,而後把編譯好的樣本程序的 .rsrc 追加到 PE 文件中,菜單就加好了!
補充說明:資源 ID 以字符串標識時,是不論大小寫,且字符串的大寫形式,以 Unicode 編碼存儲於 .rsrc 段中的。例如本題目,菜單的 ID 在 .rsrc 中被存儲爲 "PEDIY.COM"。
有關資源表的結構的更多信息,請參考個人博客文章(這裏不作更多說明):《讀取PE文件的資源表》。
(2)添加菜單處理函數(子類化窗口):
菜單加好之後,如今點擊菜單尚未任何反應。接下來爲菜單添加命令處理函數,所以觀察窗口過程 WndProc 的彙編代碼,能夠發現:WndProc 沒有爲 WM_COMMAND 留出任何空隙和空間供咱們插入本身的代碼,即沒有辦法 hack 已有代碼來完成這個功能。所以只能在 .text 中追加新的代碼。
方法是,在 .text 尾部追加一個函數做爲新的窗口過程,這個過程和在 MFC 中子類化一個控件的本質相同,也相似於一般所說的 Hook,即掛鉤一個新的函數,由新的 Hook 函數添加本身的處理邏輯,而後再把控制權交回到原來的函數。
這裏還須要說明另外一個問題,題目要求點擊菜單時彈出 MessageBox。可是在現有的導入表中能夠看到,程序並無導入 MessageBoxA 這個函數,因此若是直接調用 MessageBoxA,則須要調整導入表。這樣相對的比較麻煩。這時候前面咱們找到的那個很是有趣的函數(sub_4059C4: ___crtMessageBoxA)就有用了,觀察那個函數,其彙編代碼以下:
Code 2.3 代碼段中的函數: 004059C4: ___crtMessageBoxA 的彙編代碼(MessageBoxA 的動態連接版本):
.text:004059C4 sub_4059C4 proc near ; .text:004059C4 arg_0 = dword ptr 8 .text:004059C4 arg_4 = dword ptr 0Ch .text:004059C4 .text:004059C4 push ebx .text:004059C5 xor ebx, ebx .text:004059C7 cmp dword_40AB70, ebx .text:004059CD push esi .text:004059CE push edi .text:004059CF jnz short loc_405A13 .text:004059D1 push offset LibFileName ; "user32.dll" .text:004059D6 call ds:LoadLibraryA .text:004059DC mov edi, eax .text:004059DE cmp edi, ebx .text:004059E0 jz short loc_405A49 .text:004059E2 mov esi, ds:GetProcAddress .text:004059E8 push offset aMessageboxa ; "MessageBoxA" .text:004059ED push edi ; hModule .text:004059EE call esi ; GetProcAddress .text:004059F0 test eax, eax .text:004059F2 mov dword_40AB70, eax .text:004059F7 jz short loc_405A49 .text:004059F9 push offset aGetactivewindo ; "GetActiveWindow" .text:004059FE push edi ; hModule .text:004059FF call esi ; GetProcAddress .text:00405A01 push offset aGetlastactivep ; "GetLastActivePopup" .text:00405A06 push edi ; hModule .text:00405A07 mov dword_40AB74, eax .text:00405A0C call esi ; GetProcAddress .text:00405A0E mov dword_40AB78, eax .text:00405A13 .text:00405A13 loc_405A13: ; CODE XREF: sub_4059C4+Bj .text:00405A13 mov eax, dword_40AB74 .text:00405A18 test eax, eax .text:00405A1A jz short loc_405A32 .text:00405A1C call eax .text:00405A1E mov ebx, eax .text:00405A20 test ebx, ebx .text:00405A22 jz short loc_405A32 .text:00405A24 mov eax, dword_40AB78 .text:00405A29 test eax, eax .text:00405A2B jz short loc_405A32 .text:00405A2D push ebx .text:00405A2E call eax .text:00405A30 mov ebx, eax .text:00405A32 .text:00405A32 loc_405A32: ; CODE XREF: sub_4059C4+56j .text:00405A32 ; sub_4059C4+5Ej ... .text:00405A32 push [esp+0Ch+arg_4] .text:00405A36 push [esp+10h+arg_0] .text:00405A3A push dword ptr [esp+18h] .text:00405A3E push ebx .text:00405A3F call dword_40AB70 .text:00405A45 .text:00405A45 loc_405A45: ; CODE XREF: sub_4059C4+87j .text:00405A45 pop edi .text:00405A46 pop esi .text:00405A47 pop ebx .text:00405A48 retn .text:00405A49 ; .text:00405A49 .text:00405A49 loc_405A49: ; CODE XREF: sub_4059C4+1Cj .text:00405A49 ; sub_4059C4+33j .text:00405A49 xor eax, eax .text:00405A4B jmp short loc_405A45 .text:00405A4B sub_4059C4 endp
這個函數內容很是簡單,內容註釋就不寫了,總之,這個函數的功能是動態獲取 MessageBoxA 的地址並調用。原型至關於:
int ___crtMessageBoxA(const char* pText, const char* pTitle, UINT nType);
因爲函數沒有復原 ESP,因此是默認的 C 調用約定。這個函數和 MessageBoxA 的區別是:
(a)調用約定不一樣,MessageBoxA 爲 __stdcall 。
(b)只比 MessageBoxA 少了第一個參數: HWND hWnd。該函數在內部獲取了一個 HWND 做爲 Owner 窗口彈出 MessageBox。
所以,不須要調整導入表,只須要在新的窗口過程當中去調用這個函數便可完成彈出 MessageBox 的功能。
同時能夠看到,MessageBox 的文本內容,在 .rdata 中並無可供使用的現成字符串,因此須要插入常量字符串,只須要在 .rdata 的尾部插入便可,經過如下函數便可完成(注意插入新的常量字符串後,須要相應的調整 IMAGE_SECTION_HEADER.VirtualSize,以容納新的字符串內容):
Code 2.4 向 .rdata 段尾部插入常量字符串的 C++ 代碼(做爲 ___crtMessageBoxA 的參數):
//在PE文件中插入常量字符串 void InsertString(LPCTSTR pFileName, int InsertPos, const char *pStr) { int BytesToWrite = strlen(pStr) + 1; FILE *fp = NULL; _tfopen_s(&fp, pFileName, _T("r+b")); fseek(fp, InsertPos, SEEK_SET); fwrite(pStr, 1, BytesToWrite, fp); fclose(fp); }
上面的 InsertString 是爲了修改 PE 文件而臨時寫成,因此比較簡單,所以其不夠易用,侷限性在於,1 須要手工調整 section header; 2 須要手工調整 section header 裏的 VirtualSize; 3 只考慮了 ASCII 字符串。所以能夠多花必定時間把它寫的更加通用一點。參見本文末尾的補充部分。
爲了獲得新的窗口過程,在樣本程序中寫出新的窗口過程函數,並以 debug 選項編譯(之因此採用 debug,是由於 release 優化幅度過大,其結果不利於咱們利用。例如我在 release 下編譯出掛鉤後的結果,編譯結果顯示原有的窗口過程的第一個參數 hWnd 被優化掉了,由於它已經再也不做爲窗口過程使用,而僅僅是被新的窗口過程調用的一個普通函數,因此編譯器能夠按照本身的喜愛對它作任何等效變換!)。
Code 2.5 爲了子類化窗口,新窗口過程的 C++ 代碼(用於獲得其彙編代碼):
BOOL ___crtMessageBoxA(const char* szText, const char* szTitle, UINT nType) { HMODULE hModule = LoadLibraryA("user32.dll"); int (__stdcall *pFunc)(HWND, LPCSTR, LPCSTR, UINT uType); pFunc = (int (__stdcall*)(HWND, LPCSTR, LPCSTR,UINT uType))
GetProcAddress(hModule, "MessageBoxA"); pFunc(NULL, szText, szTitle, nType); return TRUE; } LRESULT CALLBACK NewWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { if(message == WM_COMMAND && LOWORD(wParam) == IDM_ABOUT) { ___crtMessageBoxA( "看雪論壇.珠海金山2007逆向分析挑戰賽\r\nhttp://www.pediy.com", //text "pediy", //caption MB_ICONINFORMATION); return TRUE; } return WndProc(hWnd, message, wParam, lParam); }
其中上面代碼中的 ___crtMessageBoxA 函數只是對實際函數的一個簡單模擬,這樣產生的窗口過程的代碼,只須要計算出一些偏移值便可。接下來反彙編上面的樣本代碼的 debug 編譯結果,把 debug 版本中作簡要處理,去掉 debug 版本特有的那些填充 INT3 和 ESP 校驗 那些沒什麼用處的代碼,就能夠獲得須要插入的彙編代碼了,經過如下函數,把新的窗口過程代碼插入到 PE 文件中(因爲段在內存中對齊到 4KB,因此每一個段的結尾基本上都有至關大的空間剩餘,能夠插入一些新的內容),以下所示:
Code 2.6 用於向 .text 尾部插入新的窗口過程的 C++ 代碼(用於窗口子類化):
//返回插入的字節數 void InsertNewWndProc(LPCTSTR pFileName, int InsertPos) {
//int __stdcall NewWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam);
//[EBP+ 8]: hWnd //[EBP+0Ch]: nMsg //[EBP+10h]: wParam //[EBP+14h]: lParam
BYTE _code[] = { 0x55, //00: push EBP 0x8B, 0xEC, //01: mov EBP, ESP 0x81, 0xEC, 0x20, 0x00, 0x00, 0x00, //03: sub ESP, 20H 0x53, //09: push EBX 0x56, //0A: push ESI 0x57, //0B: push EDI 0x81, 0x7D, 0x0C, 0x11, 0x01, 0x00, 0x00, //0C: cmp [EBP + nMsg], WM_COMMAND 0x75, 0x2B, //13: jne _CALL_OLD_WNDPROC 0x8B, 0x45, 0x10, //15: mov EAX, [EBP + wParam] 0x25, 0xFF, 0xFF, 0x00, 0x00, //18: and EAX, 0xFFFF 0x0F, 0xB7, 0xC8, //1D: movzx ECX, AX 0x83, 0xF9, 0x68, //20: cmp ECX, 0x68 (IDM_ABOUT = 104) 0x75, 0x1B, //23: jne _CALL_OLD_WNDPROC 0x6A, 0x40, //25: push MB_ICONINFORMATION 0x68, 0x90, 0x7C, 0x40, 0x00, //27: push pTitle (0x00407C90: "pediy") 0x68, 0xA0, 0x7C, 0x40, 0x00, //2C: push pText (0x00407CA0 : "...") 0xE8, 0x00, 0x00, 0x00, 0x00, //31: call ___crtMessageBoxA (rel32,須要調整) 0x83, 0xC4, 0x0C, //36: add ESP, 0Ch 調用方復原esp 0xB8, 0x01, 0x00, 0x00, 0x00, //39: mov EAX, 1 0xEB, 0x15, //3E: jmp _RETURN //_CALL_OLD_WNDPROC: 0x8B, 0x45, 0x14, //40: mov EAX, [EBP + lParam] 0x50, //43: push EAX 0x8B, 0x4D, 0x10, //44: mov ECX, [EBP + wParam] 0x51, //47: push ECX 0x8B, 0x55, 0x0C, //48: mov EDX, [EBP + nMsg] 0x52, //4B: push EDX 0x8B, 0x45, 0x08, //4C: mov EAX, [EBP + hWnd] 0x50, //4F: push EAX 0xE8, 0x00, 0x00, 0x00, 0x00, //50: call oldWndProc (rel32,須要調整) //_RETURN: 0x5F, //55: pop EDI 0x5E, //56: pop ESI 0x5B, //57: pos EBX 0x81, 0xC4, 0x20, 0x00, 0x00, 0x00, //58: add ESP, 20h 0x5D, //5E: pop EBP, 0xC2, 0x10, 0x00 //5F: retn 10h }; union { int offset; UINT dwVal; BYTE bytes[4]; } rel32; //計算 ___crtMessageBoxA 的偏移地址 int nextAddr = InsertPos + 0x36; //注意nextAddr是文件地址,也就是 rva (沒有加ImageBase) //0x59C4 是 showMsgBox 函數的 rva rel32.offset = 0x59C4 - nextAddr; _code[0x32] = rel32.bytes[0]; _code[0x33] = rel32.bytes[1]; _code[0x34] = rel32.bytes[2]; _code[0x35] = rel32.bytes[3]; //計算 oldWndProc 的偏移地址 nextAddr = InsertPos + 0x55; //0x12D5 是 WndProc 函數的rva rel32.offset = 0x12D5 - nextAddr; _code[0x51] = rel32.bytes[0]; _code[0x52] = rel32.bytes[1]; _code[0x53] = rel32.bytes[2]; _code[0x54] = rel32.bytes[3]; int BytesToWrite = sizeof(_code); FILE *fp = NULL; _tfopen_s(&fp, pFileName, _T("r+b")); fseek(fp, InsertPos, SEEK_SET); fwrite(_code, 1, BytesToWrite, fp); fclose(fp); }
在上面的代碼中,_code 數組的內容是根據 NewWndProc 的 debug 版本的彙編代碼的基礎上,通過刪減獲得的,已經增長了註釋。在全部相關的調整步驟完成後,能夠再次反彙編目標文件,查看新插入的窗口過程是否正常,因爲上面對 _code 內容的註釋將和反彙編工具中看到的同樣,因此這裏就再也不重複給出在反彙編工具中看到的「新的窗口過程」的代碼了。
【注意】:插入新的函數到 .text 尾部後,可能依然須要手工更新 section header 中的 VirtualSize 。
代碼中由兩處偏移地址須要進行調整,分別是 ___crtMessageBoxA 和 oldWndProc 的偏移地址。showMsgBox 的前兩個參數爲新插入到 .rdata 尾部的兩個常量字符串,其地址(VA)已經直接編入 _code 數組中了。即,經過如下方式完成插入新的窗口過程:
Code 2.7 插入 「常量字符串」 和 「新的窗口過程」 到 PE 文件的執行動做:
//[2] 向修改後的PE文件中插入常量字符串 InsertString(szPath, 0x7C80, "---OurString---"); InsertString(szPath, 0x7C90, "pediy"); InsertString(szPath, 0x7CA0, "看雪論壇.珠海金山2007逆向分析挑戰賽\r\nhttp://www.pediy.com"); //[3] 插入新的窗口過程!至關於對其子類化 InsertNewWndProc(szPath, 0x6B80);
先在尾部插入一個沒用的但容易識別的分隔字符串(其目的是幫助咱們在 16 進制編輯器中快速定位到插入的內容):」---OurString---"。(剛好16Bytes,且 InsertString 函數對插入地址作了 16 Bytes 對齊,所以它在16進制編輯器中將佔據一個整行),接下來插入兩個常量字符串(做爲題目要求彈出的 MessageBox 的標題和文本):
0x7C90: "pediy" // Title of MsgBox; ( 這裏採用的是 「文件地址」 或者說 RVA。)
0x7CA0: "看雪論壇..珠海金山2007..." // Text of MsgBox;
注意:插入新的字符串常量後,不要忘記同步調整 .rdata 的 VirtualSize !
本文結尾補充了一個更通用的插入字符串的函數。請參考補充討論。
新的窗口過程已經被插入到了 PE 文件中。接下來再修改 WinMain 中註冊窗口類的代碼,把新的窗口過程掛鉤上去。窗口類的窗口過程是用 VA 提供的絕對地址,修改起來很簡單,不須要計算偏移值,把對應的 VA 修改成咱們插入的新的窗口過程的 VA (0x00406B80)便可。
一樣的,找到 WinMain 函數中,設置窗口過程的指令:
.text:0040121A mov [ebp+WndClass.lpfnWndProc], offset OldWndProc
指令的機器碼:
FA:0000121A: C7 45 B4 XX XX 40 00
來到文件地址 121A h 處,這條指令的後面 4 個字節就是窗口過程的 VA。把它修改成剛剛插入的新的窗口過程的 VA (0x 00406B80) 便可。即把 XX 位置調整爲以下,即完成掛鉤咱們新插入的窗口過程:
FA:0000121A: C7 45 B4 80 6B 40 00
這樣題目的三部分要求(文本將後兩個要求合併)就所有完成了。修改後的 PE 文件運行效果以下:
【補充】對該條指令 ( .text:0040121A mov [ebp+WndClass.lpfnWndProc], offset OldWndProc ) 的機器碼解讀:
Prefixes | Opcode | ModR/M | SIB | Displacement | Immediate | ||||||||
Mod | Reg/Opcode | R/M | Scale | Index | Base | ||||||||
B | 11000111 | 01 | 000 | 101 | 10110100 | 略 | |||||||
H | <absent> | C7 | 45 | <absent> | B4 | 80 | 6B | 40 | 00 | ||||
+disp8 | <無心義> | [EBP] | |||||||||||
說 明 |
MOV | [EBP] + disp8 | disp8 = -76 |
imm32 | |||||||||
[EBP - 4Ch], | 0x00406B80 | ||||||||||||
Dest Operand, | Src Operand | ||||||||||||
r/m32, | imm32 | ||||||||||||
&WndCls = EBP - 0x50; //描述窗口類的數據結構的地址 Offset of WndCls.lpfnWndProc = 4; //結構體成員偏移 所以: EBP - 0x4C => &WndCls + 4 => &WndCls.lpfnWndProc; 翻譯到高級語言: WndCls.lpfnWndProc = 0x00406B80; |
imm32 當即數: 窗口過程的入口地址; VA (已包含 ImageBase); |
||||||||||||
對此 Opcode (C7)的特定說明(屬於比較晦澀繁瑣的細節,可忽略本單元格內容): Move imm32 to r/m32 (或 Move imm16 to r/m16). 尋址: Operand1 (destination operand): ModRM: r/m (w); Operand2 (source operand): imm8/16/32/64; |
在參考資料(5)中,C7 操做碼的說明是「C7 /0」; 這裏 「/0」 表示 ModR/M 字節僅僅使用 r/m (寄存機或主存)操做數。
ModR/M 字節的各個字段含義解釋以下:
a). r/m = 101 (二進制), 表示 CH / BP / EBP / MM5 / XMM5 寄存器。
b). Mod = 01 (二進制),表示由 r/m 字段尋址的寄存器 + disp8 。也就是 ModR/M 字節後面將出現一個字節的 Displacement, 做爲對此寄存器值的偏移量。(此字節被有符號擴展到寄存器數據尺寸後,做爲對寄存器的值的修正。所以,這裏 disp8 = B4h = -4C h;
c). Reg/Opcode = 000 (二進制),或者指定一個寄存器號,或者做爲操做碼的擴展信息,具體用途由主操做碼指定。在該指令中此字段沒有實際意義。在 OpCode = 7C 時,看起來咱們只須要關心 R/M 的值(選擇下表所在的某一行),在行內橫向移動時改變的是 Reg/Opcode 字段的值,看起來彷佛是可有可無的。但實際證實,CPU 要求這個字節只能取第一列的值(也就是該字段必須爲 0 )。下表爲來自參考資料(5)(Intel 文檔)中的 ModR/M 字節尋址表。在本指令(Opcode = C7)
中,ModR/M 只能在第一列(圖中紅色方框內的數據,即尋址寄存器爲 AL/AX/EAX/MM0/XMM0 )中取值,若是在其餘列取值將會引起運行時異常(參見以下實驗)。
我作了一個實驗,當改變 ModR/M 字節的值(另其在行內橫向移動到第二列),例如將 0x0040121A 處的指令改成 C7 4D B4 XX XX 40 00 時,在 IDA 中能夠正常解析出和修改前同樣的指令, 可是運行時會提示異常,用 VS2005 調試,顯示其反彙編代碼,也會出現指令解釋錯誤,以下圖所示:
能夠看到在 VS 反彙編器中 0040121A 處指令(原指令爲 7 Bytes)沒法識別,和後面三個字節(.text:00401221 mov [ebp+WndClass.cbClsExtra], esi)混淆在一塊兒,沒法正確識別原有指令(上圖中紅色方框中的部分),直到 00401224 處,才恢復成正常解釋。
SIB 字節:
主要由 base + index 和 scale + index 尋址模式須要使用。scale 字段指定縮放因子,index 字段指定索引寄存器號。base 字段指定做爲基址的寄存器號。
【一些有趣的補充】
(1)能夠發現一個有趣的現象,在 EXE 類型的 Windows 程序中,傳遞給 WinMain 的第一個參數 hInstance 是一個 hardcode 的常數:0x0040 0000。也就是說,因爲 EXE 是進程的第一個被加載的 Module,而且 linker 對 EXE 的默認 ImageBase 是 0x0040 0000,因此 EXE 自身的 Module 老是位於進程空間的 0x0040 0000 位置。
(2)在資源表中的字符串是以 Unicode 編碼存儲的,而導入表中的字符串,是以 ASCII 編碼存儲的。二者分別採用了兩種編碼,這意味着程序要讀取 PE 文件的這兩個表,確定要作編碼轉換。爲何會這樣的?大概緣由多是:
導入表的字符串都是 DLL 和 函數的名稱,很明顯它們均可以也應該以 ASCII 編碼,也就是說,DLL 和 函數名稱一概都是英文的(字母+數字),至今我沒有據說過有誰用本身國家民族的特殊語言字符來爲 DLL 和函數命名,因此導入表中的字符串都是 ASCII 編碼,這對於存儲和網絡傳輸來講比較經濟(咱們知道,Windows 系統從 NT 開始內部已經統一採用 Unicode 字符串,在這種環境下,採用 Unicode 編碼的程序比採用多字節編碼的程序的運行效率更高,關於這一點 Matt Pietrek 在他的專欄曾經寫過文章比較這兩種編碼之間的性能差別,因此在如今所處的時代應該優先採用 Unicode 編碼,儘管 ASCII 編碼的 C-Style / STL 字符串更爲人們熟悉和慣用,但早就是時候改變習慣了)。
而資源就不同了,資源能夠由字符串來標識,徹底能夠用個性化的語言文字來定義,好比說用戶把菜單名字取名爲「個人上下文菜單」這樣的名稱,是徹底可能也被容許的,因此資源表中的字符串一概採用 Unicode 編碼。
(3)因爲個人筆記本安裝的是 Win7 / 64-bit 版本操做系統,因此在 IDA 中調試時竟然是 64 位模式,有一些不適應。
【下載連接】本題目的附件,和文本中提到的代碼的下載連接:
http://files.cnblogs.com/hoodlum1980/pediy02_Answer.zip
【參考資料】
[1]. hoodlum1980 (myself),讀取文件的導入表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/08/1821778.html。
[2]. hoodlum1980 (myself),讀取文件的資源表,http://www.cnblogs.com/hoodlum1980/archive/2010/09/10/1822906.html。
[3]. hoodlum1980 (myself),[VC6] 圖像文件格式數據查看器,http://www.cnblogs.com/hoodlum1980/archive/2010/09/05/1818308.html。
[4]. Billy Belceb,《病毒編寫教程---Win32篇》,「PE文件頭」章節,翻譯:onlyu。
來自:看雪論壇精華6 \ 病毒木馬技術 \ 病毒編寫 \ Billy Belceb 病毒教程Win32篇。
[5]. Intel® 64 and IA-32 Architectures Software Developer’s Manual,Volume 2 (2A, 2B & 2C): "Instruction Set Reference, A-Z",
--> CHAPTER 2. INSTRUCTION FORMAT
\ 2.1 INSTRUCTION FORMAT FOR PROTECTED MODE, REAL-ADDRESS MODE, AND VIRTUAL-8086 MODE
\ 2.1.3 ModR/M and SIB Bytes;
--> CHAPTER 3. INSTRUCTION SET REFERENCE, A-L \ MOV-Move;
--> APPENDIX B. INSTRUCTION FORMATS AND ENCODINGS;
【本文維護歷史】:
[1]. 從新制做本文中的插圖:圖 1 和圖 2,使其更加美觀,內容更加準確。2014-6。
[2]. 修訂對機器碼解讀表格中的部分說明。2014-6-27。
另:本文中的插圖(圖1,圖2),採用 Office 2007 - Excel 製做基礎資料,在 Photoshop CS 中進一步加工獲得。
【補充討論】
討論 1. 一個更通用一點的向 PE 文件插入常量字符串的函數。
文中使用的向 PE 插入字符串的函數過於簡單,其目前主要侷限在於:
(1)須要給出插入位置的文件地址。(人工計算得出)
(2)須要調整插入字符串後,受影響的 section header 中的 VirualSize 字段的值。
(3)僅僅考慮了 ASCII 字符串。
所以,我徹底能夠把這個函數作的更加簡單易用一些,但依然創建在如下假設條件下:
(1)文件具備一個只讀的 section; 且該 section 尾部有足夠的空間容納要插入的字符串。
加強易用性的函數的優勢是,僅僅須要給出待修改的 PE 文件的路徑,要插入的字符串,要寫入的字節數就能夠了,文件同時向調用方返回如下信息:只讀 section 的名稱,該字符串的文件地址,相對地址(RVA,不含 ImageBase)。
函數代碼以下(目前並無設置 ErrorMsg 的值,因此在目前版本中該參數目前僅佔位):
//.text 的 section.Characters #define INCLUDE_TEXT (IMAGE_SCN_MEM_READ \ | IMAGE_SCN_CNT_CODE \ | IMAGE_SCN_MEM_EXECUTE) #define EXCLUDE_TEXT (IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_DISCARDABLE) //.rdata 的 section.Characters; #define INCLUDE_RDATA IMAGE_SCN_MEM_READ #define EXCLUDE_RDATA (IMAGE_SCN_MEM_EXECUTE \ | IMAGE_SCN_MEM_WRITE \ | IMAGE_SCN_MEM_DISCARDABLE \ | IMAGE_SCN_CNT_CODE) BOOL InsertStringEx(LPCTSTR pFileName, //[in]要修改的 PE 文件路徑 LPVOID pStr, //[in]要插入的字符串 int nBytesToWrite, //[in]要寫入的字節數(包括 null terminator) DWORD dwInclude, //[in]節屬性中應該包含的屬性 DWORD dwExclude, //[in]節屬性中不該該包含的屬性 LPTSTR pSectionName, //[out] 輸出插入到了哪一個section中, 要求至少爲 9 chars LPDWORD pRVA, //[out]返回插入後字符串的RVA LPDWORD pFA, //[out] 返回插入的文件地址 LPTSTR pErrorMsg, //[out] 錯誤信息 UINT nBufSize); //更加智能的插入常量字符串,自動調整 SectionHeader.VirtualSize //假設 .rdata 段尾部具備足夠的空間 BOOL InsertStringEx(LPCTSTR pFileName, //[in]要修改的PE文件路徑 LPVOID pStr, //[in]要插入的字符串 int nBytesToWrite, //[in]要寫入的字節數(包括 null terminator) DWORD dwInclude, //[in]節屬性中應該包含的屬性 DWORD dwExclude, //[in]節屬性中不該該包含的屬性 LPTSTR pSectionName, //[out] 輸出插入到了哪一個section中, 要求至少爲 9 chars LPDWORD pRVA, //[out]返回插入後字符串的RVA LPDWORD pFA, //[out] 返回插入的文件地址 LPTSTR pErrorMsg, //[out] 錯誤信息 UINT nBufSize) { IMAGE_DOS_HEADER DosHdr; IMAGE_NT_HEADERS NtHdrs; PIMAGE_SECTION_HEADER pSectionHdrs = NULL; BOOL bRet = FALSE; int InsertPos; //須要計算 FILE *fp = NULL; errno_t nErr = _tfopen_s(&fp, pFileName, _T("r+b")); if(nErr != 0 || fp == NULL) goto _CLEANUP; fread(&DosHdr, 1, sizeof(IMAGE_DOS_HEADER), fp); fseek(fp, DosHdr.e_lfanew, SEEK_SET); fread(&NtHdrs, 1, sizeof(IMAGE_NT_HEADERS), fp); pSectionHdrs = (PIMAGE_SECTION_HEADER)malloc( sizeof(IMAGE_SECTION_HEADER) * NtHdrs.FileHeader.NumberOfSections); if(pSectionHdrs == NULL) goto _CLEANUP; fread(pSectionHdrs, sizeof(IMAGE_SECTION_HEADER), NtHdrs.FileHeader.NumberOfSections, fp); //找到只讀的section int i, iSection = -1; DWORD dwChar; //section 屬性 for(i = 0; i < NtHdrs.FileHeader.NumberOfSections; i++) { dwChar = pSectionHdrs[i].Characteristics; if((dwChar & dwInclude) == dwInclude && (dwChar & dwExclude) == 0) { iSection = i; break; } } //沒找到符合要求的section? if(iSection < 0) goto _CLEANUP; //計算section的插入地址 PIMAGE_SECTION_HEADER p1 = pSectionHdrs + iSection; if(pSectionName != NULL) { for(i = 0; i < 8; i++) pSectionName[i] = p1->Name[i]; pSectionName[i] = 0; } //計算當前的下一個section的地址 DWORD nNextAddr0 = GetAligned(p1->PointerToRawData + p1->Misc.VirtualSize, NtHdrs.OptionalHeader.SectionAlignment); //把它對齊到 16 bytes InsertPos = p1->PointerToRawData + p1->Misc.VirtualSize; InsertPos = GetAligned(InsertPos, 0x10); //判斷是否有足夠插入空間 DWORD nNewSectionSize = InsertPos + nBytesToWrite - p1->PointerToRawData; DWORD nNextAddr1 = GetAligned(p1->PointerToRawData + nNewSectionSize, NtHdrs.OptionalHeader.SectionAlignment); if(nNextAddr1 > nNextAddr0) goto _CLEANUP; //設置兩種地址 if(pFA != NULL) *pFA = InsertPos; if(pRVA != NULL) *pRVA = p1->VirtualAddress + (InsertPos - p1->PointerToRawData); //修改section hdr裏的值 fseek(fp, DosHdr.e_lfanew + sizeof(IMAGE_NT_HEADERS) + sizeof(IMAGE_SECTION_HEADER) * iSection + 8, //sizeof(IMAGE_SECTION_HEADER.Name) SEEK_SET); fwrite(&nNewSectionSize, sizeof(DWORD), 1, fp); //插入字符串 fseek(fp, InsertPos, SEEK_SET); fwrite(pStr, 1, nBytesToWrite, fp); bRet = TRUE; _CLEANUP: if(fp != NULL) fclose(fp); if(pSectionHdrs != NULL) free(pSectionHdrs); return bRet; }
其中,代碼中忘了附上 GetAligned 函數,其函數內容多是:
UINT GetAligned(UINT nVal, UINT nAlignUnit)
{
return (nVal + nAlignUnit - 1) / nAlignUnit * nAlignUnit;
}
能夠看到,加強版本函數去除了以前的三個侷限。使用起來更加方便(只須要提供 PE 的路徑和要插入的字符串內容就能夠了),徹底再也不須要關心那些瑣碎細節。例如:
//要追加字符串的 PE 文件路徑 TCHAR szExePath[MAX_PATH]; _tcscpy_s(szExePath, MAX_PATH, _T("E:\\pediy02_Test.exe")); DWORD dwRVA, dwFA; TCHAR szSectionName[16]; char ascii_str[256]; strcpy_s(ascii_str, _ARRAYSIZE(ascii_str), "this is a MultiByte ascii string."); InsertStringEx(szExePath, ascii_str, (strlen(ascii_str) + 1) * sizeof(char),
INCLUDE_RDATA, EXCLUDE_RDATA, szSectionName, &dwRVA, &dwFA, NULL, 0); wchar_t unicode_str[256]; wcscpy_s(unicode_str, _ARRAYSIZE(unicode_str), L"that is a WideChar unicode string."); InsertStringEx(szExePath, unicode_str, (wcslen(unicode_str) + 1) * sizeof(wchar_t),
INCLUDE_RDATA, EXCLUDE_RDATA, szSectionName, &dwRVA, &dwFA, NULL, 0);
調用了該函數成功後,PE文件就已經就緒了,再也不須要作其餘調整。只須要手工記錄下來函數返回的 RVA 地址便可,它能夠用於替換掉 .code 中的常量字符串的地址,例如替換 MessageBox 的參數,就可使得彈出的消息框顯示新的內容/標題。FA (文件地址)僅僅用於肯定在 16 進制編輯器中觀察插入的字符串是否正常和正確。
討論2. 導入表和 IAT 在內存中的佈局。
在本文的圖 1 給出了導入表的指針結構,但我但願對這些元素在內存空間(文件)中的佈局和位置有一個更直觀的認識,所以我寫了下面這個程序,來輸出位於 .rdata section 起始位置的導入表的全部元素。程序讀取全部的 Import Table Descriptors, Thunks, Ascii Strings, 根據這些元素的地址進行排序,以此復現他們在內存空間中的位置/出現次序。採用的 PE 文件即爲我給出的題目答案爲樣例。完整的程序代碼以下:
// ImportTable.cpp // 打印出一個 PE 文件的導入表的佈局分佈圖(在 .rdata 的頭部的位置) // #include "stdafx.h" #include <stdlib.h> #include <windows.h> #include <vector> #include <algorithm> #include <functional> //#include <stdarg.h> using namespace std; enum TypesDef { T_Descriptor = 0, //descriptor; T_Thunk = 1, //trunk T_String = 2, //常量字符串 }; typedef struct tagNODE { int RVA; //RVA int RVA_End; //在本身身後的RVA(不包含在本元素中) int FA; //文件地址 int type; char name[256]; } NODE, *LPNODE; int MyComparer(const void *pA, const void *pB); bool IsSuccessive(NODE a, NODE b); DWORD RVAToFA(DWORD rva, PIMAGE_SECTION_HEADER pSectionHdrs, int NumberOfSections); int ReadAsciiString(FILE* fp, char *pBuf); int ReadInt32(FILE* fp); int ReadInt16(FILE* fp); int _tmain(int argc, _TCHAR* argv[]) { //先遍歷PE文件,找出須要多少個NODE節點 BOOL bPrintGap = TRUE; vector<NODE> nodes; vector<NODE>::const_iterator pos; NODE node; TCHAR szPath[MAX_PATH]; _tcscpy_s(szPath, MAX_PATH, _T("E:\\pediy02_new.exe")); IMAGE_DOS_HEADER DosHdr; IMAGE_NT_HEADERS NtHdrs; PIMAGE_SECTION_HEADER pSectionHdrs = NULL; FILE *fp = NULL; errno_t nErr = _tfopen_s(&fp, szPath, _T("rb")); if(nErr != 0 || fp == NULL) goto _CLEANUP; fread(&DosHdr, 1, sizeof(IMAGE_DOS_HEADER), fp); fseek(fp, DosHdr.e_lfanew, SEEK_SET); fread(&NtHdrs, 1, sizeof(IMAGE_NT_HEADERS), fp); pSectionHdrs = (PIMAGE_SECTION_HEADER)malloc( sizeof(IMAGE_SECTION_HEADER) * NtHdrs.FileHeader.NumberOfSections); if(pSectionHdrs == NULL) goto _CLEANUP; fread(pSectionHdrs, sizeof(IMAGE_SECTION_HEADER), NtHdrs.FileHeader.NumberOfSections, fp); //已經讀出文件頭: int RVA_import_table, RVA_thunk, RVA_name, RVA_import_by_name; int FA_import_table, FA_thunk, FA_name, FA_import_by_name; int RvaArray[2]; RVA_import_table = NtHdrs.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; FA_import_table = RVAToFA(RVA_import_table, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); char buf[256]; int nDescriptorCount = 0; //非空元素數量 int nThunkCount = 0; //非空元素數量 int i; int Hint, BytesRead; IMAGE_IMPORT_DESCRIPTOR import_descriptor, null_descriptor; IMAGE_THUNK_DATA32 thunk_data; //IMAGE_IMPORT_BY_NAME import_by_name; memset(&null_descriptor, 0, sizeof(IMAGE_IMPORT_DESCRIPTOR)); while(TRUE) { fseek(fp, FA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount, SEEK_SET); fread(&import_descriptor, sizeof(IMAGE_IMPORT_DESCRIPTOR), 1, fp); if(memcmp(&import_descriptor, &null_descriptor, sizeof(IMAGE_IMPORT_DESCRIPTOR)) == 0) { node.RVA = RVA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.RVA_End = node.RVA + sizeof(IMAGE_IMPORT_DESCRIPTOR); node.FA = FA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.type = T_Descriptor; _tcscpy_s(node.name, _ARRAYSIZE(node.name), _T(" 00000000 <null>\n") _T(" FirstTrunk: 00000000 <null>\n") _T(" OriginalFirstTrunk: 00000000 <null>\n") _T(" Name: 00000000 <null>") ); nodes.push_back(node); break; } RVA_name = import_descriptor.Name; FA_name = RVAToFA(RVA_name, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); //DLL Name 字符串節點 (沒有Hint,因此Hint用 「----」 表示) fseek(fp, FA_name, SEEK_SET); BytesRead = ReadAsciiString(fp, buf); node.RVA = RVA_name; node.RVA_End = node.RVA + BytesRead; node.FA = FA_name; node.type = T_String; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("---- \"%s\""), buf); nodes.push_back(node); //descriptor節點 node.RVA = RVA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.RVA_End = node.RVA + sizeof(IMAGE_IMPORT_DESCRIPTOR); node.FA = FA_import_table + sizeof(IMAGE_IMPORT_DESCRIPTOR) * nDescriptorCount; node.type = T_Descriptor; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("%s\n") _T(" FirstTrunk: %08X\n") _T(" OriginalFirstTrunk: %08X\n") _T(" Name: %08X"), buf, import_descriptor.FirstThunk, import_descriptor.OriginalFirstThunk, import_descriptor.Name); nodes.push_back(node); //讀取FirstTrunk & OriginaTrunk; RvaArray[0] = import_descriptor.FirstThunk; RvaArray[1] = import_descriptor.OriginalFirstThunk; for(i = 0; i < 2; i++) { RVA_thunk = RvaArray[i]; FA_thunk = RVAToFA(RVA_thunk, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); nThunkCount = 0; while(TRUE) { fseek(fp, FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount, SEEK_SET); fread(&thunk_data, sizeof(IMAGE_THUNK_DATA32), 1, fp); if(thunk_data.u1.AddressOfData == 0) { node.type = T_Thunk; node.RVA = RVA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.RVA_End = node.RVA + sizeof(IMAGE_THUNK_DATA32); node.FA = FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; _tcscpy_s(node.name, _ARRAYSIZE(node.name), _T("00000000 ========[null]========")); nodes.push_back(node); break; } //按照什麼方式導入? if(thunk_data.u1.AddressOfData & IMAGE_ORDINAL_FLAG32) { node.RVA = RVA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.FA = FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.RVA_End = node.RVA + sizeof(IMAGE_THUNK_DATA32); node.type = T_Thunk; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("Ordinal: %ld"), (thunk_data.u1.Ordinal & 0x7FFFFFFF)); nodes.push_back(node); } else { RVA_import_by_name = thunk_data.u1.AddressOfData; FA_import_by_name = RVAToFA(RVA_import_by_name, pSectionHdrs, NtHdrs.FileHeader.NumberOfSections); fseek(fp, FA_import_by_name, SEEK_SET); Hint = ReadInt16(fp); BytesRead = ReadAsciiString(fp, buf); //字符串節點 if(i == 1) { //由於兩個數組的內容如出一轍,因此字符串只加一次就夠了 node.RVA = RVA_import_by_name; node.RVA_End = node.RVA + sizeof(WORD) + BytesRead; node.FA = FA_import_by_name; node.type = T_String; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("%04X \"%s\""), Hint, buf); nodes.push_back(node); } //Trunk節點 node.RVA = RVA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.RVA_End = node.RVA + sizeof(IMAGE_THUNK_DATA32); node.FA = FA_thunk + sizeof(IMAGE_THUNK_DATA32) * nThunkCount; node.type = T_Thunk; _stprintf_s(node.name, _ARRAYSIZE(node.name), _T("%08X %s"), RVA_import_by_name, buf); nodes.push_back(node); } nThunkCount++; } } nDescriptorCount++; } //打印結果 sort(nodes.begin(), nodes.end(), IsSuccessive); FILE *fpLog = NULL; _tfopen_s(&fpLog, _T("D:\\ImportTable_log.txt"), _T("w")); //fpLog = stdout; int PrevRVA_End = -1; i = 0; for(pos = nodes.begin(); pos != nodes.end(); ++pos) { i++; if((i & 0xFF) == 0) fflush(fpLog); //相鄰兩個元素之間存在空隙? if(bPrintGap && pos->type != T_String && PrevRVA_End >= 0 && pos->RVA > PrevRVA_End) { fprintf(fpLog, "-------------------------------------\n"); fprintf(fpLog, " GAP: 0x%08X (%ld Bytes)\n", pos->RVA - PrevRVA_End, pos->RVA - PrevRVA_End); fprintf(fpLog, "-------------------------------------\n"); } PrevRVA_End = pos->RVA_End; fprintf(fpLog, "%08X: ", pos->RVA); switch(pos->type) { case T_Descriptor: fprintf(fpLog, "Descriptor: "); break; case T_Thunk: fprintf(fpLog, " Trunk: "); break; case T_String: break; } fprintf(fpLog, "%s\n", pos->name); } fclose(fpLog); _CLEANUP: nodes.clear(); if(fp == NULL) printf("Canot open PE file.\n"); else fclose(fp); if(pSectionHdrs != NULL) free(pSectionHdrs); //printf("press any key to continue...\n"); //getchar(); return 0; } // qsort 用到的比較函數,本例中沒有用到 int MyComparer(const void *pA, const void *pB) { LPNODE pNode1 = (LPNODE)pA; LPNODE pNode2 = (LPNODE)pB; return (pNode1->RVA - pNode2->RVA); } //sort 用到的函數,兩個元素是已經排好序的嗎? bool IsSuccessive(NODE a, NODE b) { return (a.RVA < b.RVA); } DWORD RVAToFA(DWORD rva, PIMAGE_SECTION_HEADER pSectionHdrs, int NumberOfSections) { int i, iSection = -1; //查找該Rva位於那個段中 for(i = 0; i < NumberOfSections; i++) { if(rva >= pSectionHdrs[i].VirtualAddress && (rva <= pSectionHdrs[i].VirtualAddress + pSectionHdrs[i].Misc.VirtualSize)) { //該rva位於該段 iSection = i; break; } } //未找到? if(iSection < 0) return 0; //換算 return pSectionHdrs[iSection].PointerToRawData + (rva - pSectionHdrs[iSection].VirtualAddress); } //從 PE 文件中讀取一個長度不固定的 Ascii 字符串到緩衝區 //返回讀取的字節數(包括了 null_terminator, 即 retval = strlen(buf) + 1;) int ReadAsciiString(FILE* fp, char *pBuf) { int i = 0; while(TRUE) { fread(pBuf + i, 1, 1, fp); if(pBuf[i] == 0) break; ++i; } return i + 1; } int ReadInt32(FILE* fp) { int val; fread(&val, sizeof(DWORD), 1, fp); return val; } int ReadInt16(FILE* fp) { WORD val; fread(&val, sizeof(WORD), 1, fp); return val; }
程序產生的輸出以下(其中,地址均爲 RVA,全部被 Thunk 引用的字符串前面有兩個字節表示的 Hint。同時,程序中給出了相鄰元素之間的空隙字節數):
//FirstThunk 即爲 IAT 地址,也是 .rdata 的起始地址 7000: Trunk:78A2 DeleteObject //GDI32.dll 的 FirstThunk 7004: Trunk:79A0 GetTextExtentPoint32A 7008: Trunk:7994 BeginPath ...(此處省略若幹函數) 7044: Trunk:7896 RestoreDC 7048: Trunk:0000 ========[null]======== 704C: Trunk:7BA0 RtlUnwind //KERNEL32.dll 的 FirstThunk 7050: Trunk:7BAC WriteFile 7054: Trunk:7BB8 GetCPInfo ...(此處省略若幹函數) 70EC: Trunk:7C5E LCMapStringW 70F0: Trunk:0000 ========[null]======== 70F4: Trunk:7878 DefWindowProcA //USER32.dll 的 FirstThunk 70F8: Trunk:786A BeginPaint 70FC: Trunk:785E EndPaint ...(此處省略若幹函數) 7124: Trunk:77BA DispatchMessageA 7128: Trunk:0000 ========[null]======== -------------------------------------
GAP: 0x04EC (1260 bytes) ------------------------------------- 7618: Descriptor: KERNEL32.dll FirstTrunk: 704C OriginalFirstTrunk: 76B4 Name: 77AC 762C: Descriptor: USER32.dll FirstTrunk: 70F4 OriginalFirstTrunk: 775C Name: 788A 7640: Descriptor: GDI32.dll FirstTrunk: 7000 OriginalFirstTrunk: 7668 Name: 79B8 7654: Descriptor: <null> FirstTrunk: <null> OriginalFirstTrunk: <null> Name: <null> 7668: Trunk:78A2 DeleteObject //GDI32.dll 的 OriginalFirstThunk 766C: Trunk:79A0 GetTextExtentPoint32A 7670: Trunk:7994 BeginPath ...(此處省略若幹函數) 76AC: Trunk:7896 RestoreDC 76B0: Trunk:0000 ========[null]======== 76B4: Trunk:7BA0 RtlUnwind //KERNEL32.dll 的 OriginalFirstThunk 76B8: Trunk:7BAC WriteFile 76BC: Trunk:7BB8 GetCPInfo ...(此處省略若幹函數) 7754: Trunk:7C5E LCMapStringW 7758: Trunk:0000 ========[null]======== 775C: Trunk:7878 DefWindowProcA //USER32.dll 的 OriginalFirstThunk 7760: Trunk:786A BeginPaint 7764: Trunk:785E EndPaint ...(此處省略若干函數) 778C: Trunk:77BA DispatchMessageA 7790: Trunk:0000 ========[null]======== 7794: 0302 "lstrcpyA" //如下是 ascii 字符串 77A0: 0308 "lstrlenA"
77AC: ---- "KERNEL32.dll"
77BA: 0095 "DispatchMessageA"
77CE: 0282 "TranslateMessage"
77E2: 012A "GetMessageA"
77F0: 0291 "UpdateWindow"
7800: 026A "ShowWindow"
780E: 0059 "CreateWindowExA"
7820: 01F2 "RegisterClassA"
7832: 019A "LoadCursorA"
7840: 019E "LoadIconA"
784C: 01E0 "PostQuitMessage"
785E: 00BB "EndPaint"
786A: 000C "BeginPaint"
7878: 0084 "DefWindowProcA"
788A: ---- "USER32.dll"
7896: 01B9 "RestoreDC"
78A2: 0053 "DeleteObject"
... (此處省略略幹字符串)
7C5E: 01C0 "LCMapStringW"
根據以上的輸出,我能夠大概畫出導出表在加載後的鏡像所在的虛擬空間(文件空間)中的元素大概分佈,以下圖所示。
【注意】下圖對應於 VC 編譯的 Release 版本的一種典型結果,若是是 Debug 版本,則元素分佈可能和下圖不一樣。
其中,Ascii Strings 部分是長度不固定的字符串,相鄰的字符串之間有可能有 1 個字節的空隙(因爲這個空隙過小,對於咱們可以利用起來的意義不大,因此在圖中沒有畫出字符串之間的空隙)。最主要的空隙位於 FirstThunk 和 Import descriptors 之間,可能有超過 1 KB 的空間看起來是好像空閒的(尚有待驗證確認)。每一個 Thunk 是 4 Bytes(大多數狀況爲指向 Ascii 字符串的指針,也可能爲函數序號 Ordinal,例如 MFC 類庫函數均以 Ordinal 導入,若是通過事先綁定,則 FirstThunk 內容爲綁定後的函數地址),由 NULL 元素標識結束。每一個 Import descriptor 固定 20 Bytes,因爲這個尺寸有點不三不四,因此在 16 進制編輯器中會顯示的很不整齊(每一個元素佔據一行外加 4 Bytes,在下圖中對它們採用了很理想的對齊,在 16 進制編輯器中不存在的這樣的視圖)。Ascii Strings 爲字符串,若是是函數名稱,則字符串前面有兩個字節的 Hint。
PE 文件頭中的 OptionalHeader.DataDirectory[1].Address 指向第一個 Import Descriptor 所在的位置,即 Import Table 的地址,Size 爲全部 descriptor 的總大小(包含最後那個 NULL 元素)。DataDirectory[12].Address 指向第一個 FirstThunk 的起始位置,也就是 Import Address Table(IAT),Size 爲全部 Thunk 元素的總大小(包括全部的 NULL 元素)。這裏也就是系統加載時對全部導入函數的綁定後的實際地址(VA),在代碼段中將經過直接跳轉或者間接 call 的方式調用導入函數,IAT 元素的地址已經被 hardcode 到代碼段中(散亂分佈於代碼段中),這意味着要增長導入函數,就須要調整代碼段中的那些 hardcode 的 IAT 元素的地址,這將是一個稍顯麻煩的工做。
在圖中能夠看到指針對於二進制文件設計的地位和意義,圖中,Import Descriptors 數組和 Thunks 數組都是「元素尺寸固定」的「長度可變」數組,由 NULL 元素標識尾部。這些數組,要求元素尺寸固定,這對於規範 loader 的工做很是重要,因此凡是長度不固定的內容,就從元素中提取到後面的離散數據區(長度不固定元素集中存放的地方),在元素中保留爲一個大小固定的指針。
此外,從程序的輸出結果能夠看到,Thunks 數組出現的順序,和 Import Descriptor 的順序未必一致。例如,一個 Import Descriptor 在數組中排在後面,它的 Thunks 數組可能排在前面。但 FirstThunk 和 OriginalFirstThunk 指針數組集合(將二者看做多個指針數組組成的集合)中,這些指針數組的排列順序將是徹底一致的。
從導入表元素的佈局能夠看到,若是經過調整 PE 文件的內容,刪除元素可能比較容易,插入函數和新的 DLL 則是一件麻煩事,由於 linker 會把數組緊湊排列,不會留下插入空隙。這也就意味着若是要插入新的元素(例如增長一個已導入 DLL 的某個函數,或者增長一個新的導入 DLL 和若干函數),必然會致使現有的 IAT 發生必定變更。也就是說,好比假設以前已經有個導入函數爲 MessageBoxA,該函數實際被映射到進程空間中的 VA 被存儲於地址爲 0x00407010 的 IAT 元素,當插入新的元素時,這個 IAT 的地址就會發生變更,從而會影響到 .text 代碼段中全部對 MessageBoxA 的調用(這些調用至關於 「hardcoding" )。因此插入新的元素意味着:(1)必然須要調整現有的 IAT,而且增長新的函數名稱字符串。(2)搜索全部 .text 對受影響的導入函數的調用,並經過適當偏移來修正這些 IAT 元素的地址。
BTW: 特定的,對於本題目,若是要從 User32.dll 導入 MessageBoxA 則相對的簡單,能夠從其導入表元素空間分佈中看出這一點。對本題目,我已經手工完成了修改導入表,使其導入 MessageBoxA 函數,並在代碼段中調用它。由於不須要移動現有的 IAT,因此也不須要修正代碼段中的導入函數的 VA,相對的仍是比較簡單的(只是一些插入字符串,移動字符串,擴充 Thunks 數組,修正數組元素的值等操做爲主,例如,擴充數組時,會把緊挨在其後的一個或多個 Ascii 字符串擠到 idata 數據段的尾部去,覺得新的數組元素提供空間,在這裏我就不詳細展現這個過程了)。
-- [1]. 2014-06-15 首次補充;
-- [2]. 2014-06-19 增長 ImportTable 元素分佈示意圖和說明。