PE文件格式分析及修改


PE 的意思是 Portable Executable(可移植的執行體)。它是 Win32環境自身所帶的執行文件格式。它的一些特性繼承自Unix的Coff(common object file format)文件格式。“Portable Executable”(可移植的執行體)意味着此文件格式是跨Win32平台的;即使Windows運行在非Intel的CPU上,任何win32平台的PE裝載器都能識別和使用該文件格式。

  PE文件在文件系統中,與存貯在磁盤上的其它文件一樣,都是二進制數據,對於操作系統來講,可以認為是特定信息的一個載體,如果要讓計算機系統執行某程序,則程序文件的載體必須符合某種特定的格式。要分析特定信息載體的格式,要求分析人員有數據分析、編碼分析的能力。在Win32系統中,PE文件可以認為.exe、.dll、.sys 、.scr類型的文件,這些文件在磁盤上存貯的格式都是有一定規律的。

  一、PE格式基礎

  下表列出了PE的總體結構

DOS MZ header
DOS stub
PE header
Section table
Section 1
Section 2

Section n

  一個完整的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 5A 4D->MZ’

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 000000F 8

  這是一個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
{
WORD Machine;//該文件運行所需要的CPU,對於Intel平台是14Ch
WORD NumberOfSections;//文件的節數目
DWORD TimeDateStamp;//文件創建日期和時間
DWORD PointerToSymbolTable;//用於調試
DWORD NumberOfSymbols;//符號表中符號個數
WORD SizeOfOptionalHeader;//OptionalHeader 結構大小
WORD Characteristics;//文件信息標記,區分文件是exe還是dll
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
重要的 Characteristics值
#define IMAGE_FILE_RELOCS_STRIPPED 0001h // 文件中是否存在重定位信息
#define IMAGE_FILE_EXECUTABLE_IMAGE 0002h // 文件是可執行的
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0020h // 程序可以觸及大於2G的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0080h // 保留的機器類型低位
#define IMAGE_FILE_32BIT_MACHINE 0100h // 32位機器
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0400h // 不可在可移動介質上運行
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0800h // 不可在網絡上運行
#define IMAGE_FILE_SYSTEM 1000h // 系統文件
#define IMAGE_FILE_DLL 2000h // 文件是一個DLL
#define IMAGE_FILE_UP_SYSTEM_ONLY 4000h // 只能在單處理器計算機上運行
#define IMAGE_FILE_BYTES_REVERSED_HI 8000h //保留的機器類型高位


typedef struct _IMAGE_OPTIONAL_HEADER
{
WORD Magic;//標志字(總是010bh)
BYTE MajorLinkerVersion;//連接器高版本號
BYTE MinorLinkerVersion;//連接器低版本號
DWORD SizeOfCode;//代碼段大小
DWORD SizeOfInitializedData;//已初始化數據塊大小
DWORD SizeOfUninitializedData;//未初始化數據塊大小
DWORD AddressOfEntryPoint;//PE裝載器准備運行的PE文件的第一個指令的RVA,若要改變整個執行的流程,

可以將該值指定到新的RVA,這樣新RVA處的指令首先被執行。DWORD BaseOfCode;//代碼段起始RVA
DWORD BaseOfData;//數據段起始RVA
DWORD ImageBase;//PE文件的裝載地址
DWORD SectionAlignment;//塊對齊因子
DWORD FileAlignment;//文件塊對齊因子
WORD MajorOperatingSystemVersion;//所需操作系統高位版本號
WORD MinorOperatingSystemVersion;// 所需操作系統低位版本號
WORD MajorImageVersion;//用戶自定義高位版本號
WORD MinorImageVersion;//用戶自定義低位版本號
WORD MajorSubsystemVersion;//win32子系統版本。若PE文件是專門為Win32設計的
WORD MinorSubsystemVersion;//該子系統版本必定是4.0否則對話框不會有3維立體感
DWORD Win32VersionValue;//保留值,系統沒用到的,一般被作為是否感染的標志
DWORD SizeOfImage;//內存中整個PE映像體的尺寸
DWORD SizeOfHeaders;//所有頭+節表的大小
DWORD CheckSum;//校驗和
WORD Subsystem;//NT用來識別PE文件屬於哪個子系統
WORD DllCharacteristics;// 用來表示一個DLL映像是否為進程和線程的初始化及終止包含入口點的標記
DWORD SizeOfStackReserve;//
DWORD SizeOfStackCommit;//
DWORD SizeOfHeapReserve;//
DWORD SizeOfHeapCommit;//
//堆棧大小 這些域控制要保留的地址空間數量,並且負責棧和默認堆的申請。在默認情況下,

棧和堆都擁有1個頁面的申請值以及16個頁面的保留值
DWORD LoaderFlags;// 告知裝載器是否在裝載時中止和調試,或者默認地正常運行
DWORD NumberOfRvaAndSizes;// 該字段標識了接下來的DataDirectory數組個數。
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 結構數組。每個結構給出一個重要數據結構的RVA,比如引入地址表等
}IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

2.2 IMAGE_DATA_DIRECTORY

typedef struct _IMAGE_DATA_DIRECTORY
{
DWORD VirtualAddress;//表的RVA地址
DWORD Size;//大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

                             

                                                   圖5 Load PE 讀取的PE Header 的重要部分數據

                                   

                                                              圖6 Subsystem 類型

                            

                                圖7 Load Pe 讀取的IMAGE_DATA_DIRECTORY 信息

  在IMAGE_OPTIONAL_HEADER32后部一般是16項IMAGE_DATA_DIRECTORY數據,其中最后一項是保留數據。每一項數據都有其固定的含義,並且位置不可改變。

Export Table 導出函數表,主要用於DLL中的導出函數
Import Table 導入函數表,使用外部函數的數據表
Resource  資源數據表
Exception  異常處理表
Security  安全處理數據表
Relocation  重定位信息表,一般和DLL相關
Debug   調試信息表
Copyright  版權信息表
Globalptr  機器值(MIPS GP)
Tls Table  線程信息表
LoadConfig  裝配信息表
BoundImport  輸入函數綁定信息表
IAT    也ImportTable對應,由Loader填寫的輸入函數地址
DelayImport  延遲裝入的函數信息
COM    公用組件信息表
Reserved  保留信息,系統沒有使用,為以后擴展使用
這16項數據,其所在位置由RVA指定,大小由Size指定。對於一般的一個可執行程序(.exe),

最重要的是導入表(Import Table和IAT)、資源數據表(Resoruce)。

  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:節名稱 
VOffset:相對於ImageBase的虛擬偏移
VSize:實際大小
ROffset:相對於文件起始處的偏移
RSize所占的文件空間大小
Flags:節屬性

                                     

重要的節屬性定義:
#define IMAGE_SCN_CNT_CODE 00000020h // 節中包含代碼
#define IMAGE_SCN_CNT_INITIALIZED_DATA 00000040h // 節中包含已初始化數據
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 00000080h // 節中包含未初始化數據
#define IMAGE_SCN_MEM_DISCARDABLE 02000000h // 是一個可丟棄的節,即節中的數據在進程開始后將被丟棄
#define IMAGE_SCN_MEM_NOT_CACHED 04000000h // 節中數據不經過緩存
#define IMAGE_SCN_MEM_NOT_PAGED 08000000h // 節中數據不被交換出內存
#define IMAGE_SCN_MEM_SHARED 10000000h // 節中數據可共享
#define IMAGE_SCN_MEM_EXECUTE 20000000h // 可執行節
#define IMAGE_SCN_MEM_READ 40000000h // 可讀節
#define IMAGE_SCN_MEM_WRITE 80000000h // 可寫節

  [注]

  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 {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;// 指向一個 IMAGE_THUNK_DATA 結構數組的RVA
}
DWORD TimeDateStamp;// 文件生成的時間
DWORD ForwarderChain;// 這個數據一般為0,可以不關心
DWORD Name1;  // RVA,指向DLL名字的指針,ASCII字符串
DWORD FirstThunk; //指向一個 IMAGE_THUNK_DATA 結構數組的RVA,這個數據與IAT所指向的地址一致
}IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR
IMAGE_THUNK_DATA 這是一個DWORD類型的集合。通常我們將其解釋為指向一個 IMAGE_IMPORT_BY_NAME 結構的指針,其定義如下:
IMAGE_THUNK_DATA{
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;//判定當前結構數據是不是以序號為輸出的,如果是的話該值為0x800000000,此時PIMAGE_IMPORT_BY_NAME不可做為名稱使用
PIMAGE_IMPORT_BY_NAME AddressOfData;
}u1;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;


typedef struct _IMAGE_IMPORT_BY_NAME{
WORD Hint;// 函數輸出序號
BYTE Name1[1];//輸出函數名稱
} IMAGE_IMPORT_BY_NAME,*PIMAGE_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     ;以下兩行為保存當前程序上下文
pushfd
mov esi, ImageBase+SectionRVA+messbody   ;提示信息體
mov edi, ImageBase+ SectionRVA +messtitle  ;提示信息標題
push 0     ;MB_OK的類型的消息框
push esi    ;參數入棧
push edi    ;參數入棧
push 0     ;hWND
call MessageBoxA  
popfd     ;恢復程序上下文
popad
mov eax,ImageBase+oldoep;原始的入口地址送到eax
jmp eax     ;跳到原始入口處
messbody db    ‘New Section Add This File’,0
messtitle db    ‘I Sucess’,0

  在這一段代碼中我們的是程序首先執行植入代碼,完成特定功能,在執行完畢后,跳到原代碼入口處繼續執行(要注意保存初始環境),此段代碼的主要目的就是給用戶一個提示,表示我們成功的感染了這個程序。這段代碼中涉及的數據有消息框的標題和內容,我們都要在此段中進行定義。我們可以通過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表及其它數據的表的內容。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM