PE結構-導入表

首先就是確定如何定位導入表的位置,導入表是位於數據目錄項的第二項

前四個字節爲相對虛擬地址(RVA),後四個字節爲大小(這裏做個參考,不依賴)

好了,既然是RVA,那麼說的就是內存中情況,那麼如何在文件中定位到導入表位置呢,這裏我們則需要做的就是將RVA轉換爲FA,這裏的話就大致來說明一下,不清楚的可以參考下其他博客

先來看一下節區的分佈情況,有2個節,分別在0x1000處和0x2000處,大小都爲0x1000

RVA=2020
1.查找RVA位於哪個節區內
    這裏很明顯位於.rdata內,該段起始爲0x2000
2.計算節內偏移
    也就是減去該節起始地址(0x2000) = 0x20
3.加上該節的文件起始偏移
    0x20 + 0x400 = 0x420

所以我們來看一下文件偏移爲0x420的地方

此時我選中的部分就是導入表的位置了。下面先不着急來分析字段,我們先來探討一下爲什麼需要導入表?

在我們程序運行的時候,可能需要調用一些外部接口,比如說動態庫,說到動態庫,很容易想到兩個API

LoadLibrary //加載動態庫
GetProcAddress //獲取模塊內函數地址

是的,我們可以使用上面兩個API來獲取到模塊中函數地址,並調用,那麼問題來了,這個地址可以寫死麼?

答案是不可以的,因爲該程序放到其他環境中運行時,該函數地址可能是會發生變化的,所以該這麼辦呢?

這裏的話就涉及到了編譯器與操作系統之間的交互了,編譯器會生成間接調用的代碼(留IAT接口),操作系統在裝載應用程序時會填寫該值,這樣子就保證了正確的函數指針了。有點抽象,下面來看一下這段彙編代碼

