本章提要
· PE文件格式概述
· PE文件结构
· 如何获取PE文件中的OEP
· 如何获取PE文件中的资源
· 如何修改PE文件使其显示MessageBox的实例
2.1 引言
通常Windows下的EXE文件都采用PE格式。PE是英文Portable Executable的缩写,它是一种针对于微软Windows NT、Windows 95和Win32s系统,由微软公司设计的可执行的二进制文件(DLLs和执行程序)格式,目标文件和库文件通常也是这种格式。这种格式由TIS(Tool Interface Standard)委员会(Microsoft、Intel、Borland、Watcom、IBM等)在1993进行了标准化。显然,它参考了一些UNIXes和VMS的COFF(Common Object File Format)格式。
认识可执行文件的结构非常重要,在DOS下是这样,在Windows系统下更是如此。了解了这种结构后就可以对可执行程序进行加密、加壳和修改等,一些黑客也利用了这些技术。为了使读者对PE文件格式有进一步的认识,本章从一个程序员的角度出发再次介绍PE文件格式。如果已经熟悉这方面的知识,可以跳过这一章。
2.2 PE文件格式概述
认识PE文件,既要懂得它的结构布局,又要知道它是如何装载到计算机内存中的。下面分别对它们进行说明。
2.2.1 PE文件结构布局
找到文件中某一结构信息有两种定位方法。第一种是通过链表方法,对于这种方法,数据在文件的存放位置比较自由。第二种方法是采用紧凑或固定位置存放,这种方法要求数据结构大小固定,它在文件中的存放位置也相对固定。在PE文件结构中同时采用以上两种方法。
因为在PE文件头中的每个数据结构大小是固定的,因此能够编写计算程序来确定某一个PE文件中的某个参数值。在编写程序时,所用到的数据结构定义,包括数据结构中变量类型、变量位置和变量数组大小都必须采用Windows提供的原型。图2.1所示的PE文件结构的总体层次分布如下:
PE文件结构总体层次分布
· DOS MZ Header
所有 PE文件(甚至32位的DLLs)必须以简单的DOS MZ header开始,它是一个IMAGE_DOS_HEADER结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ Header之后的DOS Stub。
· DOS Stub
DOS Stub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This program requires Windows”或者程序员可根据自己的意图实现完整的DOS代码。大多数情况下DOS Stub由汇编器/编译器自动生成。
· PE Header
紧接着DOS Stub的是PE Header。它是一个IMAGE_NT_HEADERS结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS MZ header中找到PE header的起始偏移量。因而跳过DOS Stub直接定位到真正的文件头 PE header。
· Section Table
PE Header之后是数组结构Section Table(节表)。如果PE文件里有5个节,那么此Section Table结构数组内就有5个(IMAGE_SECTION_HEADER)成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。排在节表中的最前面的第一个默认成员是text,即代码节头。通过遍历查找方法可以找到其他节表成员(节表头)。
· Sections
PE文件的真正内容划分成块,称为Sections(节)。每个标准节的名字均以圆点开头,但也可以不以圆点开头,节名的最大长度为8个字节。Sections是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,可以找到这些节。程序的代码,资源等就放在这些节中。
节的划分是基于各组数据的共同属性,而不是逻辑概念。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。节名称仅仅是个区别不同节的符号而已,类似“data”,“code”的命名只为了便于识别,唯有节的属性设置决定了节的特性和功能。
2.2.2 PE文件内存映射
在Windows系统下,当一个PE应用程序运行时,这个PE文件在磁盘中的数据结构布局和内存中的数据结构布局是一致的。系统在载入一个可执行程序时,首先是Windows装载器(又称PE装载器)把磁盘中的文件映射到进程的地址空间,它遍历PE文件并决定文件的哪一部分被映射。其方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被装入内存中,其某项的偏移地址可能与原始的偏移地址有所不同,但所表现的是一种从磁盘文件偏移到内存偏移的转换,如图2.2所示。
PE文件内存映射
当PE文件被加载到内存后,内存中的版本称为模块(Module),映射文件的起始地址称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为文件映像基址(ImageBase)。载入一个PE程序的主要步骤如下:
(1)当PE文件被执行时,PE装载器首先为进程分配一个4GB的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中0x的位置。装载一个应用程序的时间比一般人所设想的要少,因为装载一个PE文件并不是把这个文件一次性地从磁盘读到内存中,而是简单地做一个内存映射,映射一个大文件和映射一个小文件所花费的时间相差无几。当然,真正执行文件中的代码时,操作系统还是要把存在于磁盘上的虚拟内存中的代码交换到物理内存(RAM)中。但是,这种交换也不是把整个文件所占用的虚拟地址空间一次性地全部从磁盘交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页,也可能被交换到磁盘中。
(2)PE装载器在内核中创建进程对象和主线程对象以及其他内容。
(3)PE装载器搜索PE文件中的Import Table(引入表),装载应用程序所使用的动态链接库。对动态链接库的装载与对应用程序的装载方法完全类似。
(4)PE装载器执行PE文件首部所指定地址处的代码,开始执行应用程序主线程。
2.2.3 Big-endian和Little-endian
PE Header中IMAGE_FILE_HEADER的成员Machine 中的值,根据winnt.h中的定义,对于Intel CPU应该为0x014c。但是用十六进制编辑器打开PE文件时,看到这个WORD显示的却是4c 01。其实4c 01就是0x014c,只不过由于Intel CPU是Little-endian,所以显示出来是这样的。对于Big-endian和Little-endian,请看下面的例子。一个整型int变量,长度为4个字节。当这个整形变量的值为0x时,对于Big-endian来说,显示的是{12,34,45,78},而对于Little-endian来说,显示的却是{78,45,34,12}。注意Intel使用的是Little-endian。
2.2.4 3种不同的地址
PE文件的各种结构中,涉及到很多地址、偏移。有些是指在文件中的偏移,有些 是指在内存中的偏移。以下的第一种是指在文件中的地址,第二、三种是指在内存中的地址。
第一种,文件中的地址。比如用十六进制编辑器打开PE文件,看到的地址(偏移)就是文件中的地址,使用某个结构的文件地址,就可以在文件中找到该结构。
第二种,当文件被整个映射到内存时,例如某些PE分析软件,把整个PE文件映射到内存中,这时是内存中的虚拟地址(VA)。如果知道在这个文件中某一个结构的内存地址的话,那么它等于这个PE文件被映射到内存的地址加上该结构在文件中的地址。
第三种,当执行PE时,PE文件会被载入器载入内存,这时经常需要的是RVA。例如知道一个结构的RVA,那么程序载入点加上RVA就可以得到该结构的内存地址。比如,如果PE文件装入虚拟地址(VA)空间的0x处,某一结构的RVA 为0x1000,那么其虚拟地址为0x。
PE文件格式要用到RVA,主要是为了减少PE装载器的负担。因为每个模块都有可能被重载到任何虚拟地址空间,如果让PE装载器修正每个重定位项,这肯定是个梦魇。相反,如果所有重定位项都使用RVA,那么PE装载器就不必操心那些东西了,即它只要将整个模块重定位到新的起始VA。这就像相对路径和绝对路径的概念:RVA类似相对路径,VA就像绝对路径。
注意,RVA和VA是指内存中,不是指文件中。是指相对于载入点的偏移而不是一个内存地址,只有RVA加上载入点的地址,才是一个实际的内存地址。
2.3 PE文件结构
有关一些PE头文件结构一般都有32位和64位之分,如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64等,除了在64位版本中的一些扩展域外,这些结构总是一样的。是采用32位还是64位,需要用#define _WIN64来定义,如果没有这种定义,则采用的是32位的文件结构。编译器将根据此定义选择相应的编译模式。
2.3.1 MS-DOS头部
MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构如下:
其中第一个域e_magic,被称为魔术数字,它用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其他的域对于MS-DOS操作系统来说都有用,但是对于Windows NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。
2.3.2 IMAGE_NT_HEADER头部
PE Header是紧跟在MS-DOS头部和实模式程序残余之后的,描述它内容的结构 如下:
其中请注意这个文件头部的大小已经定义在这个包含文件之中了,这样一来,想要得到这个结构的大小就很方便了。
Machine:表示该程序要执行的环境及平台,现在已知的值如表2.1所示。
NumberOfSections:段的个数。
TimeDateStamp:文件建立的时间。可用这个值来区分同一个文件的不同的版本,即使它们的商业版本号相同。这个值的格式并没有明确的规定,但是很显然地大多数的C编译器都把它定为从1970.1.1 00:00:00以来的秒数(time_t)。这个值有时也被用做绑定输入目录表。注意:一些编译器将忽略这个值。
PointerToSymbolTable及NumberOfSymbols:用在调试信息中,用途不太明确,不过它们的值总为0。
SizeOfOptionalHeader:可选头的长度(sizeof IMAGE_OPTIONAL_HEADER),可以用它来检验PE文件的正确性。
Characteristics:是一个标志的集合,其大部分位用于OBJ或LIB文件中。
文件头下面就是可选择头,这是一个叫做IMAGE_OPTIONALHEADER的结构,由224个字节组成。虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。可选头部包含了很多关于可执行映像的重要信息。例如,初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等。IMAGE OPTIONAL_HEADER结构如下:
其中参数含义如下所述。
Magic:这个值好像总是0x010b。
MajorLinkerVersion及MinorLinkerVersion:链接器的版本号,这个值不太可靠。
SizeOfCode:可执行代码的长度。
SizeOfInitializedData:初始化数据的长度(数据段)。
SizeOfUninitializedData:未初始化数据的长度(bss段)。
AddressOfEntryPoint:代码的入口RVA地址,程序从这儿开始执行,常称为程序的原入口点OEP(Original Entry Point)。
BaseOfCode:可执行代码起始位置。
BaseOfData:初始化数据起始位置。
ImageBase:载入程序首选的RVA地址。这个地址可被Loader改变。
SectionAlignment:段加载后在内存中的对齐方式。
FileAlignment:段在文件中的对齐方式。
MajorOperatingSystemVersion及MinorOperatingSystemVersion:操作系统版本。
MajorImageVersion及MinorImageVersion:程序版本。
MajorSubsystemVersion及MinorSubsystemVersion:子系统版本号,这个域系统支持。例如,程序运行于NT下,子系统版本号如果不是4.0,对话框不能显示3D风格。
Win32VersionValue:这个值总是为0。
SizeOfImage:程序调入后占用内存大小(字节),等于所有段的长度之和。
SizeOfHeaders:所有文件头长度之和,它等于从文件开始到第一个段的原始数据之间的大小。
CheckSum:校验和,仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft不公开,在imagehelp.dll中的CheckSumMappedFile()函数可以计算它。
Subsystem:一个标明可执行文件所期望的子系统的枚举值。
DllCharacteristics:DLL状态。
SizeOfStackReserve:保留堆栈大小。
SizeOfStackCommit:启动后实际申请的堆栈数,可随实际情况变大。
SizeOfHeapReserve:保留堆大小。
SizeOfHeapCommit:实际堆大小。
LoaderFlags:目前没有用。
NumberOfRvaAndSizes:下面的目录表入口个数,这个值也不可靠,可用常数IMAGE_NUMBEROF_DIRECTORY_ENTRIES来代替它,这个值在目前Windows版本中设为16。注意,如果这个值不等于16,那么这个数据结构大小就不能固定下来,也就不能确定其他变量位置。
DataDirectory:是一个IMAGE_DATA_DIRECTORY数组,数组元素个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,结构如下:
;
2.3.3 IMAGE_SECTION_HEADER头部
PE文件格式中,所有的节头部位于可选头部之后。每个节头部为40个字节长,并且没有任何填充信息。节头部被定义为以下的结构:
l
其中IMAGE_SIZEOF_SHORT_NAME等于8。注意,如果不是这个值,那么这个数据结构大小就不能固定下来,也就不能确定其他变量位置。
2.4 如何获取PE文件中的OEP
OEP(Original Entry Point)是每个PE文件被加载时的起始地址,如何获得这个地址很重要,因为修改程序中的这个值是文件加壳和脱壳时的必须步骤,一些黑客程序也是通过修改OEP值来获得对目标程序的控制权从而实施攻击。下面分别介绍如何通过文件直接访问和通过内存映射访问读取OEP值的方法,并给出完整的程序代码。
2.4.1 通过文件读取OEP值
获得OEP值的最简单方法是,直接从一个PE文件中读取OEP。根据以上对PE文件结构的介绍可知,OEP是PE文件的IMAGE_OPTIONAL_HEADER结构的AddressOfEntryPoint成员,在偏移此结构头40个字节处。而IMAGEOPTIONAL HEADER在PE文件的起始位置由IMAGE_DOS_HEADER的elfanew成员来计算。注意,以上两个结构在PE文件中不是紧跟在一起的,它之间是DOS Stub,而在每个PE文件DOS Stub的长度可能不一定相等。在PE文件的头部是IMAGE DOS_HEADER结构,读取这个结构可以得到elfanew的值,因而可以得到IMAGE OPTIONAL_HEADER在PE文件中的位置,也就得到了OEP值。以下是通过文件访问的方法读取OEP的程序代码,即:
l
2.4.2 通过内存映射读取OEP值
获得OEP值的另一种方法是通过内存映射来实现,此方法也需要熟悉PE的文件结构。与直接访问PE的方法不同,内存映射的方法首先把PE文件映射到计算机的内存,再通过内存的基指针获得IMAGE_DOSHEADER的头指针,由此再获得IMAGE OPTIONAL_HEADER指针,这样就可以得到AddressOfEntryPoint的值。下面是通过内存映射获得OEP值的方法:
l
2.4.3 读取OEP值方法的测试
为了检验以上两种获取OEP值方法的正确性和一致性,可以用以下的方法来测试:
运行以上代码后,可以得到如图2.3所示的结果。从图中可以看出,以上两种获取OEP值方法所得到的结果是一致的。
获取OEP值方法的测试结果
2.5 PE文件中的资源
2.5.1 查找资源在文件中的起始位置
要找出一个PE文件中的某种资源,首先需要确定资源节在PE文件中的起始位置。有两种方法来确定资源在文件中的起始位置。
第一种方法,首先根据FileHeader中的成员NumberOfSections的值,确定文件中节的数目,再根据节的数目,遍历节表数组。也就是从0到(节表数–1)的每一个节表项。比较每一个节表项的Name字段,看看是否等于“.rsrc”,如果是,就找到了资源节的节表项。这个节表项的PointerToRawData 中的值,就是资源节在文件中的位置。

第二种方法,取得PE Header中的IMAGE_OPTIONAL_HEADER中的DataDirectory数组中的第三项,也就是资源项。DataDirectory[]数组的每项都是IMAGEDATA DIRECTORY结构,该结构定义如下:
从以上结构对象取得DataDirectory数组中的第三项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。然后根据节的数目,遍历节表数组,也就是从0~(节表数–1)的每一个节表项。每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。遍历整个节表,看看所取得的资源节的RVA是否在那个节表项的RVA范围之内。如果在范围之内,就找到了资源节的节表项。这个节表项中的PointerToRawData 中的值,就是资源节在文件中的位置。如果这个PE文件没有资源 的话,DataDirectory数组中的第三项内容为0。这样也可以得到了资源在文件中开始的位置。
2.5.2 确定PE文件中的资源
得到了资源节在文件中的位置后,就可以确定某个资源类型及其二进制数据在PE文件中的位置和数据块的大小。
资源节最开始是一个IMAGE_RESOURCE_DIRECTORY结构,在winnt.h文件中有这个结构的定义。这个结构长度为16字节,共有6个参数,其结构的原型如下:
l
其中各个参数的含义如下所述
Characteristics: 标识此资源的类型。
TimeDateStamp:资源编译器产生资源的时间。
MajorVersion:资源主版本号。
MinorVersion:资源次版本号。
NumberOfNamedEntries和NumberofIDEntries:分别为用字符串和整形数字来进行标识的IMAGE_RESOURCE_DIRECTORY_ENTRY项数组的成员个数。
紧跟着IMAGE_RESOURCE_DIRECTORY后面的是一个IMAGERESOURCE DIRECTORY_ENTRY数组。这个结构长度为8个字节,共有两个字段,每个字段4个字节。其结构原型如下:
其中,对于第一个字段,当其最高位为1(0x)时,这个DWORD剩下的31位表明相对于资源开始位置的偏移,偏移的内容是一个IMAGE_RESOURCEDIR STRING_U,用其中的字符串来标明这个资源类型;当第一个字段的最高位为0时,表示这个DWORD的低WORD中的值作为Id标明这个资源类型。
对于第二个字段,当第二个字段的最高位为1时,表示还有下一层的结构。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容将是一个下一层的IMAGE_RESOURCE_DIRECTORY结构;当第二个字段的最高位为0时,表示已经没有下一层的结构了。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容会是一个IMAGE_RESOURCE_DATA _ENTRY结构,此结构会说明资源的位置。对于资源标示号Id,当Id等于1时,表示资源为光标,等于2时表示资源为位图等,等于3时表示资源为图标等。在winuser.h文件中有定义。
标识一个IMAGE_RESOURCE_DIRECTORY_ENTRY一般都是使用Id,就是一个整数。但是也有少数使用IMAGE_RESOURCE_DIR_STRING_U来标识一个资源类型。这个结构定义如下:
这个结构中将有一个Unicode的字符串,是字对齐的。这个结构的长度可变,由第一个字段Length指明后面的Unicode字符串的长度。
经过3层IMAGE_RESOURCE_DIRECTORY_ENTRY(一般是3层,也有可能更少些)最终可以找到一个IMAGE_RESOURCE_DATA_ENTRY结构,这个结构中存有相应资源的位置和大小。这个结构长16个字节,有4个参数,其原型如下:
其中各个参数的含义如下所述。
OffsetToData:这是一个内存中的RVA,可以用来转化成文件中的位置。用这个值减去资源节的开始RVA,就可以得到相对于资源节开始的偏移。再加上资源节在文件中的开始位置,即节表中资源节中PointerToRawData的值,就是资源在文件中的位置。注意,资源节的开始RVA可以由Optional Header中的DataDirectory数组中的第三项中的VirtualAddress的值得到,或者节表中资源节那项中的VirtualAddress的值得到。
Size:资源的大小,以字节为单位。
CodePage:代码页。
Reserved:保留项。
总之,资源一般使用树来保存,通常包含3层,最高层是类型,其次是名字,最后是语言。在资源节开始的位置,首先是一个IMAGE_RESOURCE_DIRECTORY结构,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组,这个数组的每个元素代表的资源类型不同;通过每个元素,可以找到第二层另一个IMAGERESOURCE DIRECTORY,后面紧跟着IMAGE_RESOURCE_DIRECTORYENTRY数组。这一层的数组的每个元素代表的资源名字不同;然后可以找到第三层的每个IMAGE RESOURCE_DIRECTORY,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组。这一层的数组的每个元素代表的资源语言不同;最后通过每个IMAGERESOURCE DIRECTORY_ENTRY可以找到每个IMAGE_RESOURCE_DATA_ENTRY。通过每个IMAGE_RESOURCE_DATA_ENTRY,就可以找到每个真正的资源。
2.6 一个修改PE可执行文件的完整实例
在下面的实例中,将把一段MessageBoxA()的计算机代码根据PE文件的格式注入到一个PE程序中。有关把代码注入到一个应用程序的技术将在后面的章节专门介绍。
2.6.1 如何获得MessageBoxA代码
要实现代码注入PE程序且能够运行,首先要做的是如何得到这段代码。为了得到这种代码,作者编写了一段汇编源程序 msgbx.asm,然后用RadASM编译器进行编译,当然也可以使用其他的方法来实现代码的注入。编写这段代码最关键的问题是如何把对话框标题字符串和显示字符串一起存放在代码段,以便提取,否则无法提取。下面是生成MessageBoxA()的源代码:
其中()中的数值表示这一行上代码的字节个数。0x6a是汇编语言中的push命令,0xe8是汇编语言中的call命令,而jmp命令为0xe9。“校验和”是从第一个push命令开始计算所得到的字节总数和(包括校验计数位),从以上代码第一个字节开始计数起到“校验和”位正好是第26位字节个数。字符串字节个数位为一个DWORD型,占4个字节,它是按Little-endian的方式存放的,要把这4个字节位的顺序颠倒才能得到实际数值,即把高位字节变成低位,把低位变换到高位。
要把以上代码注入到一个PE文件中,需要修改4个地方:(1)修改PE文件的入口地址,使PE装载器首先装载以上代码;(2)修改以上代码MessageBoxA()的地址,使以上的代码能够显示出一个对话框;(3)把“校验和”位变成跳转位,即变成jmp (0xe9);(4)修改返回地址,把程序引入到原来的装载点上。
2.6.2 把MessageBoxA()代码写入PE文件的完整实例
根据以上的对MessageBoxA()的分析,可以直接把以上代码注入到一个PE可执行 文件中。为了使程序有通用性,这里编写了一个产生显示任意长度字符的对话框的函数WriteMessageBox()。
下面是用于注入MessageBoxA()代码的头文件,取名为Pe.h,其中用 #include包含了相关的文件头,定义了peHeader结构,且定义了CPe类,其源代码如下:
其中peHeader结构是前面所讲的PE Header结构与节表(Section Table)头结构(6个表头成员)的总结构。因为它们在PE文件中是紧凑排列的,所以可以这样写。其实只用一个节表头就可以。
下面分别介绍CPe类成员函数的定义,它们包含在Pe.cpp文件中。在这个文件开始用#include包含了stdafx.h和Pe.h文件。用MFC VC++编译器编译时,必须包括stdafx.h文件,即使这个文件是空的,也需要包括它,这是编译器设置所致,除非修改MFC的编译器的默认设置。CPe类的构造和析构函数这里没有用上,对系统内存的访问和其他操作主要是通过主成员函数ModifyPe()来进行。它们的源代码如下:
其中对一个PE文件进行MessageBoxA()代码的注入是通过ModifyPe()函数进行,它的入口参数是要被修改的PE可执行文件名。在这个函数中,首先创建所修改文件的句柄,然后创建映射文件,再通过映射文件的句柄获得这个PE文件的文件头指针,最后把这个指针传给函数CalcAddress()。通过CalcAddress()函数来计算PE Header的开始偏移、保存旧的程序入口地址、计算新的程序入口地址和计算PE文件的空隙空间等。
CalcAddress()函数的源代码如下:
下面的StrOfDWord()函数是把一个DWORD值转换成一个字符串,因为一个DWORD值占有4个字节,因此把一个DWORD值变成一个字符串,若保持数值不变,就变成了一个4个字节的字符串。同时把这个值的位置顺序颠倒,这是为了把一个实际的值变成按Little-endian的方式写入PE文件中,其转换方法如下:
l
下面的WriteMessageBox()函数是把MessageBoxA()的机器代码写入到PE文件中。这个函数显示的对话框标题和显示的字符串内容和长度不是固定的。在这个函数中,首先就计算MessageBoxA()函数的地址和函数的返回地址,然后把重新生成的对话框代码写入到程序中。WriteMessageBox()函数的源代码如下:
下面的WriteFile()函数是总的写入函数。在这个函数中,先打开被修改的PE文件,然后调用WriteNewEntry()和WriteMessageBox()函数。WriteFile()函数的源代码如下:
这个修改后的PE文件运行时,就会先显示对话框,单击“确定”按钮后又继续执行。总之,在了解了PE文件格式后,就可以对某一个PE文件进行修改。本实例只是对PE文件处理的一种应用,在实际中还有更多的其他方面的应用。
2.7 本章小结
本章所介绍的向PE文件注入代码的实例只是用来说明如何修改PE文件,有关如何向一个应用程序中注入代码的技术还要在以后的章节专门介绍。此外,还有其他的技术没有介绍,例如如何提取程序中的代码,在以后的章节中对此也还要专门介绍。总之,了解了PE文件结构,就可以很容易地对某个应用程序进行加壳、挂钩或捆绑。
参考文献
[1] 看雪学院编著. 软件加密技术内幕. 电子工业出版社,2004
[2] Prince. PE文件之旅. www.pediy.com,2005
[3] Ilsy. 关于PE可执行文件的修改. www.xfocus.net,2001

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/184138.html