PE文件在文件系統中,與存貯在磁盤上的其它文件一樣,都是二進制數據,對於操作系統來講,可以認為是特定信息的一個載體,如果要讓計算機系統執行某程序,則程序文件的載體必須符合某種特定的格式。要分析特定信息載體的格式,要求分析人員有數據分析、編碼分析的能力。在Win32系統中,PE文件可以認為.exe、.dll、.sys 、.scr類型的文件,這些文件在磁盤上存貯的格式都是有一定規律的。
一、PE格式基礎
下表列出了PE的總體結構
DOS MZ header |
一個完整的PE文件,前五項是必定要有的,如果缺少或者數據出錯,系統會拒絕執行該文件如下圖
圖1 文件頭格式錯誤
圖2 格式數據錯誤
圖3 代碼錯誤
DOS MZ header部分是DOS時代遺留的產物,是PE文件的一個遺傳基因,一個Win32程序如果在DOS下也是可以執行,只是提示:“This program cannot be run in DOS mode.”然后就結束執行,提示執行者,這個程序要在Win32系統下執行。
DOS stub 部分是DOS插樁代碼,是DOS下的16位程序代碼,只是為了顯示上面的提示數據。這段代碼是編譯器在程序編譯過程中自動添加的。
PE header 是真正的Win32程序的格式頭部,其中包括了PE格式的各種信息,指導系統如何裝載和執行此程序代碼。
Section table部分是PE代碼和數據的結構數據,指示裝載系統代碼段在哪里,數據段在哪里等。對於不同的PE文件,設計者可能要求該文件包括不同的數據的Section。所以有一個Section Table 作為索引。Section多少可以根據實際情況而不同。但至少要有一個Section。如果一個程序連代碼都沒有,那么他也不能稱為可執行代碼。在Section Table后,Section數目的多少是不定的。
二、程序的裝入
當我們在explorer.exe(資源管理器)中雙擊某文件,執行一個可執行程序,系統會根據文件擴展名啟動一個程序裝載器,稱之為Loader。Loader會首先檢查DOS MZ Header,如果存在,就繼續尋找PE header,如果這兩項都不存在,就認為是DOS 16位代碼,如果只存在DOS MZ Header,而其中又指示了而其中又指示了PE Header 的位置,那么Loader 就判定此文件不一個有效的PE文件,拒絕執行。
如果DOS Header 和PE Header都正常有效,那么Loader就會根據PE Header 及Section Table的指示,將相應的代碼和數據映射到內存中,然后根據不同的Section進行數據的初始化,最后開始執行程序段代碼。
三、PE格式高級分析
下面我們以一個真實的程序為例詳細分析PE格式,分析PE格式最好有PE分析器,常用的軟件是Lord PE,也有其它的分析工具和軟件如PE Editor 、Stud PE等。
先分析一下磁盤文件的內容,這里我們使用UltraEdit32(UE)工具,這是一個實用的文件編輯器,可以編輯文本和二進制文件。
圖4 PE文件開始的磁盤數據
在文件的一開始有兩位16進制數據4D 5A,其對應的ASCII字符是MZ,這個標志就是DOS MZ Header 的標志。下面是通過Load PE列出的DOS MZ Header
1. DOS Header
數據結構名稱
|
值
|
e_magic:
|
0x
|
e_cblp:
|
0x0090
|
e_cp:
|
0x0003
|
e_crlc:
|
0x0000
|
e_cparhdr:
|
0x0004
|
e_minalloc:
|
0x0000
|
e_maxalloc:
|
0xFFFF
|
e_ss:
|
0x0000
|
e_sp:
|
0x00B8
|
e_csum:
|
0x0000
|
e_ip:
|
0x0000
|
e_cs:
|
0x0000
|
e_lfarlc:
|
0x0040
|
e_ovno:
|
0x0000
|
e_res:
|
0x0000000000000000
|
e_oemid:
|
0x0000
|
e_oeminfo:
|
0x0000
|
e_res2:
|
0x0000000000000000000000000000000000000000
|
e_lfanew:
|
0x
|
這是一個PE文件的DOS Header,其中我們最關心的就是e_lfanew這個字段的值,它指向了PE Header 在磁盤文件中相對於文件開始的偏移地址,這里是F8。在本文件00F8h處果然找到了“PE”兩個字符,那么在00F8h處就是PE Header 的有效頭載荷。
2. PE Header
我們可以在winnt.h這個文件中找到關於PE文件頭的定義:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature;//PE文件頭標志:PE/0/0。在開始DOS header的偏移3CH(e_lfanew)處所指向的地址開始 IMAGE_FILE_HEADER FileHeader;//PE文件物理分布的信息 IMAGE_OPTIONAL_HEADER32 OptionalHeader;//PE文件邏輯分布的信息 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; |
2.1 IMAGE_FILE_HEADER和IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_FILE_HEADER typedef struct _IMAGE_OPTIONAL_HEADER 可以將該值指定到新的RVA,這樣新RVA處的指令首先被執行。DWORD BaseOfCode;//代碼段起始RVA 棧和堆都擁有1個頁面的申請值以及16個頁面的保留值 |
|
圖5 Load PE 讀取的PE Header 的重要部分數據
圖6 Subsystem 類型
圖7 Load Pe 讀取的IMAGE_DATA_DIRECTORY 信息
在IMAGE_OPTIONAL_HEADER32后部一般是16項IMAGE_DATA_DIRECTORY數據,其中最后一項是保留數據。每一項數據都有其固定的含義,並且位置不可改變。
|
2.3 IMAGE_SECTION_HEADER
PE文件頭后是節表,在winnt.h下如下定義 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//節表名稱,如“.text” //IMAGE_SIZEOF_SHORT_NAME=8 union { DWORD PhysicalAddress;//物理地址 DWORD VirtualSize;//真實長度,這兩個值是一個聯合結構,可以使用其中的任何一個, //一般是節的數據大小 } Misc; DWORD VirtualAddress;//RVA DWORD SizeOfRawData;//物理長度 DWORD PointerToRawData;//節基於文件的偏移量 DWORD PointerToRelocations;//重定位的偏移 DWORD PointerToLinenumbers;//行號表的偏移 WORD NumberOfRelocations;//重定位項數目 WORD NumberOfLinenumbers;//行號表的數目 DWORD Characteristics;//節屬性 如可讀,可寫,可執行等 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; |

