程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 【學習】Windows PE文件學習(一:導出表),pe導出

【學習】Windows PE文件學習(一:導出表),pe導出

編輯:C++入門知識

【學習】Windows PE文件學習(一:導出表),pe導出


  今天做了一個讀取PE文件導出表的小程序,用來學習。

  參考了《Windows PE權威指南》一書。

  首先, PE文件的全稱是Portable Executable,可移植的可執行的文件,常見的EXE、DLL、OCX、SYS、COM都是PE文件。

  我們知道,一個Windows程序,它所實現的所有功能最終幾乎都是調用系統DLL提供的API函數。要使用任何一個DLL所提供的函數,我們需要將它導入,也就是用到了導入表。然而對於那些提供了被導出的函數的DLL程序來說,他們必須使用導出表將函數導出,之後別的程序才可以使用。無論是系統提供的標准DLL還是個人編寫的DLL,只要想提供自己的函數給別人使用就必須建立導出表。一般使用任何開發環境編寫具有導出功能的程序,導出表都是由鏈接器自動建立的。程序員只需指定被導出的函數名稱或序號即可。

  導出表通常出現在DLL文件的.edata節中。

  知道了導出表的位置,我們可以得到導出函數的地址,進而對這些函數進行Hook。而我們現在的目的是為了學習PE文件中導出表的構成,所以有必要了解PE文件的結構。

  

1 基本概念

  注:以下引用部分均來自網絡

下表描述了貫穿於本文中的一些概念:

名稱 描述 地址 是“虛擬地址”而不是“物理地址”。為什麼不是“物理地址”呢?因為數據在內存的位置經常在變,這樣可以節省內存開支、避開錯誤的內存位置等的優勢。同時用戶並不需要知道具體的“真實地址”,因為系統自己會為程序准備好內存空間的(只要內存足夠大) 鏡像文件 包含以EXE文件為代表的“可執行文件”、以DLL文件為代表的“動態鏈接庫”。為什麼用“鏡像”?這是因為他們常常被直接“復制”到內存,有“鏡像”的某種意思。看來西方人挺有想象力的哦^0^ RVA 英文全稱Relatively Virtual Address。偏移(又稱“相對虛擬地址”)。相對鏡像基址的偏移。(有時候不一定是相對鏡像的基址,還可能以某個結構的首地址為基址) 節 節是PE文件中代碼或數據的基本單元。原則上講,節只分為“代碼節”和“數據節”。(文件中節大小通常以磁盤的一個物理扇區也就是512B對齊,若是鏡像文件加載到內存中,以一個內存頁大小對齊,32位為4K,64位為8K) VA 英文全稱Virtual Address。虛擬地址(虛擬內存中的正常地址,不需要進行轉換)

    有特殊的節無論是在文件中還是在內存中,對齊粒度與其他的節都不同,如:資源字節碼以雙字對齊

2 PE文件的結構

  PE文件的總體結構:如果形象地說,即是3個頭和身子。3個頭是Dos頭、Nt頭和節表(節頭),身子就是一個一個地節(存放數據和代碼的地方)以上的各個頭部都是數據結構,可以在winnt.h頭文件中找到它們對應的struct定義(Nt頭分為32位和64位)。

  由於PE文件是兼容Windows NT以前的Dos系統的,所以現在的任何一個PE文件拿到Dos系統上都是可以運行的,不過大多數可能也只能打出一句話:“This program cannot be run in DOS mode”。這是由PE文件的結構中的Dos頭決定的。

用記事本打開任何一個鏡像文件,其頭2個字節必為字符串“MZ”,這是Mark Zbikowski的姓名縮寫,他是最初的MS-DOS設計者之一。然後是一些在MS-DOS下的一些參數,這些參數是在MS-DOS下運行該程序時要用到的。在這些參數的末尾也就是文件的偏移0x3C(第60字節)處是是一個4字節的PE文件簽名的偏移地址。該地址有一個專用名稱叫做“E_lfanew”。這個簽名是“PE00”(字母“P”和“E”後跟著兩個空字節)。緊跟著E_lfanew的是一個MS-DOS程序。那是一個運行於MS-DOS下的合法應用程序。當可執行文件(一般指exe、com文件)運行於MS-DOS下時,這個程序顯示“This program cannot be run in DOS mode(此程序不能在DOS模式下運行)”這條消息。用戶也可以自己更改該程序,有些還原軟件就是這麼干的。同時,有些程序既能運行於DOS又能運行於Windows下就是這個原因。Notepad.exe整個DOS頭大小為224個字節,大部分不能在DOS下運行的Win32文件都是這個值。MS-DOS程序是可有可無的,如果你想使文件大小盡可能的小可以省掉MS-DOS程序,同時把前面的參數都清0。

