本章提要
· PE文件格式概述
· PE文件結(jié)構(gòu)
· 如何獲取PE文件中的OEP
· 如何獲取PE文件中的資源
· 如何修改PE文件使其顯示MessageBox的實(shí)例
2.1 引言
通常Windows下的EXE文件都采用PE格式。PE是英文Portable Executable的縮寫,它是一種針對(duì)于微軟Windows NT、Windows 95和Win32s系統(tǒng),由微軟公司設(shè)計(jì)的可執(zhí)行的二進(jìn)制文件(DLLs和執(zhí)行程序)格式,目標(biāo)文件和庫(kù)文件通常也是這種格式。這種格式由TIS(Tool Interface Standard)委員會(huì)(Microsoft、Intel、Borland、Watcom、IBM等)在1993進(jìn)行了標(biāo)準(zhǔn)化。顯然,它參考了一些UNIXes和VMS的COFF(Common Object File Format)格式。
認(rèn)識(shí)可執(zhí)行文件的結(jié)構(gòu)非常重要,在DOS下是這樣,在Windows系統(tǒng)下更是如此。了解了這種結(jié)構(gòu)后就可以對(duì)可執(zhí)行程序進(jìn)行加密、加殼和修改等,一些黑客也利用了這些技術(shù)。為了使讀者對(duì)PE文件格式有進(jìn)一步的認(rèn)識(shí),本章從一個(gè)程序員的角度出發(fā)再次介紹PE文件格式。如果已經(jīng)熟悉這方面的知識(shí),可以跳過(guò)這一章。
2.2 PE文件格式概述
認(rèn)識(shí)PE文件,既要懂得它的結(jié)構(gòu)布局,又要知道它是如何裝載到計(jì)算機(jī)內(nèi)存中的。下面分別對(duì)它們進(jìn)行說(shuō)明。
2.2.1 PE文件結(jié)構(gòu)布局
找到文件中某一結(jié)構(gòu)信息有兩種定位方法。第一種是通過(guò)鏈表方法,對(duì)于這種方法,數(shù)據(jù)在文件的存放位置比較自由。第二種方法是采用緊湊或固定位置存放,這種方法要求數(shù)據(jù)結(jié)構(gòu)大小固定,它在文件中的存放位置也相對(duì)固定。在PE文件結(jié)構(gòu)中同時(shí)采用以上兩種方法。
因?yàn)樵赑E文件頭中的每個(gè)數(shù)據(jù)結(jié)構(gòu)大小是固定的,因此能夠編寫計(jì)算程序來(lái)確定某一個(gè)PE文件中的某個(gè)參數(shù)值。在編寫程序時(shí),所用到的數(shù)據(jù)結(jié)構(gòu)定義,包括數(shù)據(jù)結(jié)構(gòu)中變量類型、變量位置和變量數(shù)組大小都必須采用Windows提供的原型。圖2.1所示的PE文件結(jié)構(gòu)的總體層次分布如下:
PE文件結(jié)構(gòu)總體層次分布
· DOS MZ Header
所有 PE文件(甚至32位的DLLs)必須以簡(jiǎn)單的DOS MZ header開始,它是一個(gè)IMAGE_DOS_HEADER結(jié)構(gòu)。有了它,一旦程序在DOS下執(zhí)行,DOS就能識(shí)別出這是有效的執(zhí)行體,然后運(yùn)行緊隨MZ Header之后的DOS Stub。
· DOS Stub
DOS Stub實(shí)際上是個(gè)有效的EXE,在不支持PE文件格式的操作系統(tǒng)中,它將簡(jiǎn)單顯示一個(gè)錯(cuò)誤提示,類似于字符串“This program requires Windows”或者程序員可根據(jù)自己的意圖實(shí)現(xiàn)完整的DOS代碼。大多數(shù)情況下DOS Stub由匯編器/編譯器自動(dòng)生成。
· PE Header
緊接著DOS Stub的是PE Header。它是一個(gè)IMAGE_NT_HEADERS結(jié)構(gòu)。其中包含了很多PE文件被載入內(nèi)存時(shí)需要用到的重要域。執(zhí)行體在支持PE文件結(jié)構(gòu)的操作系統(tǒng)中執(zhí)行時(shí),PE裝載器將從DOS MZ header中找到PE header的起始偏移量。因而跳過(guò)DOS Stub直接定位到真正的文件頭 PE header。
· Section Table
PE Header之后是數(shù)組結(jié)構(gòu)Section Table(節(jié)表)。如果PE文件里有5個(gè)節(jié),那么此Section Table結(jié)構(gòu)數(shù)組內(nèi)就有5個(gè)(IMAGE_SECTION_HEADER)成員,每個(gè)成員包含對(duì)應(yīng)節(jié)的屬性、文件偏移量、虛擬偏移量等。排在節(jié)表中的最前面的第一個(gè)默認(rèn)成員是text,即代碼節(jié)頭。通過(guò)遍歷查找方法可以找到其他節(jié)表成員(節(jié)表頭)。
· Sections
PE文件的真正內(nèi)容劃分成塊,稱為Sections(節(jié))。每個(gè)標(biāo)準(zhǔn)節(jié)的名字均以圓點(diǎn)開頭,但也可以不以圓點(diǎn)開頭,節(jié)名的最大長(zhǎng)度為8個(gè)字節(jié)。Sections是以其起始位址來(lái)排列,而不是以其字母次序來(lái)排列。通過(guò)節(jié)表提供的信息,可以找到這些節(jié)。程序的代碼,資源等就放在這些節(jié)中。
節(jié)的劃分是基于各組數(shù)據(jù)的共同屬性,而不是邏輯概念。每節(jié)是一塊擁有共同屬性的數(shù)據(jù),比如代碼/數(shù)據(jù)、讀/寫等。如果PE文件中的數(shù)據(jù)/代碼擁有相同屬性,它們就能被歸入同一節(jié)中。節(jié)名稱僅僅是個(gè)區(qū)別不同節(jié)的符號(hào)而已,類似“data”,“code”的命名只為了便于識(shí)別,唯有節(jié)的屬性設(shè)置決定了節(jié)的特性和功能。
2.2.2 PE文件內(nèi)存映射
在Windows系統(tǒng)下,當(dāng)一個(gè)PE應(yīng)用程序運(yùn)行時(shí),這個(gè)PE文件在磁盤中的數(shù)據(jù)結(jié)構(gòu)布局和內(nèi)存中的數(shù)據(jù)結(jié)構(gòu)布局是一致的。系統(tǒng)在載入一個(gè)可執(zhí)行程序時(shí),首先是Windows裝載器(又稱PE裝載器)把磁盤中的文件映射到進(jìn)程的地址空間,它遍歷PE文件并決定文件的哪一部分被映射。其方式是將文件較高的偏移位置映射到較高的內(nèi)存地址中。磁盤文件一旦被裝入內(nèi)存中,其某項(xiàng)的偏移地址可能與原始的偏移地址有所不同,但所表現(xiàn)的是一種從磁盤文件偏移到內(nèi)存偏移的轉(zhuǎn)換,如圖2.2所示。
PE文件內(nèi)存映射
當(dāng)PE文件被加載到內(nèi)存后,內(nèi)存中的版本稱為模塊(Module),映射文件的起始地址稱為模塊句柄(hModule),可以通過(guò)模塊句柄訪問(wèn)內(nèi)存中的其他數(shù)據(jù)結(jié)構(gòu)。這個(gè)初始內(nèi)存地址也稱為文件映像基址(ImageBase)。載入一個(gè)PE程序的主要步驟如下:
(1)當(dāng)PE文件被執(zhí)行時(shí),PE裝載器首先為進(jìn)程分配一個(gè)4GB的虛擬地址空間,然后把程序所占用的磁盤空間作為虛擬內(nèi)存映射到這個(gè)4GB的虛擬地址空間中。一般情況下,會(huì)映射到虛擬地址空間中0x400000的位置。裝載一個(gè)應(yīng)用程序的時(shí)間比一般人所設(shè)想的要少,因?yàn)檠b載一個(gè)PE文件并不是把這個(gè)文件一次性地從磁盤讀到內(nèi)存中,而是簡(jiǎn)單地做一個(gè)內(nèi)存映射,映射一個(gè)大文件和映射一個(gè)小文件所花費(fèi)的時(shí)間相差無(wú)幾。當(dāng)然,真正執(zhí)行文件中的代碼時(shí),操作系統(tǒng)還是要把存在于磁盤上的虛擬內(nèi)存中的代碼交換到物理內(nèi)存(RAM)中。但是,這種交換也不是把整個(gè)文件所占用的虛擬地址空間一次性地全部從磁盤交換到物理內(nèi)存中,操作系統(tǒng)會(huì)根據(jù)需要和內(nèi)存占用情況交換一頁(yè)或多頁(yè)。當(dāng)然,這種交換是雙向的,即存在于物理內(nèi)存中的一部分當(dāng)前沒(méi)有被使用的頁(yè),也可能被交換到磁盤中。
(2)PE裝載器在內(nèi)核中創(chuàng)建進(jìn)程對(duì)象和主線程對(duì)象以及其他內(nèi)容。
(3)PE裝載器搜索PE文件中的Import Table(引入表),裝載應(yīng)用程序所使用的動(dòng)態(tài)鏈接庫(kù)。對(duì)動(dòng)態(tài)鏈接庫(kù)的裝載與對(duì)應(yīng)用程序的裝載方法完全類似。
(4)PE裝載器執(zhí)行PE文件首部所指定地址處的代碼,開始執(zhí)行應(yīng)用程序主線程。
2.2.3 Big-endian和Little-endian
PE Header中IMAGE_FILE_HEADER的成員Machine 中的值,根據(jù)winnt.h中的定義,對(duì)于Intel CPU應(yīng)該為0x014c。但是用十六進(jìn)制編輯器打開PE文件時(shí),看到這個(gè)WORD顯示的卻是4c 01。其實(shí)4c 01就是0x014c,只不過(guò)由于Intel CPU是Little-endian,所以顯示出來(lái)是這樣的。對(duì)于Big-endian和Little-endian,請(qǐng)看下面的例子。一個(gè)整型int變量,長(zhǎng)度為4個(gè)字節(jié)。當(dāng)這個(gè)整形變量的值為0x12345678時(shí),對(duì)于Big-endian來(lái)說(shuō),顯示的是{12,34,45,78},而對(duì)于Little-endian來(lái)說(shuō),顯示的卻是{78,45,34,12}。注意Intel使用的是Little-endian。
2.2.4 3種不同的地址
PE文件的各種結(jié)構(gòu)中,涉及到很多地址、偏移。有些是指在文件中的偏移,有些 是指在內(nèi)存中的偏移。以下的第一種是指在文件中的地址,第二、三種是指在內(nèi)存中的地址。
第一種,文件中的地址。比如用十六進(jìn)制編輯器打開PE文件,看到的地址(偏移)就是文件中的地址,使用某個(gè)結(jié)構(gòu)的文件地址,就可以在文件中找到該結(jié)構(gòu)。
第二種,當(dāng)文件被整個(gè)映射到內(nèi)存時(shí),例如某些PE分析軟件,把整個(gè)PE文件映射到內(nèi)存中,這時(shí)是內(nèi)存中的虛擬地址(VA)。如果知道在這個(gè)文件中某一個(gè)結(jié)構(gòu)的內(nèi)存地址的話,那么它等于這個(gè)PE文件被映射到內(nèi)存的地址加上該結(jié)構(gòu)在文件中的地址。
第三種,當(dāng)執(zhí)行PE時(shí),PE文件會(huì)被載入器載入內(nèi)存,這時(shí)經(jīng)常需要的是RVA。例如知道一個(gè)結(jié)構(gòu)的RVA,那么程序載入點(diǎn)加上RVA就可以得到該結(jié)構(gòu)的內(nèi)存地址。比如,如果PE文件裝入虛擬地址(VA)空間的0x400000處,某一結(jié)構(gòu)的RVA 為0x1000,那么其虛擬地址為0x401000。
PE文件格式要用到RVA,主要是為了減少PE裝載器的負(fù)擔(dān)。因?yàn)槊總€(gè)模塊都有可能被重載到任何虛擬地址空間,如果讓PE裝載器修正每個(gè)重定位項(xiàng),這肯定是個(gè)夢(mèng)魘。相反,如果所有重定位項(xiàng)都使用RVA,那么PE裝載器就不必操心那些東西了,即它只要將整個(gè)模塊重定位到新的起始VA。這就像相對(duì)路徑和絕對(duì)路徑的概念:RVA類似相對(duì)路徑,VA就像絕對(duì)路徑。
注意,RVA和VA是指內(nèi)存中,不是指文件中。是指相對(duì)于載入點(diǎn)的偏移而不是一個(gè)內(nèi)存地址,只有RVA加上載入點(diǎn)的地址,才是一個(gè)實(shí)際的內(nèi)存地址。
2.3 PE文件結(jié)構(gòu)
在win32 SDK的文件winnt.h中有PE文件格式的定義。本文所用到的變量,如果沒(méi)有特別說(shuō)明,都在文件winnt.h中定義。
有關(guān)一些PE頭文件結(jié)構(gòu)一般都有32位和64位之分,如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64等,除了在64位版本中的一些擴(kuò)展域外,這些結(jié)構(gòu)總是一樣的。是采用32位還是64位,需要用#define _WIN64來(lái)定義,如果沒(méi)有這種定義,則采用的是32位的文件結(jié)構(gòu)。編譯器將根據(jù)此定義選擇相應(yīng)的編譯模式。
2.3.1 MS-DOS頭部
MS-DOS頭部占據(jù)了PE文件的頭64個(gè)字節(jié),描述它內(nèi)容的結(jié)構(gòu)如下:
l
// 此結(jié)構(gòu)包含于WINNT.H中
//
typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE頭部
WORD e_magic; // 魔術(shù)數(shù)字
WORD e_cblp; // 文件最后頁(yè)的字節(jié)數(shù)
WORD e_cp; // 文件頁(yè)數(shù)
WORD e_crlc; // 重定義元素個(gè)數(shù)
WORD e_cparhdr; // 頭部尺寸,以段落為單位
WORD e_minalloc; // 所需的最小附加段
WORD e_maxalloc; // 所需的最大附加段
WORD e_ss; // 初始的SS值(相對(duì)偏移量)
WORD e_sp; // 初始的SP值
WORD e_csum; // 校驗(yàn)和
WORD e_ip; // 初始的IP值
WORD e_cs; // 初始的CS值(相對(duì)偏移量)
WORD e_lfarlc; // 重分配表文件地址
WORD e_ovno; // 覆蓋號(hào)
WORD e_res[4]; // 保留字
WORD e_oemid; // OEM標(biāo)識(shí)符(相對(duì)e_oeminfo)
WORD e_oeminfo; // OEM信息
WORD e_res2[10]; // 保留字
LONG e_lfanew; // 新exe頭部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
l
其中第一個(gè)域e_magic,被稱為魔術(shù)數(shù)字,它用于表示一個(gè)MS-DOS兼容的文件類型。所有MS-DOS兼容的可執(zhí)行文件都將這個(gè)值設(shè)為0x5A4D,表示ASCII字符MZ。MS-DOS頭部之所以有的時(shí)候被稱為MZ頭部,就是這個(gè)緣故。還有許多其他的域?qū)τ贛S-DOS操作系統(tǒng)來(lái)說(shuō)都有用,但是對(duì)于Windows NT來(lái)說(shuō),這個(gè)結(jié)構(gòu)中只有一個(gè)有用的域——最后一個(gè)域e_lfnew,一個(gè)4字節(jié)的文件偏移量,PE文件頭部就是由它定位的。
2.3.2 IMAGE_NT_HEADER頭部
PE Header是緊跟在MS-DOS頭部和實(shí)模式程序殘余之后的,描述它內(nèi)容的結(jié)構(gòu) 如下:
l
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE文件頭標(biāo)志:"PE\0\0"
IMAGE_FILE_HEADER FileHeader; // PE文件物理分布的信息
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE文件邏輯分布的信息
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
緊接PE文件頭標(biāo)志之后是PE文件頭結(jié)構(gòu),由20個(gè)字節(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
其中請(qǐng)注意這個(gè)文件頭部的大小已經(jīng)定義在這個(gè)包含文件之中了,這樣一來(lái),想要得到這個(gè)結(jié)構(gòu)的大小就很方便了。
Machine:表示該程序要執(zhí)行的環(huán)境及平臺(tái),現(xiàn)在已知的值如表2.1所示。
應(yīng)用程序執(zhí)行的環(huán)境及平臺(tái)代碼
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:段的個(gè)數(shù)。
TimeDateStamp:文件建立的時(shí)間。可用這個(gè)值來(lái)區(qū)分同一個(gè)文件的不同的版本,即使它們的商業(yè)版本號(hào)相同。這個(gè)值的格式并沒(méi)有明確的規(guī)定,但是很顯然地大多數(shù)的C編譯器都把它定為從1970.1.1 00:00:00以來(lái)的秒數(shù)(time_t)。這個(gè)值有時(shí)也被用做綁定輸入目錄表。注意:一些編譯器將忽略這個(gè)值。
PointerToSymbolTable及NumberOfSymbols:用在調(diào)試信息中,用途不太明確,不過(guò)它們的值總為0。
SizeOfOptionalHeader:可選頭的長(zhǎng)度(sizeof IMAGE_OPTIONAL_HEADER),可以用它來(lái)檢驗(yàn)PE文件的正確性。
Characteristics:是一個(gè)標(biāo)志的集合,其大部分位用于OBJ或LIB文件中。
文件頭下面就是可選擇頭,這是一個(gè)叫做IMAGE_OPTIONAL_HEADER的結(jié)構(gòu),由224個(gè)字節(jié)組成。雖然它的名字是“可選頭部”,但是請(qǐng)確信:這個(gè)頭部并非“可選”,而是“必需”的。可選頭部包含了很多關(guān)于可執(zhí)行映像的重要信息。例如,初始的堆棧大小、程序入口點(diǎn)的位置、首選基地址、操作系統(tǒng)版本、段對(duì)齊的信息等。IMAGE_ OPTIONAL_HEADER結(jié)構(gòu)如下:
l
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// 標(biāo)準(zhǔn)域
//
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
其中參數(shù)含義如下所述。
Magic:這個(gè)值好像總是0x010b。
MajorLinkerVersion及MinorLinkerVersion:鏈接器的版本號(hào),這個(gè)值不太可靠。
SizeOfCode:可執(zhí)行代碼的長(zhǎng)度。
SizeOfInitializedData:初始化數(shù)據(jù)的長(zhǎng)度(數(shù)據(jù)段)。
SizeOfUninitializedData:未初始化數(shù)據(jù)的長(zhǎng)度(bss段)。
AddressOfEntryPoint:代碼的入口RVA地址,程序從這兒開始執(zhí)行,常稱為程序的原入口點(diǎn)OEP(Original Entry Point)。
BaseOfCode:可執(zhí)行代碼起始位置。
BaseOfData:初始化數(shù)據(jù)起始位置。
ImageBase:載入程序首選的RVA地址。這個(gè)地址可被Loader改變。
SectionAlignment:段加載后在內(nèi)存中的對(duì)齊方式。
FileAlignment:段在文件中的對(duì)齊方式。
MajorOperatingSystemVersion及MinorOperatingSystemVersion:操作系統(tǒng)版本。
MajorImageVersion及MinorImageVersion:程序版本。
MajorSubsystemVersion及MinorSubsystemVersion:子系統(tǒng)版本號(hào),這個(gè)域系統(tǒng)支持。例如,程序運(yùn)行于NT下,子系統(tǒng)版本號(hào)如果不是4.0,對(duì)話框不能顯示3D風(fēng)格。
Win32VersionValue:這個(gè)值總是為0。
SizeOfImage:程序調(diào)入后占用內(nèi)存大小(字節(jié)),等于所有段的長(zhǎng)度之和。
SizeOfHeaders:所有文件頭長(zhǎng)度之和,它等于從文件開始到第一個(gè)段的原始數(shù)據(jù)之間的大小。
CheckSum:校驗(yàn)和,僅用在驅(qū)動(dòng)程序中,在可執(zhí)行文件中可能為0。它的計(jì)算方法Microsoft不公開,在imagehelp.dll中的CheckSumMappedFile()函數(shù)可以計(jì)算它。
Subsystem:一個(gè)標(biāo)明可執(zhí)行文件所期望的子系統(tǒng)的枚舉值。
DllCharacteristics:DLL狀態(tài)。
SizeOfStackReserve:保留堆棧大小。
SizeOfStackCommit:?jiǎn)?dòng)后實(shí)際申請(qǐng)的堆棧數(shù),可隨實(shí)際情況變大。
SizeOfHeapReserve:保留堆大小。
SizeOfHeapCommit:實(shí)際堆大小。
LoaderFlags:目前沒(méi)有用。
NumberOfRvaAndSizes:下面的目錄表入口個(gè)數(shù),這個(gè)值也不可靠,可用常數(shù)IMAGE_NUMBEROF_DIRECTORY_ENTRIES來(lái)代替它,這個(gè)值在目前Windows版本中設(shè)為16。注意,如果這個(gè)值不等于16,那么這個(gè)數(shù)據(jù)結(jié)構(gòu)大小就不能固定下來(lái),也就不能確定其他變量位置。
DataDirectory:是一個(gè)IMAGE_DATA_DIRECTORY數(shù)組,數(shù)組元素個(gè)數(shù)為IMAGE_NUMBEROF_DIRECTORY_ENTRIES,結(jié)構(gòu)如下:
l
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 起始RVA地址
DWORD Size; // 長(zhǎng)度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
2.3.3 IMAGE_SECTION_HEADER頭部
PE文件格式中,所有的節(jié)頭部位于可選頭部之后。每個(gè)節(jié)頭部為40個(gè)字節(jié)長(zhǎng),并且沒(méi)有任何填充信息。節(jié)頭部被定義為以下的結(jié)構(gòu):
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; // 真實(shí)長(zhǎng)度
} Misc;
DWORD VirtualAddress; // RVA
DWORD SizeOfRawData; // 物理長(zhǎng)度
DWORD PointerToRawData; // 節(jié)基于文件的偏移量
DWORD PointerToRelocations; // 重定位的偏移
DWORD PointerToLinenumbers; // 行號(hào)表的偏移
WORD NumberOfRelocations; // 重定位項(xiàng)數(shù)目
WORD NumberOfLinenumbers; // 行號(hào)表的數(shù)目
DWORD Characteristics; // 節(jié)屬性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
l
其中IMAGE_SIZEOF_SHORT_NAME等于8。注意,如果不是這個(gè)值,那么這個(gè)數(shù)據(jù)結(jié)構(gòu)大小就不能固定下來(lái),也就不能確定其他變量位置。
2.4 如何獲取PE文件中的OEP
OEP(Original Entry Point)是每個(gè)PE文件被加載時(shí)的起始地址,如何獲得這個(gè)地址很重要,因?yàn)樾薷某绦蛑械倪@個(gè)值是文件加殼和脫殼時(shí)的必須步驟,一些黑客程序也是通過(guò)修改OEP值來(lái)獲得對(duì)目標(biāo)程序的控制權(quán)從而實(shí)施攻擊。下面分別介紹如何通過(guò)文件直接訪問(wèn)和通過(guò)內(nèi)存映射訪問(wèn)讀取OEP值的方法,并給出完整的程序代碼。
2.4.1 通過(guò)文件讀取OEP值
獲得OEP值的最簡(jiǎn)單方法是,直接從一個(gè)PE文件中讀取OEP。根據(jù)以上對(duì)PE文件結(jié)構(gòu)的介紹可知,OEP是PE文件的IMAGE_OPTIONAL_HEADER結(jié)構(gòu)的AddressOfEntryPoint成員,在偏移此結(jié)構(gòu)頭40個(gè)字節(jié)處。而IMAGE_OPTIONAL_ HEADER在PE文件的起始位置由IMAGE_DOS_HEADER的e_lfanew成員來(lái)計(jì)算。注意,以上兩個(gè)結(jié)構(gòu)在PE文件中不是緊跟在一起的,它之間是DOS Stub,而在每個(gè)PE文件DOS Stub的長(zhǎng)度可能不一定相等。在PE文件的頭部是IMAGE_ DOS_HEADER結(jié)構(gòu),讀取這個(gè)結(jié)構(gòu)可以得到e_lfanew的值,因而可以得到IMAGE_ OPTIONAL_HEADER在PE文件中的位置,也就得到了OEP值。以下是通過(guò)文件訪問(wèn)的方法讀取OEP的程序代碼,即:
l
// 通過(guò)文件讀取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;
}
// 關(guān)閉文件
CloseHandle(hFile);
// 顯示OEP地址
printf("OEP by file:%d\n",dwOEP);
return TRUE;
}
2.4.2 通過(guò)內(nèi)存映射讀取OEP值
獲得OEP值的另一種方法是通過(guò)內(nèi)存映射來(lái)實(shí)現(xiàn),此方法也需要熟悉PE的文件結(jié)構(gòu)。與直接訪問(wèn)PE的方法不同,內(nèi)存映射的方法首先把PE文件映射到計(jì)算機(jī)的內(nèi)存,再通過(guò)內(nèi)存的基指針獲得IMAGE_DOS_HEADER的頭指針,由此再獲得IMAGE_ OPTIONAL_HEADER指針,這樣就可以得到AddressOfEntryPoint的值。下面是通過(guò)內(nèi)存映射獲得OEP值的方法:
l
// 通過(guò)文件內(nèi)存映射讀取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)建內(nèi)存映射文件
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;
// 清除內(nèi)存映射和關(guān)閉文件
UnmapViewOfFile(basepointer);
CloseHandle(hMapping);
CloseHandle(hFile);
// 顯示OEP地址
printf("OEP by memory:%d\n",dwOEP);
return TRUE;
}
2.4.3 讀取OEP值方法的測(cè)試
為了檢驗(yàn)以上兩種獲取OEP值方法的正確性和一致性,可以用以下的方法來(lái)測(cè)試:
l
// oep.cpp:讀取OEP的實(shí)例
//
#include <windows.h>
#include <stdio.h>
BOOL ReadOEPbyMemory(LPCSTR szFileName);
BOOL ReadOEPbyFile(LPCSTR szFileName);
void main()
{
ReadOEPbyFile("..\\calc.exe");
ReadOEPbyMemory("..\\calc.exe");
}
l
運(yùn)行以上代碼后,可以得到如圖2.3所示的結(jié)果。從圖中可以看出,以上兩種獲取OEP值方法所得到的結(jié)果是一致的。
獲取OEP值方法的測(cè)試結(jié)果
2.5 PE文件中的資源
一些PE格式(Portable Executable)的EXE文件常常存在很多資源,如圖標(biāo)、位圖、對(duì)話框、聲音等。若要把這些資源取出為自己所用,或修改這些文件中的資源,則需要對(duì)PE文件中資源數(shù)據(jù)結(jié)構(gòu)有所了解。
2.5.1 查找資源在文件中的起始位置
要找出一個(gè)PE文件中的某種資源,首先需要確定資源節(jié)在PE文件中的起始位置。有兩種方法來(lái)確定資源在文件中的起始位置。
第一種方法,首先根據(jù)FileHeader中的成員NumberOfSections的值,確定文件中節(jié)的數(shù)目,再根據(jù)節(jié)的數(shù)目,遍歷節(jié)表數(shù)組。也就是從0到(節(jié)表數(shù)–1)的每一個(gè)節(jié)表項(xiàng)。比較每一個(gè)節(jié)表項(xiàng)的Name字段,看看是否等于“.rsrc”,如果是,就找到了資源節(jié)的節(jié)表項(xiàng)。這個(gè)節(jié)表項(xiàng)的PointerToRawData 中的值,就是資源節(jié)在文件中的位置。
第二種方法,取得PE Header中的IMAGE_OPTIONAL_HEADER中的DataDirectory數(shù)組中的第三項(xiàng),也就是資源項(xiàng)。DataDirectory[]數(shù)組的每項(xiàng)都是IMAGE_DATA_ DIRECTORY結(jié)構(gòu),該結(jié)構(gòu)定義如下:
l
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
l
從以上結(jié)構(gòu)對(duì)象取得DataDirectory數(shù)組中的第三項(xiàng)中的成員VirtualAddress的值。這個(gè)值就是在內(nèi)存中資源節(jié)的RVA。然后根據(jù)節(jié)的數(shù)目,遍歷節(jié)表數(shù)組,也就是從0~(節(jié)表數(shù)–1)的每一個(gè)節(jié)表項(xiàng)。每個(gè)節(jié)在內(nèi)存中的RVA的范圍是從該節(jié)表項(xiàng)的成員VirtualAddress字段的值開始(包括這個(gè)值),到VirtualAddress+Misc.VirtualSize的值結(jié)束(不包括這個(gè)值)。遍歷整個(gè)節(jié)表,看看所取得的資源節(jié)的RVA是否在那個(gè)節(jié)表項(xiàng)的RVA范圍之內(nèi)。如果在范圍之內(nèi),就找到了資源節(jié)的節(jié)表項(xiàng)。這個(gè)節(jié)表項(xiàng)中的PointerToRawData 中的值,就是資源節(jié)在文件中的位置。如果這個(gè)PE文件沒(méi)有資源 的話,DataDirectory數(shù)組中的第三項(xiàng)內(nèi)容為0。這樣也可以得到了資源在文件中開始的位置。
2.5.2 確定PE文件中的資源
得到了資源節(jié)在文件中的位置后,就可以確定某個(gè)資源類型及其二進(jìn)制數(shù)據(jù)在PE文件中的位置和數(shù)據(jù)塊的大小。
資源節(jié)最開始是一個(gè)IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu),在winnt.h文件中有這個(gè)結(jié)構(gòu)的定義。這個(gè)結(jié)構(gòu)長(zhǎng)度為16字節(jié),共有6個(gè)參數(shù),其結(jié)構(gòu)的原型如下:
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
其中各個(gè)參數(shù)的含義如下所述
Characteristics: 標(biāo)識(shí)此資源的類型。
TimeDateStamp:資源編譯器產(chǎn)生資源的時(shí)間。
MajorVersion:資源主版本號(hào)。
MinorVersion:資源次版本號(hào)。
NumberOfNamedEntries和NumberofIDEntries:分別為用字符串和整形數(shù)字來(lái)進(jìn)行標(biāo)識(shí)的IMAGE_RESOURCE_DIRECTORY_ENTRY項(xiàng)數(shù)組的成員個(gè)數(shù)。
緊跟著IMAGE_RESOURCE_DIRECTORY后面的是一個(gè)IMAGE_RESOURCE_ DIRECTORY_ENTRY數(shù)組。這個(gè)結(jié)構(gòu)長(zhǎng)度為8個(gè)字節(jié),共有兩個(gè)字段,每個(gè)字段4個(gè)字節(jié)。其結(jié)構(gòu)原型如下:
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
其中,對(duì)于第一個(gè)字段,當(dāng)其最高位為1(0x80000000)時(shí),這個(gè)DWORD剩下的31位表明相對(duì)于資源開始位置的偏移,偏移的內(nèi)容是一個(gè)IMAGE_RESOURCE_DIR_ STRING_U,用其中的字符串來(lái)標(biāo)明這個(gè)資源類型;當(dāng)?shù)谝粋€(gè)字段的最高位為0時(shí),表示這個(gè)DWORD的低WORD中的值作為Id標(biāo)明這個(gè)資源類型。
對(duì)于第二個(gè)字段,當(dāng)?shù)诙€(gè)字段的最高位為1時(shí),表示還有下一層的結(jié)構(gòu)。這個(gè)DWORD的剩下31位表明一個(gè)相對(duì)于資源開始位置的偏移,這個(gè)偏移的內(nèi)容將是一個(gè)下一層的IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu);當(dāng)?shù)诙€(gè)字段的最高位為0時(shí),表示已經(jīng)沒(méi)有下一層的結(jié)構(gòu)了。這個(gè)DWORD的剩下31位表明一個(gè)相對(duì)于資源開始位置的偏移,這個(gè)偏移的內(nèi)容會(huì)是一個(gè)IMAGE_RESOURCE_DATA _ENTRY結(jié)構(gòu),此結(jié)構(gòu)會(huì)說(shuō)明資源的位置。對(duì)于資源標(biāo)示號(hào)Id,當(dāng)Id等于1時(shí),表示資源為光標(biāo),等于2時(shí)表示資源為位圖等,等于3時(shí)表示資源為圖標(biāo)等。在winuser.h文件中有定義。
標(biāo)識(shí)一個(gè)IMAGE_RESOURCE_DIRECTORY_ENTRY一般都是使用Id,就是一個(gè)整數(shù)。但是也有少數(shù)使用IMAGE_RESOURCE_DIR_STRING_U來(lái)標(biāo)識(shí)一個(gè)資源類型。這個(gè)結(jié)構(gò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
這個(gè)結(jié)構(gòu)中將有一個(gè)Unicode的字符串,是字對(duì)齊的。這個(gè)結(jié)構(gòu)的長(zhǎng)度可變,由第一個(gè)字段Length指明后面的Unicode字符串的長(zhǎng)度。
經(jīng)過(guò)3層IMAGE_RESOURCE_DIRECTORY_ENTRY(一般是3層,也有可能更少些)最終可以找到一個(gè)IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu),這個(gè)結(jié)構(gòu)中存有相應(yīng)資源的位置和大小。這個(gè)結(jié)構(gòu)長(zhǎng)16個(gè)字節(jié),有4個(gè)參數(shù),其原型如下:
l
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;
DWORD Size;
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
l
其中各個(gè)參數(shù)的含義如下所述。
OffsetToData:這是一個(gè)內(nèi)存中的RVA,可以用來(lái)轉(zhuǎn)化成文件中的位置。用這個(gè)值減去資源節(jié)的開始RVA,就可以得到相對(duì)于資源節(jié)開始的偏移。再加上資源節(jié)在文件中的開始位置,即節(jié)表中資源節(jié)中PointerToRawData的值,就是資源在文件中的位置。注意,資源節(jié)的開始RVA可以由Optional Header中的DataDirectory數(shù)組中的第三項(xiàng)中的VirtualAddress的值得到,或者節(jié)表中資源節(jié)那項(xiàng)中的VirtualAddress的值得到。
Size:資源的大小,以字節(jié)為單位。
CodePage:代碼頁(yè)。
Reserved:保留項(xiàng)。
總之,資源一般使用樹來(lái)保存,通常包含3層,最高層是類型,其次是名字,最后是語(yǔ)言。在資源節(jié)開始的位置,首先是一個(gè)IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu),后面緊跟著IMAGE_RESOURCE_DIRECTORY_ENTRY數(shù)組,這個(gè)數(shù)組的每個(gè)元素代表的資源類型不同;通過(guò)每個(gè)元素,可以找到第二層另一個(gè)IMAGE_RESOURCE_ DIRECTORY,后面緊跟著IMAGE_RESOURCE_DIRECTORY_ENTRY數(shù)組。這一層的數(shù)組的每個(gè)元素代表的資源名字不同;然后可以找到第三層的每個(gè)IMAGE_ RESOURCE_DIRECTORY,后面緊跟著IMAGE_RESOURCE_DIRECTORY_ENTRY數(shù)組。這一層的數(shù)組的每個(gè)元素代表的資源語(yǔ)言不同;最后通過(guò)每個(gè)IMAGE_RESOURCE_ DIRECTORY_ENTRY可以找到每個(gè)IMAGE_RESOURCE_DATA_ENTRY。通過(guò)每個(gè)IMAGE_RESOURCE_DATA_ENTRY,就可以找到每個(gè)真正的資源。
2.6 一個(gè)修改PE可執(zhí)行文件的完整實(shí)例
在下面的實(shí)例中,將把一段MessageBoxA()的計(jì)算機(jī)代碼根據(jù)PE文件的格式注入到一個(gè)PE程序中。有關(guān)把代碼注入到一個(gè)應(yīng)用程序的技術(shù)將在后面的章節(jié)專門介紹。
2.6.1 如何獲得MessageBoxA代碼
要實(shí)現(xiàn)代碼注入PE程序且能夠運(yùn)行,首先要做的是如何得到這段代碼。為了得到這種代碼,作者編寫了一段匯編源程序 msgbx.asm,然后用RadASM編譯器進(jìn)行編譯,當(dāng)然也可以使用其他的方法來(lái)實(shí)現(xiàn)代碼的注入。編寫這段代碼最關(guān)鍵的問(wèn)題是如何把對(duì)話框標(biāo)題字符串和顯示字符串一起存放在代碼段,以便提取,否則無(wú)法提取。下面是生成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()對(duì)話框的標(biāo)題,"Hello"是要顯示的字符串。Message- BoxA()所用的Windows句柄為NULL。
用RadASM編譯器對(duì)以上代碼編譯后,可以生成一個(gè)msgbx.obj文件,用VC++ 編輯器打開后,如圖2.4所示,可以查看這個(gè)文件的機(jī)器代碼。
Msgbx.obj文件的機(jī)器代碼
把圖2.4中所選擇的計(jì)算機(jī)機(jī)器代碼取出變成一個(gè)命令字符串,即:
l
unsigned char cmdline[35]={
0x6a, // (1) push 命令
0x40, // (1) MB_ICONINFORMATION|MB_OK
0xe8, // (1) call命令
0x05,0x00,0x00,0x00, // (4) 標(biāo)題字符串字節(jié)個(gè)數(shù),包括結(jié)束位
(DWORD)
0x54,0x65,0x73,0x74, 0x00, // (5) "Test",0(標(biāo)題)
0xe8, // (1) call命令
0x06,0x00,0x00,0x00, // (4) 標(biāo)題字符串字節(jié)個(gè)數(shù),包括結(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位,校驗(yàn)和
0x00,0x00,0x00,0x0b // (4) 返回地址 (DWORD)
};
l
其中()中的數(shù)值表示這一行上代碼的字節(jié)個(gè)數(shù)。0x6a是匯編語(yǔ)言中的push命令,0xe8是匯編語(yǔ)言中的call命令,而jmp命令為0xe9。“校驗(yàn)和”是從第一個(gè)push命令開始計(jì)算所得到的字節(jié)總數(shù)和(包括校驗(yàn)計(jì)數(shù)位),從以上代碼第一個(gè)字節(jié)開始計(jì)數(shù)起到“校驗(yàn)和”位正好是第26位字節(jié)個(gè)數(shù)。字符串字節(jié)個(gè)數(shù)位為一個(gè)DWORD型,占4個(gè)字節(jié),它是按Little-endian的方式存放的,要把這4個(gè)字節(jié)位的順序顛倒才能得到實(shí)際數(shù)值,即把高位字節(jié)變成低位,把低位變換到高位。
要把以上代碼注入到一個(gè)PE文件中,需要修改4個(gè)地方:(1)修改PE文件的入口地址,使PE裝載器首先裝載以上代碼;(2)修改以上代碼MessageBoxA()的地址,使以上的代碼能夠顯示出一個(gè)對(duì)話框;(3)把“校驗(yàn)和”位變成跳轉(zhuǎn)位,即變成jmp (0xe9);(4)修改返回地址,把程序引入到原來(lái)的裝載點(diǎn)上。
2.6.2 把MessageBoxA()代碼寫入PE文件的完整實(shí)例
根據(jù)以上的對(duì)MessageBoxA()的分析,可以直接把以上代碼注入到一個(gè)PE可執(zhí)行 文件中。為了使程序有通用性,這里編寫了一個(gè)產(chǎn)生顯示任意長(zhǎng)度字符的對(duì)話框的函數(shù)WriteMessageBox()。
下面是用于注入MessageBoxA()代碼的頭文件,取名為Pe.h,其中用 #include包含了相關(guān)的文件頭,定義了peHeader結(jié)構(gòu),且定義了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結(jié)構(gòu)是前面所講的PE Header結(jié)構(gòu)與節(jié)表(Section Table)頭結(jié)構(gòu)(6個(gè)表頭成員)的總結(jié)構(gòu)。因?yàn)樗鼈冊(cè)赑E文件中是緊湊排列的,所以可以這樣寫。其實(shí)只用一個(gè)節(jié)表頭就可以。
下面分別介紹CPe類成員函數(shù)的定義,它們包含在Pe.cpp文件中。在這個(gè)文件開始用#include包含了stdafx.h和Pe.h文件。用MFC VC++編譯器編譯時(shí),必須包括stdafx.h文件,即使這個(gè)文件是空的,也需要包括它,這是編譯器設(shè)置所致,除非修改MFC的編譯器的默認(rèn)設(shè)置。CPe類的構(gòu)造和析構(gòu)函數(shù)這里沒(méi)有用上,對(duì)系統(tǒng)內(nèi)存的訪問(wèn)和其他操作主要是通過(guò)主成員函數(shù)ModifyPe()來(lái)進(jìn)行。它們的源代碼如下:
l
// Pe.cpp: 實(shí)現(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)建一個(gè)映射文件
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); // 得到相關(guān)地址
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);
}
其中對(duì)一個(gè)PE文件進(jìn)行MessageBoxA()代碼的注入是通過(guò)ModifyPe()函數(shù)進(jìn)行,它的入口參數(shù)是要被修改的PE可執(zhí)行文件名。在這個(gè)函數(shù)中,首先創(chuàng)建所修改文件的句柄,然后創(chuàng)建映射文件,再通過(guò)映射文件的句柄獲得這個(gè)PE文件的文件頭指針,最后把這個(gè)指針傳給函數(shù)CalcAddress()。通過(guò)CalcAddress()函數(shù)來(lái)計(jì)算PE Header的開始偏移、保存舊的程序入口地址、計(jì)算新的程序入口地址和計(jì)算PE文件的空隙空間等。
CalcAddress()函數(shù)的源代碼如下:
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)
{
// 此段的真實(shí)長(zhǎng)度
dwVirtSize=header->section_header[0].Misc.VirtualSize;
// 此段的物理偏移
dwPhysAddress=header->section_header[0].PointerToRawData;
// 此段的物理長(zhǎng)度
dwPhysSize=header->section_header[0].SizeOfRawData;
// 得到PE文件頭的開始偏移
dwPeAddress=dos_head->e_lfanew;
// 得到代碼段的可用空間,用以判斷可不可以寫入我們的代碼
// 用此段的物理長(zhǎng)度減去此段的真實(shí)長(zhǎng)度就可以得到
dwSpace=dwPhysSize-dwVirtSize;
// 得到程序的裝載地址,一般為0x400000
dwProgRAV=header->opt_head.ImageBase;
// 得到代碼偏移,用代碼段起始RVA減去此段的物理偏移
// 應(yīng)為程序的入口計(jì)算公式是一個(gè)相對(duì)的偏移地址,計(jì)算公式為:
// 代碼的寫入地址+dwCodeOffset
dwCodeOffset=header->opt_head.BaseOfCode-dwPhysAddress;
// 代碼寫入的物理偏移
dwEntryWrite=header->section_header[0].PointerToRawData+header->
section_header[0].Misc.VirtualSize;
//對(duì)齊邊界
mods=dwEntryWrite%16;
if(mods!=0)
{
dwEntryWrite+=(16-mods);
}
// 保存舊的程序入口地址
dwOldEntryAddress=header->opt_head.AddressOfEntryPoint;
// 計(jì)算新的程序入口地址
dwNewEntryAddress=dwEntryWrite+dwCodeOffset;
return;
}
}
l
下面的StrOfDWord()函數(shù)是把一個(gè)DWORD值轉(zhuǎn)換成一個(gè)字符串,因?yàn)橐粋€(gè)DWORD值占有4個(gè)字節(jié),因此把一個(gè)DWORD值變成一個(gè)字符串,若保持?jǐn)?shù)值不變,就變成了一個(gè)4個(gè)字節(jié)的字符串。同時(shí)把這個(gè)值的位置順序顛倒,這是為了把一個(gè)實(shí)際的值變成按Little-endian的方式寫入PE文件中,其轉(zhuǎn)換方法如下:
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()函數(shù)把新的入口點(diǎn)寫入PE程序原來(lái)的入口點(diǎn)處,使PE裝載器在載入程序時(shí),直接跳入到MessageBoxA()的入口處,該函數(shù)的源代碼如下:
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()函數(shù)是把MessageBoxA()的機(jī)器代碼寫入到PE文件中。這個(gè)函數(shù)顯示的對(duì)話框標(biāo)題和顯示的字符串內(nèi)容和長(zhǎng)度不是固定的。在這個(gè)函數(shù)中,首先就計(jì)算MessageBoxA()函數(shù)的地址和函數(shù)的返回地址,然后把重新生成的對(duì)話框代碼寫入到程序中。WriteMessageBox()函數(shù)的源代碼如下:
l
BOOL CPe::WriteMessageBox(int ret,long offset,CString strCap,CString
strTxt)
{
CString strAddress1,strAddress2;
unsigned char waddress[4]={0};
DWORD dwAddress;
// 獲取MessageBox在內(nèi)存中的地址
HINSTANCE gLibMsg=LoadLibrary("user32.dll");
dwMessageBoxAadaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
// 計(jì)算校驗(yàn)位
int nLenCap1 =strCap.GetLength()+1; // 加上字符串后面的結(jié)束位
int nLenTxt1 =strTxt.GetLength()+1; // 加上字符串后面的結(jié)束位
int nTotLen=nLenCap1+nLenTxt1+24;
// 重新計(jì)算MessageBox函數(shù)的地址
dwAddress=dwMessageBoxAadaddress-(dwProgRAV+dwNewEntryAddress+nTot
Len-5);
strAddress1=StrOfDWord(dwAddress);
// 計(jì)算返回地址
dwAddress=0-(dwNewEntryAddress-dwOldEntryAddress+nTotLen);
strAddress2=StrOfDWord(dwAddress);
// 對(duì)話框頭代碼(固定)
unsigned char cHeader[2]={0x6a,0x40};
// 標(biāo)題定義
unsigned char cDesCap[5]={0xe8,nLenCap1,0x00,0x00,0x00};
// 內(nèi)容定義
unsigned char cDesTxt[5]={0xe8,nLenTxt1,0x00,0x00,0x00};
// 對(duì)話框后部分的代碼段
unsigned char cFix[12]
={0x6a,0x00,0xe8,0x00,0x00,0x00,0x00,0xe9,0x00,0x00,0x00,0x00};
// 修改對(duì)話框后部分的代碼段
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;
// 生成對(duì)話框命令字符串
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);
// 向應(yīng)用程序?qū)懭雽?duì)話框代碼
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()函數(shù)是總的寫入函數(shù)。在這個(gè)函數(shù)中,先打開被修改的PE文件,然后調(diào)用WriteNewEntry()和WriteMessageBox()函數(shù)。WriteFile()函數(shù)的源代碼如下:
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;
// 把對(duì)話框代碼寫入到應(yīng)用程序中
if(!WriteMessageBox(ret,(long)dwEntryWrite,"Test","We are the world!"))
return;
_close(ret);
}
l
僅僅利用以上CPe類還是不能對(duì)一個(gè)PE文件進(jìn)行注入MessageBoxA()代碼的修改,還必須要一個(gè)“載體程序”。例如:
l
// Pefile.cpp:修改PE文件實(shí)例
//
#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
這個(gè)修改后的PE文件運(yùn)行時(shí),就會(huì)先顯示對(duì)話框,單擊“確定”按鈕后又繼續(xù)執(zhí)行。總之,在了解了PE文件格式后,就可以對(duì)某一個(gè)PE文件進(jìn)行修改。本實(shí)例只是對(duì)PE文件處理的一種應(yīng)用,在實(shí)際中還有更多的其他方面的應(yīng)用。
2.7 本章小結(jié)
本章首先介紹了PE文件的基本結(jié)構(gòu),對(duì)一些容易混淆的名詞進(jìn)行了解釋。通過(guò)介紹一個(gè)對(duì)PE文件注入對(duì)話框代碼的實(shí)例,加強(qiáng)了對(duì)PE文件結(jié)構(gòu)的認(rèn)識(shí)。
本章所介紹的向PE文件注入代碼的實(shí)例只是用來(lái)說(shuō)明如何修改PE文件,有關(guān)如何向一個(gè)應(yīng)用程序中注入代碼的技術(shù)還要在以后的章節(jié)專門介紹。此外,還有其他的技術(shù)沒(méi)有介紹,例如如何提取程序中的代碼,在以后的章節(jié)中對(duì)此也還要專門介紹。總之,了解了PE文件結(jié)構(gòu),就可以很容易地對(duì)某個(gè)應(yīng)用程序進(jìn)行加殼、掛鉤或捆綁。