隨筆-17  評(píng)論-64  文章-79  trackbacks-1

          本章提要

          ·           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)行加殼、掛鉤或捆綁。

          posted on 2007-07-04 13:45 飛鳥 閱讀(2235) 評(píng)論(1)  編輯  收藏 所屬分類: VC

          評(píng)論:
          # re: PE文件結(jié)構(gòu) 2011-10-24 22:07 | 易元
          為什么沒(méi)有圖片??  回復(fù)  更多評(píng)論
            
          主站蜘蛛池模板: 崇州市| 沈丘县| 平安县| 宁海县| 麻江县| 安庆市| 溧水县| 资阳市| 海城市| 武宣县| 镇坪县| 军事| 齐齐哈尔市| 上犹县| 德昌县| 隆昌县| 青海省| 湘西| 宽甸| 伽师县| 宁强县| 巴南区| 新乡县| 无锡市| 岐山县| 扶绥县| 河津市| 湟源县| 黑龙江省| 迁安市| 会同县| 兰坪| 万宁市| 克拉玛依市| 南开区| 白山市| 青岛市| 中山市| 三穗县| 凤冈县| 凌云县|