我們大家都知道,在Windows 9x、NT、2000下,所有的可執行文件都是基于Microsoft設計的一種新的文件格式Portable Executable File Format(可移植的執行體),即PE格式。有一些時候,我們需要對這些可執行文件進行修改,下面文字試圖詳細的描述PE文件的格式及對PE格式文件的修改。
PE文件框架構成
DOS MZ header
DOS Stub
PE header
Section table
Section 1
Section 2
Section...
Section n
??? 上表是PE文件結構的總體層次分布。所有 PE文件(甚至32位的 DLLs) 必須以一個簡單的 DOS MZ header開始,在偏移0處有DOS下可執行文件的“MZ標志”,有了它,一旦程序在DOS下執行,DOS就能識別出這是有效的執行體,然后運行緊隨 MZ header之后的DOS Stub。緊接著DOS Stub的是PE header。PE header是PE相關結構IMAGE_NT_HEADERS的簡稱,其中包含了許多PE裝載器用到的重要域。可執行文件在支持PE文件結構的操作系統 中執行時,PE裝載器將從DOS MZ header的偏移3CH處找到PE header的起始偏移量。因而跳過了DOS Stub直接定位到真正的文件頭PE header。
??? 小知識:DOS Stub實際上是個有效的EXE,在不支持PE文件格式的操作系統中,它將簡單顯示一個錯誤提示,類似于字符串“This program cannot run in DOS mode”或者程序員可根據自己的意圖實現完整的DOS代碼。通常DOS Stub由匯編器/編譯器自動生成,對我們的用處不是很大,它簡單調用中斷21h服務9來顯示字符串“This program cannot run in DOS mode”。
??? PE文件的真正內容劃分成塊,稱之為Sections(節)。每節是一塊擁有共同屬性的數據,比如“.text”節等,那么,每一節的內容都是什么呢?實際上PE格式的文件把具有相同屬性的內容放入同一個節中,而不必關心類似“.text”、“.data”的命名,其命名只是為了便于識別,所有,我們如果對PE格式的文件進行修改,理論上講可以寫入任何一個節內,并調整此節的屬性就可以了。
??? PE header 接下來的數組結構Section table(節表)。每個結構包含對應節的屬性、文件偏移量、虛擬偏移量等。如果PE文件里有5個節,那么此結構數組內就有5個成員。
??? 以上就是PE文件格式的物理分布,下面將總結一下裝載一PE文件的主要步驟:
1.PE文件被執行,PE裝載器檢查DOS MZ header里的PE header偏移量。如果找到,則跳轉到PE header。
2.PE裝載器檢查PE header的有效性。如果有效,就跳轉到PE header的尾部。
3.緊跟 PE header的是節表。PE裝載器讀取其中的節信息,并采用文件映射方法將這些節映射到內存 ,同時附上節表里指定的節屬性。
4.PE文件映射入內存后,PE裝載器將處理PE文件中類似Import table(引入表)邏輯部分。
??? PE文件頭定義
我們可以在Winnt.h這個文件中找到關于PE文件頭的定義:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
//PE文件頭標志 :“PE/0/0”。在開始DOS header的偏移3CH處所指向的地址開始
IMAGE_FILE_HEADER FileHeader;??????? //PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader;??? //PE文件邏輯分布的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
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;
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處的指令首先被執行(以往許多文章都有介紹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;??????? //
DWORD?? SizeOfStackReserve;??????? //
DWORD?? SizeOfStackCommit;??????? //
DWORD?? SizeOfHeapReserve;??????? //
DWORD?? SizeOfHeapCommit;??????? //
DWORD?? LoaderFlags;??????????? //
DWORD?? NumberOfRvaAndSizes;??? //
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
//IMAGE_DATA_DIRECTORY 結構數組。每個結構給出一個重要數據結構的RVA,比如引入地址表等
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD?? VirtualAddress;??????? //表的RVA地址
DWORD?? Size;??????????????? //大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
PE文件頭后是節表,在winnt.h下如下定義
typedef struct _IMAGE_SECTION_HEADER {
BYTE??? Name[IMAGE_SIZEOF_SHORT_NAME];//節表名稱,如“.text”
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;
以上結構就是在Winnt.h中關于PE文件頭的定義,如何我們用C/C++來進行PE可執行文件操作,就要用到上面的所有結構,它詳細的描述了PE文件頭的結構。
修改PE可執行文件
??? 現在讓我們把一段代碼寫入任何一個PE格式的可執行文件,代碼如下:
-- test.asm --
.386p
.model flat, stdcall
option casemap:none
include /masm32/include/windows.inc
include /masm32/include/user32.inc
includelib /masm32/lib/user32.lib
.code
start:
??? INVOKE MessageBoxA,0,0,0,MB_ICONINFORMATION or MB_OK
??? ret
end start
以上代碼只顯示一個MessageBox框,編譯后得到二進制代碼如下:
unsigned char writeline[18]=;
好,現在讓我們看看該把這些代碼寫到那。現在用Tdump.exe顯示一個PE格式得可執行文件信息,可以發現如下描述:
Object table:
#?? Name????? VirtSize??? RVA???? PhysSize Phys off Flags??
-- -------- -------- -------- -------- -------- --------
01 .text???? 0000CCC0 00001000 0000CE00 00000600 60000020 [CER]
02 .data???? 00004628 0000E000 00002C00 0000D400 C0000040 [IRW]
03 .rsrc???? 000003C8 00013000 00000400 00010000 40000040 [IR]
Key to section flags:
C - contains code
E - executable
I - contains initialized data
R - readable
W - writeable
??? 上面描述此文件中存在3個段及每個段的信息,實際上我們的代碼可以寫入任何一個段,這里我選擇“.text”段。用光盤中提供的代碼可以得到一個PE格式可執行文件的頭信息。
??? 由于在PE格式的文件中,所有的地址都使用RVA地址,所以一些函數調用和返回地址都要經過計算才可以得到。
2.1? 引言
通常Windows下的EXE文件都采用PE格式。PE是英文Portable Executable的縮寫,它是一種針對于微軟Windows NT、Windows 95和Win32s系統,由微軟公司設計的可執行的二進制文件(DLLs和執行程序)格式,目標文件和庫文件通常也是這種格式。這種格式由TIS(Tool Interface Standard)委員會(Microsoft、Intel、Borland、Watcom、IBM等)在1993進行了標準化。顯然,它參考了一些UNIXes和VMS的COFF(Common Object File Format)格式。
認識可執行文件的結構非常重要,在DOS下是這樣,在Windows系統下更是如此。了解了這種結構后就可以對可執行程序進行加密、加殼和修改等,一些黑客也利用了這些技術。為了使讀者對PE文件格式有進一步的認識,本章從一個程序員的角度出發再次介紹PE文件格式。如果已經熟悉這方面的知識,可以跳過這一章。
2.2? PE文件格式概述
認識PE文件,既要懂得它的結構布局,又要知道它是如何裝載到計算機內存中的。下面分別對它們進行說明。
2.2.1? PE文件結構布局
找到文件中某一結構信息有兩種定位方法。第一種是通過鏈表方法,對于這種方法,數據在文件的存放位置比較自由。第二種方法是采用緊湊或固定位置存放,這種方法要求數據結構大小固定,它在文件中的存放位置也相對固定。在PE文件結構中同時采用以上兩種方法。
因為在PE文件頭中的每個數據結構大小是固定的,因此能夠編寫計算程序來確定某一個PE文件中的某個參數值。在編寫程序時,所用到的數據結構定義,包括數據結構中變量類型、變量位置和變量數組大小都必須采用Windows提供的原型。圖2.1所示的PE文件結構的總體層次分布如下:
PE文件結構總體層次分布
·???????? DOS MZ Header
所有 PE文件(甚至32位的DLLs)必須以簡單的DOS MZ header開始,它是一個IMAGE_DOS_HEADER結構。有了它,一旦程序在DOS下執行,DOS就能識別出這是有效的執行體,然后運行緊隨MZ Header之后的DOS Stub。
·???????? DOS Stub?
DOS Stub實際上是個有效的EXE,在不支持PE文件格式的操作系統中,它將簡單顯示一個錯誤提示,類似于字符串“This program requires Windows”或者程序員可根據自己的意圖實現完整的DOS代碼。大多數情況下DOS Stub由匯編器/編譯器自動生成。
·???????? PE Header
緊接著DOS Stub的是PE Header。它是一個IMAGE_NT_HEADERS結構。其中包含了很多PE文件被載入內存時需要用到的重要域。執行體在支持PE文件結構的操作系統中執行時,PE裝載器將從DOS MZ header中找到PE header的起始偏移量。因而跳過DOS Stub直接定位到真正的文件頭 PE header。
·???????? Section Table
PE Header之后是數組結構Section Table(節表)。如果PE文件里有5個節,那么此Section Table結構數組內就有5個(IMAGE_SECTION_HEADER)成員,每個成員包含對應節的屬性、文件偏移量、虛擬偏移量等。排在節表中的最前面的第一個默認成員是text,即代碼節頭。通過遍歷查找方法可以找到其他節表成員(節表頭)。
·???????? Sections
PE文件的真正內容劃分成塊,稱為Sections(節)。每個標準節的名字均以圓點開頭,但也可以不以圓點開頭,節名的最大長度為8個字節。Sections是以其起始位址來排列,而不是以其字母次序來排列。通過節表提供的信息,可以找到這些節。程序的代碼,資源等就放在這些節中。
節的劃分是基于各組數據的共同屬性,而不是邏輯概念。每節是一塊擁有共同屬性的數據,比如代碼/數據、讀/寫等。如果PE文件中的數據/代碼擁有相同屬性,它們就能被歸入同一節中。節名稱僅僅是個區別不同節的符號而已,類似“data”,“code”的命名只為了便于識別,唯有節的屬性設置決定了節的特性和功能。
2.2.2? PE文件內存映射
在Windows系統下,當一個PE應用程序運行時,這個PE文件在磁盤中的數據結構布局和內存中的數據結構布局是一致的。系統在載入一個可執行程序時,首先是Windows裝載器(又稱PE裝載器)把磁盤中的文件映射到進程的地址空間,它遍歷PE文件并決定文件的哪一部分被映射。其方式是將文件較高的偏移位置映射到較高的內存地址中。磁盤文件一旦被裝入內存中,其某項的偏移地址可能與原始的偏移地址有所不同,但所表現的是一種從磁盤文件偏移到內存偏移的轉換,如圖2.2所示。
PE文件內存映射
當PE文件被加載到內存后,內存中的版本稱為模塊(Module),映射文件的起始地址稱為模塊句柄(hModule),可以通過模塊句柄訪問內存中的其他數據結構。這個初始內存地址也稱為文件映像基址(ImageBase)。載入一個PE程序的主要步驟如下:
(1)當PE文件被執行時,PE裝載器首先為進程分配一個4GB的虛擬地址空間,然后把程序所占用的磁盤空間作為虛擬內存映射到這個4GB的虛擬地址空間中。一般情況下,會映射到虛擬地址空間中0x400000的位置。裝載一個應用程序的時間比一般人所設想的要少,因為裝載一個PE文件并不是把這個文件一次性地從磁盤讀到內存中,而是簡單地做一個內存映射,映射一個大文件和映射一個小文件所花費的時間相差無幾。當然,真正執行文件中的代碼時,操作系統還是要把存在于磁盤上的虛擬內存中的代碼交換到物理內存(RAM)中。但是,這種交換也不是把整個文件所占用的虛擬地址空間一次性地全部從磁盤交換到物理內存中,操作系統會根據需要和內存占用情況交換一頁或多頁。當然,這種交換是雙向的,即存在于物理內存中的一部分當前沒有被使用的頁,也可能被交換到磁盤中。
(2)PE裝載器在內核中創建進程對象和主線程對象以及其他內容。
(3)PE裝載器搜索PE文件中的Import Table(引入表),裝載應用程序所使用的動態鏈接庫。對動態鏈接庫的裝載與對應用程序的裝載方法完全類似。
(4)PE裝載器執行PE文件首部所指定地址處的代碼,開始執行應用程序主線程。
2.2.3? Big-endian和Little-endian
PE Header中IMAGE_FILE_HEADER的成員Machine 中的值,根據winnt.h中的定義,對于Intel CPU應該為0x014c。但是用十六進制編輯器打開PE文件時,看到這個WORD顯示的卻是4c 01。其實4c 01就是0x014c,只不過由于Intel CPU是Little-endian,所以顯示出來是這樣的。對于Big-endian和Little-endian,請看下面的例子。一個整型int變量,長度為4個字節。當這個整形變量的值為0x12345678時,對于Big-endian來說,顯示的是{12,34,45,78},而對于Little-endian來說,顯示的卻是{78,45,34,12}。注意Intel使用的是Little-endian。
2.2.4? 3種不同的地址
PE文件的各種結構中,涉及到很多地址、偏移。有些是指在文件中的偏移,有些??? 是指在內存中的偏移。以下的第一種是指在文件中的地址,第二、三種是指在內存中的地址。
第一種,文件中的地址。比如用十六進制編輯器打開PE文件,看到的地址(偏移)就是文件中的地址,使用某個結構的文件地址,就可以在文件中找到該結構。
第二種,當文件被整個映射到內存時,例如某些PE分析軟件,把整個PE文件映射到內存中,這時是內存中的虛擬地址(VA)。如果知道在這個文件中某一個結構的內存地址的話,那么它等于這個PE文件被映射到內存的地址加上該結構在文件中的地址。
第三種,當執行PE時,PE文件會被載入器載入內存,這時經常需要的是RVA。例如知道一個結構的RVA,那么程序載入點加上RVA就可以得到該結構的內存地址。比如,如果PE文件裝入虛擬地址(VA)空間的0x400000處,某一結構的RVA 為0x1000,那么其虛擬地址為0x401000。
PE文件格式要用到RVA,主要是為了減少PE裝載器的負擔。因為每個模塊都有可能被重載到任何虛擬地址空間,如果讓PE裝載器修正每個重定位項,這肯定是個夢魘。相反,如果所有重定位項都使用RVA,那么PE裝載器就不必操心那些東西了,即它只要將整個模塊重定位到新的起始VA。這就像相對路徑和絕對路徑的概念:RVA類似相對路徑,VA就像絕對路徑。
注意,RVA和VA是指內存中,不是指文件中。是指相對于載入點的偏移而不是一個內存地址,只有RVA加上載入點的地址,才是一個實際的內存地址。
2.3? PE文件結構
在win32 SDK的文件winnt.h中有PE文件格式的定義。本文所用到的變量,如果沒有特別說明,都在文件winnt.h中定義。
有關一些PE頭文件結構一般都有32位和64位之分,如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64等,除了在64位版本中的一些擴展域外,這些結構總是一樣的。是采用32位還是64位,需要用#define _WIN64來定義,如果沒有這種定義,則采用的是32位的文件結構。編譯器將根據此定義選擇相應的編譯模式。
2.3.1? MS-DOS頭部
MS-DOS頭部占據了PE文件的頭64個字節,描述它內容的結構如下:
l????????? ?
// 此結構包含于WINNT.H中
//
typedef struct _IMAGE_DOS_HEADER {?? // DOS的.EXE頭部
??? WORD e_magic;?????? // 魔術數字
??? WORD e_cblp; ?????? // 文件最后頁的字節數
??? WORD e_cp; ???????? // 文件頁數
??? WORD e_crlc; ?????? // 重定義元素個數
??? WORD e_cparhdr; ??? // 頭部尺寸,以段落為單位
??? WORD e_minalloc; ?? // 所需的最小附加段
??? WORD e_maxalloc; ?? // 所需的最大附加段
??? WORD e_ss; ???????? // 初始的SS值(相對偏移量)
??? WORD e_sp; ???????? // 初始的SP值
??? WORD e_csum; ?????? // 校驗和
??? WORD e_ip; ???????? // 初始的IP值
??? WORD e_cs; ???????? // 初始的CS值(相對偏移量)
??? WORD e_lfarlc;????? // 重分配表文件地址
??? WORD e_ovno; ?????? // 覆蓋號
??? WORD e_res[4];????? // 保留字
??? WORD e_oemid; ????? // OEM標識符(相對e_oeminfo)
??? WORD e_oeminfo; ??? // OEM信息
??? WORD e_res2[10]; ?? // 保留字
??? LONG e_lfanew; ???? // 新exe頭部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
l????????? ?
其中第一個域e_magic,被稱為魔術數字,它用于表示一個MS-DOS兼容的文件類型。所有MS-DOS兼容的可執行文件都將這個值設為0x5A4D,表示ASCII字符MZ。MS-DOS頭部之所以有的時候被稱為MZ頭部,就是這個緣故。還有許多其他的域對于MS-DOS操作系統來說都有用,但是對于Windows NT來說,這個結構中只有一個有用的域——最后一個域e_lfnew,一個4字節的文件偏移量,PE文件頭部就是由它定位的。
2.3.2? IMAGE_NT_HEADER頭部
PE Header是緊跟在MS-DOS頭部和實模式程序殘余之后的,描述它內容的結構?? 如下:
l????????? ?
typedef struct? _IMAGE_NT_HEADERS {
??? DWORD Signature;?????????????? ??????????? // PE文件頭標志:"PE/0/0"
??? IMAGE_FILE_HEADER FileHeader;?????????????? // PE文件物理分布的信息
??? IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE文件邏輯分布的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
緊接PE文件頭標志之后是PE文件頭結構,由20個字節組成,它被定義為:
l????????? ?
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
l????????? ?
其中請注意這個文件頭部的大小已經定義在這個包含文件之中了,這樣一來,想要得到這個結構的大小就很方便了。
Machine:表示該程序要執行的環境及平臺,現在已知的值如表2.1所示。
應用程序執行的環境及平臺代碼
IMAGE_FILE_MACHINE_I386(0x14c) | Intel 80386? 處理器以上 |
0x014d | Intel 80486 處理器以上 |
0x014e | Intel Pentium 處理器以上 |
0x0160 | R3000(MIPS)處理器,big?endian |
IMAGE_FILE_MACHINE_R3000(0x162) | R3000(MIPS)處理器,little?endian |
IMAGE_FILE_MACHINE_R4000(0x166) | R4000(MIPS)處理器,little?endian |
IMAGE_FILE_MACHINE_R10000(0x168) | R10000(MIPS)處理器,little?endian |
IMAGE_FILE_MACHINE_ALPHA(0x184) | DEC Alpha AXP處理器 |
IMAGE_FILE_MACHINE_POWERPC(0x1f0) | IBM Power PC,little?endian |
? | ? |
NumberOfSections:段的個數。
TimeDateStamp:文件建立的時間。可用這個值來區分同一個文件的不同的版本,即使它們的商業版本號相同。這個值的格式并沒有明確的規定,但是很顯然地大多數的C編譯器都把它定為從1970.1.1 00:00:00以來的秒數(time_t)。這個值有時也被用做綁定輸入目錄表。注意:一些編譯器將忽略這個值。
PointerToSymbolTable及NumberOfSymbols:用在調試信息中,用途不太明確,不過它們的值總為0。
SizeOfOptionalHeader:可選頭的長度(sizeof IMAGE_OPTIONAL_HEADER),可以用它來檢驗PE文件的正確性。
Characteristics:是一個標志的集合,其大部分位用于OBJ或LIB文件中。
文件頭下面就是可選擇頭,這是一個叫做IMAGE_OPTIONAL_HEADER的結構,由224個字節組成。雖然它的名字是“可選頭部”,但是請確信:這個頭部并非“可選”,而是“必需”的。可選頭部包含了很多關于可執行映像的重要信息。例如,初始的堆棧大小、程序入口點的位置、首選基地址、操作系統版本、段對齊的信息等。IMAGE_ OPTIONAL_HEADER結構如下:
l????????? ?
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES???? 16
typedef struct _IMAGE_OPTIONAL_HEADER {
??? //
??? // 標準域
??? //
??? WORD??? Magic;?????????? ? ???????
??? BYTE??? MajorLinkerVersion; ???????
??? BYTE??? MinorLinkerVersion; ???????
??? DWORD?? SizeOfCode;??? ?? ???????????
??? DWORD?? SizeOfInitializedData;? ????
??? DWORD?? SizeOfUninitializedData; ???????
??? DWORD?? AddressOfEntryPoint;? ??????
??? DWORD?? BaseOfCode;??? ??????????????
??? DWORD?? BaseOfData;??? ??????????????
??? //
??? // NT附加域??????
??? //
??? 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_HEADER, *PIMAGE_OPTIONAL_HEADER;
l????????? ?
其中參數含義如下所述。
Magic:這個值好像總是0x010b。
MajorLinkerVersion及MinorLinkerVersion:鏈接器的版本號,這個值不太可靠。
SizeOfCode:可執行代碼的長度。
SizeOfInitializedData:初始化數據的長度(數據段)。
SizeOfUninitializedData:未初始化數據的長度(bss段)。
AddressOfEntryPoint:代碼的入口RVA地址,程序從這兒開始執行,常稱為程序的原入口點OEP(Original Entry Point)。
BaseOfCode:可執行代碼起始位置。
BaseOfData:初始化數據起始位置。
ImageBase:載入程序首選的RVA地址。這個地址可被Loader改變。
SectionAlignment:段加載后在內存中的對齊方式。
FileAlignment:段在文件中的對齊方式。
MajorOperatingSystemVersion及MinorOperatingSystemVersion:操作系統版本。
MajorImageVersion及MinorImageVersion:程序版本。
MajorSubsystemVersion及MinorSubsystemVersion:子系統版本號,這個域系統支持。例如,程序運行于NT下,子系統版本號如果不是4.0,對話框不能顯示3D風格。
Win32VersionValue:這個值總是為0。
SizeOfImage:程序調入后占用內存大小(字節),等于所有段的長度之和。
SizeOfHeaders:所有文件頭長度之和,它等于從文件開始到第一個段的原始數據之間的大小。
CheckSum:校驗和,僅用在驅動程序中,在可執行文件中可能為0。它的計算方法Microsoft不公開,在imagehelp.dll中的CheckSumMappedFile()函數可以計算它。
Subsystem:一個標明可執行文件所期望的子系統的枚舉值。
DllCharacteristics:DLL狀態。
SizeOfStackReserve:保留堆棧大小。
SizeOfStackCommit:啟動后實際申請的堆棧數,可隨實際情況變大。
SizeOfHeapReserve:保留堆大小。
SizeOfHeapCommit:實際堆大小。
LoaderFlags:目前沒有用。
NumberOfRvaAndSizes:下面的目錄表入口個數,這個值也不可靠,可用常數IMAGE_NUMBEROF_DIRECTORY_ENTRIES來代替它,這個值在目前Windows版本中設為16。注意,如果這個值不等于16,那么這個數據結構大小就不能固定下來,也就不能確定其他變量位置。
DataDirectory:是一個IMAGE_DATA_DIRECTORY數組,數組元素個數為IMAGE_NUMBEROF_DIRECTORY_ENTRIES,結構如下:
l????????? ?
typedef struct _IMAGE_DATA_DIRECTORY {
??? DWORD?? VirtualAddress;???????? // 起始RVA地址
??? DWORD?? Size;????????????????? ??? // 長度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
2.3.3? IMAGE_SECTION_HEADER頭部
PE文件格式中,所有的節頭部位于可選頭部之后。每個節頭部為40個字節長,并且沒有任何填充信息。節頭部被定義為以下的結構:
l????????? ?
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
??? BYTE??? Name[IMAGE_SIZEOF_SHORT_NAME];?? // 節表名稱,如".text"
??? 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;
l????????? ?
其中IMAGE_SIZEOF_SHORT_NAME等于8。注意,如果不是這個值,那么這個數據結構大小就不能固定下來,也就不能確定其他變量位置。
2.4? 如何獲取PE文件中的OEP
OEP(Original Entry Point)是每個PE文件被加載時的起始地址,如何獲得這個地址很重要,因為修改程序中的這個值是文件加殼和脫殼時的必須步驟,一些黑客程序也是通過修改OEP值來獲得對目標程序的控制權從而實施攻擊。下面分別介紹如何通過文件直接訪問和通過內存映射訪問讀取OEP值的方法,并給出完整的程序代碼。
2.4.1? 通過文件讀取OEP值
獲得OEP值的最簡單方法是,直接從一個PE文件中讀取OEP。根據以上對PE文件結構的介紹可知,OEP是PE文件的IMAGE_OPTIONAL_HEADER結構的AddressOfEntryPoint成員,在偏移此結構頭40個字節處。而IMAGE_OPTIONAL_ HEADER在PE文件的起始位置由IMAGE_DOS_HEADER的e_lfanew成員來計算。注意,以上兩個結構在PE文件中不是緊跟在一起的,它之間是DOS Stub,而在每個PE文件DOS Stub的長度可能不一定相等。在PE文件的頭部是IMAGE_ DOS_HEADER結構,讀取這個結構可以得到e_lfanew的值,因而可以得到IMAGE_ OPTIONAL_HEADER在PE文件中的位置,也就得到了OEP值。以下是通過文件訪問的方法讀取OEP的程序代碼,即:
l????????? ?
// 通過文件讀取OEP值
BOOL ReadOEPbyFile(LPCSTR szFileName)
{
??? HANDLE hFile;
???
??? // 打開文件
??? if ((hFile = CreateFile(szFileName, GENERIC_READ,
??????? FILE_SHARE_READ, 0, OPEN_EXISTING,
??????? FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_VALUE)
??? {
??????? printf("Can't not open file./n");
??????? return FALSE;
??? }
???
??? DWORD dwOEP,cbRead;
??? IMAGE_DOS_HEADER dos_head[sizeof(IMAGE_DOS_HEADER)];
??? if (!ReadFile(hFile, dos_head, sizeof(IMAGE_DOS_HEADER), &cbRead, NULL)){
??????? printf("Read image_dos_header failed./n");
??????? CloseHandle(hFile);
??????? return FALSE;
??? }
???
??? int nEntryPos=dos_head->e_lfanew+40;
??? SetFilePointer(hFile, nEntryPos, NULL, FILE_BEGIN);
???
??? if (!ReadFile(hFile, &dwOEP, sizeof(dwOEP), &cbRead, NULL)){
??????? printf("read OEP failed./n");
??????? CloseHandle(hFile);
??????? return FALSE;
??? }
???
??? // 關閉文件
??? CloseHandle(hFile);
???
??? // 顯示OEP地址
??? printf("OEP by file:%d/n",dwOEP);
??? return TRUE;
}
2.4.2? 通過內存映射讀取OEP值
獲得OEP值的另一種方法是通過內存映射來實現,此方法也需要熟悉PE的文件結構。與直接訪問PE的方法不同,內存映射的方法首先把PE文件映射到計算機的內存,再通過內存的基指針獲得IMAGE_DOS_HEADER的頭指針,由此再獲得IMAGE_ OPTIONAL_HEADER指針,這樣就可以得到AddressOfEntryPoint的值。下面是通過內存映射獲得OEP值的方法:
l????????? ?
// 通過文件內存映射讀取OEP值
BOOL ReadOEPbyMemory(LPCSTR szFileName)
{
??? struct PE_HEADER_MAP
??? {
??????? DWORD signature;
??????? IMAGE_FILE_HEADER _head;
??????? IMAGE_OPTIONAL_HEADER opt_head;
??????? IMAGE_SECTION_HEADER section_header[6];
??? } *header;
??? HANDLE hFile;
??? HANDLE hMapping;
??? void *basepointer;
???
??? // 打開文件
??? if ((hFile = CreateFile(szFileName, GENERIC_READ,
??????? FILE_SHARE_READ,0,OPEN_EXISTING,
??????? FILE_FLAG_SEQUENTIAL_SCAN,0)) == INVALID_HANDLE_VALUE)
??? {
??????? printf("Can't open file./n");
??????? return FALSE;
??? }
???
??? // 創建內存映射文件
?? if (!(hMapping = CreateFileMapping(hFile,0,PAGE_READONLY|SEC_COMMIT, 0,0,0)))
??? {
??????? printf("Mapping failed./n");
??????? CloseHandle(hFile);
??????? return FALSE;
??? }
???
??? // 把文件頭映象存入baseointer
??? if (!(basepointer = MapViewOfFile(hMapping,FILE_MAP_READ,0,0,0)))
??? {
??????? printf("View failed./n");
??????? CloseHandle(hMapping);
??????? CloseHandle(hFile);
??????? return FALSE;
??? }
??? IMAGE_DOS_HEADER * dos_head =(IMAGE_DOS_HEADER *)basepointer;
???
??? // 得到PE文件頭
??? header = (PE_HEADER_MAP *)((char *)dos_head + dos_head->e_lfanew);
???
??? // 得到OEP地址.
??? DWORD dwOEP=header->opt_head.AddressOfEntryPoint;
???
??? // 清除內存映射和關閉文件
??? UnmapViewOfFile(basepointer);
??? CloseHandle(hMapping);
??? CloseHandle(hFile);
???
??? // 顯示OEP地址
??? printf("OEP by memory:%d/n",dwOEP);
??? return TRUE;
}
2.4.3? 讀取OEP值方法的測試
為了檢驗以上兩種獲取OEP值方法的正確性和一致性,可以用以下的方法來測試:
l????????? ?
// oep.cpp:讀取OEP的實例
//
#include <windows.h>
#include <stdio.h>
BOOL ReadOEPbyMemory(LPCSTR szFileName);
BOOL ReadOEPbyFile(LPCSTR szFileName);
void main()
{
??? ReadOEPbyFile("..//calc.exe");
??? ReadOEPbyMemory("..//calc.exe");
}
l????????? ?
運行以上代碼后,可以得到如圖2.3所示的結果。從圖中可以看出,以上兩種獲取OEP值方法所得到的結果是一致的。
獲取OEP值方法的測試結果
2.5 ?PE文件中的資源
一些PE格式(Portable Executable)的EXE文件常常存在很多資源,如圖標、位圖、對話框、聲音等。若要把這些資源取出為自己所用,或修改這些文件中的資源,則需要對PE文件中資源數據結構有所了解。
2.5.1? 查找資源在文件中的起始位置
要找出一個PE文件中的某種資源,首先需要確定資源節在PE文件中的起始位置。有兩種方法來確定資源在文件中的起始位置。
第一種方法,首先根據FileHeader中的成員NumberOfSections的值,確定文件中節的數目,再根據節的數目,遍歷節表數組。也就是從0到(節表數–1)的每一個節表項。比較每一個節表項的Name字段,看看是否等于“.rsrc”,如果是,就找到了資源節的節表項。這個節表項的PointerToRawData 中的值,就是資源節在文件中的位置。
第二種方法,取得PE Header中的IMAGE_OPTIONAL_HEADER中的DataDirectory數組中的第三項,也就是資源項。DataDirectory[]數組的每項都是IMAGE_DATA_ DIRECTORY結構,該結構定義如下:
l????????? ?
typedef struct _IMAGE_DATA_DIRECTORY {
??? DWORD VirtualAddress;
??? DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
l????????? ?
從以上結構對象取得DataDirectory數組中的第三項中的成員VirtualAddress的值。這個值就是在內存中資源節的RVA。然后根據節的數目,遍歷節表數組,也就是從0~(節表數–1)的每一個節表項。每個節在內存中的RVA的范圍是從該節表項的成員VirtualAddress字段的值開始(包括這個值),到VirtualAddress+Misc.VirtualSize的值結束(不包括這個值)。遍歷整個節表,看看所取得的資源節的RVA是否在那個節表項的RVA范圍之內。如果在范圍之內,就找到了資源節的節表項。這個節表項中的PointerToRawData 中的值,就是資源節在文件中的位置。如果這個PE文件沒有資源?? 的話,DataDirectory數組中的第三項內容為0。這樣也可以得到了資源在文件中開始的位置。
2.5.2? 確定PE文件中的資源
得到了資源節在文件中的位置后,就可以確定某個資源類型及其二進制數據在PE文件中的位置和數據塊的大小。
資源節最開始是一個IMAGE_RESOURCE_DIRECTORY結構,在winnt.h文件中有這個結構的定義。這個結構長度為16字節,共有6個參數,其結構的原型如下:
l????????? ?
typedef struct _IMAGE_RESOURCE_DIRECTORY {
??? DWORD Characteristics;
??? DWORD TimeDateStamp;
??? WORD MajorVersion;
??? WORD MinorVersion;
??? WORD NumberOfNamedEntries;
??? WORD NumberOfIdEntries;
??? // IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
l????????? ?
其中各個參數的含義如下所述
Characteristics: 標識此資源的類型。
TimeDateStamp:資源編譯器產生資源的時間。
MajorVersion:資源主版本號。
MinorVersion:資源次版本號。
NumberOfNamedEntries和NumberofIDEntries:分別為用字符串和整形數字來進行標識的IMAGE_RESOURCE_DIRECTORY_ENTRY項數組的成員個數。
緊跟著IMAGE_RESOURCE_DIRECTORY后面的是一個IMAGE_RESOURCE_ DIRECTORY_ENTRY數組。這個結構長度為8個字節,共有兩個字段,每個字段4個字節。其結構原型如下:
l????????? ?
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
??? union {
??????? struct {
??????????? DWORD NameOffset:31;
?????? ?????DWORD NameIsString:1;
??????? };
??????? DWORD?? Name;
??????? WORD??? Id;
??? };
??? union {
??????? DWORD?? OffsetToData;
??????? struct {
??????????? DWORD?? OffsetToDirectory:31;
??????????? DWORD?? DataIsDirectory:1;
??????? };
??? };
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
l????????? ?
其中,對于第一個字段,當其最高位為1(0x80000000)時,這個DWORD剩下的31位表明相對于資源開始位置的偏移,偏移的內容是一個IMAGE_RESOURCE_DIR_ STRING_U,用其中的字符串來標明這個資源類型;當第一個字段的最高位為0時,表示這個DWORD的低WORD中的值作為Id標明這個資源類型。
對于第二個字段,當第二個字段的最高位為1時,表示還有下一層的結構。這個DWORD的剩下31位表明一個相對于資源開始位置的偏移,這個偏移的內容將是一個下一層的IMAGE_RESOURCE_DIRECTORY結構;當第二個字段的最高位為0時,表示已經沒有下一層的結構了。這個DWORD的剩下31位表明一個相對于資源開始位置的偏移,這個偏移的內容會是一個IMAGE_RESOURCE_DATA _ENTRY結構,此結構會說明資源的位置。對于資源標示號Id,當Id等于1時,表示資源為光標,等于2時表示資源為位圖等,等于3時表示資源為圖標等。在winuser.h文件中有定義。
標識一個IMAGE_RESOURCE_DIRECTORY_ENTRY一般都是使用Id,就是一個整數。但是也有少數使用IMAGE_RESOURCE_DIR_STRING_U來標識一個資源類型。這個結構定義如下:
l????????? ?
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
??? WORD Length;
??? WCHAR NameString[1];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
l????????? ?
這個結構中將有一個Unicode的字符串,是字對齊的。這個結構的長度可變,由第一個字段Length指明后面的Unicode字符串的長度。
經過3層IMAGE_RESOURCE_DIRECTORY_ENTRY(一般是3層,也有可能更少些)最終可以找到一個IMAGE_RESOURCE_DATA_ENTRY結構,這個結構中存有相應資源的位置和大小。這個結構長16個字節,有4個參數,其原型如下:
l????????? ?
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
??? DWORD OffsetToData;
??? DWORD Size;
??? DWORD CodePage;
??? DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
l????????? ?
其中各個參數的含義如下所述。
OffsetToData:這是一個內存中的RVA,可以用來轉化成文件中的位置。用這個值減去資源節的開始RVA,就可以得到相對于資源節開始的偏移。再加上資源節在文件中的開始位置,即節表中資源節中PointerToRawData的值,就是資源在文件中的位置。注意,資源節的開始RVA可以由Optional Header中的DataDirectory數組中的第三項中的VirtualAddress的值得到,或者節表中資源節那項中的VirtualAddress的值得到。
Size:資源的大小,以字節為單位。
CodePage:代碼頁。
Reserved:保留項。
總之,資源一般使用樹來保存,通常包含3層,最高層是類型,其次是名字,最后是語言。在資源節開始的位置,首先是一個IMAGE_RESOURCE_DIRECTORY結構,后面緊跟著IMAGE_RESOURCE_DIRECTORY_ENTRY數組,這個數組的每個元素代表的資源類型不同;通過每個元素,可以找到第二層另一個IMAGE_RESOURCE_ DIRECTORY,后面緊跟著IMAGE_RESOURCE_DIRECTORY_ENTRY數組。這一層的數組的每個元素代表的資源名字不同;然后可以找到第三層的每個IMAGE_ RESOURCE_DIRECTORY,后面緊跟著IMAGE_RESOURCE_DIRECTORY_ENTRY數組。這一層的數組的每個元素代表的資源語言不同;最后通過每個IMAGE_RESOURCE_ DIRECTORY_ENTRY可以找到每個IMAGE_RESOURCE_DATA_ENTRY。通過每個IMAGE_RESOURCE_DATA_ENTRY,就可以找到每個真正的資源。
2.6? 一個修改PE可執行文件的完整實例
在下面的實例中,將把一段MessageBoxA()的計算機代碼根據PE文件的格式注入到一個PE程序中。有關把代碼注入到一個應用程序的技術將在后面的章節專門介紹。
2.6.1? 如何獲得MessageBoxA代碼
要實現代碼注入PE程序且能夠運行,首先要做的是如何得到這段代碼。為了得到這種代碼,作者編寫了一段匯編源程序 msgbx.asm,然后用RadASM編譯器進行編譯,當然也可以使用其他的方法來實現代碼的注入。編寫這段代碼最關鍵的問題是如何把對話框標題字符串和顯示字符串一起存放在代碼段,以便提取,否則無法提取。下面是生成MessageBoxA()的源代碼:
l????????? ?
;msgbx.asm 文件.
;
.386p
.model flat, stdcall
option casemap:none
include ??????? /masm32/include/windows.inc
include ???????? /masm32/include/user32.inc
includelib ???? /masm32/lib/user32.lib
.code
start:
??? push MB_ICONINFORMATION or MB_OK
??? call Func1
??? db "Test",0
Func1:
??? call Func2
??? db "Hello",0
Func2:
??? push NULL???
??? call MessageBoxA
;??? ret
end start
l????????? ?
其中"Test"是MessageBoxA()對話框的標題,"Hello"是要顯示的字符串。Message- BoxA()所用的Windows句柄為NULL。
用RadASM編譯器對以上代碼編譯后,可以生成一個msgbx.obj文件,用VC++ 編輯器打開后,如圖2.4所示,可以查看這個文件的機器代碼。
Msgbx.obj文件的機器代碼
把圖2.4中所選擇的計算機機器代碼取出變成一個命令字符串,即:
l????????? ?
unsigned char cmdline[35]={
??? 0x6a,???? ????????????????????????? // (1) push 命令
??? 0x40,?????????????????????????? ??? // (1) MB_ICONINFORMATION|MB_OK
??? 0xe8,?????????????????????????? ??? // (1) call命令
??? 0x05,0x00,0x00,0x00,???????? // (4) 標題字符串字節個數,包括結束位
(DWORD)
??? 0x54,0x65,0x73,0x74, 0x00,????? // (5) "Test",0(標題)
??? 0xe8,?????????????????????????? // (1) call命令
??? 0x06,0x00,0x00,0x00,??????????? // (4) 標題字符串字節個數,包括結束位
(DWORD)
??? 0x48,0x65,0x6c,0x6c,0x6f,0x00,? // (6) "Hello",0(顯示字符串)
??? 0x6a,?????????????????????????? ??? // (1) push 命令
??? 0x00,?????????????????????????? ??? // (1) 窗口句柄hWnd,NULL
??? 0xe8, ????????????????????????????? // (1) call命令
??? 0x00,0x00,0x00,0x00,??????????? ? // (4) MessageBoxA的地址 (DWORD)
??? 0x1a,?????????????????????????? ??? // (1) 第26位,校驗和
??? 0x00,0x00,0x00,0x0b???????????? ? // (4) 返回地址 (DWORD)
};
l????????? ?
其中()中的數值表示這一行上代碼的字節個數。0x6a是匯編語言中的push命令,0xe8是匯編語言中的call命令,而jmp命令為0xe9。“校驗和”是從第一個push命令開始計算所得到的字節總數和(包括校驗計數位),從以上代碼第一個字節開始計數起到“校驗和”位正好是第26位字節個數。字符串字節個數位為一個DWORD型,占4個字節,它是按Little-endian的方式存放的,要把這4個字節位的順序顛倒才能得到實際數值,即把高位字節變成低位,把低位變換到高位。
要把以上代碼注入到一個PE文件中,需要修改4個地方:(1)修改PE文件的入口地址,使PE裝載器首先裝載以上代碼;(2)修改以上代碼MessageBoxA()的地址,使以上的代碼能夠顯示出一個對話框;(3)把“校驗和”位變成跳轉位,即變成jmp (0xe9);(4)修改返回地址,把程序引入到原來的裝載點上。
2.6.2? 把MessageBoxA()代碼寫入PE文件的完整實例
根據以上的對MessageBoxA()的分析,可以直接把以上代碼注入到一個PE可執行 文件中。為了使程序有通用性,這里編寫了一個產生顯示任意長度字符的對話框的函數WriteMessageBox()。
下面是用于注入MessageBoxA()代碼的頭文件,取名為Pe.h,其中用 #include包含了相關的文件頭,定義了peHeader結構,且定義了CPe類,其源代碼如下:
l????????? ?
// Pe.h: 定義CPe類
//
#ifndef _PE_H__INCLUDED
#define _PE_H__INCLUDED
#include <io.h>
#include <fcntl.h>
#include <sys/stat.h>
typedef struct PE_HEADER_MAP
{
??? DWORD signature;
??? IMAGE_FILE_HEADER _head;
??? IMAGE_OPTIONAL_HEADER opt_head;
??? IMAGE_SECTION_HEADER section_header[6];
} peHeader;
class CPe?
{
public:
??? CPe();
??? virtual ~CPe();
public:
??? void CalcAddress(const void *base);
??? void ModifyPe(CString strFileName,CString strMsg);
??? void WriteFile(CString strFileName,CString strMsg);
??? BOOL WriteNewEntry(int ret,long offset,DWORD dwAddress);
??? BOOL WriteMessageBox(int ret,long offset,CString strCap,CString
??? strTxt);
??? CString StrOfDWord(DWORD dwAddress);
public:
??? DWORD dwSpace;
??? DWORD dwEntryAddress;
??? DWORD dwEntryWrite;
??? DWORD dwProgRAV;
??? DWORD dwOldEntryAddress;
??? DWORD dwNewEntryAddress;
??? DWORD dwCodeOffset;
??? DWORD dwPeAddress;
??? DWORD dwFlagAddress;
??? DWORD dwVirtSize;
??? DWORD dwPhysAddress;
??? DWORD dwPhysSize;
??? DWORD dwMessageBoxAadaddress;
};
#endif
l????????? ?
其中peHeader結構是前面所講的PE Header結構與節表(Section Table)頭結構(6個表頭成員)的總結構。因為它們在PE文件中是緊湊排列的,所以可以這樣寫。其實只用一個節表頭就可以。
下面分別介紹CPe類成員函數的定義,它們包含在Pe.cpp文件中。在這個文件開始用#include包含了stdafx.h和Pe.h文件。用MFC VC++編譯器編譯時,必須包括stdafx.h文件,即使這個文件是空的,也需要包括它,這是編譯器設置所致,除非修改MFC的編譯器的默認設置。CPe類的構造和析構函數這里沒有用上,對系統內存的訪問和其他操作主要是通過主成員函數ModifyPe()來進行。它們的源代碼如下:
l????????? ?
// Pe.cpp: 實現 CPe類
//
#include "stdafx.h"
#include "Pe.h"
CPe::CPe()
{
}
CPe::~CPe()
{
}
void CPe::ModifyPe(CString strFileName,CString strMsg)
{
??? CString strErrMsg;
??? HANDLE hFile, hMapping;
??? void *basepointer;
???
??? // 打開要修改的文件
??? if ((hFile = CreateFile(strFileName, GENERIC_READ|GENERIC_WRITE,
??????? FILE_SHARE_READ|FILE_SHARE_WRITE, 0,
??????? OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_ VALUE)
??? {
??????? AfxMessageBox("Could not open file.");
??????? return;
??? }
??? // 創建一個映射文件
??? if (!(hMapping = CreateFileMapping(hFile, 0, PAGE_READONLY | SEC_ COMMIT, 0, 0, 0)))
??? {
??????? AfxMessageBox("Mapping failed.");
??????? CloseHandle(hFile);
??????? return;
??? }
??? // 把文件頭映象存入baseointer
??? if (!(basepointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0)))
??? {
??????? AfxMessageBox("View failed.");
??????? CloseHandle(hMapping);
??????? CloseHandle(hFile);
??????? return;
??? }
??? CloseHandle(hMapping);
??? CloseHandle(hFile);
??? CalcAddress(basepointer); // 得到相關地址
??? UnmapViewOfFile(basepointer);
???
??? if(dwSpace<50)
??? {
??????? AfxMessageBox("No room to write the data!");
??? }
??? else
??? {
??????? WriteFile(strFileName,strMsg); // 寫文件
??? }
???
??? if ((hFile = CreateFile(strFileName, GENERIC_READ|GENERIC_WRITE,
??????? FILE_SHARE_READ|FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_ VALUE)
??? {
??????? AfxMessageBox("Could not open file.");
??????? return;
??? }
???
??? CloseHandle(hFile);
}
其中對一個PE文件進行MessageBoxA()代碼的注入是通過ModifyPe()函數進行,它的入口參數是要被修改的PE可執行文件名。在這個函數中,首先創建所修改文件的句柄,然后創建映射文件,再通過映射文件的句柄獲得這個PE文件的文件頭指針,最后把這個指針傳給函數CalcAddress()。通過CalcAddress()函數來計算PE Header的開始偏移、保存舊的程序入口地址、計算新的程序入口地址和計算PE文件的空隙空間等。
CalcAddress()函數的源代碼如下:
l????????? ?
void CPe::CalcAddress(const void *base)
{
??? IMAGE_DOS_HEADER * dos_head =(IMAGE_DOS_HEADER *)base;
??? if (dos_head->e_magic != IMAGE_DOS_SIGNATURE)
??? {
??????? AfxMessageBox("Unknown type of file.");
??????? return;
??? }
???
??? peHeader * header;
??? // 得到PE文件頭
??? header = (peHeader *)((char *)dos_head + dos_head->e_lfanew);
??? if(IsBadReadPtr(header, sizeof(*header)))
??? {
??????? AfxMessageBox("No PE header, probably DOS executable.");
??????? return;
??? }
??? DWORD mods;
??? char tmpstr[4]={0};
??? if(strstr((const char *)header->section_header[0].Name,".text")!=
??? NULL)
??? {
??????? // 此段的真實長度
??????? dwVirtSize=header->section_header[0].Misc.VirtualSize;
??????? // 此段的物理偏移
??????? dwPhysAddress=header->section_header[0].PointerToRawData;
??????? // 此段的物理長度
??????? dwPhysSize=header->section_header[0].SizeOfRawData;
???????
??????? // 得到PE文件頭的開始偏移
??????? dwPeAddress=dos_head->e_lfanew;
???????
??????? // 得到代碼段的可用空間,用以�%