本章提要
· PE文件格式概述
· PE文件結構
· 如何獲取PE文件中的OEP
· 如何獲取PE文件中的資源
· 如何修改PE文件使其顯示MessageBox的實例
2.1 引言
通常Windows下的EXE文件都采用PE格式。PE是英文Portable Executable的縮寫,它是一種針對于微軟Windows NT、Windows 95和Win32s系統(tǒng),由微軟公司設計的可執(zhí)行的二進制文件(DLLs和執(zhí)行程序)格式,目標文件和庫文件通常也是這種格式。這種格式由TIS(Tool Interface Standard)委員會(Microsoft、Intel、Borland、Watcom、IBM等)在1993進行了標準化。顯然,它參考了一些UNIXes和VMS的COFF(Common Object File Format)格式。
認識可執(zhí)行文件的結構非常重要,在DOS下是這樣,在Windows系統(tǒng)下更是如此。了解了這種結構后就可以對可執(zhí)行程序進行加密、加殼和修改等,一些黑客也利用了這些技術。為了使讀者對PE文件格式有進一步的認識,本章從一個程序員的角度出發(fā)再次介紹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下執(zhí)行,DOS就能識別出這是有效的執(zhí)行體,然后運行緊隨MZ Header之后的DOS Stub。
· DOS Stub
DOS Stub實際上是個有效的EXE,在不支持PE文件格式的操作系統(tǒng)中,它將簡單顯示一個錯誤提示,類似于字符串“This program requires Windows”或者程序員可根據自己的意圖實現(xiàn)完整的DOS代碼。大多數情況下DOS Stub由匯編器/編譯器自動生成。
· PE Header
緊接著DOS Stub的是PE Header。它是一個IMAGE_NT_HEADERS結構。其中包含了很多PE文件被載入內存時需要用到的重要域。執(zhí)行體在支持PE文件結構的操作系統(tǒng)中執(zhí)行時,PE裝載器將從DOS MZ header中找到PE header的起始偏移量。因而跳過DOS Stub直接定位到真正的文件頭 PE header。
· Section Table
PE Header之后是數組結構Section Table(節(jié)表)。如果PE文件里有5個節(jié),那么此Section Table結構數組內就有5個(IMAGE_SECTION_HEADER)成員,每個成員包含對應節(jié)的屬性、文件偏移量、虛擬偏移量等。排在節(jié)表中的最前面的第一個默認成員是text,即代碼節(jié)頭。通過遍歷查找方法可以找到其他節(jié)表成員(節(jié)表頭)。
· Sections
PE文件的真正內容劃分成塊,稱為Sections(節(jié))。每個標準節(jié)的名字均以圓點開頭,但也可以不以圓點開頭,節(jié)名的最大長度為8個字節(jié)。Sections是以其起始位址來排列,而不是以其字母次序來排列。通過節(jié)表提供的信息,可以找到這些節(jié)。程序的代碼,資源等就放在這些節(jié)中。
節(jié)的劃分是基于各組數據的共同屬性,而不是邏輯概念。每節(jié)是一塊擁有共同屬性的數據,比如代碼/數據、讀/寫等。如果PE文件中的數據/代碼擁有相同屬性,它們就能被歸入同一節(jié)中。節(jié)名稱僅僅是個區(qū)別不同節(jié)的符號而已,類似“data”,“code”的命名只為了便于識別,唯有節(jié)的屬性設置決定了節(jié)的特性和功能。
2.2.2 PE文件內存映射
在Windows系統(tǒng)下,當一個PE應用程序運行時,這個PE文件在磁盤中的數據結構布局和內存中的數據結構布局是一致的。系統(tǒng)在載入一個可執(zhí)行程序時,首先是Windows裝載器(又稱PE裝載器)把磁盤中的文件映射到進程的地址空間,它遍歷PE文件并決定文件的哪一部分被映射。其方式是將文件較高的偏移位置映射到較高的內存地址中。磁盤文件一旦被裝入內存中,其某項的偏移地址可能與原始的偏移地址有所不同,但所表現(xiàn)的是一種從磁盤文件偏移到內存偏移的轉換,如圖2.2所示。

PE文件內存映射
當PE文件被加載到內存后,內存中的版本稱為模塊(Module),映射文件的起始地址稱為模塊句柄(hModule),可以通過模塊句柄訪問內存中的其他數據結構。這個初始內存地址也稱為文件映像基址(ImageBase)。載入一個PE程序的主要步驟如下:
(1)當PE文件被執(zhí)行時,PE裝載器首先為進程分配一個4GB的虛擬地址空間,然后把程序所占用的磁盤空間作為虛擬內存映射到這個4GB的虛擬地址空間中。一般情況下,會映射到虛擬地址空間中0x400000的位置。裝載一個應用程序的時間比一般人所設想的要少,因為裝載一個PE文件并不是把這個文件一次性地從磁盤讀到內存中,而是簡單地做一個內存映射,映射一個大文件和映射一個小文件所花費的時間相差無幾。當然,真正執(zhí)行文件中的代碼時,操作系統(tǒng)還是要把存在于磁盤上的虛擬內存中的代碼交換到物理內存(RAM)中。但是,這種交換也不是把整個文件所占用的虛擬地址空間一次性地全部從磁盤交換到物理內存中,操作系統(tǒng)會根據需要和內存占用情況交換一頁或多頁。當然,這種交換是雙向的,即存在于物理內存中的一部分當前沒有被使用的頁,也可能被交換到磁盤中。
(2)PE裝載器在內核中創(chuàng)建進程對象和主線程對象以及其他內容。
(3)PE裝載器搜索PE文件中的Import Table(引入表),裝載應用程序所使用的動態(tài)鏈接庫。對動態(tài)鏈接庫的裝載與對應用程序的裝載方法完全類似。
(4)PE裝載器執(zhí)行PE文件首部所指定地址處的代碼,開始執(zhí)行應用程序主線程。
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個字節(jié)。當這個整形變量的值為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文件被映射到內存的地址加上該結構在文件中的地址。
第三種,當執(zhí)行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個字節(jié),描述它內容的結構如下:
l
// 此結構包含于WINNT.H中
//
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE頭部
WORD e_magic; // 魔術數字
WORD e_cblp; // 文件最后頁的字節(jié)數
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兼容的可執(zhí)行文件都將這個值設為0x5A4D,表示ASCII字符MZ。MS-DOS頭部之所以有的時候被稱為MZ頭部,就是這個緣故。還有許多其他的域對于MS-DOS操作系統(tǒng)來說都有用,但是對于Windows NT來說,這個結構中只有一個有用的域——最后一個域e_lfnew,一個4字節(jié)的文件偏移量,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個字節(jié)組成,它被定義為:
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:表示該程序要執(zhí)行的環(huán)境及平臺,現(xiàn)在已知的值如表2.1所示。
應用程序執(zhí)行的環(huán)境及平臺代碼
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:文件建立的時間。可用這個值來區(qū)分同一個文件的不同的版本,即使它們的商業(yè)版本號相同。這個值的格式并沒有明確的規(guī)定,但是很顯然地大多數的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個字節(jié)組成。雖然它的名字是“可選頭部”,但是請確信:這個頭部并非“可選”,而是“必需”的。可選頭部包含了很多關于可執(zhí)行映像的重要信息。例如,初始的堆棧大小、程序入口點的位置、首選基地址、操作系統(tǒng)版本、段對齊的信息等。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:可執(zhí)行代碼的長度。
SizeOfInitializedData:初始化數據的長度(數據段)。
SizeOfUninitializedData:未初始化數據的長度(bss段)。
AddressOfEntryPoint:代碼的入口RVA地址,程序從這兒開始執(zhí)行,常稱為程序的原入口點OEP(Original Entry Point)。
BaseOfCode:可執(zhí)行代碼起始位置。
BaseOfData:初始化數據起始位置。
ImageBase:載入程序首選的RVA地址。這個地址可被Loader改變。
SectionAlignment:段加載后在內存中的對齊方式。
FileAlignment:段在文件中的對齊方式。
MajorOperatingSystemVersion及MinorOperatingSystemVersion:操作系統(tǒng)版本。
MajorImageVersion及MinorImageVersion:程序版本。
MajorSubsystemVersion及MinorSubsystemVersion:子系統(tǒng)版本號,這個域系統(tǒng)支持。例如,程序運行于NT下,子系統(tǒng)版本號如果不是4.0,對話框不能顯示3D風格。
Win32VersionValue:這個值總是為0。
SizeOfImage:程序調入后占用內存大小(字節(jié)),等于所有段的長度之和。
SizeOfHeaders:所有文件頭長度之和,它等于從文件開始到第一個段的原始數據之間的大小。
CheckSum:校驗和,僅用在驅動程序中,在可執(zhí)行文件中可能為0。它的計算方法Microsoft不公開,在imagehelp.dll中的CheckSumMappedFile()函數可以計算它。
Subsystem:一個標明可執(zhí)行文件所期望的子系統(tǒng)的枚舉值。
DllCharacteristics:DLL狀態(tài)。
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文件格式中,所有的節(jié)頭部位于可選頭部之后。每個節(jié)頭部為40個字節(jié)長,并且沒有任何填充信息。節(jié)頭部被定義為以下的結構:
l
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 節(jié)表名稱,如".text"
union {
DWORD PhysicalAddress; // 物理地址
DWORD VirtualSize; // 真實長度
} Misc;
DWORD VirtualAddress; // RVA
DWORD SizeOfRawData; // 物理長度
DWORD PointerToRawData; // 節(jié)基于文件的偏移量
DWORD PointerToRelocations; // 重定位的偏移
DWORD PointerToLinenumbers; // 行號表的偏移
WORD NumberOfRelocations; // 重定位項數目
WORD NumberOfLinenumbers; // 行號表的數目
DWORD Characteristics; // 節(jié)屬性
} 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個字節(jié)處。而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值的另一種方法是通過內存映射來實現(xiàn),此方法也需要熟悉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;
}
// 創(chuàng)建內存映射文件
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文件中的某種資源,首先需要確定資源節(jié)在PE文件中的起始位置。有兩種方法來確定資源在文件中的起始位置。
第一種方法,首先根據FileHeader中的成員NumberOfSections的值,確定文件中節(jié)的數目,再根據節(jié)的數目,遍歷節(jié)表數組。也就是從0到(節(jié)表數–1)的每一個節(jié)表項。比較每一個節(jié)表項的Name字段,看看是否等于“.rsrc”,如果是,就找到了資源節(jié)的節(jié)表項。這個節(jié)表項的PointerToRawData 中的值,就是資源節(jié)在文件中的位置。
第二種方法,取得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的值。這個值就是在內存中資源節(jié)的RVA。然后根據節(jié)的數目,遍歷節(jié)表數組,也就是從0~(節(jié)表數–1)的每一個節(jié)表項。每個節(jié)在內存中的RVA的范圍是從該節(jié)表項的成員VirtualAddress字段的值開始(包括這個值),到VirtualAddress+Misc.VirtualSize的值結束(不包括這個值)。遍歷整個節(jié)表,看看所取得的資源節(jié)的RVA是否在那個節(jié)表項的RVA范圍之內。如果在范圍之內,就找到了資源節(jié)的節(jié)表項。這個節(jié)表項中的PointerToRawData 中的值,就是資源節(jié)在文件中的位置。如果這個PE文件沒有資源 的話,DataDirectory數組中的第三項內容為0。這樣也可以得到了資源在文件中開始的位置。
2.5.2 確定PE文件中的資源
得到了資源節(jié)在文件中的位置后,就可以確定某個資源類型及其二進制數據在PE文件中的位置和數據塊的大小。
資源節(jié)最開始是一個IMAGE_RESOURCE_DIRECTORY結構,在winnt.h文件中有這個結構的定義。這個結構長度為16字節(jié),共有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個字節(jié),共有兩個字段,每個字段4個字節(jié)。其結構原型如下:
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個字節(jié),有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,可以用來轉化成文件中的位置。用這個值減去資源節(jié)的開始RVA,就可以得到相對于資源節(jié)開始的偏移。再加上資源節(jié)在文件中的開始位置,即節(jié)表中資源節(jié)中PointerToRawData的值,就是資源在文件中的位置。注意,資源節(jié)的開始RVA可以由Optional Header中的DataDirectory數組中的第三項中的VirtualAddress的值得到,或者節(jié)表中資源節(jié)那項中的VirtualAddress的值得到。
Size:資源的大小,以字節(jié)為單位。
CodePage:代碼頁。
Reserved:保留項。
總之,資源一般使用樹來保存,通常包含3層,最高層是類型,其次是名字,最后是語言。在資源節(jié)開始的位置,首先是一個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可執(zhí)行文件的完整實例
在下面的實例中,將把一段MessageBoxA()的計算機代碼根據PE文件的格式注入到一個PE程序中。有關把代碼注入到一個應用程序的技術將在后面的章節(jié)專門介紹。
2.6.1 如何獲得MessageBoxA代碼
要實現(xiàn)代碼注入PE程序且能夠運行,首先要做的是如何得到這段代碼。為了得到這種代碼,作者編寫了一段匯編源程序 msgbx.asm,然后用RadASM編譯器進行編譯,當然也可以使用其他的方法來實現(xiàn)代碼的注入。編寫這段代碼最關鍵的問題是如何把對話框標題字符串和顯示字符串一起存放在代碼段,以便提取,否則無法提取。下面是生成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) 標題字符串字節(jié)個數,包括結束位
(DWORD)
0x54,0x65,0x73,0x74, 0x00, // (5) "Test",0(標題)
0xe8, // (1) call命令
0x06,0x00,0x00,0x00, // (4) 標題字符串字節(jié)個數,包括結束位
(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
其中()中的數值表示這一行上代碼的字節(jié)個數。0x6a是匯編語言中的push命令,0xe8是匯編語言中的call命令,而jmp命令為0xe9。“校驗和”是從第一個push命令開始計算所得到的字節(jié)總數和(包括校驗計數位),從以上代碼第一個字節(jié)開始計數起到“校驗和”位正好是第26位字節(jié)個數。字符串字節(jié)個數位為一個DWORD型,占4個字節(jié),它是按Little-endian的方式存放的,要把這4個字節(jié)位的順序顛倒才能得到實際數值,即把高位字節(jié)變成低位,把低位變換到高位。
要把以上代碼注入到一個PE文件中,需要修改4個地方:(1)修改PE文件的入口地址,使PE裝載器首先裝載以上代碼;(2)修改以上代碼MessageBoxA()的地址,使以上的代碼能夠顯示出一個對話框;(3)把“校驗和”位變成跳轉位,即變成jmp (0xe9);(4)修改返回地址,把程序引入到原來的裝載點上。
2.6.2 把MessageBoxA()代碼寫入PE文件的完整實例
根據以上的對MessageBoxA()的分析,可以直接把以上代碼注入到一個PE可執(zhí)行 文件中。為了使程序有通用性,這里編寫了一個產生顯示任意長度字符的對話框的函數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結構與節(jié)表(Section Table)頭結構(6個表頭成員)的總結構。因為它們在PE文件中是緊湊排列的,所以可以這樣寫。其實只用一個節(jié)表頭就可以。
下面分別介紹CPe類成員函數的定義,它們包含在Pe.cpp文件中。在這個文件開始用#include包含了stdafx.h和Pe.h文件。用MFC VC++編譯器編譯時,必須包括stdafx.h文件,即使這個文件是空的,也需要包括它,這是編譯器設置所致,除非修改MFC的編譯器的默認設置。CPe類的構造和析構函數這里沒有用上,對系統(tǒng)內存的訪問和其他操作主要是通過主成員函數ModifyPe()來進行。它們的源代碼如下:
l
// Pe.cpp: 實現(xiàn) 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;
}
// 創(chuàng)建一個映射文件
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可執(zhí)行文件名。在這個函數中,首先創(chuàng)建所修改文件的句柄,然后創(chuàng)建映射文件,再通過映射文件的句柄獲得這個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;
// 得到代碼段的可用空間,用以判斷可不可以寫入我們的代碼
// 用此段的物理長度減去此段的真實長度就可以得到
dwSpace=dwPhysSize-dwVirtSize;
// 得到程序的裝載地址,一般為0x400000
dwProgRAV=header->opt_head.ImageBase;
// 得到代碼偏移,用代碼段起始RVA減去此段的物理偏移
// 應為程序的入口計算公式是一個相對的偏移地址,計算公式為:
// 代碼的寫入地址+dwCodeOffset
dwCodeOffset=header->opt_head.BaseOfCode-dwPhysAddress;
// 代碼寫入的物理偏移
dwEntryWrite=header->section_header[0].PointerToRawData+header->
section_header[0].Misc.VirtualSize;
//對齊邊界
mods=dwEntryWrite%16;
if(mods!=0)
{
dwEntryWrite+=(16-mods);
}
// 保存舊的程序入口地址
dwOldEntryAddress=header->opt_head.AddressOfEntryPoint;
// 計算新的程序入口地址
dwNewEntryAddress=dwEntryWrite+dwCodeOffset;
return;
}
}
l
下面的StrOfDWord()函數是把一個DWORD值轉換成一個字符串,因為一個DWORD值占有4個字節(jié),因此把一個DWORD值變成一個字符串,若保持數值不變,就變成了一個4個字節(jié)的字符串。同時把這個值的位置順序顛倒,這是為了把一個實際的值變成按Little-endian的方式寫入PE文件中,其轉換方法如下:
l
CString CPe::StrOfDWord(DWORD dwAddress)
{
unsigned char waddress[4]={0};
waddress[3]=(char)(dwAddress>>24)&0xFF;
waddress[2]=(char)(dwAddress>>16)&0xFF;
waddress[1]=(char)(dwAddress>>8)&0xFF;
waddress[0]=(char)(dwAddress)&0xFF;
return waddress;
}
l
下面的WriteNewEntry()函數把新的入口點寫入PE程序原來的入口點處,使PE裝載器在載入程序時,直接跳入到MessageBoxA()的入口處,該函數的源代碼如下:
l
BOOL CPe::WriteNewEntry(int ret,long offset, DWORD dwAddress)
{
CString strErrMsg;
long retf;
unsigned char waddress[4]={0};
retf=_lseek(ret,offset,SEEK_SET);
if(retf==-1)
{
AfxMessageBox("Error seek.");
return FALSE;
}
memcpy(waddress,StrOfDWord(dwAddress),4);
retf=_write(ret,waddress,4);
if(retf==-1)
{
strErrMsg.Format("Error write: %d",GetLastError());
AfxMessageBox(strErrMsg);
return FALSE;
}
return TRUE;
}
l
下面的WriteMessageBox()函數是把MessageBoxA()的機器代碼寫入到PE文件中。這個函數顯示的對話框標題和顯示的字符串內容和長度不是固定的。在這個函數中,首先就計算MessageBoxA()函數的地址和函數的返回地址,然后把重新生成的對話框代碼寫入到程序中。WriteMessageBox()函數的源代碼如下:
l
BOOL CPe::WriteMessageBox(int ret,long offset,CString strCap,CString
strTxt)
{
CString strAddress1,strAddress2;
unsigned char waddress[4]={0};
DWORD dwAddress;
// 獲取MessageBox在內存中的地址
HINSTANCE gLibMsg=LoadLibrary("user32.dll");
dwMessageBoxAadaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
// 計算校驗位
int nLenCap1 =strCap.GetLength()+1; // 加上字符串后面的結束位
int nLenTxt1 =strTxt.GetLength()+1; // 加上字符串后面的結束位
int nTotLen=nLenCap1+nLenTxt1+24;
// 重新計算MessageBox函數的地址
dwAddress=dwMessageBoxAadaddress-(dwProgRAV+dwNewEntryAddress+nTot
Len-5);
strAddress1=StrOfDWord(dwAddress);
// 計算返回地址
dwAddress=0-(dwNewEntryAddress-dwOldEntryAddress+nTotLen);
strAddress2=StrOfDWord(dwAddress);
// 對話框頭代碼(固定)
unsigned char cHeader[2]={0x6a,0x40};
// 標題定義
unsigned char cDesCap[5]={0xe8,nLenCap1,0x00,0x00,0x00};
// 內容定義
unsigned char cDesTxt[5]={0xe8,nLenTxt1,0x00,0x00,0x00};
// 對話框后部分的代碼段
unsigned char cFix[12]
={0x6a,0x00,0xe8,0x00,0x00,0x00,0x00,0xe9,0x00,0x00,0x00,0x00};
// 修改對話框后部分的代碼段
for(int i=0;i<4;i++)
cFix[3+i]=strAddress1.GetAt(i);
for(i=0;i<4;i++)
cFix[8+i]=strAddress2.GetAt(i);
char* cMessageBox=new char[nTotLen];
char* cMsg;
// 生成對話框命令字符串
memcpy((cMsg = cMessageBox),(char*)cHeader,2);
memcpy((cMsg += 2),cDesCap,5);
memcpy((cMsg += 5),strCap,nLenCap1);
memcpy((cMsg += nLenCap1),cDesTxt,5);
memcpy((cMsg += 5),strTxt,nLenTxt1);
memcpy((cMsg += nLenTxt1),cFix,12);
// 向應用程序寫入對話框代碼
CString strErrMsg;
long retf;
retf=_lseek(ret,(long)dwEntryWrite,SEEK_SET);
if(retf==-1)
{
delete[] cMessageBox;
AfxMessageBox("Error seek.");
return FALSE;
}
retf=_write(ret,cMessageBox,nTotLen);
if(retf==-1)
{
delete[] cMessageBox;
strErrMsg.Format("Error write: %d",GetLastError());
AfxMessageBox(strErrMsg);
return FALSE;
}
delete[] cMessageBox;
return TRUE;
}
l
下面的WriteFile()函數是總的寫入函數。在這個函數中,先打開被修改的PE文件,然后調用WriteNewEntry()和WriteMessageBox()函數。WriteFile()函數的源代碼如下:
l
void CPe::WriteFile(CString strFileName)
{
CString strAddress1,strAddress2;
int ret;
unsigned char waddress[4]={0};
ret=_open(strFileName,_O_RDWR | _O_CREAT | _O_BINARY,_S_IREAD | _S_
IWRITE);
if(!ret)
{
AfxMessageBox("Error open");
return;
}
// 把新的入口地址寫入文件,程序的入口地址在偏移PE文件頭開始第40位
if(!WriteNewEntry(ret,(long)(dwPeAddress+40),dwNewEntryAddress))
return;
// 把對話框代碼寫入到應用程序中
if(!WriteMessageBox(ret,(long)dwEntryWrite,"Test","We are the world!"))
return;
_close(ret);
}
l
僅僅利用以上CPe類還是不能對一個PE文件進行注入MessageBoxA()代碼的修改,還必須要一個“載體程序”。例如:
l
// Pefile.cpp:修改PE文件實例
//
#include "stdafx.h"
#include "Pe.h"
void main()
{
CopyFile("..\\calc.exe","..\\calc_shell.exe",FALSE);
CPe a;
a.ModifyPe("..\\calc_shell.exe","We are the world!");
}
l
這個修改后的PE文件運行時,就會先顯示對話框,單擊“確定”按鈕后又繼續(xù)執(zhí)行。總之,在了解了PE文件格式后,就可以對某一個PE文件進行修改。本實例只是對PE文件處理的一種應用,在實際中還有更多的其他方面的應用。
2.7 本章小結

本章首先介紹了PE文件的基本結構,對一些容易混淆的名詞進行了解釋。通過介紹一個對PE文件注入對話框代碼的實例,加強了對PE文件結構的認識。
本章所介紹的向PE文件注入代碼的實例只是用來說明如何修改PE文件,有關如何向一個應用程序中注入代碼的技術還要在以后的章節(jié)專門介紹。此外,還有其他的技術沒有介紹,例如如何提取程序中的代碼,在以后的章節(jié)中對此也還要專門介紹。總之,了解了PE文件結構,就可以很容易地對某個應用程序進行加殼、掛鉤或捆綁。
posted on 2007-07-04 13:45
飛鳥 閱讀(2221)
評論(1) 編輯 收藏 所屬分類:
VC