關(guān)于PE可執(zhí)行文件的修改
在windows 9x、NT、2000下,所有的可執(zhí)行文件都是基于Microsoft設(shè)計(jì)的一種新的文件格式Portable Executable File Format(可移植的執(zhí)行體),即PE格式。有一些時(shí)候,我們需要對(duì)這些可執(zhí)行文件進(jìn)行修改,下面文字試圖詳細(xì)的描述PE文件的格式及對(duì)PE格式文件的 修改。 1、PE文件框架構(gòu)成 DOS MZ header DOS stub PE header Section table Section 1 Section 2 Section ... Section n 上 表是PE文件結(jié)構(gòu)的總體層次分布。所有 PE文件(甚至32位的 DLLs) 必須以一個(gè)簡(jiǎn)單的 DOS MZ header 開(kāi)始,在偏移0處有DOS下可執(zhí)行文件的“MZ標(biāo)志”,有了它,一旦程序在DOS下執(zhí)行,DOS就能識(shí)別出這是有效的執(zhí)行體,然后運(yùn)行緊隨 MZ header 之后的 DOS stub。DOS stub實(shí)際上是個(gè)有效的EXE,在不支持 PE文件格式的操作系統(tǒng)中,它將簡(jiǎn)單顯示一個(gè)錯(cuò)誤提示,類似于字符串 " This program cannot run in DOS mode " 或者程序員可根據(jù)自己的意圖實(shí)現(xiàn)完整的 DOS代碼。通常DOS stub由匯編器/編譯器自動(dòng)生成,對(duì)我們的用處不是很大,它簡(jiǎn)單調(diào)用中斷21h服務(wù)9來(lái)顯示字符串"This program cannot run in DOS mode"。 緊接著 DOS stub 的是 PE header。 PE header 是PE相關(guān)結(jié)構(gòu) IMAGE_NT_HEADERS 的簡(jiǎn)稱,其中包含了許多PE裝載器用到的重要域。可執(zhí)行文件在支持PE文件結(jié)構(gòu)的操作系統(tǒng)中執(zhí)行時(shí),PE裝載器將從 DOS MZ header的偏移3CH處找到 PE header 的起始偏移量。因而跳過(guò)了 DOS stub 直接定位到真正的文件頭 PE header。 PE文件的真正內(nèi)容劃分成塊,稱之為sections(節(jié))。每節(jié)是一塊擁有共同屬性的數(shù)據(jù),比如“.text”節(jié)等,那么,每一節(jié)的內(nèi)容都是什么呢?實(shí) 際上PE格式的文件把具有相同屬性的內(nèi)容放入同一個(gè)節(jié)中,而不必關(guān)心類似“.text”、“.data”的命名,其命名只是為了便于識(shí)別,所有,我們?nèi)绻? 對(duì)PE格式的文件進(jìn)行修改,理論上講可以寫入任何一個(gè)節(jié)內(nèi),并調(diào)整此節(jié)的屬性就可以了。 PE header 接下來(lái)的數(shù)組結(jié)構(gòu) section table(節(jié)表)。 每個(gè)結(jié)構(gòu)包含對(duì)應(yīng)節(jié)的屬性、文件偏移量、虛擬偏移量等。如果PE文件里有5個(gè)節(jié),那么此結(jié)構(gòu)數(shù)組內(nèi)就有5個(gè)成員。 以上就是PE文件格式的物理分布,下面將總結(jié)一下裝載一PE文件的主要步驟: 1、 PE文件被執(zhí)行,PE裝載器檢查 DOS MZ header 里的 PE header 偏移量。如果找到,則跳轉(zhuǎn)到 PE header。 2、PE裝載器檢查 PE header 的有效性。如果有效,就跳轉(zhuǎn)到PE header的尾部。 3、緊跟 PE header 的是節(jié)表。PE裝載器讀取其中的節(jié)信息,并采用文件映射方法將這些節(jié)映射到內(nèi)存,同時(shí)付上節(jié)表里指定的節(jié)屬性。 4、PE文件映射入內(nèi)存后,PE裝載器將處理PE文件中類似 import table(引入表)邏輯部分。 上述步驟是一些前輩分析的結(jié)果簡(jiǎn)述。 2、PE文件頭概述 我們可以在winnt.h這個(gè)文件中找到關(guān)于PE文件頭的定義: typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //PE文件頭標(biāo)志 :“PE\0\0”。在開(kāi)始DOS header的偏移3CH處所指向的地址開(kāi)始 IMAGE_FILE_HEADER FileHeader; //PE文件物理分布的信息 IMAGE_OPTIONAL_HEADER32 OptionalHeader; //PE文件邏輯分布的信息 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; typedef struct _IMAGE_FILE_HEADER { WORD Machine; //該文件運(yùn)行所需要的CPU,對(duì)于Intel平臺(tái)是14Ch WORD NumberOfSections; //文件的節(jié)數(shù)目 DWORD TimeDateStamp; //文件創(chuàng)建日期和時(shí)間 DWORD PointerToSymbolTable; //用于調(diào)試 DWORD NumberOfSymbols; //符號(hào)表中符號(hào)個(gè)數(shù) WORD SizeOfOptionalHeader; //OptionalHeader 結(jié)構(gòu)大小 WORD Characteristics; //文件信息標(biāo)記,區(qū)分文件是exe還是dll } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic; //標(biāo)志字(總是010bh) BYTE MajorLinkerVersion; //連接器版本號(hào) BYTE MinorLinkerVersion; // DWORD SizeOfCode; //代碼段大小 DWORD SizeOfInitializedData; //已初始化數(shù)據(jù)塊大小 DWORD SizeOfUninitializedData; //未初始化數(shù)據(jù)塊大小 DWORD AddressOfEntryPoint; //PE裝載器準(zhǔn)備運(yùn)行的PE文件的第一個(gè)指令的RVA,若要改變整個(gè)執(zhí)行的流程,可以將該值指定到新的RVA,這樣新RVA處的指令首先被執(zhí)行。(許多文章都有介紹RVA,請(qǐng)去了解) DWORD BaseOfCode; //代碼段起始RVA DWORD BaseOfData; //數(shù)據(jù)段起始RVA DWORD ImageBase; //PE文件的裝載地址 DWORD SectionAlignment; //塊對(duì)齊 DWORD FileAlignment; //文件塊對(duì)齊 WORD MajorOperatingSystemVersion;//所需操作系統(tǒng)版本號(hào) WORD MinorOperatingSystemVersion;// WORD MajorImageVersion; //用戶自定義版本號(hào) WORD MinorImageVersion; // WORD MajorSubsystemVersion; //win32子系統(tǒng)版本。若PE文件是專門為Win32設(shè)計(jì)的 WORD MinorSubsystemVersion; //該子系統(tǒng)版本必定是4.0否則對(duì)話框不會(huì)有3維立體感 DWORD Win32VersionValue; //保留 DWORD SizeOfImage; //內(nèi)存中整個(gè)PE映像體的尺寸 DWORD SizeOfHeaders; //所有頭+節(jié)表的大小 DWORD CheckSum; //校驗(yàn)和 WORD Subsystem; //NT用來(lái)識(shí)別PE文件屬于哪個(gè)子系統(tǒng) WORD DllCharacteristics; // DWORD SizeOfStackReserve; // DWORD SizeOfStackCommit; // DWORD SizeOfHeapReserve; // DWORD SizeOfHeapCommit; // DWORD LoaderFlags; // DWORD NumberOfRvaAndSizes; // IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //IMAGE_DATA_DIRECTORY 結(jié)構(gòu)數(shù)組。每個(gè)結(jié)構(gòu)給出一個(gè)重要數(shù)據(jù)結(jié)構(gòu)的RVA,比如引入地址表等 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //表的RVA地址 DWORD Size; //大小 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; PE文件頭后是節(jié)表,在winnt.h下如下定義 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; 以上結(jié)構(gòu)就是在winnt.h中關(guān)于PE文件頭的定義,如何我們用C/C++來(lái)進(jìn)行PE可執(zhí)行文件操作,就要用到上面的所有結(jié)構(gòu),它詳細(xì)的描述了PE文件頭的結(jié)構(gòu)。 3、修改PE可執(zhí)行文件 現(xiàn)在讓我們把一段代碼寫入任何一個(gè)PE格式的可執(zhí)行文件,代碼如下: -- test.asm -- .386p .model flat, stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\user32.inc includelib \masm32\lib\user32.lib .code start: INVOKE MessageBoxA,0,0,0,MB_ICONINFORMATION or MB_OK ret end start 以上代碼只顯示一個(gè)MessageBox框,編譯后得到二進(jìn)制代碼如下: unsigned char writeline[18]={ 0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0 }; 好,現(xiàn)在讓我們看看該把這些代碼寫到那?,F(xiàn)在用Tdump.exe顯示一個(gè)PE格式得可執(zhí)行文件信息,可以發(fā)現(xiàn)如下描述: Object table: # Name VirtSize RVA PhysSize Phys off Flags -- -------- -------- -------- -------- -------- -------- 01 .text 0000CCC0 00001000 0000CE00 00000600 60000020 [CER] 02 .data 00004628 0000E000 00002C00 0000D400 C0000040 [IRW] 03 .rsrc 000003C8 00013000 00000400 00010000 40000040 [IR] Key to section flags: C - contains code E - executable I - contains initialized data R - readable W - writeable 上面描述此文件中存在3個(gè)段及每個(gè)段得信息,實(shí)際上我們的代碼可以寫入任何一個(gè)段,這里我選擇“.text”段。 用如下代碼得到一個(gè)PE格式可執(zhí)行文件的頭信息: //writePE.cpp #include <windows.h> #include <stdio.h> #include <io.h> #include <fcntl.h> #include <time.h> #include <SYS\STAT.H> unsigned char writeline[18]={ 0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0 }; DWORD space; DWORD entryaddress; DWORD entrywrite; DWORD progRAV; DWORD oldentryaddress; DWORD newentryaddress; DWORD codeoffset; DWORD peaddress; DWORD flagaddress; DWORD flags; DWORD virtsize; DWORD physaddress; DWORD physsize; DWORD MessageBoxAadaddress; int main(int argc,char * * argv) { HANDLE hFile, hMapping; void *basepointer; FILETIME * Createtime; FILETIME * Accesstime; FILETIME * Writetime; Createtime = new FILETIME; Accesstime = new FILETIME; Writetime = new FILETIME; if ((hFile = CreateFile(argv[1], GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_VALUE)//打開(kāi)要修改的文件 { puts("(could not open)"); return EXIT_FAILURE; } if(!GetFileTime(hFile,Createtime,Accesstime,Writetime)) { printf("\nerror getfiletime: %d\n",GetLastError()); } //得到要修改文件的創(chuàng)建、修改等時(shí)間 if (!(hMapping = CreateFileMapping(hFile, 0, PAGE_READONLY | SEC_COMMIT, 0, 0, 0))) { puts("(mapping failed)"); CloseHandle(hFile); return EXIT_FAILURE; } if (!(basepointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0))) { puts("(view failed)"); CloseHandle(hMapping); CloseHandle(hFile); return EXIT_FAILURE; } //把文件頭映象存入baseointer CloseHandle(hMapping); CloseHandle(hFile); map_exe(basepointer);//得到相關(guān)地址 UnmapViewOfFile(basepointer); printaddress(); printf("\n\n"); if(space<50) { printf("\n空隙太小,數(shù)據(jù)不能寫入.\n"); } else { writefile();//寫文件 } if ((hFile = CreateFile(argv[1], GENERIC_READ|GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_VALUE) { puts("(could not open)"); return EXIT_FAILURE; } if(!SetFileTime(hFile,Createtime,Accesstime,Writetime)) { printf("error settime : %d\n",GetLastError()); } //恢復(fù)修改后文件的建立時(shí)間等 delete Createtime; delete Accesstime; delete Writetime; CloseHandle(hFile); return 0; } void map_exe(const void *base) { IMAGE_DOS_HEADER * dos_head; dos_head =(IMAGE_DOS_HEADER *)base; #include <pshpack1.h> typedef struct PE_HEADER_MAP { DWORD signature; IMAGE_FILE_HEADER _head; IMAGE_OPTIONAL_HEADER opt_head; IMAGE_SECTION_HEADER section_header[]; } peHeader; #include <poppack.h> if (dos_head->e_magic != IMAGE_DOS_SIGNATURE) { puts("unknown type of file"); return; } peHeader * header; header = (peHeader *)((char *)dos_head + dos_head->e_lfanew);//得到PE文件頭 if (IsBadReadPtr(header, sizeof(*header)) { puts("(no PE header, probably DOS executable)"); return; } DWORD mods; char tmpstr[4]={0}; DWORD tmpaddress; DWORD tmpaddress1; if(strstr((const char *)header->section_header[0].Name,".text")!=NULL) { virtsize=header->section_header[0].Misc.VirtualSize; //此段的真實(shí)長(zhǎng)度 physaddress=header->section_header[0].PointerToRawData; //此段的物理偏移 physsize=header->section_header[0].SizeOfRawData; //此段的物理長(zhǎng)度 peaddress=dos_head->e_lfanew; //得到PE文件頭的開(kāi)始偏移 peHeader peH; tmpaddress=(unsigned long )&peH; //得到結(jié)構(gòu)的偏移 tmpaddress1=(unsigned long )&(peH.section_header[0].Characteristics); //得到變量的偏移 flagaddress=tmpaddress1-tmpaddress+2; //得到屬性的相對(duì)偏移 flags=0x8000; //一般情況下,“.text”段是不可讀寫的,如果我們要把數(shù)據(jù)寫入這個(gè)段需要改變其屬性,實(shí)際上這個(gè)程序并沒(méi)有把數(shù)據(jù)寫入“.text”段,所以并不需要更改,但如果你實(shí)現(xiàn)復(fù)雜的功能,肯定需要數(shù)據(jù),肯定需要更改這個(gè)值, space=physsize-virtsize; //得到代碼段的可用空間,用以判斷可不可以寫入我們的代碼 //用此段的物理長(zhǎng)度減去此段的真實(shí)長(zhǎng)度就可以得到 progRAV=header->opt_head.ImageBase; //得到程序的裝載地址,一般為400000 codeoffset=header->opt_head.BaseOfCode-physaddress; //得到代碼偏移,用代碼段起始RVA減去此段的物理偏移 //應(yīng)為程序的入口計(jì)算公式是一個(gè)相對(duì)的偏移地址,計(jì)算公式為: //代碼的寫入地址+codeoffset entrywrite=header->section_header[0].PointerToRawData+header->section_header[0].Misc.VirtualSize; //代碼寫入的物理偏移 mods=entrywrite%16; //對(duì)齊邊界 if(mods!=0) { entrywrite+=(16-mods); } oldentryaddress=header->opt_head.AddressOfEntryPoint; //保存舊的程序入口地址 newentryaddress=entrywrite+codeoffset; //計(jì)算新的程序入口地址 return; } void printaddress() { HINSTANCE gLibMsg=NULL; DWORD funaddress; gLibMsg=LoadLibrary("user32.dll"); funaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA"); MessageBoxAadaddress=funaddress; gLibAMsg=LoadLibrary("kernel32.dll"); //得到MessageBox在內(nèi)存中的地址,以便我們使用 } void writefile() { int ret; long retf; DWORD address; int tmp; unsigned char waddress[4]={0}; ret=_open(filename,_O_RDWR | _O_CREAT | _O_BINARY,_S_IREAD | _S_IWRITE); if(!ret) { printf("error open\n"); return; } retf=_lseek(ret,(long)peaddress+40,SEEK_SET); //程序的入口地址在PE文件頭開(kāi)始的40處 if(retf==-1) { printf("error seek\n"); return; } address=newentryaddress; tmp=address>>24; waddress[3]=tmp; tmp=address<<8; tmp=tmp>>24; waddress[2]=tmp; tmp=address<<16; tmp=tmp>>24; waddress[1]=tmp; tmp=address<<24; tmp=tmp>>24; waddress[0]=tmp; retf=_write(ret,waddress,4); //把新的入口地址寫入文件 if(retf==-1) { printf("error write: %d\n",GetLastError()); return; } retf=_lseek(ret,(long)entrywrite,SEEK_SET); if(retf==-1) { printf("error seek\n"); return; } retf=_write(ret,writeline,18); if(retf==-1) { printf("error write: %d\n",GetLastError()); return; } //把writeline寫入我們計(jì)算出的空間 retf=_lseek(ret,(long)entrywrite+9,SEEK_SET); //更改MessageBox函數(shù)地址,它的二進(jìn)制代碼在writeline[10]處 if(retf==-1) { printf("error seek\n"); return; } address=MessageBoxAadaddress-(progRAV+newentryaddress+9+4); //重新計(jì)算MessageBox函數(shù)的地址,MessageBox函數(shù)的原地址減去程序的裝載地址加上新的入口地址加9(它的二進(jìn)制代碼相對(duì)偏移)加上4(地址長(zhǎng)度) tmp=address>>24; waddress[3]=tmp; tmp=address<<8; tmp=tmp>>24; waddress[2]=tmp; tmp=address<<16; tmp=tmp>>24; waddress[1]=tmp; tmp=address<<24; tmp=tmp>>24; waddress[0]=tmp; retf=_write(ret,waddress,4); //寫入重新計(jì)算的MessageBox地址 if(retf==-1) { printf("error write: %d\n",GetLastError()); return; } retf=_lseek(ret,(long)entrywrite+14,SEEK_SET); //更改返回地址,用jpm返回原程序入口地址,其它的二進(jìn)制代碼在writeline[15]處 if(retf==-1) { printf("error seek\n"); return; } address=0-(newentryaddress-oldentryaddress+4+15); //返回地址計(jì)算的方法是新的入口地址減去老的入口地址加4(地址長(zhǎng)度)加15(二進(jìn)制代碼相對(duì)偏移)后取反 tmp=address>>24; waddress[3]=tmp; tmp=address<<8; tmp=tmp>>24; waddress[2]=tmp; tmp=address<<16; tmp=tmp>>24; waddress[1]=tmp; tmp=address<<24; tmp=tmp>>24; waddress[0]=tmp; retf=_write(ret,waddress,4); //寫入返回地址 if(retf==-1) { printf("error write: %d\n",GetLastError()); return; } _close(ret); printf("\nall done...\n"); return; } //end 由于在PE格式的文件中,所有的地址都使用RVA地址,所以一些函數(shù)調(diào)用和返回地址都要經(jīng)過(guò)計(jì)算才可以得到,以上是我在實(shí)踐中的心得,如果你有更好的辦法,真心的希望你能告訴我。 如果存在錯(cuò)誤,請(qǐng)告訴我,以免誤導(dǎo)看這篇文章的人。寫的較亂,請(qǐng)?jiān)彙? 文章出處:netguard.com.cn 文章作者:ilsy(ilsy@netguard.com.cn ) Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1595030
[收藏到我的網(wǎng)摘] 走盡天涯路發(fā)表于 2007年05月02日 19:32:15
|
|
來(lái)自: ShangShujie > 《asm》