3 Nt頭部 IMAGE_NT_HEADERS

  PE文件中較為復雜的部分就是這裡了。

  在 2 中說到的DosHeader->E_lfanew所指向的簽名“PE\0\0”就是Nt頭的第一個成員了,我們在編程中得到Nt頭的方法也是這樣做的,因為Dos頭的第二部分MS-DOS程序部分的大小是可以改變的,連帶著整個Dos就是不定長的了,只有其中的E_lfanew指向它自己的末尾。

  Nt頭同樣分為兩部分(除去簽名4個字節):

  給出winnt.h中的定義

1 typedef struct _IMAGE_NT_HEADERS {
2     DWORD Signature;                        //4 bytes PE文件頭標志:(e_lfanew)->‘PE\0\0’
3     IMAGE_FILE_HEADER FileHeader;           //20 bytes PE文件物理分布的信息
4     IMAGE_OPTIONAL_HEADER32 OptionalHeader; //224bytes PE文件邏輯分布的信息
5 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

  其中的IMAGE_FILE_HEADER我們稱作文件頭,IMAGE_OPTIONAL_HEADER32稱作可選映像頭(我習慣稱之為選項頭)。有點滑稽的是,選項頭可以說是PE文件中最重要、最復雜的部分了,卻是可選的。。

同時我們看到,選項頭在32位和64位PE文件中結構是有所不同的,注意,只是有所不同而已,大致上還是沒什麼區別的。但是在編程中我們必須將其考慮進去,由於選項頭是不同的,所以Nt頭也會是不同的。

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;                //運行平台
    WORD    NumberOfSections;        //文件區塊數目
    DWORD   TimeDateStamp;            //文件創建日期和時間
    DWORD   PointerToSymbolTable;    //指向符號表(主要用於調試)
    DWORD   NumberOfSymbols;        //符號表中符號個數
    WORD    SizeOfOptionalHeader;        //IMAGE_OPTIONAL_HEADER32 結構大小
    WORD    Characteristics;            //文件屬性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

  但是文件頭還是很清晰明了的,其中比較常用的成員就是Machine和Characteristics了。都是用來判斷的,其中Machine標志了PE文件需要運行的目標平台,也就是期望在哪種指令集的CPU的平台上被加載,一般可以用來判斷PE文件是64位還是32位的;Characteristics是采用標志位的方式來判斷許多關於PE文件的信息,其中最重要的是判斷其是不是dll,使用的時候與(&)上就行了。

#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved external references).這是標志其能不能獨立運行,像dll就必須讓別的模塊來加載自己,但是exe和sys是自己加載運行的
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED       0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Aggressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE       0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED            0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP         0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                    0x1000  // System File.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.重要
#define IMAGE_FILE_UP_SYSTEM_ONLY            0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.32位
#define IMAGE_FILE_MACHINE_R3000             0x0162  // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000             0x0166  // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000            0x0168  // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2         0x0169  // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA             0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64 64位
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8) 64位
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

   接下來重點介紹選項頭 IMAGE_OPTIONAL_HEADER。

 

