1. 程式人生 > >PE知識復習之PE的導出表

PE知識復習之PE的導出表

位置 別人 而是 cep 當前 inf 指向 glob 17.

                   PE知識復習之PE的導出表

一丶簡介

 在說明PE導出表之前.我們要理解.一個PE可執行程序.是由一個文件組成的嗎.

答案: 不是.是由很多PE文件組成.DLL也是PE文件.如果我們PE文件運行.那麽就需要依賴DLL.系統DLL就是Kerner32.dll user32.dll等等.這些都是PE文件.

什麽是導出表:

    導出表就是當前的PE文件提供了那些函數.給別人用. 舉個例子: PE文件相當於一個飯店.那麽菜單就是導出表.

導出表解盲:

    有人認為exe可執行文件.沒有導出表.而DLL有導出表.這個是錯誤的. 不管是exe.還是DLL 本質都是PE文件. exe文件也可以導出函數給別人使用. 一般EXE沒有.但不是不可以有. 註意分清.

二丶導出表講解

    在講解導出表之前.我們要確定導出表在哪裏.

在講解擴展頭的時候.裏面有一個結構體數組.我們稱之為數據目錄.裏面有16項成員.

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;                        虛擬地址
    DWORD   Size;                                  大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

導入表.導出表都在數據目錄中存儲著.

這個結構存儲的是導出表在哪裏.以及導出表有多大.

其中數據目錄每一項都是保存著不同的表

例如第一項就是導出表. 記錄了導出表的虛擬地址 以及大小.

如下:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   //
Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

因為結構體記錄的是導出表的RVA. 所以我們需要轉換為FOA 去PE文件中查看.

RVA 判斷在那個節. RVA-節.VirtuallAddress == 差值偏移

FOA == 差值偏移+ 節.PointerToRawData

前邊所說.是定位導出表在哪裏. 定位之後.才是真正的導出表結構體.

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    // 不加紅的不重要
    DWORD   TimeDateStamp;      //時間戳.  編譯的時間. 把秒轉為時間.可以知道這個DLL是什麽時候編譯出來的.
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;           //指向該導出表文件名的字符串,也就是這個DLL的名稱  輔助信息.修改不影響  存儲的RVA 如果想在文件中查看.自己計算一下FOA即可.
    DWORD   Base;           // 導出函數的起始序號
    DWORD   NumberOfFunctions;     //所有的導出函數的個數
    DWORD   NumberOfNames;         //以名字導出的函數的個數
    DWORD   AddressOfFunctions;     // 導出的函數地址的 地址表  RVA  也就是 函數地址表  
    DWORD   AddressOfNames;         // 導出的函數名稱表的  RVA      也就是 函數名稱表
    DWORD   AddressOfNameOrdinals;  // 導出函數序號表的RVA         也就是 函數序號表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

一個導出表大小是 0x28個字節. 也就是兩行半.

其中重要成員都標紅了. 最重要的是導出表中最後三個成員.是三個子表.

都是RVA

PS: 數據目錄中的 Size成員.保存的是導出表中以及導出表子表中的所有成員大小. 這個值不影響.編譯器計算後填寫好的.

三丶導出表各成員解析

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;    // 不加紅的不重要
    DWORD   TimeDateStamp;      //時間戳.  編譯的時間. 把秒轉為時間.可以知道這個DLL是什麽時候編譯出來的.
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;           //指向該導出表文件名的字符串,也就是這個DLL的名稱  輔助信息.修改不影響  存儲的RVA 如果想在文件中查看.自己計算一下FOA即可.
    DWORD   Base;           // 導出函數的起始序號
    DWORD   NumberOfFunctions;     //所有的導出函數的個數
    DWORD   NumberOfNames;         //以名字導出的函數的個數
    DWORD   AddressOfFunctions;     // 導出的函數地址的 地址表  RVA  也就是 函數地址表  
    DWORD   AddressOfNames;         // 導出的函數名稱表的  RVA      也就是 函數名稱表
    DWORD   AddressOfNameOrdinals;  // 導出函數序號表的RVA         也就是 函數序號表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

解析導出表.所需要的是一個DLL. 這裏我拷貝一下系統的DLL kerner32.dll進行解析.

解析導出表的第一步就是定位導出表.求出FOA. 也就是在文件中的位置.

數據目錄中查看導出表RVA

技術分享圖片

在數據目錄中得出導出表RVA == 0x90380 大小 == D4DC

查看屬於那個節.求出FOA

技術分享圖片

得出在.rdata節中. 節.虛擬地址 == 0x80000 節.文件偏移 == 0x65000

FOA = 0x90380 - 0x80000 + 0x65000 == 0x75380

所以在PE文件中.文件偏移 0x75380為導出表結構.

技術分享圖片

1.Name成員解析

首先解析導出表重要的成員

Nmae: 在導出表一行位置處. 存儲0x9416A. 這是一個RVA 所以我們要進行FOA轉換. 這裏直接計算了. FOA == 7916A

技術分享圖片

可見這個成員保存的就是自己DLL的名稱.

2.Base成員解析. 導出函數起始序號

導出函數的序號起始位置. 你DLL導出的函數.如果給序號了.那麽就從這個序號開始.

3.NumberOfFunctions 以及 NumberOfNmaes 函數導出總個數.以及函數以名字導出的個數

這個兩個成員很簡單. 一個就是所有函數導出的個數.一個就是以名字進行導出的個數. DLL是可以以序號導出的.

技術分享圖片

所有函數導出是 62d個函數. 名字導出是 62d個函數. 如果有按照序號導出.那麽以函數名導出的個數就會跟所有函數導出個數不一樣.

那麽就有公式可以計算出. 未導出的函數是多少個.

