「金山杯2007逆向分析挑戰賽」第一階段第二題

  注:題目來自於如下連接地址: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 窗口:網絡

  點擊菜單彈出的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)。

 

  補充說明:在沒有通過事先綁定時,OriginalFirstTrunkFirstTrunk 指向的數組內容在加載以前都指向 .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 進行精心調整,從而避免在加載時重定向) 和 「事先綁定」 提升程序在客戶運行環境的加載速度,系統經過時間戳斷定綁定信息是否有效,若是時間戳不一致,或者發生重定向,系統則必須再次進行加載時綁定。

  

  OriginalFirstTrunkFirstTrunk 指向的這兩個指針數組位於 .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 )中取值,若是在其餘列取值將會引起運行時異常(參見以下實驗)。

 

  Table 2-2. 32-Bit Addressing Forms with the ModR/M Byte

 

  我作了一個實驗,當改變 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;
}
InsertStringEx_cpp

 

  其中,代碼中忘了附上 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;
}
ImportTable_Layout_cpp

 

  程序產生的輸出以下(其中,地址均爲 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 元素分佈示意圖和說明。

相關文章
相關標籤/搜索