偏移(32/64) 大小 英文名 中文名 描述 0 2 Magic 魔數 這個無符號整數指出了鏡像文件的狀態。
0x10B表明這是一個32位鏡像文件。
0x107表明這是一個ROM鏡像。
0x20B表明這是一個64位鏡像文件。 2 1 MajorLinkerVersion 鏈接器的主版本號 鏈接器的主版本號。 3 1 MinorLinkerVersion 鏈接器的次版本號 鏈接器的次版本號。 4 4 SizeOfCode 代碼節大小 一般放在“.text”節裡。如果有多個代碼節的話,它是所有代碼節的和。必須是FileAlignment的整數倍,是在文件裡的大小。 8 4 SizeOfInitializedData 已初始化數大小 一般放在“.data”節裡。如果有多個這樣的節話,它是所有這些節的和。必須是FileAlignment的整數倍,是在文件裡的大小。 12 4 SizeOfUninitializedData 未初始化數大小 一般放在“.bss”節裡。如果有多個這樣的節話,它是所有這些節的和。必須是FileAlignment的整數倍,是在文件裡的大小。 16 4 AddressOfEntryPoint 入口點 當可執行文件被加載進內存時其入口點RVA。對於一般程序鏡像來說,它就是啟動地址。為0則從ImageBase開始執行。對於dll文件是可選的。 20 4 BaseOfCode 代碼基址 當鏡像被加載進內存時代碼節的開頭RVA。必須是SectionAlignment的整數倍。 24 4 BaseOfData 數據基址 當鏡像被加載進內存時數據節的開頭RVA。(在64位文件中此處被並入緊隨其後的ImageBase中。)必須是SectionAlignment的整數倍。 28/24 4/8 ImageBase 鏡像基址 當加載進內存時鏡像的第1個字節的首選地址。它必須是64K的倍數。DLL默認是10000000H。Windows CE 的EXE默認是00010000H。Windows 系列的EXE默認是00400000H。 32 4 SectionAlignment 內存對齊 當加載進內存時節的對齊值(以字節計)。它必須≥FileAlignment。默認是相應系統的頁面大小。 36 4 FileAlignment 文件對齊 用來對齊鏡像文件的節中的原始數據的對齊因子(以字節計)。它應該是界於512和64K之間的2的冪(包括這兩個邊界值)。默認是512。如果SectionAlignment小於相應系統的頁面大小,那麼FileAlignment必須與SectionAlignment相等。 40 2 MajorOperatingSystemVersion 主系統的主版本號 操作系統的版本號可以從“我的電腦”→“幫助”裡面看到,Windows XP是5.1。5是主版本號,1是次版本號 42 2 MinorOperatingSystemVersion 主系統的次版本號 44 2 MajorImageVersion 鏡像的主版本號 46 2 MinorImageVersion 鏡像的次版本號 48 2 MajorSubsystemVersion 子系統的主版本號 50 2 MinorSubsystemVersion 子系統的次版本號 52 2 Win32VersionValue 保留,必須為0 56 4 SizeOfImage 鏡像大小 當鏡像被加載進內存時的大小,包括所有的文件頭。向上捨入為SectionAlignment的倍數。 60 4 SizeOfHeaders 頭大小 所有頭的總大小,向上捨入為FileAlignment的倍數。可以以此值作為PE文件第一節的文件偏移量。 64 4 CheckSum 校驗和 鏡像文件的校驗和。計算校驗和的算法被合並到了Imagehlp.DLL 中。以下程序在加載時被校驗以確定其是否合法:所有的驅動程序、任何在引導時被加載的DLL以及加載進關鍵Windows進程中的DLL。 68 2 Subsystem 子系統類型 運行此鏡像所需的子系統。參考後面的“Windows子系統”部分。 70 2 DllCharacteristics DLL標識 參考後面的“DLL特征”部分。 72 4/8 SizeOfStackReserve 堆棧保留大小 最大大小。CPU的堆棧。默認是1MB。 76/80 4/8 SizeOfStackCommit 堆棧提交大小 初始提交的堆棧大小。默認是4KB。 80/88 4/8 SizeOfHeapReserve 堆保留大小 最大大小。編譯器分配的。默認是1MB。 84/96 4/8 SizeOfHeapCommit 堆棧交大小 初始提交的局部堆空間大小。默認是4KB。 88/104 4 LoaderFlags 保留,必須為0 92/108 4 NumberOfRvaAndSizes 目錄項數目

數據目錄項的個數。由於以前發行的Windows NT的原因,它只能為16。

96/112 8*16 DataDirectory 數據目錄