所有導出函數個數 - 以名字導出的個數 == 差值個數. 未導出的.或者是以序號導出的.

4.1函數地址表

  前面的都很簡單.下面的就是子表了.

技術分享圖片

三個子表都是RVA 我們直接都進行一下FOA轉換.

函數地址表 FOA == 0x753A8

函數名稱表 FOA == 0x76c5c

函數序號表 FOA == 0x78510

  函數地址表: 函數地址表指向一個偏移. 這個偏移存放了函數所有導出個數的 函數的地址.

例如所有導出函數有2個. 那麽函數地址表中就有2項. 沒一個占4個字節. 存放的是函數地址的 RVA偏移.

技術分享圖片

函數地址表. 4個字節進行存儲. 總共有函數所有導出函數個數大小個字節. 例如第一項 RVA偏移為 0x0162A0 函數地址偏移 + ImageBase 就是函數地址.

技術分享圖片

例如我電腦上Kerner32.dll加載的Imagebase為 76360000 我們在文件中看的函數偏移為 0x162A0 相加就得出一個導出函數地址了 0x763762A0

PS: 因為我們在文件中查看導出表.所以一直在轉換FOA ,如果在內存中查看就很簡單了.

數據目錄的RVA + ImageBase 定位導出表位置.

導出表結構體中定義的RVA偏移+Imagebase就能得出其它表的位置.就不用我們進行轉換了.

還需要註意的就是,如果你按照序號導出. 1 3 4 5導出了4個函數. 在導入表中我們的函數地址表中的地址會有5個.原因就是.序號會給我們用0填充. 1 2 3 4 5 雖然第二項並沒有.但是也會給我們導出.

如果函數地址我們已經知道了.我們要怎麽只有函數地址的情況下.確定是哪個函數?

4.2 函數名稱表

  函數名稱表也是存儲的名稱RVA. 4個字節存儲一個. 存儲的大小 跟導出表的以函數名字導出個數 這個成員來決定的.

以名稱導出函數的個數 例如為10 .那麽函數名稱表就可以存儲10個RVA. 每一個為4個字節.

裏面的RVA指向了當前導出函數的函數名稱.

例如上面已經算出 函數地址表的FOA位置

函數名稱表 FOA == 0x76c5c

那麽我們去函數名稱表中查看.

技術分享圖片

表中存儲的都是RVA. 如果在內存中.我們直接RVA + 當前PE的ImageBase就可以看到函數導出的名稱了.不過我們現在算一下.

FOA = 0x941D6 - 0x80000 + 0x65000 = 0x791D6

我們表中的第一項的FOA位置為0x791d6 在文件中就保存這導出函數的名稱

例如下圖文件偏移處:

技術分享圖片

註意: 函數名稱表保存的並不是函數名稱.而是指向函數名稱的RVA偏移. 還有RVA偏移是按照字母排序的.並不是按照你導出的時候函數的順序進行排序的.

例如:

  EXPORT

    SUB

    ADD

    MUL

導出三個函數.那麽第一項就為 ADD.因為按照字母排序.A在前邊.後面依次類推. 所以我們上面看到的函數名稱 ACquireSRW 這個函數名稱.並不是Kerner32.dll第一個導出的函數.

4.3函數序號表

  我們DLL導出函數的時候.會有序號進行導出.但是並不是說.如果按照名字導出名稱表中有.序號表中就沒有.

序號表的個數跟函數名稱表個數是一樣的.都依賴成員 導出表.函數名稱導出表個數 這個成員來決定的.

序號表是給名稱表的使用的. 序號表占兩個字節.存儲序號.

函數序號表 FOA == 0x78510

技術分享圖片

0300 0400 0500 序號.兩個字節進行存儲的

常用函數 GetProcAddress(模塊,名字或者序號)

我們這個函數就是遍歷PE文件中導出表進行返回的. 那麽他是如何實現的.如何通過名字查找函數地址. 或者如何通過序號進行查找函數地址的?

首先我們要分成三張表,函數地址表中序號開始的位置是導出表成員Base指定的.假設為0開始.

函數地址表 序號表 函數名稱表

0  0x1010 sub 0 0x0100 0 Add

1 0x2020 Add 1 0x0000 1 Sub

2  0x3030 Div 2 0x0200 2 DiV

首先GetProcAddress 如果按照名稱查找的話.會先去遍歷函數名稱表. 比如我們要獲取Sub的地址. 遍歷函數名稱表的時候.找到了Sub. 並獲取當前Sub的索引. sub是在第二項中.所以索引為1 (從0開始)

然後拿著這個索引.去序號表中進行查找對比. 在序號表中查到了.對比成功.序號表中第2項的值跟這個索引一樣的.所以就拿序號表的序號. 去函數地址表中獲取函數地址.

序號為0x0000. 那麽他就在函數地址表中.找到了第0項. 當函數地址進行返回. (並不是直接返回,加上了當前DLL模塊的ImageBase才返回的,所以為什麽需要DLL模塊地址)

所以上面就是GetProcAddress的名字查找的實現流程

如果是序號來查找的話.比如我們尋找 14序號. 他會先根據導出表中Base成員屬性.將表的起始位置進行一次定義.

例如上面.我們找的14序號並不存在. 但是他會先看看Base起始位置是多少. 假設為13. 那麽我們函數地址表中 0索引 相當於 13 1索引相當於 14 2索引相當於15了.依次類推.

這樣我們雖然說尋找14. 但是根據Base起始位置的指定.那麽也會尋找到我們的函數地址.

總結來說 :

    1.遍歷函數名稱表 得出索引

    2.當前索引.去序號表中查找.如果有.則取出當前序號表的序號.當做函數地址表的下標

    3.得出下標. 返回函數地址 (RVA +IMAGEbase)

PE知識復習之PE的導出表