Name:節名稱 |
重要的節屬性定義: |
[注]
RVA:虛擬偏移地址。RAV是指的某一處由Loader裝入內存后,這一處應該在虛擬內存的什么地方,RAV也稱為虛擬偏移地址。
Alignment:對齊因子。與對齊因子相關的值有2個地方,一處是文件對齊因子,另一處是內存對齊因子。對齊因子指示出某一類型的對齊方式,以文件對齊為例,如果Alignment 為200h,說明文件中的內容是以200h為單位的,如果數據大小正好是200h的整數倍,則不存在對齊問題,如果數據大小是非200h的整數倍,則要使用Alignment 對數據所占的空間進行修正,取其上限數值(如310h->400h),使其所占的空間是200h的整數倍。
2.3 Improt Table 和IAT
IMAGE_DATA_DIRECTORY的第2項和第13項,指示導入表和導入函數地址表的位置。這部分對於一個PE文件相當重要,很多系統函數都是由此導入。
Import Table 的VirtualAddress指向了一個RVA,他是一個導入表結構數組,數組以全0作為結束標記,該結構定義如下:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { typedef struct _IMAGE_IMPORT_BY_NAME{ |
3.IMP和IAT的關系
靜態分析時,OriginalFirstThunk與FirstThunk指向的數據是同一組IMAGE_IMPROT_BY_NAME。
OriginalFirstThunk
|
|
IMAGE_IMPORT_BY_NAME
|
|
FirstThunk
|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
|
--->
--->
--->
--->
--->
--->
|
Function 1
Function 2
Function 3
Function 4
...
Function n
|
<---
<---
<---
<---
<---
<---
|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA |
Loader 在裝入一個可執行的代碼時,會分析該文件的導入表(IMP),然后通過導入表的指引,修改IAT指向的數據,這們在裝載完成后,數據會變成如下形式
OriginalFirstThunk
|
|
IMAGE_IMPORT_BY_NAME
|
|
FirstThunk
|
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
IMAGE_THUNK_DATA
...
IMAGE_THUNK_DATA
|
--->
--->
--->
--->
--->
--->
|
Function 1
Function 2
Function 3
Function 4
...
Function n
|
|
Address of Function 1
Address of Function 2
Address of Function 3
Address of Function 4
...
Address of Function n
|
可以看出,OriginalFirstThunk與FirstThunk所指向的數據分離。FirstThunk指向的不再是IMAGE_IMPORT_BY_NAME,而是指向了函數的真實地址。代碼段對於一個外部函數的引用始終使用的是FirstThunk處的RVA。當程序真正執行時,就可以跳到真正的函數入口了。
四、修改PE文件
PE文件是由源代碼經由編譯器編譯、鏈接后形成的可執行文件,由系統加載執行。通過對PE文件的分析,給理論上修改PE文件提供了可能。下面我們分幾個步驟修改PE文件。這些步驟不是修改PE的必須步驟,但從中我們可以討論如何修改PE文件。
1.給PE文件增加一個新節
PE文件節的信息保存在文件頭的最后部分,如果預留磁盤空間足夠大(大於或等於了個節的結構數據大小),我們就可以為其增加一個新節。
由IMAGE_SECTION_HEADER的結構定義可知,IMAGE_SECTION_HEADER的大小為10個DWORD類型數據的大小,也就是40(28h)個字節,我們觀察要修改的目標
圖10 PE文件的節部分數據
再由圖8的數據我們得知,該PE文件的第一個節的數據的文件偏移地址為400h,而最后一節.rsrc,的末尾偏移是中28fh,400h-28fh=171h>28h,可以增加新的節標志。增加新的節標志數據,首先要修改IMAGE_FILE_HEADER結構的NumberOfSections數值(4->5),然后在290h處開始按IMAGE_SECTION_HEADER結構填入相應的數值。這里我們使用Load PE添加新的數據
圖11 添加了新節的信息
由於新的節沒有實際數據,所以其VSize和RSize大小為0,新節的屬性與.text代碼段相同,添加新節后,這里只是添加了節信息,還要補充節數據,補充數據我們使用UE進行復制粘貼就可,這一步涉及了兩處Alignment,要注意使用,我們先補充100h字節,但由於文件對齊因子,所在至少在文件末尾處添加200h個空白數據,最后修改節的信息和IMAGE_OPTIONAL_HEADER的SizeOfImage(003f000h ->0040000h),使得我們添加的數據也可以由Loader加載到內存中。添加數據完成后,先執行程序,確定PE頭信息的正確。

圖12修正后的文件頭部信息

圖13 修改后的程序正常執行的界面
2. 在新節中添加代碼
在新節中添加代碼是本文修改PE文件的關鍵,我們的目的不僅僅是添加數據,而是添加可執行的代碼,通過添加代碼研究PE文件的可感染性。由於高級編譯器都會將數據段和代碼段分開來編譯,所以我們添加的代碼將會因為找不到數據而使程序崩潰,因此我們要將我們需要的數據和代碼放在同一個Section 內,方便編程。
示例匯編代碼
pushad ;以下兩行為保存當前程序上下文 |
在這一段代碼中我們的是程序首先執行植入代碼,完成特定功能,在執行完畢后,跳到原代碼入口處繼續執行(要注意保存初始環境),此段代碼的主要目的就是給用戶一個提示,表示我們成功的感染了這個程序。這段代碼中涉及的數據有消息框的標題和內容,我們都要在此段中進行定義。我們可以通過NASM編譯這段代碼為純二進制代碼。(關於NASM編譯器,可以通過網絡查找其編譯程序和文檔)。
為使編譯通過,我們首先確定MessageBoxA的地址和oldoep的地址
圖14 Load PE 文件的人Import Table
通過查閱MSDN,我們知道MessageBoxA的函數由USER32.dll導出,而應用程序使用的這個信息就在導入表中。通過Load PE 查看文件的Import Table,我們找到USER32.dll 和MessageBox的地址在0002743Ch,要執行0002743Ch處的索引函數,就在在其前面加上ImageBase的值。也就在此代碼在0042743Ch處,實際調用參考為 Call dword [0042743ch]。Oldoep由IMAGE_OPTIONAL_HEADER的AddressOfEntryPoint得到
(0000D1B5h+00400000h=0040d1b5h)

圖15 通過NASM編譯后的代碼
將這段代碼復制到修改后的.exe 的38c00h處,然后保存。如下圖:

圖16 修改后的新節的數據
運行程序:
圖17 節的程序可以照常執行
程序首先彈出對話框,單擊確定后看到了程序的初始界面
3. 其它植入方法
啟動OllDbg(Ring3)調試器,調試上面剛處理過的程序,發現調試器會給出一個警告,如下圖
圖18 OllDbg的警告信息
通過實驗得出,之所以OllDbg 會發出如此的警告,是因為該文件的PE信息中AddressOfEntryPoint超出了Code Section(.text)段所記錄的地址(0000000h~00270000h)。將.text判定為代碼段,由PE頭的BaseOfCode得出。如果將此段代碼植入.text 段,那么將不會出現此提示。
PE文件能否正常加載執行,與磁盤文件結構密切相關,但一旦將磁盤文件映射為內存鏡像后,就與磁盤文件脫離了關系。所以磁盤僅僅是一個規范的數據結構。
通過實驗,我們可以得出這樣的結論:對於一個小的代碼段(其二進制代碼長度小於代碼段下一節的RVA-(BaseOfCode+代碼段的VirtualSize)),植入是成功的。
那么我們通過分析磁盤文件和內存映射的關系,就可以修改代碼段,將代碼植入到代碼段。在植入代碼段后,要對PE文件的磁盤數據和進行一次定位修復,就可以完成代碼的整體植入。
代碼段一般是PE文件的第一個Section,如果此段變長,就要將其后續段的RVA和磁盤偏移地址都要進行修正。修正完成后,僅僅是保證了PE文件的磁盤格式正確,接下來主要就是修改導入表數據,資源表數據。最后要參照原PE文件將新PE文件對數據段的數據的引用進行修正。
至此,我們完成了一次對PE文件新代碼的引入問題的研究。但對於一個復雜的PE文件,修改還遠不如此。還要處理輸出表、TLS表及其它數據的表的內容。