目錄項數組,包含16個目錄項

 

  這是完整的選項頭的結構,其中只提Magic和DataDirectory,至於鏡像加載時的基址與重定向問題,本文不做介紹,因為PE文件解析並不需要把鏡像給加載到我們自己的程序中,只需要映射到內存中,對其內容進行解析即可。

  對Magic域進行判斷,可以區分文件是64位還是32位,所以到現在我們有兩種方法來區分。

  本文的主角——導出表就是由DataDirectory[0]中的目錄項指出的,具體如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

  由此我們可以知道,DataDirectory並不是直接指向導出表的,真相是這樣的:DataDirectory是一個數組,每個項都是一樣的,IMAGE_DATA_DIRECTORY,每一項都由一個地址和大小,這就告訴我們導出表的基地址和其大小(別小看這個大小,我們會用到的)。

  得到了導出表的地址和大小,那麼我們就可以搞些事情了(23333~)。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;            // 這是這個PE文件的模塊名
    DWORD   Base;           
    DWORD   NumberOfFunctions;     // 這兩個域按字面意思理解,這個為總的導出函數的個數
    DWORD   NumberOfNames;      // 這個是有名稱的函數的個數,因為有的導出函數是沒有名字的,只有序號
    DWORD   AddressOfFunctions;     // RVA from base of image 這三個就是所謂的EAT,導出地址表
    DWORD   AddressOfNames;         // RVA from base of image Nt頭基址加上這個偏移得到的數組中存放所有的名稱字符串
    DWORD   AddressOfNameOrdinals;  // RVA from base of image Nt頭基址加上這個偏移得到的數組中存放所有的函數序號,並不一定是連續的,但一般和導出地址表是一一對應的
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

 

   這是導出表的結構,其中重要的域我用紅色的字標注了出來。

  我在網上查的資料說的比較清晰:

導出地址表(Export Address Table,EAT)

導出地址表的格式為下表所述的兩種格式之一。如果指定的地址不是位於導出節(其地址和長度由NT頭給出)中,那麼這個域就是一個Export RVA;否則這個域是一個Forwarder RVA,它給出了一個位於其它DLL中的符號的名稱。

偏移 大小 域 描述 0 4 Export RVA 當加載進內存時,導出函數RVA。 0 4 Forwarder RVA 這是指向導出節中一個以NULL結尾的ASCII碼字符串的指針。這個字符串必須位於Export Table(導出表)數據目錄項給出的范圍之內。這個字符串給出了導出函數所在DLL的名稱以及導出函數的名稱(例如“MYDLL.expfunc”),或者DLL的名稱以及導出函數的序數值(例如“MYDLL.#27”)。

Forwarder RVA導出了其它鏡像中定義的函數,使它看起來好像是當前鏡像導出的一樣。因此對於當前鏡像來說,這個符號同時既是導入函數又是導出函數。

例如對於Windows XP系統中的Kernel32.dll文件來說,它導出的“HeapAlloc”被轉發到“NTDLL.RtlAllocateHeap”。這樣就允許應用程序使用Windows XP系統中的Ntdll.dll模塊而不需要實際包含任何相關的導入信息。應用程序的導入表只與Kernel32.dll有關。

導出地址表的的值有時為0,此時表明這裡沒有導出函數。這是為了能與以前版本兼容,省去修改的麻煩。

導出名稱指針表

導出名稱指針表是由導出名稱表中的字符串的地址(RVA)組成的數組。二進制進行排序的,以便於搜索。

只有當導出名稱指針表中包含指向某個導出名稱的指針時,這個導出名稱才算被定義。換句話說,導出名稱指針表的值有可能為0,這是為了能與前面版本兼容。

導出序數表

導出序數表是由導出地址表的索引組成的一個數組,每個序數長16位。必須從序數值中減去Ordinal Base域的值得到的才是導出地址表真正的索引。注意,導出地址表真正的索引真正的索引是從0開始的。由此可見,微軟弄出Ordinal Base是找麻煩的。導出序數表的值和導出地址表的索引的值都是無符號數。

導出名稱指針表和導出名稱序數表是兩個並列的數組,將它們分開是為了使它們可以分別按照各自的邊界(前者是4個字節,後者是2個字節)對齊。在進行操作時,由導出名稱指針這一列給出導出函數的名稱,而由導出序數這一列給出這個導出函數對應的序數。導出名稱指針表的成員和導出序數表的成員通過同一個索引相關聯。

導出名稱表(Export Name Table,ENT)

導出名稱表的結構就是長度可變的一系列以NULL結尾的ASCII碼字符串。 導出名稱表包含的是導出名稱指針表實際指向的字符串。這個表的RVA是由導出名稱指針表的第1個值來確定的。這個表中的字符串都是函數名稱,其它文件可以通過它們調用函。

  這裡需要特別注意的是,有時候你在遍歷導出地址表的時候,有可能得到的並不是一個地址(或者說並不是目標函數的地址),而是一個字符串。那麼這就是遇到了函數轉發的情況。判斷方法就是上面所說的判斷這個指針是不是在導出表的范圍內。

  學習PE文件可能比較難想象其中的數據結構組織,因為比較復雜,所以我建議可以上網找關於PE文件各個結構的示意圖看看。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved