新闻  |   论坛  |   博客  |   在线研讨会
关于PE可执行文件的修改
tongxin | 2009-04-13 18:14:55    阅读:732   发布文章

在windows 9x、NT、2000下,所有的可执行文件都是基于Microsoft设计的一种新的文件格式Portable Executable File Format(可移植的执行体),即PE格式。有一些时候,我们需要对这些可执行文件进行修改,下面文字试图详细的描述PE文件的格式及对PE格式文件的修改。
%A 1、PE文件框架构成
%A DOS MZ header
%A DOS stub
%A PE header
%A Section table
%A Section 1
%A Section 2
%A Section ...
%A Section n
%A 上表是PE文件结构的总体层次分布。所有 PE文件(甚至32位的 DLLs) 必须以一个简单的 DOS MZ header 开始,在偏移0处有DOS下可执行文件的“MZ标志”,有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随 MZ header 之后的 DOS stub。DOS stub实际上是个有效的EXE,在不支持 PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串 " This program cannot run in DOS mode " 或者程序员可根据自己的意图实现完整的 DOS代码。通常DOS stub由汇编器/编译器自动生成,对我们的用处不是很大,它简单调用中断21h服务9来显示字符串"This program cannot run in DOS mode"。
%A 紧接着 DOS stub 的是 PE header。 PE header 是PE相关结构 IMAGE_NT_HEADERS 的简称,其中包含了许多PE装载器用到的重要域。可执行文件在支持PE文件结构的操作系统中执行时,PE装载器将从 DOS MZ header的偏移3CH处找到 PE header 的起始偏移量。因而跳过了 DOS stub 直接定位到真正的文件头 PE header。
%A PE文件的真正内容划分成块,称之为sections(节)。每节是一块拥有共同属性的数据,比如“.text”节等,那么,每一节的内容都是什么呢?实际上PE格式的文件把具有相同属性的内容放入同一个节中,而不必关心类似“.text”、“.data”的命名,其命名只是为了便于识别,所有,我们如果对PE格式的文件进行修改,理论上讲可以写入任何一个节内,并调整此节的属性就可以了。
%A PE header 接下来的数组结构 section table(节表)。 每个结构包含对应节的属性、文件偏移量、虚拟偏移量等。如果PE文件里有5个节,那么此结构数组内就有5个成员。
%A 以上就是PE文件格式的物理分布,下面将总结一下装载一PE文件的主要步骤:
%A 1、    PE文件被执行,PE装载器检查 DOS MZ header 里的 PE header 偏移量。如果找到,则跳转到 PE header。
%A 2、PE装载器检查 PE header 的有效性。如果有效,就跳转到PE header的尾部。
%A 3、紧跟 PE header 的是节表。PE装载器读取其中的节信息,并采用文件映射方法将这些节映射到内存,同时付上节表里指定的节属性。
%A 4、PE文件映射入内存后,PE装载器将处理PE文件中类似 import table(引入表)逻辑部分。
%A 上述步骤是一些前辈分析的结果简述。
%A 2、PE文件头概述
%A 我们可以在winnt.h这个文件中找到关于PE文件头的定义:
%A typedef struct _IMAGE_NT_HEADERS {
%A DWORD Signature;                
%A //PE文件头标志 :“PE\0\0”。在开始DOS header的偏移3CH处所指向的地址开始
%A IMAGE_FILE_HEADER FileHeader;        //PE文件物理分布的信息
%A IMAGE_OPTIONAL_HEADER32 OptionalHeader;    //PE文件逻辑分布的信息
%A } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
%A
%A typedef struct _IMAGE_FILE_HEADER {
%A WORD    Machine;            //该文件运行所需要的CPU,对于Intel平台是14Ch
%A WORD    NumberOfSections;        //文件的节数目
%A DWORD   TimeDateStamp;        //文件创建日期和时间
%A DWORD   PointerToSymbolTable;    //用于调试
%A DWORD   NumberOfSymbols;        //符号表中符号个数
%A WORD    SizeOfOptionalHeader;    //OptionalHeader 结构大小
%A WORD    Characteristics;        //文件信息标记,区分文件是exe还是dll
%A } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
%A
%A typedef struct _IMAGE_OPTIONAL_HEADER {
%A WORD    Magic;            //标志字(总是010bh)
%A BYTE    MajorLinkerVersion;        //连接器版本号
%A BYTE    MinorLinkerVersion;        //
%A DWORD   SizeOfCode;            //代码段大小
%A DWORD   SizeOfInitializedData;    //已初始化数据块大小
%A DWORD   SizeOfUninitializedData;    //未初始化数据块大小
%A DWORD   AddressOfEntryPoint;    //PE装载器准备运行的PE文件的第一个指令的RVA,若要改变整个执行的流程,可以将该值指定到新的RVA,这样新RVA处的指令首先被执行。(许多文章都有介绍RVA,请去了解)
%A DWORD   BaseOfCode;            //代码段起始RVA
%A DWORD   BaseOfData;            //数据段起始RVA
%A DWORD   ImageBase;            //PE文件的装载地址
%A DWORD   SectionAlignment;        //块对齐
%A DWORD   FileAlignment;        //文件块对齐
%A WORD    MajorOperatingSystemVersion;//所需操作系统版本号
%A WORD    MinorOperatingSystemVersion;//
%A WORD    MajorImageVersion;        //用户自定义版本号
%A WORD    MinorImageVersion;        //
%A WORD    MajorSubsystemVersion;    //win32子系统版本。若PE文件是专门为Win32设计的
%A WORD    MinorSubsystemVersion;    //该子系统版本必定是4.0否则对话框不会有3维立体感
%A DWORD   Win32VersionValue;        //保留
%A DWORD   SizeOfImage;            //内存中整个PE映像体的尺寸
%A DWORD   SizeOfHeaders;        //所有头+节表的大小
%A DWORD   CheckSum;            //校验和
%A WORD    Subsystem;            //NT用来识别PE文件属于哪个子系统
%A WORD    DllCharacteristics;        //
%A DWORD   SizeOfStackReserve;        //
%A DWORD   SizeOfStackCommit;        //
%A DWORD   SizeOfHeapReserve;        //
%A DWORD   SizeOfHeapCommit;        //
%A DWORD   LoaderFlags;            //
%A DWORD   NumberOfRvaAndSizes;    //
%A IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
%A //IMAGE_DATA_DIRECTORY 结构数组。每个结构给出一个重要数据结构的RVA,比如引入地址表等
%A } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
%A
%A typedef struct _IMAGE_DATA_DIRECTORY {
%A DWORD   VirtualAddress;        //表的RVA地址
%A DWORD   Size;                //大小
%A } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
%A
%A PE文件头后是节表,在winnt.h下如下定义
%A typedef struct _IMAGE_SECTION_HEADER {
%A BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];//节表名称,如“.text”
%A union {
%A     DWORD   PhysicalAddress;    //物理地址            
%A     DWORD   VirtualSize;        //真实长度
%A } Misc;
%A DWORD   VirtualAddress;        //RVA
%A DWORD   SizeOfRawData;        //物理长度
%A DWORD   PointerToRawData;        //节基于文件的偏移量
%A DWORD   PointerToRelocations;    //重定位的偏移
%A DWORD   PointerToLinenumbers;    //行号表的偏移
%A WORD    NumberOfRelocations;    //重定位项数目
%A WORD    NumberOfLinenumbers;    //行号表的数目
%A DWORD   Characteristics;        //节属性
%A } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
%A
%A 以上结构就是在winnt.h中关于PE文件头的定义,如何我们用C/C++来进行PE可执行文件操作,就要用到上面的所有结构,它详细的描述了PE文件头的结构。
%A
%A 3、修改PE可执行文件
%A 现在让我们把一段代码写入任何一个PE格式的可执行文件,代码如下:
%A -- test.asm --
%A .386p
%A .model flat, stdcall
%A option casemap:none
%A
%A include \masm32\include\windows.inc
%A include \masm32\include\user32.inc
%A includelib \masm32\lib\user32.lib
%A
%A .code
%A
%A start:
%A     INVOKE MessageBoxA,0,0,0,MB_ICONINFORMATION or MB_OK
%A     ret
%A end start
%A
%A 以上代码只显示一个MessageBox框,编译后得到二进制代码如下:
%A unsigned char writeline[18]={
%A 0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0
%A };
%A
%A 好,现在让我们看看该把这些代码写到那。现在用Tdump.exe显示一个PE格式得可执行文件信息,可以发现如下描述:
%A Object table:
%A #   Name      VirtSize    RVA     PhysSize  Phys off  Flags  
%A --  --------  --------  --------  --------  --------  --------
%A 01  .text     0000CCC0  00001000  0000CE00  00000600  60000020 [CER]
%A 02  .data     00004628  0000E000  00002C00  0000D400  C0000040 [IRW]
%A 03  .rsrc     000003C8  00013000  00000400  00010000  40000040 [IR]
%A
%A Key to section flags:
%A   C - contains code
%A   E - executable
%A   I - contains initialized data
%A   R - readable
%A   W - writeable
%A
%A 上面描述此文件中存在3个段及每个段得信息,实际上我们的代码可以写入任何一个段,这里我选择“.text”段。
%A
%A 用如下代码得到一个PE格式可执行文件的头信息:
%A
%A //writePE.cpp
%A
%A #include <windows.h>
%A #include <stdio.h>
%A #include <io.h>
%A #include <fcntl.h>
%A #include <time.h>
%A #include <SYS\STAT.H>
%A
%A unsigned char writeline[18]={
%A 0x6a,0x40,0x6a,0x0,0x6a,0x0,0x6a,0x0,0xe8,0x01,0x0,0x0,0x0,0xe9,0x0,0x0,0x0,0x0
%A };
%A
%A DWORD space;
%A DWORD entryaddress;
%A DWORD entrywrite;
%A DWORD progRAV;
%A DWORD oldentryaddress;
%A DWORD newentryaddress;
%A DWORD codeoffset;
%A DWORD peaddress;
%A DWORD flagaddress;
%A DWORD flags;
%A
%A DWORD virtsize;
%A DWORD physaddress;
%A DWORD physsize;
%A DWORD MessageBoxAadaddress;
%A
%A int main(int argc,char * * argv)
%A {
%A HANDLE hFile, hMapping;
%A void *basepointer;
%A FILETIME * Createtime;
%A FILETIME * Accesstime;
%A FILETIME * Writetime;
%A Createtime = new FILETIME;
%A Accesstime = new FILETIME;
%A Writetime = new FILETIME;
%A
%A 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)//打开要修改的文件
%A {
%A puts("(could not open)");
%A return EXIT_FAILURE;
%A }
%A if(!GetFileTime(hFile,Createtime,Accesstime,Writetime))
%A {
%A printf("\nerror getfiletime: %d\n",GetLastError());
%A }
%A //得到要修改文件的创建、修改等时间
%A if (!(hMapping = CreateFileMapping(hFile, 0, PAGE_READONLY | SEC_COMMIT, 0, 0, 0)))
%A {
%A puts("(mapping failed)");
%A CloseHandle(hFile);
%A return EXIT_FAILURE;
%A }
%A if (!(basepointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0)))
%A {
%A puts("(view failed)");
%A CloseHandle(hMapping);
%A CloseHandle(hFile);
%A return EXIT_FAILURE;
%A }
%A //把文件头映象存入baseointer
%A CloseHandle(hMapping);
%A CloseHandle(hFile);
%A map_exe(basepointer);//得到相关地址
%A UnmapViewOfFile(basepointer);
%A printaddress();
%A printf("\n\n");
%A if(space<50)
%A {
%A printf("\n空隙太小,数据不能写入.\n");
%A }
%A else
%A {
%A writefile();//写文件
%A }
%A
%A 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)
%A {
%A puts("(could not open)");
%A return EXIT_FAILURE;
%A }
%A
%A if(!SetFileTime(hFile,Createtime,Accesstime,Writetime))
%A {
%A printf("error settime : %d\n",GetLastError());
%A }
%A //恢复修改后文件的建立时间等
%A delete Createtime;
%A delete Accesstime;
%A delete Writetime;
%A CloseHandle(hFile);
%A return 0;
%A }
%A
%A void map_exe(const void *base)
%A {
%A IMAGE_DOS_HEADER * dos_head;
%A dos_head =(IMAGE_DOS_HEADER *)base;
%A #include <pshpack1.h>
%A typedef struct PE_HEADER_MAP
%A {
%A DWORD signature;
%A IMAGE_FILE_HEADER _head;
%A IMAGE_OPTIONAL_HEADER opt_head;
%A IMAGE_SECTION_HEADER section_header[];
%A } peHeader;
%A #include <poppack.h>
%A
%A if (dos_head->e_magic != IMAGE_DOS_SIGNATURE)
%A {
%A puts("unknown type of file");
%A return;
%A }
%A
%A peHeader * header;
%A header = (peHeader *)((char *)dos_head + dos_head->e_lfanew);//得到PE文件头
%A if (IsBadReadPtr(header, sizeof(*header))
%A {
%A puts("(no PE header, probably DOS executable)");
%A return;
%A }
%A
%A DWORD mods;
%A char tmpstr[4]={0};
%A DWORD  tmpaddress;
%A DWORD  tmpaddress1;
%A
%A if(strstr((const char *)header->section_header[0].Name,".text")!=NULL)
%A {
%A virtsize=header->section_header[0].Misc.VirtualSize;
%A //此段的真实长度
%A physaddress=header->section_header[0].PointerToRawData;
%A //此段的物理偏移
%A physsize=header->section_header[0].SizeOfRawData;
%A //此段的物理长度
%A peaddress=dos_head->e_lfanew;
%A //得到PE文件头的开始偏移
%A
%A peHeader peH;
%A tmpaddress=(unsigned long )&peH;
%A //得到结构的偏移
%A tmpaddress1=(unsigned long )&(peH.section_header[0].Characteristics);
%A //得到变量的偏移
%A flagaddress=tmpaddress1-tmpaddress+2;
%A //得到属性的相对偏移
%A flags=0x8000;
%A //一般情况下,“.text”段是不可读写的,如果我们要把数据写入这个段需要改变其属性,实际上这个程序并没有把数据写入“.text”段,所以并不需要更改,但如果你实现复杂的功能,肯定需要数据,肯定需要更改这个值,
%A
%A space=physsize-virtsize;
%A //得到代码段的可用空间,用以判断可不可以写入我们的代码
%A //用此段的物理长度减去此段的真实长度就可以得到
%A progRAV=header->opt_head.ImageBase;
%A //得到程序的装载地址,一般为400000
%A codeoffset=header->opt_head.BaseOfCode-physaddress;
%A //得到代码偏移,用代码段起始RVA减去此段的物理偏移
%A //应为程序的入口计算公式是一个相对的偏移地址,计算公式为:
%A //代码的写入地址+codeoffset
%A
%A entrywrite=header->section_header[0].PointerToRawData+header->section_header[0].Misc.VirtualSize;
%A //代码写入的物理偏移
%A mods=entrywrite%16;
%A //对齐边界
%A if(mods!=0)
%A {
%A entrywrite+=(16-mods);
%A }
%A oldentryaddress=header->opt_head.AddressOfEntryPoint;
%A //保存旧的程序入口地址
%A newentryaddress=entrywrite+codeoffset;
%A //计算新的程序入口地址        
%A return;
%A }
%A
%A void printaddress()
%A {
%A HINSTANCE gLibMsg=NULL;
%A DWORD funaddress;
%A gLibMsg=LoadLibrary("user32.dll");
%A funaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
%A MessageBoxAadaddress=funaddress;
%A gLibAMsg=LoadLibrary("kernel32.dll");
%A //得到MessageBox在内存中的地址,以便我们使用
%A }
%A
%A void writefile()
%A {
%A int ret;
%A long retf;
%A DWORD address;
%A int tmp;
%A unsigned char waddress[4]={0};
%A
%A ret=_open(filename,_O_RDWR | _O_CREAT | _O_BINARY,_S_IREAD | _S_IWRITE);
%A if(!ret)
%A {
%A printf("error open\n");
%A return;
%A }
%A     
%A retf=_lseek(ret,(long)peaddress+40,SEEK_SET);
%A //程序的入口地址在PE文件头开始的40处
%A if(retf==-1)
%A {
%A printf("error seek\n");
%A return;
%A }
%A address=newentryaddress;
%A tmp=address>>24;
%A waddress[3]=tmp;
%A tmp=address<<8;
%A tmp=tmp>>24;
%A waddress[2]=tmp;
%A tmp=address<<16;
%A tmp=tmp>>24;
%A waddress[1]=tmp;
%A tmp=address<<24;
%A tmp=tmp>>24;
%A waddress[0]=tmp;
%A retf=_write(ret,waddress,4);
%A //把新的入口地址写入文件
%A if(retf==-1)
%A {
%A printf("error write: %d\n",GetLastError());
%A return;
%A }
%A     
%A retf=_lseek(ret,(long)entrywrite,SEEK_SET);
%A if(retf==-1)
%A {
%A printf("error seek\n");
%A return;
%A }
%A retf=_write(ret,writeline,18);
%A if(retf==-1)
%A {
%A printf("error write: %d\n",GetLastError());
%A return;
%A }
%A //把writeline写入我们计算出的空间
%A
%A retf=_lseek(ret,(long)entrywrite+9,SEEK_SET);
%A //更改MessageBox函数地址,它的二进制代码在writeline[10]处
%A if(retf==-1)
%A {
%A printf("error seek\n");
%A return;
%A }
%A
%A address=MessageBoxAadaddress-(progRAV+newentryaddress+9+4);
%A //重新计算MessageBox函数的地址,MessageBox函数的原地址减去程序的装载地址加上新的入口地址加9(它的二进制代码相对偏移)加上4(地址长度)
%A tmp=address>>24;
%A waddress[3]=tmp;
%A tmp=address<<8;
%A tmp=tmp>>24;
%A waddress[2]=tmp;
%A tmp=address<<16;
%A tmp=tmp>>24;
%A waddress[1]=tmp;
%A tmp=address<<24;
%A tmp=tmp>>24;
%A waddress[0]=tmp;
%A retf=_write(ret,waddress,4);
%A //写入重新计算的MessageBox地址
%A if(retf==-1)
%A {
%A printf("error write: %d\n",GetLastError());
%A return;
%A }
%A
%A retf=_lseek(ret,(long)entrywrite+14,SEEK_SET);
%A //更改返回地址,用jpm返回原程序入口地址,其它的二进制代码在writeline[15]处
%A if(retf==-1)
%A {
%A printf("error seek\n");
%A return;
%A }
%A
%A address=0-(newentryaddress-oldentryaddress+4+15);
%A //返回地址计算的方法是新的入口地址减去老的入口地址加4(地址长度)加15(二进制代码相对偏移)后取反
%A tmp=address>>24;
%A waddress[3]=tmp;
%A tmp=address<<8;
%A tmp=tmp>>24;
%A waddress[2]=tmp;
%A tmp=address<<16;
%A tmp=tmp>>24;
%A waddress[1]=tmp;
%A tmp=address<<24;
%A tmp=tmp>>24;
%A waddress[0]=tmp;
%A retf=_write(ret,waddress,4);
%A //写入返回地址
%A if(retf==-1)
%A {
%A printf("error write: %d\n",GetLastError());
%A return;
%A }
%A
%A _close(ret);
%A printf("\nall done...\n");
%A return;
%A }
%A
%A //end
%A 由于在PE格式的文件中,所有的地址都使用RVA地址,所以一些函数调用和返回地址都要经过计算才可以得到,以上是我在实践中的心得,如果你有更好的办法,真心的希望你能告诉我。
%A
%A 如果存在错误,请告诉我,以免误导看这篇文章的人。
%A 写的较乱,请原谅。
%A
%A%A
%A

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
最近文章
寂寞如雪
2009-05-19 19:01:18
夜色花
2009-05-19 18:56:22
没有爱可以重来
2009-05-19 18:54:59
推荐文章
最近访客