PE文件和COFF文件格式分析——導出表

回顧前文 PE文件和COFF文件格式分析(1),並以典型的 msvcp80.dll 來分析。

在這裏插入圖片描述
文件最開始是一個0x40字節的結構。

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

根據該結構中最後的 e_lfanew 知道下一個結構的地址是 0xf0。簡單的可以從文件開頭找PE 兩個字符也可以。該結構體是:

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
 
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

可以看到前面2個成員是一樣的,不區分32.64位,一共0x18個字節。
在這裏插入圖片描述
再次,按上圖看到2個紅色框出來的標記,0x010b 說明這是一個32位結構,也就是IMAGE_OPTIONAL_HEADER32 ( 如果是 0x020b 就是 IMAGE_OPTIONAL_HEADER64 )。0x00e0 是SizeOfOptionalHeader 的值。其中 IMAGE_OPTIONAL_HEADER32 如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //
 
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;
 
    //
    // NT additional fields.
    //
 
    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

可以看到大小是 96 字節+ 一個數組,如下圖紅色線之間就是96字節,綠色後就是連續 8 * 16 個結構。數組長度就是16。爲什麼是16個?因爲前面說到了:(0x00e0 是SizeOfOptionalHeader 的值)
在這裏插入圖片描述
根據如下定義,知道綠色部分就是導出表的位置信息:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16
 
#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

