PE文件格式范文(精选6篇)
PE文件格式 第1篇
关键词:PE文件格式,段,PE扩展
1 引言
PE文件格式是Win32平台上(包括Windows9x/NT/2000/XP/2003/Vista/CE)主流的可执行文件格式,是Portable Executable File Format(可移植的执行体)简写。它衍生于早期建立在VAX/VMS上的COFF(Common Object File Format)文件格式。对PE格式和COFF文件的主要描述存放在winnt.h文件中,它是PE文件定义的最终决定者。
EXE和DLL文件是PE文件格式的两种主要文件,它们的区别完全是语义上的,二者使用完全相同的PE格式。用IMAGE_FILE_HEADER中的Characteristics字段的第13位来标识出这个文件到底是EXE还是DLL。
64位的Windows只对PE格式作了一些简单的修饰,新格式叫做PE32+,并未加入新的结构,只简单的将以前的32位字段扩展为64位。图1是PE文件的基本结构图。
2 PE的基本概念
文件偏移地址是指当PE文件存贮在磁盘上时,某个数据的位置相对于头文件的偏移量,称为文件偏移地址(File Offset)或物理地址(RAW Offset)。文件偏移地址从PE文件的第一个字节开始计数,起始值为0。
虚拟地址是指当PE文件装入内存时,各数据在内存中的位置。当PE文件通过Windows加载器被装入内存后,内存中的版本被称作模块(Module)。映射文件的起始地址被称为模块句柄(h Module),可以通过模块句柄访问内存中其他的数据结构。这个初始内存地址就称为基地址(Image Base)。基地址的值是由PE文件本身设定的。按照默认设置,Visual C++建立的EXE文件基地址是00400000h,DLL文件基地址是10000000h。
相对虚拟地址(RVA)只是内存中的一个简单的相对于PE文件装入地址的偏移位置,它是一个“相对”地址,或称“偏移量”。因为尽管PE文件有一个首选的装入地址,但是并非一定会装入指定的位置,在这种情况下需要根据实际的装入位置重新定位,这就需要各部分相对于基地址的偏移量。图2显示了PE文件在装入前后的相对位置变化。
虚拟地址(VA)=基地址(Image Base)+相对虚拟地址(RVA)
3 MS-DOS头部
3.1 DOS MZ header
每个PE文件都由一个DOS头开始的,DOS头包括两部分:“DOS MZ header”和“DOS stub”。PE文件的第一个字节起始于一个传统的MS_DOS头部,被称作IMAGE_DOS_HEADER,其结构如下:
此结构中,有两个字段比较重要,分别是e_magic和e_lfanew字段,e_magic被设置为5A4Dh,在winnt.h中定义为IMAGE_DOS_SIGNATURE,它的ASCⅡ值为“MZ”。
3.2 DOS stub
DOS stub实际上是一个有效的EXE,在不支持PE文件格式的操作系统中,它简单显示一个错误提示“This program cannot be run in MS-DOS mode”。程序员也可以根据自己的意图写出完整的DOS代码。
4 PE文件头
PE Header紧跟在DOS stub之后,它是PE相关结构NT映像头(IMAGE_NT_HEADERS)的简称。可以从前面的IMAGE_DOS_HEADER结构中的e_lfanew字段里找到PE Header的起始偏移量,加上基址得到PE文件头的指针。
IMAGE_NT_HEADERS是由三个字段组成的:
下面分别作以介绍。
4.1 Signature字段
在PE Header的起始位置就是Signature字段,它被设置为00004550h,在winnt.h中定义为image_nt_signature,它的ASCⅡ值为“PE00”。这个“PE00”字符串才是PE文件真正的开始。
4.2 IMAGE-FILE-HEADER结构
IMAGE_FILE_HEADER(映像文件头)结构包含了PE文件的一些基本信息。其具体结构如下:
4.3 IMAGE-OPTIONAL-HEADER32结构
IMAGE_OPTIONAL_HEADER32(可选映像头)是一个可选结构,但实际上是必选的,因为IMAGE_FILE_HEADER结构无法完全定义PE文件的属性,甚至更多的数据要靠IMAGE-OPTIONAL-HEADER32来定义。其具体结构如下:
5 区块
5.1 区块表
紧跟着IMAGE_NT_HEADERS后面的是区块表,它是一个IMAGE_SECTION_HEADER结构的数组。每一个IMAGE_NT_HEADER结构包含了它所关联的区块的信息,如位置、长度、属性;该数组的数目由IMAGE_NT_HEADERS.File Header.Number Of Section指出。
5.2 区块的对齐值
区块的大小是要对齐的,有两种对齐值,一种用于磁盘中,另一种用于内存中。PE文件头指出了这两个值。PE文件头里的File Aligment定义了磁盘区块的对齐值。每个区块从对齐值的整倍数的偏移位置开始,不足部分用0补足。PE文件中典型的磁盘对齐值是200h.PE文件头里的Section Aligment定义了内存中区块的对齐值,PE文件被映射到内存中时,区块总是至少从一个页边界处开始。PE文件在内存中典型的对齐值是1000h。如果不考虑节省磁盘空间,可以将磁盘上的对齐值也设为1000h,这样当程序被装入内存时,不必进行文件偏移地址与相对虚拟地址间的转换,因为此时它们是相等的。
5.3 文件偏移与虚拟地址转换
由于一些PE文件为减小体积,磁盘对齐值不是一个内存页1000h,而是200h,这就造成了同一数据相对于头文件的位置在磁盘中和在内存中是不同的,这引出了文件偏移地址与相对虚拟地址的转换问题。如图2所示,在对齐值不同的情况下,文件被映射到内存中,DOS文件头、PE文件头和块表的偏移位置与大小都没有变化。但是各区块的偏移位置发生了变化。
如果要进行某一区块的地址转换,就必须先知道此区块起始位置在磁盘中和在内存中相对于文件头和基地址的差距Δk。然后再结合基址,算出虚拟地址。
File Offset=RVA-Δk
File Offset=VA-Image Base-Δk
必须注意的是,在不同的区块中,Δk是不同的。
6 输入表
输入函数就是指被程序调用,但其代码又不在程序中的函数,这些函数的代码在程序相关的DLL中,当PE文件被装入时,Windows装载器确保所有它所需的DLL都被加载,然后完成PE文件同DLL的链接。
在PE文件IMAGE_OPTIONAL_HEADER32中数据目录IMAGE_DATA_DIRECTORY的第二个成员指向输入表。输入表以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式的链接进来的DLL都有一个IID。数组结束时,再以一个全0的IID结构作为结束。
这里的Original First Thunk指向INT(Import Name Table),First Thunk指向IAT(Import Address Table)。INT和IAT都是IMAGE_THUNK_DATA结构的数组。每一个IMAGE_IMPORT_DATA结构指向一个IMAGE_IMPORT_BY_NAME结构,而IMAGE_IMPORT_BY_NAME中的Hint和Name分别存放着输入函数在DLL中的序号和输入函数的名称。由Original First Thunk和First Thunk分别指向的INT和IAT是两组并行的指针,二者本质上是相同的。Original First Thunk指向的INT是不可改写的,而First Thunk指向的IAT由PE装载器重写,用函数真正的入口地址来代替。之所以有不同是为了满足不同的函数输入方式。如图3所示。
7 输出表
当创建一个DLL时,实际上创建了一组能让EXE或其它DLL调用的函数,DLL文件通过输出表向系统提供输出函数名、序号和入口地址等信息。EXE文件一般不存在输出表,大部分DLL文件有输出表。
在PE文件IMAGE_OPTIONAL_HEADER32中数据目录IMAGE_DATA_DIRECTORY的第一个成员指向输出表。输出表是一个IMAGE_EXPORT_DESCRIPTOR(简称IED)结构。输出表的结构如图4所示。
8 基址重定位
当PE文件中包含直接寻址指令时,就有可能需要基址重定位。因为,如果Windows装载器没有将模块装入到指定的位置,例如00400000h处,那基于默认值设定的地址都要重新计算,这就是重定位。
对于EXE文件来说,不需要考虑基址重定位,因为每个文件都使用独立的虚拟地址空间。但是对于DLL文件就必要了,因为可能会有多个DLL共用一个EXE文件的虚拟地址空间,那么就不一定能装入到默认的位置了。
基址重定位表(Base Relocation Table)位于一个叫.reloc的区块内,可以通过IMAGE_OPTIONAL_HEADER32中数据目录IMAGE_DATA_DIRECTORY的第六个成员指向它。基址重定位数据的组织方式采用类似于按页分割的方法,由许多重定位块串接而成,每块大小为4KB,其中的数据必须以4字节对齐。
9 资源
PE文件中资源以类似于磁盘目录结构的方式存储,通常包括3层。第一层是资源类型,第二层是资源名称,第三层是资源代码页。
9.1 资源目录结构
资源目录中的每一个节点都有相类似的结构:由一个IMAGE_RESOURCE_DIRECTORY结构和紧随其后的的数个IMAGE_RESOURCE_DIRCETORY_ENTRY结构构成。
IMAGE_RESOURCE_DIRECTORY结构中的Number Of Named Entries字段是以字符串命名的资源数目,Number Of Id Entries字段是以整型数字命名的资源数量,两者加起来是本目录的目录项总和,即紧随其后的IMAGE_RESOURCE_DIRCETORY_ENTRY数量。
9.2 资源目录入口结构
资源目录入口结构IMAGE_RESOURCE_DIRCETORY_ENTRY只包含两个DWORD字段:Name和Offset To Data。这两个字段在不同的情况下含义不同。
Name字段,在第一层目录中表示资源类型;在第二层目录中表示资源名称;在第三层目录中表示代码页编号。
Offset To Data字段,存贮一个指针,当最高位为1时,后面的31位表示下一层目录的起始地址;当最高位为0时,指针指向。
当Name和Offset To Data用作指针时需要注意,该指针是从资源区块开始的偏移量,不是RVA。
9.3 资源数据入口
经过三层IMAGE_RESOURCE_DIRECTORY_ENTRY,第三层目录中的Offset To Data指向IMAGE_RESOURCE_DATA_ENTRY结构。该结构描述了资源数据的位置和大小,这才是真正的资源数据,其中的Offset To Data指向了资源数据的指针,其为RVA值。
10. NET头部
.Net文件为Microsoft.Net环境生成的可执行文件。.Net下对PE文件的扩展主要是增加了公共语言运行时头部(CLR Header)和公共语言运行时数据部分(CLR Data),从而使得PE文件能对.Net Framework给以支持。CLR Header由.Net Framework SDK的Cor Hdr.h中的IMAGECOR20一HEADER结构来定义,该头部中包含有元数据的版本信息,模块的方法入口点,元数据的大小及位置偏移等信息。CLR Data则包含,Net元数据(Metadata),中间语言方法体(IL Method Bodies)等。其中元数据中包含有各种表的定义:定义表(Definition Table)、引用表(Reference Table)、清单表(Manifest Table).而中间语言方法体则包含了被、Net Framework中JIT编译器所识别的一种中间语言,在通过JIT编译之后形成与机器相关的可执行代码。
11 结束语
PE文件是Windows操作系统重要的部件,对其格式的分析将有助于对Windows操作系统的理解,同时能协助一些可执行程序的汉化和升级等。本文主要特点是以图形化的方式有选择地讲解相对抽象的PE文件的格式,舍弃了一些相对次要的内容。而有关.Net下PE文件格式的更详细的研究将是本文的后继工作。
参考文献
[1]段钢.加密与解密[M].北京:电子工业出版社,2008.
[2]罗云彬.Windows环境下的32位汇编语言程序涉及[M].北京:电子工业出版社,2002.
“打印”出来的 PDF格式文件 第2篇
一、必备条件——装一台虚拟打印机
首先,你需要有一台特殊的打印机——虚拟打印机。这台打印机是随抓图软件SnagIt附带的一种虚拟打印机,有两种安装方法。
1. 自动安装虚拟打印机
如果选择用安装版安装7.0以上版本的SnagIt抓图软件,通过完全安装的方法,安装完成后会获得这台打印机。
2. 手动安装虚拟打印机
如果你使用的是不用安装的绿色版SnagIt软件,那么你不会在控制面板中找到SnagIt打印机的影子。你需要通过设置的方法进行安装。
二、格式转换——下达一个打印命令
1. 打开任意文档
2. 执行打印命令
3. 选择虚拟打印机
4. 进行输出设置
5. 得到输出结果
6. 输出结果预览
PE文件格式 第3篇
PE (Portable Execute) 文件被称为可移植的执行体, 是微软制定的一种文件标准, 常见的EXE、DLL、OCX、SYS、COM都是PE文件, 在Windows操作系统中举足轻重。在加密与解密、软件汉化、逆向工程、反病毒等安全领域都涉及到PE文件的应用。
但PE文件格式定义复杂, 难以理解, 导致入门较难, 学生很难深入去研究。通过多媒体教学, 课堂上直接利用Winhex打开任意EXE文件或DLL文件, 从原始的十六进制找到我们感兴趣的值, 可以明了地分析出Windows操作系统如何载入EXE文件及DLL文件的工作机制, 将抽象的格式定义以直观、互动的方式展示给学生, 提高学生的学习兴趣。
本文介绍了如何应用Winhex理解PE文件的教学过程, 以研究输入表和输入地址表为例, 并对两者的运行机制进行分析, 使得学生理解Windows操作系统如何载入PE文件, 以及可执行文件如何调用API函数。
1 Winhex软件简介
winhex是一款以十六进制编辑器为核心的数据处理高级工具, 可以直接打开硬盘上的文件和内存, 并以十六进制形式显示数据, 可以实现数据恢复、低级数据处理等强大功能, 可分析RAW格式原始数据镜像文件中的完整目录结构。Winhex可用于分析静态PE文件 (硬盘上) 和动态PE文件 (调入内存后) , 并通过静、动态PE文件的比较, 重点讨论两个非常重要的数据结构:输入表和输入地址表, 从而掌握Windows操作系统中PE文件的工作机制。
2 PE文件格式分析
通过理解PE文件格式中的字段, 可以逐步理解PE文件的结构及其原理。PE文件格式如图1所示。所有的PE文件都是从MS-DOS头开始, 该头部后面的是PE头, 其位置可从MS-DOS头中的一个e_lfanew字段的值得到, PE头是由PE Signature、IMAGE_FILE_HEADER、IMAGE_OPTIONAL_HEADER构成, 在IMAGE_NT_HEADERS头部后面是节表 (Section Table) , 节表是一个数组, 数组中的每一个元素用来描述后继每一节的信息, 如节名、节大小、节偏移、节属性等。在节表后面是具体的节, 如代码节 (.text) 、数据节 (.data) 等, PE文件中非常重要的输入表、输出表等就存在上述节中。
2.1 判断合法PE文件
利用Winhex打开一个可执行文件thunder.exe (迅雷安装程序) , 通过对字段值的分析, 即DOS头的“MZ”和PE头的“PE”这两个标志可以初步判断当前程序是否是目标是PE文件。也就是通过IMAGE_DOS_HEADER结构来识别一个合法的DOS头, 可以看到文件起始为0X4D5AH (“MZ”) , 接着通过该结构的e_lfanew (偏移3CH, 32bits) 的值是0x00000118H (注意字节序) 即为PE头开始的偏移, 定位到该偏移, 其值为PE文件开始的标志0X00004550H (“PE”) , 即可初步确定该文件是一个合法的PE文件。
2.2 输入表 (Import Table) 和输入地址表 (IAT, Import Address Table)
数据目录表 (DataDirectory) 中有两项:IMAGE_DIRECTORY_ENTRY_IMPORT (输入表, 索引为1) 和IMAGE_DIRECTORY_ENTRY_IAT (输入地址表, 索引为12) 。
PE文件用到来自其他EXE或DLL的函数称为输入, 输入表中保存的是函数名和其驻留的DLL名等动态链接所需要的信息。当Windows PE装载器载入该PE文件时, 检查输入表并将相关的DLL/EXE映射到其地址空间, 定位要用到的由这些EXE或DLL提供的函数, 从而可以使用这些函数的地址 (这些地址保存在输入地址表) , 即可调用这些的函数。
PE文件有两种状态, 静态 (尚未运行) 和动态 (调入内存运行) 。在这两种状态下, 输入地址表的内容是不相同的, 代表的含义也不同, 只有充分理解含义的变化才能理解输入的概念, 初步掌握PE文件的精髓。
输入表是一个IMAGE_IMPORT_DESCRIPTOR (IID) 数据结构的数组, 数组长度不定, 但它最后是以一个全为NULL (0) 的IID作为结束的标志。每个结构对应着一个用到的DLL及其提供的函数的信息, 也就是说, 如果用到5个DLL, 那么就有5个对应的数据结构。这个结构总共20个字节 (5个DWORD) , 在本文中我们讨论最重要的三个:Original First Thunk、Name、First Thunk。
打开静态的thunder.exe, PE文件头相对偏移0x80H指明了输入表的RVA (相对虚拟地址) 为0X000B8E4CH, 即输入表位于.rdata节, 由于打开的是静态PE文件, 所以减去偏移C00H, 得到物理地址0X000B824CH, 该偏移为输入表结构数组开始的地方。
上图阴影部分即为一个IMAGE_IMPORT_DESCRIPTOR数据结构, 也就是涉及到的DLL的相关信息, 可以找到我们将讨论的值, 首先讨论Name, 其作用是指向用到的DLL的名字, 在图2中, 可以看到Name值为0X000B9682H, 同前所述, 物理地址为0X000B8A82H, 图3的阴影部分我们可以看到描述的DLL文件为WININET.dll。
那么该DLL提供的API函数和函数的入口地址保存在哪儿呢?在图2中, 我们可以查到Original First Thunk值为0X000B9590H (物理地址为0X000B8990H) , First Thunk值为0X0009F5DCH (物理地址为0X0009E9DCH) 。可以看到这2个偏移地址存储的值都为0X00B9672H (物理地址为0X000B8A72H) , 也就是说静态情况下, Original First Thunk和First Thun都指向相同的地方, 可以看到指向WININET.dll提供的API函数当中的一个 (下图阴影部分) , 该函数名字是Internet Openw。
的确如此, Original First Thunk指向INT表, 用以指向WININET.dll提供的API函数名字。在静态时, First Thunk同样指向INT表, 上述操作过程验证了这点, 但当动态情况下, 也就是PE装载器将PE文件载入到内存时, 将会改变First Thunk指向的数组的内容, 将所存储的API函数名字的地址变换为相应API函数的入口地址, 即IAT表, 为PE文件的执行做好了准备。
为了说明这点, 可以通过Winhex打开RAM中运行的thunder.exe (动态) 来研究First Thunk发生的变化。此时, 我们看到First Thunk值为0X0009F5DCH没有变化, 但该偏移处的值已变为0X75CA629EH (图5中阴影部分) 。
需说明的是, 由于载入基址为0X01160000H, 所以上图中偏移为0X011FF5DCH (即0X0009F5DCH与0X01160000H相加) , 进一步验证该值是否为API函数Internet Openw的地址, 我们打开WININET.dll, 发现其基址为0X75C80000H, 而其提供的API函数Internet Openw的相对虚拟地址为0X0002629EH, 将两者相加恰好就是刚才我们看到的0X75CA629EH, 也就是说, First Thunk指向的内容已经发生变化, 所存储的API函数名字的地址变换为相应API函数的入口地址。
3 小结
在利用Winhex对PE文件的输入表和输入地址表的分析过程中, 我们应注意以下内容, 由于操作系统按照一定的规则将PE文件装载到内存中, 装入后的整个文件头内容不会发生变化, 但PE文件的某一部分如节的内容会按照字段中的对齐方式在内存中对齐, 分析静态PE文件我们用到物理地址, 因而我们要进行RVA到物理地址的转换。
其次, 通过分析, 我们得知, PE运行前, PE装载器首先搜索Original First Thunk, 并通过DLL文件的输出表找到相应的API函数的地址替代First Thunk指向的数组, 也就是输入地址表。那么在程序加载完成之后, API函数的调用与运行就仅仅跟输入地址表有关系, 不会再涉及到输入表的内容了。
参考文献
[1]戚利.Windows PE权威指南[M].北京:机械工业出版社, 2011.
[2]段刚.加密与解密[M].3版.北京:电子工业出版社, 2008.
PE文件中脱壳技术的研究 第4篇
PE文件格式是WIN32环境自带的跨平台可执行文件格式,常见的EXE、DLL、OCX、SYS、COM等文件均是PE格式。使用该格式,在非Intel芯片的CPU上,Windows一样能识别和使用。
对PE文件加壳,能较好地保护原程序。但病毒和木马也会利用加壳技术来保护自己,因为加壳后程序执行结果不变,但代码发生了变化,从而使杀毒软件无法查杀。作为一名病毒分析师或者软件安全研究员,如果不懂得脱壳技术,将很难对这些恶意程序进行分析。据瑞星公司截获的病毒样本统计,90%以上的病毒文件都经过加壳处理,可见掌握脱壳技术十分重要。现有文章大都进行加壳技术的探讨[1,2,3],本文则着重研究脱壳技术。
1 壳的介绍
壳是一段附加在原程序上的代码,它先于真正的程序运行并拿到控制权,在完成程序保护任务后(检测程序是否被修改,是否被跟踪等),再将控制权转交给真正的程序,其运行过程与病毒有些相似。在形式上又与WINRAR类的压缩软件类似,运行前都需要将原程序解压。但壳对程序的解压是在内存中进行,对用户来说完全透明,用户感觉不到壳的存在。
壳分为压缩壳和加密壳。压缩壳只是为了减少程序体积而对资源进行压缩,便于传输,具有一定的保护作用。常见的压缩壳有UPX、ASPCAK、TELOCK 、PELITE 、NSPACK 等。加密壳是使用各种手段对程序资源进行保护,防止其被反汇编或跟踪,文件加壳后是否变小不是其主要目标。常见的加密壳有ARMADILLO 、ASPROTECT 、ACPROTECT、 EPE 、SVKP 等。目前一些壳已兼具有两种功能,即能压缩资源,又能加密资源。
2 PE结构框架
PE文件使用一个平面地址空间,所有代码和数据都被合并成一个很大的结构。文件内容由属性相同的区块组成,各区块按页边界对齐,大小没有限制。每个区块都有不同的名字,用来表示区块的功能。图1是Windows98下记事本Notepad.exe程序的PE结构图。从图中可以看出PE文件由几个连续的区块组成。先后由DOS头部、PE头部、区段表以及各个区段组成。其中.text是代码段,.data是已初始化的数据段,.idata是输入表段,.rsrc是资源段,.reloc是基址重定位表段。
虚拟偏移也称相对虚拟地址,是PE程序载入到内存后在386保护模式下相对于基地址的偏移量。物理偏移也称文件偏移,是PE文件存储在磁盘上相对于文件头的偏移量。原始大小是文件在磁盘上的结构大小。从图1中可以清晰地看到DOS头、PE头、区段表和各个区段的排列顺序与大小。使用Winhex打开未加壳记事本程序,其DOS头部和PE头部的十六进制代码如图2所示。
由图2可以看出DOS头以“MZ”(0x4D5A)标志开始,在偏移0x14和0x16处指明DOS代码的入口IP和CS,在偏移0x3C处指明PE头的起始位置(0x00000080)。紧接着是PE头(在0x80处),它由“PE\0\0”(0x50450000)标识开始,其后是20个字节的文件头和224个字节的可选头,整个PE头有248个字节。在偏移0x86处指明区段个数0x0005,如图1所示正好5个区段。在偏移0xA8处指明OEP(Original Entry Point原始入口点,程序执行的第一条指令位置)地址为0x000010CC,在0xAC处指明代码段基址(.text段)为0x00001000,在0xB4处指明程序装载基址0x00400000。另外PE头还包含文件对齐大小、内存对齐大小等等其他信息。其后是节表(section table),它是一个结构型数组,其成员数表示该PE文件的节数,每个成员中包含对应节的属性。每个节成员有40个字节,该记事本有5个成员,共200个字节(十六进制为C8)。最后就是各个区段结构,区段的大小没有限制。
3 加壳原理
加壳是应用某种算法对原程序进行压缩(压缩壳)或加密(加密壳),并使程序运行时先运行对应的解压或解密代码。加壳过程一般为,首先在原程序中新建一个区段用来存放加密加压和解密解压代码,其次将原程序数据压缩或加密,最后将程序的入口点修改为壳代码入口。程序的入口点即程序开始执行的第一条指令的地址。如图3是Notepad.exe加Aspack 2.12壳后的PE结构图。从图中可以看出加壳后,程序会多.aspack和.adata两个区段,区段表的大小也增加到280个字节(十六进制:118)。其中.aspack是Aspack 2.12壳代码区段,.adata是附加数据区段。Aspack属于压缩壳,通过对比发现,壳对原程序中的数据做了压缩处理,加壳后的区段原始大小均比未加壳时小。
加壳会修改原程序的执行参数,如表1所示,是加壳前后Notepad.exe的执行参数对比表。从表中可以看出加壳后其入口点被修改成壳代码的区段,其首字节和EP区段均发生了变化。
4 脱壳原理
脱壳就是将加壳后的程序解压或者解密,使程序从原始入口点开始运行。脱壳分为硬脱壳和软脱壳。硬脱壳也称为静态脱壳,是根据加壳程序的算法,写出逆向的算法,就像压缩与解压缩一样。如UPX的壳就有相应的脱壳程序。软脱壳也叫动态脱壳,该方法将程序加载到内存运行,使其自行脱壳后,抓取内存镜像,再重新构造标准的执行文件。该方法能较好对付加密壳和变形壳。脱壳一般会按查壳、寻找OEP、dump程序、修复的顺序进行。
4.1 查 壳
现有工具如PEID、FI、PE-SCAN等都能较好查出壳的类型。查壳的原理是将程序OEP附近的代码和各种壳代码进行比较,从而判断程序是否加壳以及加哪种壳。类似于杀毒软件查杀病毒,都是通过特征码进行扫描对比。只是查壳的特征码对比一般都是在程序OEP附近,而病毒的特征码则可以定位在程序的任何部分。
4.2 寻找OEP
壳在完成自己的任务后,会将控制权转交给真正的程序。由于壳代码和原始代码在不同的区段,两者之间距离较远,那么由壳代码区段运行到原始代码区段必定会存在一个大的跳转,因此可以根据大跳转来确定OEP。另外,每种语言编写的程序其OEP处的代码都有自己的特征,类似查壳的原理,就可以根据代码特征来判定OEP。如表2所示,是不同的语言编写的程序在OEP处的反汇编代码。
4.3 dump内存
所谓dump就是转存,将内存中的进程数据抓取出来转存为文件格式。一般调试跟踪到程序OEP时,就可以dump程序。首先获取内存映像大小。其值在PE文件头0x50偏移处存放,如图1所示在0xD0偏移处指明内存映像大小为0x0000CA9C,使用ReadProcessMemory系统函数读取SizeOfImage即可获取。另外也可以通过MUDULEENTRY32结构中的modBaseSize变量获取。然后使用CreateFile和WriteFile将内存数据写入磁盘。最后再将相对虚拟地址和文件地址对齐。
4.4 修 复
加密壳都会对输入表中的IAT进行加密处理,IAT(Import Address Table)是输入地址表,它保存着PE程序要调用的外部函数地址信息。如果IAT表出错,程序将无法运行。修复原理是根据原加壳程序的IAT表重新构造一份IAT表,当加壳程序运行到OEP处时,其IAT已经释放出来,此时就可以重新构造一个区段存放输入表,从而解密IAT。
5 脱壳方法
压缩壳只是对资源进行压缩,其脱壳比较简单。加密壳则有着强大的保护功能,其反跟踪反调试还有加密资源的功能都很强,需要采用多种不同的方法配合使用。区分加密壳和压缩壳,可将程序载入到OllyDbg等调试器,如果载入时出现“入口点超出代码范围,可能这是一个自解压或自修改文件”的类似提示信息,说明是压缩壳;如果调试器未出现任何提示,则说明是加密壳。
5.1 压缩壳
单步跟踪法
单步跟踪法就是一步步跟踪指令的执行,直到程序真正的OEP。它的原理是模拟程序在内存中运行的顺序逐条语句执行,因此在理论上是可行的。只是当壳的入口点和原程序的入口点距离较远时,单步到OEP将耗费大量时间。在壳的代码中会有很多循环,所以单步跟踪保持一个原则,只让程序往下运行,而不能让程序往上跳转。当遇到一个很大的跳转(大跨段),比如 jmp、JE 或者RETN命令时,一般就到达到程序的OEP。如下代码所示,是加壳后的记事本程序在调试器中单步跟踪到0040E3B0处的反汇编代码,在0040E3BF处返回到004010CC,两个地址之间相距很大,是一个大的跳转。在004010CC处,通过与表2对比可知,此处是程序的OEP,采用VC++ 编写。
ESP定律法
利用堆栈平衡原理。所谓堆栈平衡,是指程序在使用CALL调用子程序或函数过程中,当运行到RET指令返回时,要保证ESP指向的是刚开始压入栈中的地址,维持堆栈的平衡。查看加壳记事本程序,首句使用“pushad”命令将所有寄存器压入堆栈保存,在壳即将运行完毕时,使用“popad”将所有寄存器出栈恢复现场,最后通过“ret”返回到原程序。可以把壳假设成一个子程序,在壳把代码解压前后,必须遵循堆栈平衡。表3是加壳后的记事本程序在使用OllyDbg载入时和到达OEP时各个寄存器的值。
通过对比发现,在壳和程序原始OEP处,除了两个EIP寄存器的值不一样,其他的寄存器值都相同。此时EIP的值就是OEP,这充分说明了寄存器是一个平衡的状态。
内存镜像法
内存镜像法的原理是依据壳代码对原程序的解压过程。壳程序运行时需要先对原来压缩的各个区段进行解压,解压完毕后将会跳到解压后的代码段(.text段)开始执行,若在此处下断点,程序则会中断在OEP处。从图3中看到,加壳后的记事本有.text段、.data段、.idata段等等,壳解压一般会按从低到高的地址顺序进行。如果在.data段下一次断点,让程序运行,此时.text段已解压完毕,.data段正准备解压,但被中断了。若再在.text段下一次断点,让程序继续运行,那么程序将接着把.data段、.idata段等都解压完成,之后把控制权移交给原程序,程序跳到.text段,正准备从.text段开始执行,但被中断了,此时被中断的地方就是程序OEP。
内存镜像法又称为两次内存断点法,但两次内存断点的本质却不一样。第一次是资源解压,程序对资源写入时中断,是内存写入中断。第二次是访问代码段,准备执行代码时中断,是内存执行中断。该方法也有其局限性,当壳在JMP到OEP前的一行代码时仍在对代码段解压,则该方法将不再适用。
快速查找法
也叫跟踪出口法或一步到达oep法,该方法是ESP定律法的变形。使用方法是直接在调试器中查找“POPAD”语句,即可达到OEP附近。根据堆栈平衡原理,在壳OEP处使用PUSHAD压栈原程序的初始状态,当壳将要运行完毕时,会使用POPAD将所有数据出栈,所以可以直接查找POPAD关键句来到OEP附近。
5.2 加密壳
加密壳与压缩壳最大的不同在于加密壳有较强的反跟踪能力,同时会设置很多SEH异常。加密壳通过对堆栈进行检查,调用IsDebugerPresent函数,检测调试器句柄等方法判断自己是否被跟踪调试。因此脱加密要在脱压缩壳的基础上,隐藏调试器进程,同时处理SEH异常机制。加密壳同样也会用到单步跟踪法,少数加密壳也可以使用ESP定律法和内存镜像法。
最后一次异常法
加密壳的反跟踪代码中,会设置很多SHE陷阱,从而使OD等调试器产生异常。但是当壳代码运行完毕后,将没有异常。所以可以通过忽略异常让程序运行,记下程序运行到正常时产生异常的次数。那么OEP便在最后一次产生异常的后面附近,通过在最后一个异常处指向的SE句柄处下断点,然后运行到该位置,继续单步跟踪就会到OEP。
6 伪装壳和多重壳
6.1 伪装壳
加伪装壳就是修改壳OEP处的特征码,干扰其他程序对OEP处的特征码识别。让查壳工具查不出来,或者识别为其他壳,或者没有加壳。加伪装壳类似于加花指令,在不破坏堆栈平衡和不改变程序执行流程的情况下,在程序OEP处添加一些无用的代码。
对于伪装壳的识别可以从以下几个方面进行。第一,查看区段信息,根据加壳的原理,壳代码一般都会存放在单独的区段,如果程序中有未知名的区段,其区段名一般都和壳的名字有关,由此可以推断程序是否加壳。第二,查看程序的OEP是否在.text代码区段,由于程序是从代码段开始执行,如果OEP在非.text区段,那么OEP所处的区段很可能就是壳代码段区段。第三,伪装壳的头部伪装代码运行完毕后,还会来到壳的OEP和程序的OEP处,这就需要根据经验来判断OEP。因此牢记各种编程语言程序的OEP处反汇编代码,将有助于判断程序OEP。
6.2 多重壳
所谓加多重壳,就是在加了一层壳的基础上再加一层壳或几层壳。这样会更好地保护病毒和木马不被查杀。对于多重壳的识别同样可以通过区段信息和OEP查看,多加一重壳都会多一个区段,因此如有多个壳的区段,便可以判断加了多重壳。另外,还可以通过OEP代码来识别。当有大的跳转,特别是区段跳转,就要注意所到之处是程序的OEP,还是壳的OEP,当然这需要熟悉各种壳和程序的OEP代码。通过单步跟踪,即使程序加了多重壳,最终还是会到达真正的OEP处。因此,对多重壳,可以一次性跟踪到OEP处脱壳,也可以一层层脱壳。
6.3 自校验
程序为了防止被脱壳或被修改,会使用自校验功能。所谓自校验就是程序自己检测自己是否被修改过,程序通过MD5或CRC来检验数据完整性,或者通过对比文件大小判断是否被脱壳。如压缩壳脱壳后其程序都会比未脱壳时大,当程序检测到文件大小不符时,就会停止运行。对自校验的解除,只需要进行对比跟踪。将脱壳后与未脱壳的程序都加载到调试器,对比程序执行的不同地方,一般为自校验的关键句,修改跳转语句或对比值即可解除自校验。还可以通过定位ExitProcess函数,回溯到哪些地方调用了此退出函数,从而定位到关键代码处。
7 结束语
本文以实例详细分析了PE文件的结构、加壳和脱壳原理,并对脱壳过程中可能遇到的伪装壳、多重壳和自校验做了分析。对于一些功能强大的壳,可能要用到ring0级别的调试器,还要修改壳代码才能脱壳。脱壳技术随着加壳技术的进步而进步,两者在一个相互较量的过程中发展。
参考文献
[1]庞立会.PE文件动态加壳技术的研究与实现[J].计算机工程,2008,34(19).
[2]陈勤,贾琳飞,张蔚.基于代码与壳互动技术的软件保护方法研究[J].计算机工程与科学,2006,28(12).
[3]于淼,孙强.对加壳技术的改进:超粒度混杂技术[J].计算机应用,2004,24(8).
[4]锻钢.加密与解密[M].3版.北京:电子工业出版社,2008.
[5]嵇海明,杨宗源.PE文件格式剖析[J].计算机应用研究,2004(3).
[6]张静盛.Windows编程循序渐进[M].北京:机械工业出版社,2008:324-333.
[7]Yang-seo Choi,Ik-kyun Kim,Jin-tae Oh,et al.PE File Header Analy-sis-based Packed PE File Detection Technique(PHAD)[C]//Inter-national Symposium on Computer Science and its Applications,2008,28:28-31.
PE文件的水印算法研究与实现 第5篇
PE文件是Microsoft公司制定的一种Win32可执行文件格式标准[1,2],常见后缀名为exe、dll的文件等均属于PE类格式。PE是英文单词Portable Executable的缩写,意味着此文件格式跟平台体系结构无关,可被任何Win32平台(如x86,MIPS等)的PE装载器识别。随着Windows操作系统在个人电脑上占据统治地位,研究基于PE文件格式的软件版权保护技术意义重大。
软件水印是一门基于信息隐藏的软件版权保护技术,它通过在软件中嵌入代表程序版权或用户身份的秘密信息。当软件产品被非法使用和篡改或归属权遭到争议时,软件所有者通过提取嵌入在其中的水印信息来达到验证版权归属的目的[3]。根据数论中的大数分解难题,软件所有者可选择一个代表版权信息的大自然数N(可分解为两个大素数P、Q的乘积)作为水印信息并转化成某种结构或编码后嵌入到软件中。由于只有合法用户才能检测到N并将其分解为P和Q,从而可据此验证软件的产权归属[4]。
目前软件水印主要的研究对象在于程序源代码和Java字节码,而针对PE文件的研究并不多。已有的关于PE文件的软件水印方法有两类,其一是向PE文件中添加新的节(section)来嵌入水印[5]或利用PE文件自身具有的冗余空间[6,7,8]。该类方法具有水印容量大,实现简单等优点,但水印的隐蔽性和鲁棒性不强,容易遭受到剪切、添加等攻击。其二是利用PE文件的结构特点来编码水印信息,用软件水印来控制文件中某类资源的排列顺序,这种方法没有增加PE文件的大小,具有不可感知性。但是经过进一步的研究发现,基于重构PE文件结构的静态水印算法无法应对攻击者的扭曲变形攻击。一旦其排列结构被改变,因软件水印与PE文件结构之间的映射关系无法恢复而使得水印提取失败。
本文先对已有的两种基于重构PE文件结构的水印算法进行了分析,并通过实验说明其无法抵抗变形攻击;然后通过分析操作系统显示PE文件位图的机理说明:调色板中各颜色项在位图数据中的出现次数不可改变。因此,提出了一种基于PE文件位图资源的水印算法。
1 相关研究
现有基于PE文件水印算法的研究主要在于通过重构PE文件结构来嵌入水印,本文接下来将对两个方案进行分析,指出其中存在的不足并提出一种新的水印算法。
1.1 文献[9]方案的分析
文献[9]提出一种基于排列图的水印算法,利用重构PE文件的资源结构排列情况来对软件水印进行编码。其大致思路是:水印认证中心在收到软件所有者的申请后,产生两个大素数P、Q,并使水印信息为Z = PQ,软件所有者得到的私钥为Z的欧拉函数Φ(Z)=(P-1)(Q-1);然后软件所有者选取一个整数Z1(Z1 < Z)作为软件资源节点的排列最小图;最后将软件的资源节点按照第Z - Z1个结构图进行重排而实现嵌入水印Z。在提取软件水印时,软件所有者根据软件的资源信息构造资源节点的结构图,进而确定结构图序号Z - Z1。由于其他非产权所有者可以修改该软件的资源结构,软件所有者在提取水印时很可能得不到被嵌入水印所对应的软件结构,因而也无法得到正确的P、Q而无法验证软件的产权归属,即该方法无法抵抗扭曲变形攻击。
1.2 文献[10]方案的分析
文献[10]提出一种变换引入表排列顺序,将软件水印隐藏于PE文件引入表模块与函数的排列顺序之中的算法,其基本思路在湖南大学陈刚2008年的硕士论文中已经提到[11]:它利用排列组合原理,首先变换引入模块的顺序,使引入模块的排列顺序所对应的V0值为m0(由计算公式得到);然后对于第n(n ∈[1,N])个引入模块的引入函数,根据计算出来的mn值变换引入函数的排列顺序,使其Vn值等于mn,直到N个引入模块的引入函数全部处理完为止。该算法在提取软件水印时,软件所有者根据软件的引入模块和第n个模块引入函数的排列顺序来计算相应的V0、Vn值,一旦引入模块的顺序或者第n个模块的引入函数顺序被更改,水印将无法被正确提取。
为了验证以上分析的正确性,本文在Microsoft Visual Studio 2008平台下对文献[9,10]中的算法进行了实现并进行扭曲攻击。如图1,实验对象为文献[9]中采用的Windows XP的“资源管理器”,点击“资源情况”按钮后,程序界面将显示出该PE文件资源节各层的具体情况。点击“嵌入水印”后,程序将文献[9]中使用的水印信息:P= 37 847,Q = 38 671嵌入到软件中,将嵌入了水印的软件保存为explorer1.exe。遭受到变形攻击后,explorer1.exe文件中的资源节排列结构被随机打乱重排,再通过提取水印功能提取出来的水印信息为:P = 12 371, Q = 9 653,因此,该算法遭受到扭曲变形攻击后无法正确提取水印。
如图2,按照文献[10]描述的算法在Windows XP系统的“资源管理器”中嵌入水印信息“2104737598”。由于该PE文件中,共引用了13个模块,所有引入函数的函数序号都为零,点击“变形攻击”按钮后,本文只对“资源管理器”中引入模块的顺序进行了重排。最后,在遭受过变形攻击后的PE文件中提取出来的信息为“1991732546”,故本实验证明了文献[10]提出的水印算法面对变形攻击的脆弱性。
2 基于位图资源的水印算法
2.1 PE文件中的位图资源
位图文件(Bitmap)是Windows 操作系统中的标准图像格式。PE文件中的位图文件存放在资源节中,在资源节根节点中寻找ID值为2的目录项后沿着该目录项向下遍历便可得到所有位图资源[12]。PE文件中的位图文件不含有一般位图文件所具有的文件头,其结构示意如图3所示:
位图信息头在WINGDI.H头文件中的定义如下:
其中,biSize字段指出了位图信息头的大小,固定为40个字节,biBitCount 指出每个像素使用多少位数据来描述,由于颜色项被定义为一个4字节的RGBQUAD结构,调色板的大小即为调色板中颜色数的4倍,而调色板中的颜色数可以由biClrUsed 字段和biBitCount字段联合计算得到。当 biClrUsed不为0时,其值即为该位图文件实际使用的颜色数,否则,显示该位图文件需要的颜色数为2biBitCount。
调色板是单色、16色和256色的位图文件所特有,若位图文件存在调色板,则位图采用的是一种基于表索引的方式来显示,位图数据即是指向调色板中颜色值的索引号。操作系统显示位图时,先利用位图数据作为颜色值在调色板中的行号索引找到相应颜色,然后将其显示到屏幕上。文献[13]利用操作系统根据位图数据来查找调色板中的颜色值而不关心调色板中颜色值数量多少的原理,将待隐藏信息嵌入到调色板中作为冗余颜色值来达到信息隐藏的目的。
PE文件中的位图资源具有尺寸小、数量多而零散、颜色数为4位和8位的位图较多等特性,本文对几种常见的PE文件的位图信息进行了分析,具体情况如表1所示。
如表2,由于操作系统依据位图数据索引调色板,不管调色板颜色值的排列顺序如何改变或者调色板中增加新的颜色值,位图数据中的每种颜色出现频数始终不会改变,否则位图将无法正确显示。因此,本文提出使用位图文件中调色板颜色项出现频数来编码软件水印的算法,建立水印信息与频数序列间的依赖关系,将水印信息与宿主文件自身不可变特征相关联,从而使嵌入的软件水印能抵抗扭曲变形攻击。
2.2 算法描述
设位图Bi, m表示第i个位图中调色板颜色项为m,颜色项出现频数按非递减顺序排列为Ni, 0、Ni, 1Ni, jNi, m,本文规定频数序列运算规则如下:
(1)规则1 所有具有调色板的位图按biBitCount值分为3类,即单色、16色、256色;
(2)规则2 对每一类位图,规定:若Ni, 0 < Nj, 0,则Bi, m < Bj, n,若Ni, 0 > Nj, 0,则Bi, m > Bj, n,否则继续比较Ni, 1 和Nj, 1。按照比较规则,将每一类位图按非递减顺序排列;
(3)规则3 若Ni, m不超过256,Ni, m用一个字节表示,否则,Ni, m用两个字节表示;对于每一类位图,依次连续选择若干个频数连接组成一个128二进制串;
(4)规则4 将得到的所有128位二进制串进行异或运算得到位图资源所对应的二进制串C。
2.2.1 水印嵌入算法
假设软件所有者从水印认证中心申请得到的基于大数分解难题原理生成的软件水印为W,W= PQ,P、Q为两个足够大的素数,Φ(W) = ( P-1)(Q-1),则水印嵌入过程可描述为:
(1)遍历PE文件资源节中的位图文件,提取出所有具有调色板的位图;
(2)根据计算规则计算该PE文件中的位图对应的二进制串C;
(3)计算密钥K1 = W⊕C,将K1和Φ(W)连在一起形成最终密钥K。
2.2.2 水印提取算法
水印的提取过程描述为:
(1)所有者从密钥K中分离出K1和Φ(W);
(2)根据计算规则计算该PE文件中的位图对应的二进制串C;
(3)计算K1⊕C得到水印信息W;
(4)软件所有者联立方程W = P Q和Φ(W) = (P-1)(Q-1),在有效时间内对大数W进行分解得到素数P、Q,进而由此来证明软件的产权归属。
3 实验结果与分析
为了验证上述算法的可行性,本文对上述算法进行了实现。如图4,为了形成对比,实验对象仍为Windows XP系统的“资源管理器”,程序使用的两个素数为文献[9]中的37 847和38 671,本例只是为了说明实验原理,在实际应用中,可选取更大的素数。点击“嵌入水印”后程序将计算得到的密钥K返回给用户。嵌入水印后,本文仍对嵌入水印后的PE文件进行了扭曲变形攻击,即改变调色板中颜色项的排列顺序,同时相应修改位图数据使得遭受攻击后的程序仍然能正常运行。最后,通过“提取水印”功能,用户可根据密钥成功从遭受到攻击的软件中提取出水印。
由算法描述可知,本文提出的水印算法没有增加或减小软件本身的空间大小,不仅克服了基于空间冗余类算法隐蔽性差的缺陷,而且建立了嵌入水印与软件自身特征之间的依赖关系。由于本文提出的水印算法只与序列的非递减排列有关,相比基于变换PE文件结构的算法,可有效抵抗扭曲变形攻击。同时,位图作为PE文件的资源部分存储在其二进制数据内部,任何对PE数据采取的剪切、添加等攻击都可能导致文件无法正常运行[12]。成功提取软件水印的关键在于用户掌握的密钥,密钥长度可根据用户需要设置为超过128位的高强度密钥,不知道密钥的非软件所有者无法从软件中提取出水印,算法鲁棒性强、软件水印安全度高。
需要说明的是,本文提出的计算规则使用全部具有调色板的位图资源,在实际应用中,完全可以根据需要,利用密钥控制选取部分带有调色板的位图文件来嵌入水印,且不同序列间可采用序列运算进行复杂计算,这可在一定程度上进一步增加水印的隐蔽性和安全性。设2色位图有x个,16色位图有y个,256色位图有z个,则理论上的用户选择有((2x+16y+256z)!)种,水印容量大。但是,由于该算法将软件水印与位图数据建立联系,若软件中不具有含调色板的位图资源,则无法嵌入,这是本算法的不足之处,有待将来进一步改进和完善。
另外,实验中发现,由于Windows操作系统规定一个扫描行所占的字节数必须是 4的倍数,不足位以0填充,具有调色板的位图多数存在着较多的冗余字段用来对齐,如实验中使用的“资源管理器”中唯一一个单色位图,其biWidth = 16,biHeight = 16,位图数据大小为64 B,由于每个像素只需要1位来表示,每行有效的位图数据只有两个字节,故每行剩下两个补充字节未利用,该单色位图共冗余32 B。本文从explorer.exe中共提取出约2 560 B的对齐空间,这部分空间相比PE文件节之间的冗余[13]显然更隐蔽,可采用类似于Moslkowitz等提出的防篡改算法思想[14],将部分关键代码隐藏于该空间,若位图被破坏,则程序将运行出错来增强水印健壮性。如何将这段隐蔽空间合理应用于PE文件的软件水印是将来进一步研究的工作。
4 结束语
PE文件是Win32平台下可执行体的主要存储形式,研究基于PE文件的软件水印意义重大。现有的两类关于PE文件软件水印的算法具有一定缺陷,本文在研究现有算法的基础上,根据大数分解难题和操作系统显示PE文件位图的有关原理,提出了一种基于位图资源的新算法。新算法既没有在软件中添加新的数据,也没有将水印信息隐藏于PE文件某种数据的排列结构中,而是建立了软件水印与PE文件自身特征之间的依赖关系,分析表明,该算法具有一定优势。
摘要:针对已有的两种基于重构PE(Portable Executable)文件结构的水印算法无法应对扭曲变形,而使水印提取错误等问题,利用位图文件的调色板颜色项在位图数据中的出现频数不可改变这一统计规律,建立软件水印与频数序列之间的依赖关系,将水印信息关联到PE文件自身的静态特征中,提出了一种基于文件位图资源的新水印算法。分析表明,该方案相比基于变换PE文件资源结构和引入表结构的算法具有更好的鲁棒性,不仅具有不可感知性,还能抵抗扭曲变形攻击。
PE文件格式 第6篇
1PE文件8导入表和外壳导入表合并的基本思路
1.1PE文件导入表的结构
要自定义导入表和实现导入表的合并,首先要了解熟悉导入表的基本结构组成。PE文件的导入表是由一系列的IM AGE_IMPORT_DESCRIPTOR结构组成 的数组 ,每一个IM AGE_IMPORT_DESCRIPTOR结构对应一个DLL,导入表的最后由一个内容全为0的IMAGE_IMPORT_DESCRIPTOR结构结束。该结构的定义如下:
字段Original First Thunk所指的导入名称表(Import Name Table,简称INT)由若干个IMAGE_THUNK_DATA结构组成的数组,每一个IMAGE_THUNK_DATA结构对应一个API导入函数,数组的最后由一个内容全为0的IMAGE_THUNK_DATA结构结束。该结构的定义如下:
从这个结构的定义可看到,该结构是一个共用体,实际上就是一个双字。当双字的最高位是1时,表示函数是以序号导入的,低31位就是函数的序号值;当最高位是0时,表示函数是以函数名称(ANSI字符串,以0结尾)导入的,双字表示是一个RVA,此时指向一个IMAGE_IMPORT_BY_NAME结构。IM-AGE_IMPORT_BY_NAME结构定义如下所示。
Windows在装入PE文件时,其工作之一是定位到导入表, 根据导入表中说明的DLL,将DLL装入内存,在DLL中搜索导入表记录的API函数,找到后将对应的函数地址(指针)写入IAT,以方便程序正确调用API函数。
1.2外壳中自定义的导入表(示例)
根据前面所叙的PE文件导入表结构,外壳中自定义的导入表如下(部分):
1.3PE文件的导入表和外壳的导入表的合并思路
为了让Windows加载PE文件和外壳中的导入表,首先在外壳中要严格按照PE文件的导入表格式定义,然后将PE文件和外壳的导入表合并成一个IMAGE_IMPORT_DESCRIPTOR数组。由于在PE文件中的.idata节或.rdata节中空隙空间有限, 不一定能装下外壳中的整个导入表,所以在这里是将PE文件的导入表移动到PE文件的原来最后一个节区的末尾处(新增加的.zzcode节区的开始处),然后再接上外壳的导入表,这样就合并成了一个完整的导入表。这又有二种拼接方法:(a)PE文件的导入表放在前面,外壳的导入表放在后面;(b)外壳导入表放在前面,PE文件的导入表放在后面。如下图2所示:
2利用Windows加载PE文件和外壳的导入表的程序实现
2.1合并PE文件和外壳的导入表
这里用图2(a)中所示的导入表合并方案来说明如何编程实现导入表的合并。
1) 首先由API函数Create File、Create File Mapping、 Map View Of File创建PE文件的内存映像,从PE开头定位到NT映像头IMAGE_NT_HEADERS,这里用ebx指向IM-AGE_NT_HEADERS结构,由字段Optional Header、Data Directory通过变量Virtual Address,也就是 [ebx].Optional Header.Data Di-rectory[8].Virtual Address定位到PE文件的导入表,然后用如下代码片段计算出导入表的字节长度。
2)在PE文件中新增加一个节区如.zzcode,将PE文件的导入表写入该文件的新增加节区的开头处,然后将整个外壳(注意:要求外壳的导入表要放在外壳的最前面)写在紧接PE文件的导入表的后面,这样就实现了PE文件的导入表和外壳导入表的合并。其后就可用任意字节代码履盖掉PE文件原来位置的导入表。
3)修改PE文件导入表的指针使其指向合并后的导入表头部,同时修改合并导入表的大小,以确保系统加载PE文件时初始化合并后的导入表。代码片段如下:
4)将外壳自定义的整个导入表读入由函数Global Alloc申请的内存块中,然后对导入表IMAGE_IMPORT_DESCRIPTOR结构中的Original First Thunk、Name1、First Thunk字段的双字地址进行修改,同时对IMAGE_THUNK_DATA结构中共用体u1中的Address Of Data字段的地址进行修改,将字段中的相对于外壳导入表头部的偏移offset转换为RVA。这样,当Windows加载外壳导入表时,通过内存PE文件的映像基地址+字段的RVA,就能准确定位需要查找DLL中的函数,并将函数地址填写入IAT,从而保证外壳中调用API函数时找到所对应的函数地址。偏移地址修改为RVA完成后,再将内存块中的整个导入表写回原来位置将原来的外壳导入表覆盖掉,至此,合并导入表的工作就完成了。进行这个地址转换的程序代码如下:
2.2合并导入表的测试与分析
1)在Windos 7和Windows XP SP2环境下,对示例PE文件和多个PE文件进行了加壳,对合并后的导入表进行了测试,程序原有各项功能运行正常,这说明PE文件的API函数调用,外壳中API函数调用工作正常,合并导入表达到预期目的。
2)用导入表查看工具软件查看加壳后的示例PE文件导入表,如图3所示是PE文件导入表(部分)磁盘映像,导入表字段Original First Thunk指向INT,字段First Thunk指向IAT;图4所示是外壳导入表(部分)磁盘映像,导入表字段Original First Thunk和字段First Thunk指向同一个IMAGE_THUNK_DATA,当被系统载入内存后它就转变成IAT了。
3结束语
1)本文示例中外壳中的导入表和PE文件的导入表合并后放在外壳的最前面,其实合并后的导入表还可放置在外壳的最后面或外壳中的任意位置,只是这样编程实现时要复杂一些。
2)测试和分 析表明 :除了可把PE文件的导 入表IM AGE_IMPORT_DESCRIPTOR结构数组移动到外壳中,实际上还可以把IMAGE_THUNK_DATA结构也移动到外壳中,不过这里就需要 修正IMAGE_IMPORT_DESCRIPTOR结构中字 段Original First Thunk、First Thunk的RVA值,以便正确的指向IM-AGE_THUNK_DATA结构数组,保证Windows加载PE文件导入表时正确寻址找到INT和IAT,但IMAGE_THUNK_DATA结构中的共用体u1中的字段Address Of Data不必修正,因为IM-AGE_IMPORT_BY_NAME结构的位置没有变动。同样,PE文件的导入表IMAGE_IMPORT_DESCRIPTOR结构数组虽然移动到外壳中,但由于IMAGE_THUNK_DATA结构数组的位置没有变化,所以不必修改其中的字段Original First Thunk、First Thunk的RVA值。
3)将PE文件的导入表IMAGE_IMPORT_DESCRIPTOR结构数组移动到外壳中,而将IMAGE_THUNK_DATA结构和IM-AGE_IMPORT_BY_NAME结构留在PE文件中,也就是把整个导入表分割成了二部分,这样可以加强外壳与原程序的联系, 如果简单地把PE文件的外壳脱去会导致系统初始化PE文件的导入表失败,从而使PE文件不能正常调用API函数而引发异常。
摘要:根据PE文件导入表的结构及系统加载导入表的原理,在外壳中自定义了导入表,将外壳中的导入表与PE文件的导入表合并,并用Win32汇编编程实现。利用Windows加载PE文件时,将PE文件和外壳的导入表初始化,从而实现了在PE文件和外壳中正常调用API函数。