前言
本博客系統講解了PE文件結構。
PE文件結構其實不復雜,但內容較多,希望朋友們能“沉心靜氣做學問”。
目錄
- 前言
- 1 PE文件及其表現形式
- 2 PE文件格式與惡意軟件的關係
- 3 PE文件格式的總體結構
- 3.1 PE文件原始數據
- 3.2 相關惡意代碼分析工具
- 3.2.1 PE文件格式查看工具1-`PEView`
- 3.2.2 PE文件格式查看工具2-`Stud_PE`
- 3.2.3 PE程序調試工具-`Ollydbg`
- 3.2.4 16進制文件編輯工具-UltraEdit
- 3.3 DOS-MZ文件頭(0x40)+DOS Stub(IMAGE_DOS_HEADER/MS-DOS Stub Program)
- 3.4 PE文件頭(IMAGE_NT_HEADERS)
- 3.5 節表(區塊表)(IMAGE_SECTION_HEADER)
- 3.6 區塊(節)
- 3.7 代碼節 .text
- 3.8 已初始化的數據節 .data
- 3.9 未初始化的數據節 .bbs
- 3.10 引入函數節 .rdata:PE文件的引入函數機制
- 3.10.1 引入目錄表 (輸入表)(IMPORT Directory Table)
- 3.10.2 引入名字表(IMPORT Name Table)
- 3.10.3 IMPORT Hints/Names &DLL names
- 3.10.4 引入地址表 IAT ( IMPORT Address Table)
- 3.11 引出函數節 .edata:DLL文件的函數引出機制
- 3.11.1 引出目錄表(導出表、輸出表)IMAGE_EXPORT_DIRECTORY
- 3.11.2 導出地址表 -EXPORT ADDRESS Table
- 3.11.3 導出名字表 -EXPORT Name Table
- 3.11.4 導出序號表 -EXPORT Oridinal Table
- 3.11.5 如何通過函數名定位函數導出地址?
- 3.12 資源節 .rsrc:文件資源索引、定位與修改
- 3.13 重定位節 .reloc :鏡像地址改變後的地址自動修正
- 4 思考題
- 5 實驗題
1 PE文件及其表現形式
-
可移植的可執行文件(PE,Portable Executable File),
Win32平臺可執行文件使用的一種格式
。 -
其他EXE文件格式:
DOS
:MZ格式Windows 3.0/3.1
:- NE,New Executable
- 16位Windows可執行文件格式
-
可執行程序的不同形態(以QQ爲例)
-
用戶眼中的QQ.
-
本質上:
-
2 PE文件格式與惡意軟件的關係
-
何爲文件感染?[或控制權獲取]
- 使目標PE文件
具備[或啓動]病毒功能[或目標程序]
- 但
不破壞
目標PE文件原有
功能和外在形態(如圖標)等
- 使目標PE文件
-
病毒代碼如何與目標PE文件融爲一體?
- 代碼植入
需要了解PE文件中哪裏可以植入代碼。 - 控制權獲取
- 圖標更改
- …
- 代碼植入
3 PE文件格式的總體結構
PE文件至少包含兩個段,即數據段和代碼段
。Windows NT 的應用程序有9個預定義的段,分別爲 .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,這些段並不是都是必須的
,當然,也可以根據需要定義更多的段(比如一些加殼程序)
。
在應用程序中最常出現的段有以下6種:
中文名 | 英文名 |
---|---|
.執行代碼段 | 通常 .text (Microsoft)或 CODE(Borland)命名; |
.數據段 | 通常以 .data 、.rdata 或 .bss(Microsoft) |
.資源段 | 通常以 .rsrc命名 |
.導出表 | 通常以 .edata命名 |
.導入表 | 通常以 .idata命名 |
.調試信息段 | 通常以 .debug命名 |
3.1 PE文件原始數據
使用UltraEdit打開之後看到的是PE文件的十六進制代碼。
以text.exe爲例:
3.2 相關惡意代碼分析工具
看雪學院論壇可以下載。
3.2.1 PE文件格式查看工具1-PEView
可按照PE文件格式對目標文件的各字段
進行詳細解析。
3.2.2 PE文件格式查看工具2-Stud_PE
可按照PE文件格式對目標文件的各字段
進行詳細解析。與 PEView
功能類似。
使用前的設置
設置之後可以使用右鍵用Stud_PE打開PE文件。
3.2.3 PE程序調試工具-Ollydbg
可跟蹤
目標程序的執行過程,屬於用戶態調試工具。
無法調試內核程序。
使用前的設置
設置之後可以使用右鍵用Ollydbg打開PE文件。
3.2.4 16進制文件編輯工具-UltraEdit
可對目標文件進行16進制查看和修改
。
3.3 DOS-MZ文件頭(0x40)+DOS Stub(IMAGE_DOS_HEADER/MS-DOS Stub Program)
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; // ChecksumWORD e_ip; // Initial IP valueWORD 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;
所有的PE文件都是以一個64字節的DOS頭(MZ文件頭)開始。這個DOS頭只是爲了兼容早期的DOS操作系統。
- 該結構體中需要掌握的字段只有 2 個,分別是第一個字段
e_magic
和最後一個字段e_lfanew
字段e_magic
字段:
DOS 可執行文件的標識符,佔用 2 字節。該位置保存着的字符是“MZ”。
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
e_lfanew
字段:
保存着PE頭的起始位置。
- 作用:
定位PE文件頭開始位置
,也可用於PE文件合法性檢測
- DOS系統下運行PE文件時,將提示用戶:“This program cannot be run in DOS mode”!
更正:上圖中的“PE文件”表述應爲“PE文件頭"。特此說明。”
- 使用PEView上圖內容:
3.4 PE文件頭(IMAGE_NT_HEADERS)
IMAGE_NT_HEADERS是一個宏,其定義如下:
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION64(ntheader)
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#define IMAGE_FIRST_SECTION(ntheader) IMAGE_FIRST_SECTION32(ntheader)
#endif
該頭分爲32位和64位兩個版本,其定義依賴於是否定義了_WIN64。這裏只討論32位的PE文件格式,來看一下IMAGE_NT_HEADERS32的定義,如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //該結構體中的Signature就是PE標識符,標識該文件是否是PE文件。該部分佔4字節,即“50 45 0000”。
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
如何簡單判斷一個文件是否爲PE文件?
首先要判斷DOS頭部的開始字節是否是“MZ”。如果是“MZ”頭部,則通過DOS頭部找到PE頭部,接着判斷PE頭部的前四個字節是否爲“PE\0\0”。如果是的話,則說明該文件是一個有效的PE文件。
可以看到,開始位置正是 00B0h 。
- PE header 由三部分組成【開始於000000B0】
字串“PE\0\0”(Signature)
:
以此識別給定文件是否爲有效PE文件。映像文件頭(FileHeader)
:
結構域包含了關於PE文件物理分佈的信息。可選映像頭(OptionalHeader)
3.4.1 字串“PE\0\0”
以此識別給定文件是否爲有效PE文件。
- Signature 一 dword類型,值爲
50h, 45h, 00h, 00h
(PE\0\0)。
- 本域爲PE標記,
可以此識別給定文件是否爲有效PE文件
。
但有時僅僅憑字串是不夠的。
- 本域爲PE標記,
3.4.2 文件頭-IMAGE_FILE_HEADER(映像文件頭)
文件頭結構體IMAGE_FILE_HEADER
是IMAGE_NT_HEADERS
結構體中的一個結構體,緊接在PE標識符的後面。IMAGE_FILE_HEADER結構體的大小爲20字節,起始位置爲0x000000CC,結束位置在0x000000DF。結構域包含了關於PE文件物理分佈的信息。
- 該結構域包含了關於PE文件物理分佈的信息
- 比如
節數目
、後續可選文件頭大小
、機器類型
等。
- 比如
- 映像文件頭的結構
包含了PE文件的一些基礎信息。
//
//File header format.
//
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;
#define IMAGE_SIZEOF_FILE_HEADER 20
- Machine: 該字段是WORD類型,佔用2字節。該字段表示可執行文件的目標CPU類型。
- NumberOfSections:該字段是WORD類型,佔用兩個字節。該字段表示PE文件的節區的個數。
- TimeDataStamp:該字段表明文件是何時被創建的,這個值是自1970年1月1日以來用格林威治時間計算的秒數。
- PointerToSymbolTable:該字段很少被使用,這裏不做介紹。
- NumberOfSymbols:該字段很少被使用,這裏不做介紹。
- SizeOfOptionalHeader:該字段爲WORD類型,佔用兩個字節。該字段指定IMAGE_OPTION AL_HEADER結構的大小。注意,在計算IMAGE_OPTIONAL_HEADER的大小時,應該從IMAGE_FILE_HEADER結構中的SizeOfOptionalHeader字段指定的值來獲取,而不應該直接使用 sizeof ( IMAGE_OPTIONAL_HEADER )來計算。
- Characteristics:該字段爲WORD類型,佔用2字節。該字段指定文件的類型。
3.4.3 可選頭-IMAGE_OPTIONAL_HEADER(可選映像頭)(可選文件頭)
IMAGE_OPTINAL_HEADER在幾乎所有的參考書中都被稱作“可選頭”。雖然被稱作可選頭,但是該頭部不是一個可選的,而是一個必須存在的頭,不可以沒有。該頭被稱作“可選頭”的原因是在該頭的數據目錄數組中,有的數據目錄項是可有可無的,數據目錄項部分是可選的,因此稱爲“可選頭”。它定義了PE文件的很多關鍵信息。大小可從 文件頭-IMAGE_FILE_HEADER 中得知。可選頭緊挨着文件頭,文件頭的結束位置在 0x000000DF,那麼可選頭的起始位置爲0x000000E0。
IMAGE_OPTIONAL_HEADER是一個宏,其定義如下:
#ifdef _WIN64
typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL64_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC
#else
typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER;
typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER;
#define IMAGE_SIZEOF_NT_OPTIONAL_HEADER IMAGE_SIZEOF_NT_OPTIONAL32_HEADER
#define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC
#endif
32位版本和64位版本的選擇是根據是否定義了_WIN64而決定的,這裏只討論其32位的版本。IMAGE_OPTIONAL_HEADER32的定義如下:
//
//Optional header format.
//
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;
-
可選頭的定位:
- 起始位置:
可選頭的定位有一定的技巧性,起始位置的定位相對比較容易找到,按照 PE 標識開始尋找是非常簡單的。 - 結束位置:
可選頭的結尾後面跟的是第一項節表的名稱,所以可選頭的結束位置就在.text節的前一個位置。
- 起始位置:
-
定義了PE文件的很多關鍵信息
可選頭是對文件頭的一個補充。文件頭主要描述文件的相關信息,而可選頭主要用來管理 PE 文件被操作系統裝載時所需要的信息。該頭同樣有 32 位版本與 64 位版本之分。
- 程序入口點(代碼從哪裏開始執行?)
即ImageBase+RVA
。Relative Virtual Address
(RVA
),相對虛擬地址,它是相對內存中ImageBase的偏移位置
,可由此得知PE文件真正的裝載地址。
一般爲
ImageBase+ RVA
,即00400000h+RVA
。- 內存鏡像加載地址(ImageBase)
PE文件在內存中的優先裝載地址
。對於大多數程序都是 00400000h和77F40000h。 - 節在文件和內存中的對齊粒度
比喻:桶的容量爲100升,現有367升水,請問需要使用多少個桶?4個。- 問題:代碼節的代碼實際長度爲0x46字節,在內存和文件中佔多少字節?
- 內存中節對齊粒度爲
0x1000
字節,故佔1000字節。
- 文件中節對齊粒度爲
0x200
字節,故佔200字節。
- 內存中節對齊粒度爲
- 問題:代碼節的代碼實際長度爲0x46字節,在內存和文件中佔多少字節?
正因爲
對齊粒度
的存在,PE文件中才有有很多“00”
字節(或是“CC”
字節)。- 本程序在
內存中的鏡像大小
、文件頭大小
- 程序入口點(代碼從哪裏開始執行?)
如何修改程序的入口地址?
本小節爲實際操作,講述如何修改程序的入口地址
。
-
AddressOfEntryPoint
的作用
-
實際操作:
問題:是否可以修改AddressOfEntryPoint指向任意代碼?
回答:可以的。下面進行演示:
在D8H處的4個字節處修改1000H爲1016H:
更正:上圖文字應爲“將地址D8h處的 00 10修改成16 10之後...”。
- 修改:
使用UltraEdit修改
- 結果:
此時程序入口地址已變成 00401016h !
DataDirectory - 數據目錄表
DataDirectory是可選映像頭的最後128個字節(16項 * 8 bytes)
,也是IMAGE_NT_HEADERS(PE文件頭)的最後一部分數據。
它由16個IMAGE_DATA_DIRECTORY結構組成的數組構成,指向輸出表、輸入表、資源塊、重定位
等數據目錄項的RVA(相對虛擬地址)和大小。
IMAGE_DATA_DIRECTORY的結構如下:
//
//Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //數據塊的起始RVA
DWORD Size; //數據塊的長度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
- 使用 PEView 查看數據目錄表:
讀圖可知Import table(輸入表)
的地址爲 00402014h ,大小爲 003Ch 。
- 使用 LordPE 查看數據目錄表:
注意。下圖所查看的exe文件與上圖查看的exe文件是不同的。
在數據目錄中,並不是所有的目錄項都會有值,很多目錄項的值都爲 0。因爲很多目錄項的值爲0,所以說數據目錄項是可選的。
可選頭的結構體介紹完了,各位可以按照該結構體中各成員變量的含義自行學習可選頭中的十六進制值的含義。只有參考結構體的說明去對照分析PE文件格式中的十六進制值,才能更好、更快地掌握PE結構。
3.5 節表(區塊表)(IMAGE_SECTION_HEADER)
- 在PE文件頭與原始數據之間存在一個區塊表(Section Table),它是一個IMAGE_SECTION_HEADER結構數組,
區塊表包含每個塊在映像中的信息
(如位置、長度、屬性),分別指向不同的區塊實體。
舉例:test.exe的代碼節表(.text)
- 全部有效結構
最後以一個空的IMAGE_SECTION_HEADER結構作爲結束
,所以節表中總的IMAGE_SECTION_HEADER結構數量等於節的數量加一
。 - 另外,節表中 IMAGE_SECTION_HEADER 結構的總數總是由PE文件頭
IMAGE_NT_HEADERS->FileHeader.NumberOfSections
字段來指定的。(因爲節表的個數是節的個數+1)
3.5.1 IMAGE_SECTION_HEADER 的結構定義
typedef struct _IMAGE_SECTION_HEADER {
Name //8個字節的塊名
union
{
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc; //區塊尺寸</span>
DWORD VirtualAddress; //區塊的RVA地址
DWORD SizeOfRawData; //在文件中對齊後的尺寸
DWORD PointerToRawData; //在文件中偏移
DWORD PointerToRelocations; //在OBJ文件中使用,重定位的偏移
DWORD PointerToLinenumbers; //行號表的偏移(供調試使用地)
WORD NumberOfRelocations; //在OBJ文件中使用,重定位項數目
WORD NumberOfLinenumbers; //行號表中行號的數目
DWORD Characteristics; //區塊屬性如可讀,可寫,可執行等
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
IMAGE_SECTION_HEADER 的成員名稱 | IMAGE_SECTION_HEADER 的成員意義 |
---|---|
Name | 這是一個8位的ASCII(不是Unicode內碼),用來定義塊名,多數塊名以,開始(如.Text),這個實際上不是必需的,注意如果塊名超過了8個字節,則沒有最後面的終止標誌NULL字節,帶有的相同名字的區塊被按字母順序合併。 |
VirtualSize | 指出實際的,被使用的區塊大小,是區塊在沒有對齊處理前的實際大小.如果VirtualSize < SizeOfRawData,那麼SizeOfRawData是可執行文件初始化數據的大小(SizeOfRawData – VirtualSize)的字節用0來填充。這個字段在OBJ文件中被設爲0。 |
VirtualAddress | 該塊時裝載到內存中的RVA,注意這個地址是按內存頁對齊的,她總是SectionAlignment的整數倍,在工具中第一個塊默認RVA爲1000,在OBJ中爲0。 |
SizeofRawData | 該塊在磁盤中所佔的大小,在可執行文件中,該字段包括經過FileAlignment調整後塊的長度。例如FileAlignment的大小爲200h,如果VirtualSize中的塊長度爲19Ah個字節,這一塊保存的長度爲200h個字節。 |
PointerToRawData | 該塊是在磁盤文件中的偏移,程序編譯或彙編後生成原始數據,這個字段用於給出原始數據塊在文件的偏移,如果程序自裝載PE或COFF文件(而不是由OS裝載),這種情況,必須完全使用線性映像方法裝入文件,需要在該塊處找到塊的數據。 |
PointerToRelocations | 在PE中無意義 |
PointerToLinenumbers | 行號表在文件中的偏移值,文件調試的信息 |
NumberOfRelocations | 在PE中無意義 |
NumberOfLinenumbers | 該塊在行號表中的行號數目 |
Characteristics | 塊屬性,(如代碼/數據/可讀/可寫)的標誌,這個值可通過鏈接器的/SECTION選項設置.下面是比較重要的標誌: |
- Characteristics-節屬性
- 使用Stud_PE查看:
- 使用Stud_PE查看:
3.6 區塊(節)
每個區塊的名稱都是唯一的,不能有同名的兩個區塊
。
但事實上節的名稱不表示任何含義
,他的存在僅僅是爲了正規統一編程的時候方便程序員查看方便而設置的一個標記而已。所以將包含代碼的區塊命名爲“.Data” (一般爲.text)或者說將包含數據的區塊命名爲“.Code”(一般爲.rdata等) 都是合法的。
當我們要從PE 文件中讀取需要的區塊的時候,不能以區塊的名稱作爲定位的標準和依據
,正確的方法是按照 IMAGE_OPTIONAL_HEADER32 結構中的數據目錄字段結合進行定位
。
疑問: 可是 IMAGE_OPTIONAL_HEADER32 中並沒有各區塊的地址啊,咋定位?
3.6.1 區塊名稱及其意義
- 常見的區塊(節)
- 代碼節
- 數據節
- 引入函數節
- 資源節等(如圖標)
- 引出函數節(DLL文件中常見)
- 重定位節(DLL文件中常見)
3.6.2 文件偏移和RVA
由於一些PE文件爲減少體積,磁盤對齊值不是一個內存頁 1000h,而是 200h,當這類文件被映射到內存後,同一數據相對於文件頭的偏移量在內存中和磁盤文件中是不同的,這樣就存在着文件偏移地址與虛擬地址的轉換
問題。
從這張圖可以看到:
- DOS頭部的起始地址(基地址)變成了00400000h
- 塊表與.text塊之間用0填充了,因爲.text塊的起始地址變成了00401000h
- .text塊.rdata塊、.data塊三者之間用0填充了,因爲要保證每一塊的大小都是1000h
3.7 代碼節 .text
- 代碼節一般名爲
.text
或CODE,該節含有程序的可執行代碼
每個PE文件都有代碼節
3.8 已初始化的數據節 .data
- 這個節一般取名爲
.data
或DATA - 已初始化的數據節中放的是在編譯時刻就已確定的數據。
如test.exe中的字符串“PE入口點測試1:進入第 一入口位置00401000H!”
3.9 未初始化的數據節 .bbs
- 節名稱一般叫
.bbs
- 這個節裏放有未初始化的全局變量和靜態變量。
- 例如“static int k;”
3.10 引入函數節 .rdata:PE文件的引入函數機制
函數引入機制。
3.10.1 引入目錄表 (輸入表)(IMPORT Directory Table)
-
如何從PE文件定位到引入目錄表(IDT)的起始位置?
PE文件可選文件頭的DataDirectory
。
-
引入目錄表由一系列的
IMAGE_IMPORT_DESCRIPTOR結構
組成- 結構的數量取決於程序要使用的DLL文件的數量,
每一個 IMAGE_IMPORT_DESCRIPTOR 結構對應一個DLL文件
- 在所有這些結構的最後,由一個
內容全爲0
的IMAGE_IMPORT_DESCRIPTOR
結構作爲結束
- 結構的數量取決於程序要使用的DLL文件的數量,
-
引入目錄表的組成部分-
IMAGE_IMPORT_DESCRIPTOR結構
是咋樣的?- IMAGE_IMPORT_DESCRIPTOR 的數據結構
其中 OriginalFirstThunk 和 FirstThunk 得特別說明一下:
- OriginalFirstThunk 和 FirstThunk
- 其中
FirstThunk
指向引入函數節(.data)中的引入名字表
(Import Name Table,與引入目錄表平級) - 其中
OriginalFirstThunk
指向引入函數節(.data)中的引入地址表
(Import Address Table,與引入目錄表平級) - 都指向一個包含一系列
IMAGE_THUNK_DATA
結構的數組
這個所謂的“數組”其實就是引入名字表
和引入地址表
。- 每個
IMAGE_THUNK_DATA
結構定義了一個
導入函數(注意不是一個.dll)的信息,實際爲一個雙字
,不同時刻可能代表不同含義。 - 以一個內容爲
0
的IMAGE_THUNK_DATA
結構作爲結束。
- 每個
- 其中
OriginalFirstThunk和FirstThunk :
在文件中:它們所指向IMAGE_THUNK_DATA結構的數組所有值都是相同
的;
在內存中:FirstThunk
所指向IMAGE_THUNK_DATA結構的數組的值會改變
。
(下圖是PE文件中的數據,不是內存中的,看得到 OriginalFirstThunk 和 FirstThunk 指向的數據都是相同的。)
注意:
一個OriginalFirstThunk或FirstThunk只指向一個.dll。上圖每一個紅框中都含有兩個.dll,所以分別由兩個OriginalFirstThunk或FirstThunk指向其對應紅框。- 一個IMAGE_THUNK_DATA結構數組(上圖一個紅框中有兩個數組)爲一個.dll中的所有導入函數數據:
如:Data 0002064 + Data 00000000(Value 0080 ExitProcess + Kenel32.dll) - 一個IMAGE_THUNK_DATA結構是一個導入函數數據(雙字):
如:Data 00002064 (Value 0080 ExitProcess)
補充:IMAGE_THUNK_DATA結構
一個IMAGE_THUNK_DATA結構實際上就是一個雙字,它在不同時刻有不同的含義。
- IMAGE_IMPORT_DESCRIPTOR 的數據結構
從上面內容可以總結出引入目錄表中的從屬關係(由高到低排序)
.rdata
↑
IMPORT Directory Table
↑
IMAGE_IMPORT_DESCRIPTOR
↑
OriginalFirstThunk和FirstThunk
(O…和 F…均指向一個包含一系列 IMAGE_THUNK_DATA
結構的數組(引入名字表和引入地址表))
3.10.2 引入名字表(IMPORT Name Table)
是上一節中的IMAGE_THUNK_DATA結構數組
。
實際操作:將通過函數名
引入函數改成通過序號
引入函數
1、
如圖,將其修改爲80002064
。(00002064 -> 80002064)
2、
通過序號引入函數
時無法在kenel32.dll中找到序號爲8292(2064h)
的函數,故程序無法執行。
3、
經過查找,函數ExitProcess序號爲183(B7h)
。
4、
5、
現在程序正常運行!
3.10.3 IMPORT Hints/Names &DLL names
當IMAGE_THUNK_DATA結構(雙字)的最高位爲0
時,表示函數以字符串類型的函數名
方式輸入,這時雙字的值是一個RVA
,指向一個(IAMGE_IMPORT_BY_NAME)結構,該結構定義如下:
STRUCT IAMGE_IMPORT_BY_NAME
{
DWORD Hint; //本函數在其所駐留DLL的輸出表中的序號
BYTE name //輸入函數的函數名,函數名是一個ASCII碼字符串,以NULL結尾
};
- IMAGE_IMPORT_BY_NAME結構
- 80 00爲Hints,ExitProcess爲引入函數名
- 62 02 爲Hints,wsprintfA爲引入函數名
- 9D 01 爲Hints,MessageBoxA爲引入函數名
- DLL names字符串
- kernel32.dll 爲dll文件名
- user32.dll 爲dll文件名
3.10.4 引入地址表 IAT ( IMPORT Address Table)
- DWORD數組[
可通過可選文件頭中的DataDirectory的第13項定位
]- 在文件中時,其內容與
Import Name Table
完全一樣。 - 在內存中時,每個雙字中存放着對應引入函數的地址。
此時這些值與其裝入內存前(在文件中)的值一般會有所不同
。
- 在文件中時,其內容與
3.11 引出函數節 .edata:DLL文件的函數引出機制
引出函數節一般名爲.edata
,這是本文件向其他程序提供調用函數的列表,函數所在的地址及具體代碼實現的區塊。有時合併入.text節
(如下圖)。
- 關鍵結構:引出目錄表(導出表、輸出表)
3.11.1 引出目錄表(導出表、輸出表)IMAGE_EXPORT_DIRECTORY
- 上圖的解析:
Base
一般爲1
,函數的序號等於Base+引出序號表裏的值
。
如何定位 Export Directory Table-引出目錄表
DataDirectory第一項。
3.11.2 導出地址表 -EXPORT ADDRESS Table
導出地址表的data兩種含義
-
dwExportRVA
- 指向導出地址
-
dwForwarderRVA
- 指向另外一個DLL中的某個API函數名。
- 舉例:Kernel32.AddVectoredExceptionHandler → NTDLL.RtlAddVectoredExceptionHandler
3.11.3 導出名字表 -EXPORT Name Table
3.11.4 導出序號表 -EXPORT Oridinal Table
該表保存的是各導出函數的函數地址在導出地址表的序號
,但序號並不是data中的值,而是待尋找函數在導出序號表排第幾個,例如AddAtomA()的排第二
個所以序號是2
。
爲何需要導出序號表?
導出函數名字和導出地址表中的地址不是一一對應關係
。
- 一個函數實現可能有多個名字
- 某些函數沒有名字,僅通過序號導出
3.11.5 如何通過函數名定位函數導出地址?
先通過AddressOfNames
查到到函數名,然後通過AddressOfNameOrdinals
查找到函數序號,再通過AddressOfFunctions
找到函數的RVA。
通過函數名定位函數的地址,下面給出實際操作步驟:
任務:查找HashData函數。
操作:按照下圖的順序操作,一共分爲4
步
-
查找
導出表
起始地址:
-
通過
AddressOfNames
查找函數對應的序號
:
-
通過
AddressOfNameOrdinals
和序號
查找函數的索引值
。
- 通過
AddressOfFunctions
和索引值
找到函數的RVA
:
3.12 資源節 .rsrc:文件資源索引、定位與修改
-
資源節一般名爲
.rsrc
-
這個節放有如圖標、對話框等程序要用到的資源
-
資源節是樹形結構的,它有一個主目錄,主目錄下又有子目錄,子目錄下可以是子目錄或數據。
通常有3層目錄(資源類型、資源標識符、資源語言ID),第4層是具體的資源
-
3個重要結構
目錄
是IMAGE_RESOURCE_DIRECTORY
結構
-
目錄項
是IMAGE_RESOURCE_DIRECTORY_ENTRY
結構
-
數據項
是IMAGE_RESOURCE_DATA_ENTRY
結構
以PEView.exe爲例(我自己分析我自己)
3個重要結構
-
如何定位資源目錄位置?
可選文件頭(也稱可選映像頭)的DataDirectory項。 -
如何定位資源?
-
資源節的部分應用
- 圖標修改
- 漢化
- …
3.13 重定位節 .reloc :鏡像地址改變後的地址自動修正
在exe文件中一般沒有,但在dll文件中基本都會有。
- 重定位節存放了一個重定位表,若裝載器不是把程序裝到程序編譯時默認的基地址時,就需要這個重定位表來做一些調整
- 重定位節以
IMAGE_BASE_RELOCATION
結構開始
從上圖我們能可以看到,定位項的數量是
不定
的。
VirtualAddress
:是一個4KB(一頁)的邊界。該值加上後面的TypeOffset數組的成員便得到了需要重定位數據的地址。SizeBlock
:爲這一結構塊的大小。該大小減去前兩項(VirtualAddress和SizeBlock本身)的字節數8便得到了第3項(下一項,也就是重定位項數組TypeOffset[]
)的大小,再除以2(因爲重定位項
大小爲2個字節)即得到了重定位項
的個數。重定位項
:每項都是16位的,其中的最高4
位代表了所需要的重定位類型
,剩下的12
位代表了頁面中重定位地址的偏移量(delta)
。重定位的類型
MAGE_REL_BASED_HIGHLOW(3)
將偏移量(delta)
添加到原來的偏移位置(RVA)
的32位字段上,它是32位地址重定位的首選
類型。
更正:上圖中的“定位項”表述應爲“重定位項數組”。
觀察上圖,可以發現重定位項數組
中最高4
位的值是3
(代表MAGE_REL_BASED_HIGHLOW(3)
),剩下的12
位就是偏移量(delta)
。
- 爲什麼需要重定位?
PE文件中部分數據是以VA地址存儲的,當PE文件無法加載到預期ImageBase
時,這些地址就需要修正。
參考資料:
雲課堂武大慕課
PE文件格式分析 https://blog.csdn.net/shitdbg/article/details/49734495
4 思考題
用於驗收PE文件結構學習情況。
如何判斷目標程序是否爲合法PE文件?
使用PEView找到PE文件頭(IMAGE_NT_HEADERS)的字串(Signature)
若其開頭雙字內容爲 “50 45 00 00
”,則說明給定文件是有效PE文件。
如果不使用引入函數節,如何使用外部DLL中的API函數?
暫時還不會。下次一定補上。
Kenel32.dll提供了GetProcAddress函數,用於獲取指定函數的地址。該函數的具體是怎樣實現的?
暫時還不會。下次一定補上。
熊貓燒香病毒感染其他PE文件後,目標文件圖標會變成容易被用戶差距的熊貓圖案,爲什麼?如何解決問題?