VirtualAddress = 0x03efa0;(其實這裏是虛擬地址,並不是文件的地址,但是太巧了,我學習時候用的這個DLL地址恰好虛擬地址和文件地址是一樣的!!,因此就數據分析是沒錯的,先分析數據,稍後再說地址應該怎麼尋找!
如下圖,發現 msvcp80.dll 導出表的虛擬地址和實際地址是一樣的!
在這裏插入圖片描述
Size = 03ea4c
跳到位置 0x03efa0 , 如下:
在這裏插入圖片描述
導出表的結構如下:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Characteristics是保留字段,要求爲0。

TimeDataStamp保存的生成導出信息的時間。

MajorVersion和MinorVersion分別是主版本號和此版本號。這些信息是我們可以決定的。

Name字段保存的該導出文件的名稱的偏移。這兒要注意一點,這個地址是系統不關心的,我們可以將其指向的地址設置爲違法的地址,這樣會干擾部分PE分析工具的分析結果。

Base是導出函數的起始序數值,該值一般爲1。如我們用View dependencies打開一個文件,紅色部分就是Base字段相關的
在這裏插入圖片描述
NumberOfFunctions標誌導出函數的函數地址數。該數據是非常重要的,我們要知道該文件導出了多少個函數就是要依據這個信息。我們之後會詳細說的。

NumberOfNames標誌導出函數的函數名數量。

AddressOfFunctions標誌導出函數的函數地址表的RVA。

AddressOfNames標誌導出函數的函數名錶的RVA。

AddressOfNameOrdinals標誌導出函數的導出序數表的RVA。

上面的數據圖中可以看到綠色的劃線,就是如下兩個數據的值。都是 0x0c6a (3178)。這是導出函數的總數。用 dependency 也可以確認這個數量。
DWORD NumberOfFunctions;
DWORD NumberOfNames;

標出各個字段的值(按順序對應):
在這裏插入圖片描述

上面說到虛擬地址和文件地址恰好一樣的問題,但往往他們是不一樣的。又如何查找呢?這裏插入查找的方法:

================================================================================

以 msvcp120.dll 爲例,先找到PE頭後面的 .rdata 節描述

在這裏插入圖片描述
用PEView.exe 查看更直觀
在這裏插入圖片描述
貼上 seciton 的定義:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;// 這裏看的就是這三個 0x55000
    DWORD   SizeOfRawData;//
    DWORD   PointerToRawData;//	0x54400
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

觀察兩個數據 0x55000 0x54400 可以知道。該文件虛擬地址相對文件地址大 (0x55000 - 0x54400) = 0xc00
上面圖中標紅了 DataDirectory[0] 的數據,也就是導出表的信息。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
 
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16
#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory

可以看到導出表的 VirtualAddress = 0x075570 Size = 0x01dd20。
上面說到有一個 0xc00 的偏差(對本文件來說)。因此實際地址就是 0x075570 - 0xc00 = ?
在這裏插入圖片描述
在這裏插入圖片描述
可以看到完全一致!
跳過去看看:
在這裏插入圖片描述

總結下如何查找導出表實際地址。通過 .rdata 基本段信息知道虛擬地址和文件地址的偏差數。然後就知道導出表的虛擬地址和文件地址如何換算了。參考 PE文件和COFF文件格式分析——RVA和RA相互計算

同時也不難想象,其他節一樣存在類似的轉換運算,也就是上面的”偏差數“不是針對文件裏面的所有偏差的。而是分類型。

================================================================================

用自己寫的一個有2個導出函數的DLL看看數據:
在這裏插入圖片描述
下圖可以看出來”差值“是 0x1000。
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
針對該DLl,對應結構體把數據寫出來:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image		 0x1a78e8 	->0x1a68e8
    DWORD   AddressOfNames;         // RVA from base of image		0x1a78f0	->0x1a68f0
    DWORD   AddressOfNameOrdinals;  // RVA from base of image		0x1a78f8	->0x1a68f8
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
上面 0x1a78e8 ->0x1a68e8 分別是虛擬地址和文件地址。差值是 0x1000。(記住這裏,稍後使用)

在這裏插入圖片描述

初次研究這個結構的同學可能會注意一個問題,該結構中有三個表的RVA(AddressOfFunctions,AddressOfNames,AddressOfNameOrdinals),而只給出了其中前兩個表的元素個數(NumberOfFunctions,NumberOfNames)。那第三個表——導出序數表的個數是多少?是按導出函數地址表(AddressOfFunctions)中元素個數(NumberOfFunctions)還是按導出函數名稱表(AddressOfNames)中元素個數(NumberOfNames)?還有個問題:爲什麼要設置Base屬性?這些問題我們先Mark下。我們先來詳細介紹這三個表。

導出地址表(AddressOfFunctions)。

顧名思義,該表中保存了函數入口RVA。但是如果僅僅是如此簡單就好了,這個地方保存的還可能是一個指向字符串的RVA!其結構是以下結構體的一個集合。

// 導出表信息
typedef struct _IMAGE_Export_Address_Table_
{
    union {
        DWORD dwExportRVA;
        DWORD dwForwarderRVA;
    };
}IMAGE_Export_Address_Table, *pIMAGE_Export_Address_Table;

上面說到:DWORD AddressOfFunctions; // RVA from base of image 0x1a78e8 ->0x1a68e8

如果它保存的是導出函數入口地址,那沒什麼好說的。我們說下它保存的是指向一個字符串的偏移的情況。在我的XP系統下Kernel32.dll中AddVectoredExceptionHandler函數的導出函數地址指向的字符串是NTDLL.RtlAddVectoredExceptionHandler。看到這樣的名字組合,我想你大概能猜出個眉目。AddVectoredExceptionHandler函數,在Kernel32.dll文件內部是沒有實現的。但是如果有程序需要加載Kernel32.dll並需要調用這個函數,則這樣的寫法會告訴加載器在加載Kernel32.dll時,要將AddVectoredExceptionHandler函數的地址直接改成Ntdll.dll中的RtlAddVectoredExceptionHandler函數地址(即自動加載Ntdll.dll)。這個特性非常有趣吧!我想做加殼的朋友應該對這個場景很熟悉。我之後會介紹利用這個特性去隱性自動加載DLL。最後說一下,我們如何辨別這個字段保存的是函數的入口地址的RVA還是字符串呢?只要判斷該偏移不在導出表節中即可:指向的地址在節中就是字符串的RVA;在節外是函數入口的RVA。

看實際數據 0x1a68e8 地址的數據
在這裏插入圖片描述
在這裏插入圖片描述
得到兩個函數的地址 0x00056820 0x00056840。

導出名稱表(AddressOfNames)。

計算機做出來是給人用的,如果給人一堆010101這樣的數據,我想沒誰會有太多興趣去看的。於是出於人性化考慮,人們發明了別名,比如發明了彙編映射二進制指令,從而幫助理解程序邏輯。導出名稱表就是出於這樣的考慮而設計的。其結構是以下結構體的一個集合。

typedef struct _IMAGE_Export_Name_Pointer_Table_ {
    DWORD dwPointer;
}IMAGE_Export_Name_Pointer_Table,*pIMAGE_Export_Name_Pointer_Table; 0x1a78f0	->0x1a68f0

它是指向字符串的RVA,該字符串是以\0結尾的。一共有2個字符串(NumberOfNames = 2)。前面提到有導出的總數。
在這裏插入圖片描述
如上分別有紅色和綠色的兩個地址,減去偏差指向的就是導出函數名稱。
同時發現者附近有一個ProxyDll.dll。也就是前面說到的 Name 。這個字符串的地址是: 0x1a68fc。加 0x1000就是 0x1a78fc。
如下圖:
在這裏插入圖片描述
再以 msvcp80.dll 爲例:
在這裏插入圖片描述
前面提到過 msvcp80.dll 這個地址沒有偏差,直接就是 0x046bec。
在這裏插入圖片描述

說到這兒,我覺得我們可以停下思考一個問題,是不是只要有這兩個表就夠了?如果對於我們自己編寫的且非常標準的DLL,只要有這兩個表的確是夠了。你想,當我們調用GetProcAddress時,我們在導入名稱表中找到該名稱對應的index,然後再返回導出函數地址表中該index的數據即可。
lpFunc = ExportAddressTable[ExportNameTable.find(FuncName)]

但是,PE文件設計的遠沒有這麼簡單。如果如此簡單,那很多事都好辦了。舉一個特殊的例子來推翻這種簡單的場景: 函數入口地址和函數名之間的關係是1對N(0~n)。我們程序運行起來後,很多時候是要調用其他邏輯,即函數入口。可以說一個函數入口可以唯一標註一個邏輯。而我們經常說的某某API,其實只是某個函數過程的一個名字。比如我們一個實現XML解析的函數,我們可以叫做ParseXML,也可以叫XMLParse。不管是叫哪個名字,該函數的功能是不變的,它的入口地址是不變的。如果入口地址變了,那就是另外一個函數了。這就是爲什麼說函數入口地址和函數名之間是1對N的關係。
在這裏插入圖片描述
針對以上問題,可能有人會想到,有多少個導出函數名(以導出函數名的數量爲標準)就設置多少個導出地址,導出地址表中數據可以重複,比如上圖中ParseXML和XMLParse函數名對應的導出地址都設置成0xXXXXXXXX就行了嘛。如
在這裏插入圖片描述
但是還有個場景:windows平臺可以通過序數導入一個函數地址(GerProcAddress的第二個參數傳序數),那麼這就意味着函數可以沒有函數名!!因爲序數也可以看成一個函數的編號嘛,雖然這樣非常不友好,但是仍然是一種可行的方法。那麼如果在這種場景下,我們還能以導出函數名的數量爲標準麼?不可以了吧,因爲函數名錶元素數量可能是0!其實這類文件挺多,如mfc40u.dll,見下圖
在這裏插入圖片描述
通過以上分析,我們可以得出,我們還是要一個能在導出函數地址表和導出函數名稱表建立紐帶的結構體。這個我們期待的輔助結構體就是我們下面介紹的導出序數表。

總結一下爲什麼兩個表不夠,還需要導出序數表:第一可能N個地址,N+M個名稱,於是需要一個明確的N+M對應到N個地址的方法。第二,Windows 允許直接用序數訪問。
導出序數表(AddressOfNameOrdinals)。

該表保存的是導出地址表的序數偏移!切記這個重要的概念。那這個偏移是相對什麼偏移的呢?是針對IMAGE_EXPORT_DIRECTORY::Base屬性的。即這個表中保存的值加上Base,就是導出地址表的序數。其結構是以下結構體的一個集合(也就是 WORD[N])。

typedef struct _IMAGE_Export_Ordinal_Table_ {
    WORD dwOrdinal;
}IMAGE_Export_Ordinal_Table,*pIMAGE_Export_Ordinal_Table;

從這個表的命名(AddressOfNameOrdinals )看,應該可以發現這個表應該和導出名稱表存在一定的關係!是的,它的元素的數量和導出名稱表的元素數量是一樣的。可能有人會疑問,什麼這個表元素的個數不是和導出地址表元素個數一致呢?因爲如上面所說,一個函數過程可以對應多個函數名,如果導出序數表元素個數和導出函數地址表元素個數一樣,則無法讓地址與函數名對應上。比如我們導出地址表有1個函數入口,而我們有2個函數名都指向這個地址,那麼導出序數表個數如果是1,則如何表示這兩個名稱與函數入口的對應呢?如果導出序數表格式是2個,則我們可以讓這兩個元素都“指向”同一個導出函數入口即可。OK,這兒我就解答了上面我們Mark過的那個問題:導出序數表個數和導出名稱表個數一致。

那麼這三個表之間具體什麼關係呢?我首先以一個簡單的、常規的文件爲例,這個文件是上面提到的msvcp80.dll。我們看一下View Dependencies的分析結果:

在這裏插入圖片描述

我們再把它的PE文件拿出來看下

在這裏插入圖片描述
我們把各個信息提取出來看下:

 Characteristics;        0x00000000
TimeDateStamp;          0x457122c8
MajorVersion;           0x0000
MinorVersion;           0x0000
Name;                   0x00046bec
Base;                   0x00000001
NumberOfFunctions;      0x00000c6a = 3178
NumberOfNames;          0x00000c6a
AddressOfFunctions;     0x0003efc8
AddressOfNames;         0x00042170
AddressOfNameOrdinals;  0x00045318

可以看到這個Dll的導出地址表有3178個元素,導出名稱表和導出序數表也是有3178個元素的。用之前《PE文件和COFF文件格式分析——RVA和RA相互計算》介紹的算法(或前文說到的偏差值,這裏msvcp80.dll 這些數據的偏差位0,請注意),我們可以得出

導出地址表RVA(0x0003efc8)對應的RA是0x0003efc8。一共連續3178個元素分別爲{ {0,0x01e1dc},{1,0x01e0c4}}…和View Dependencis分析結果對比發現,這組數據是一致的。如下圖:
在這裏插入圖片描述
在這裏插入圖片描述

導出名稱表上面分析 pryxodll.dll 已經分析了,此處省略。

導出序數表RVA(0x45318)對應的RA是 0x45318,其數據是{{0,0x0000},{1,0x0001}}…但是這並不是最終數據,剛纔我在介紹導出序數表時,說過這個表保存的是相對Base的偏移,該文件的Base是1,於是真實的數據是{{0,0x0001},{1,0x0002}}。
如下圖:
在這裏插入圖片描述
可以看到從 1.2.3…再看最後一個,理論上是 3178 - Base(1) = 3177 = 0x0c69
如圖:
在這裏插入圖片描述
可以看到0c69 之後就是 msvcp80.dll。(這個名稱前面有提到)
我們用圖來說一下這三者的關係。

在這裏插入圖片描述
有關 Base 作用及相關問題,可以參考 PE文件和COFF文件格式分析——導出表

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章