00401000 >/$  6A 00         push    0                                ; Style = MB_OK|MB_APPLMODAL
00401002  |.  68 1D204000   push    0040201D                         ; |Title = "PE"
00401007  |.  68 10204000   push    00402010                         ; |Text = "Hello World!"
0040100C  |.  6A 00         push    0                                ; |hOwner = NULL
0040100E  |.  E8 07000000   call    <jmp.&USER32.MessageBoxA>        ; 到 40101A處
00401013  |.  6A 00         push    0                                ; ExitCode = 0
00401015  \.  E8 06000000   call    <jmp.&KERNEL32.ExitProcess>      ; 到 401020處
;          jmp     dword ptr [402008]  間接調用-編譯器留的接口 402008,裏面的值由操作系統填寫
0040101A   $- FF25 08204000 jmp     dword ptr [<&USER32.MessageBoxA>>;  user32.MessageBoxA
;          jmp     dword ptr [402000]  與上同理
00401020   .- FF25 00204000 jmp     dword ptr [<&KERNEL32.ExitProces>;  kernel32.ExitProcess

上面彙編代碼處的兩個JMP,相當於就是編譯器留下的接口,供操作系統填寫,所以在不同的環境中,操作系統會負責該函數指針的正確性

其實這裏就是IAT表了(導入地址表)。好了下面再來說一說操作系統在填寫IAT表時,需要哪些信息呢?

通過上面的API可得,我們只需要知道哪個動態庫,以及動態庫中的函數名(或序號),這樣子就能獲取到函數地址了

那麼我們會發現動態庫和動態庫中的函數名是一個一對多的關係,我們自己先來簡易的設計一下

由於是一對多的關係,其實結構上我們可以設計成每個動態庫後面掛一個對應的函數信息表的結構

這樣子我們可以試想一下,我們遍歷動態庫信息表(外層循環),可以調用LoadLibrary來加載模塊,然後在遍歷其對應的函數信息表(內層循環),調用GetProcAddress來獲取函數地址填入IAT中。這樣子的的設計,相當於操作系統只要寫個雙層循環,就能將完成獲取到這些函數地址。

好了,上面理解完後,其實導入表的大概框架結構也就是上圖所示,下面我們來看一下大致的結構框架

IMAGE_DATA_DIRECTORY  //數據目錄結構
    IMAGE_IMPORT_DESCRIPTOR //動態庫信息 多個  以全零結構表示結尾
        IMAGE_THUNK_DATA //函數信息 多個 4字節 以全零表示結尾
            1.如果該值最高位爲1,那麼說明使用序號,取低WORD作爲序號
            2.否則說明是名字導出 指向 IMAGE_IMPORT_BY_NAME結構

是不是和外面上面畫的結構很像,首先數據目錄結構會指向IMAGE_IMPORT_DESCRIPTOR,該結構會有多個(相當於表中有多條記錄),以全零結構結束,所以上面說IMAGE_DATA_DIRECTORY裏面的大小是供參考的。

IMAGE_IMPORT_DESCRIPTOR裏的某個字段會指向IMAGE_THUNK_DATA,該結構就相當於函數信息,也會有多個,以全零結構結束。複雜的是該結構體其實只是一個四字節大小的數據,它分2種情況討論,因爲動態庫中的函數導出也是有2種情況的,分爲導出序號和導出函數名,正好相對應。

下面來看一下細節的結構

//導入描述結構-動態庫結構
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    }; //指向IMAGE_THUNK_DATA結構,也稱INT表(導入名稱表)
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;                           //指向動態庫名稱 RVA
    DWORD   FirstThunk;                     //指向IAT表,獲取地址後對應填寫到此處表中
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;


//函數信息描述表
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // 如果是函數名,則指向 IMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;


//函數名稱信息
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;  //通常填寫編譯時這個函數的序號,操作系統不做參考
    BYTE    Name[1];  //函數名稱
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

具體的一些解釋寫在上面的字段後面了,有個別沒寫註釋的,說明是不關鍵字段,可忽略。

下面,我們在文件中來跟着結構識別一個,首先上面可以定位到導入表了

我在每個結構之間使用了紅色豎線進行劃分,可以發現,該應用程序有兩個動態庫,最後一個全零表示結尾。

我們就來看一下第一個結構吧,首先先來看第二個紅框字段,該字段表示指向動態庫的名字,RVA 0x207A轉FA也就是0x47A

可以看出來是User32.DLL。再來看第一個紅框,表示指向INT,也就是函數信息結構0x2064轉FA也就是0x464

可以發現INT表中只有一項(以零結尾),說明我們只使用了User32.DLL中一個函數

我們來看第一項,0x206c,最高位不爲1,說明該函數以名字導出,所以該值又會指向IMAGE_IMPORT_BY_NAME,0x206c轉FA就是0x46C

該結構的第二個結構(紅框處)也就是名字了,說明我們程序使用了User32.DLL中的MessageBoxA函數。

好了,因爲只有一個函數,所以該動態庫已經分析差不多了,那麼現在回到IMAGE_IMPORT_DESCRIPTOR結構,第三個字段就是指向IAT表,因爲我們已經拿到了動態庫名和函數名,那麼自然就能獲取到地址了。

我們來看一下這個IAT表的值爲0x2008,加上ImageBase後就是0x402008,我們來回想一下上面的彙編調用代碼,可以發現正好吻合上了,當獲取函數指針後,操作系統就會負責填入其內。

;          jmp     dword ptr [402008]  間接調用-編譯器留的接口 402008,裏面的值由操作系統填寫
0040101A   $- FF25 08204000 jmp     dword ptr [<&USER32.MessageBoxA>>;  user32.MessageBoxA

好了,一個動態庫差不多就分析完了,可以繼續分析下一個了,這裏我就不繼續分析了,第二個其實說明的是Kernel32中的ExitProcess函數。

下面我們來嘗試一個修改導入表信息,首先,我們先來自己寫一個DLL,功能就是彈一個對話框,用於替代原先的MessageBoxA

extern "C"
int __stdcall MyBoxA(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType)
{
    MessageBoxA(hWnd,"Hook",NULL,NULL);
    return MessageBoxA(hWnd, lpText, lpCaption, uType);
}

該函數的功能就是先彈一個Hook的提示對話框,然後在彈出原先的對話框。

將生成的DLL放入應用程序的同目錄下,下面來修改下導入信息

雙擊運行後,你就能發現先彈出了Hook提示的對話框了

發佈了80 篇原創文章 · 獲贊 34 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章