PE文件是windows下的32位可执行文件格式,64位称为PE+或PE32+
文件中使用偏移(offset)内存中使用VA(Virtual Address)来表示位置,VA指的是进程虚拟内存中的绝对地址,RVA(Relative Virtual Address)指从某个基准位置(ImageBase)开始的相对地址,RVA+ImageBase=VA
在PE头的最前面有IMAGE_DOS_HEADER结构体,即DOS头,大小为64字节,比较重要的成员是e_magic(DOS签名,4D5A=>ASCII值"MZ",位于结构体的第一个),e_lfanew (指示NT头的偏移【根据不同文件拥有可变值】),使用winhex打开windows自带的notepad文件,结构体情况如下(用notepad.exe文件演示)
文件开始的2个字节为4D5A,e_lfanew值为0000000E(Intel系列的CPU以逆序存储数据,称为小端序标识法)
DOS存根在DOS头下方,目的是让程序可以在DOS环境中运行
NT头IMAGE_NT_HEADERS由三个成员组成,大小为F8
Signature从0xE0开始
File Header为表现文件大致属性的文件头,IMAGE_FILE_HEADER结构体,重要成员:
1、Machine:标识兼容的CPU,本程序中值为0x014C,兼容Intel386
2、NumberOfSection:用于指出文件中存在的节区数量,该值一定要大于0,且当定于的节区数量与实际节区不同时,将发生运行错误,本程序中值为0x0003
3、SizeOfOptionalHeader:用于指出最后一个NT头成员的大小,本程序中值为0x00E0
4、Characteristics:用于标识文件的属性,文件是否是可运行的形态,是否为DLL文件等信息,本程序中值为0x010F
Optional Header为IMAGE_OPTIONAL_HEADER32结构体,重要成员:
1、Magic:Magic码为0x10B时,为IMAGE_OPTIONAL_HEADER32结构体,Magic码为0x20B时,为IMAGE_OPTIONAL_HEADER64结构体
2、AddressOfEntryPoint:持有EP的RVA值,指出程序最先执行的代码起始地址
3、ImageBase:指出文件的优先装入地址,一般情况下EXE文件的ImageBase值为0x00400000,DLL文件为0x10000000,执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器的值设置为ImageBase+AddressOfEntryPoint
4、SectionAlignment, FileAlignment:FileAlignment指定了节区在磁盘文件中的最小单位,SectionAlignment指定了节区在内存中的最小单位。磁盘文件或内存的大小必为FileFlignment或SectionAlignment值的整数倍
5、SizeOfImage:指定了PE Image在虚拟内存中所占空间的大小
6、SizeOfHeaders:用来指出整个PE头的大小,该值也必须是FileAlignment的整数倍,第一节区所在位置与SizeOfHeaders距文件开始偏移的量相同
7、Subsystem:用于区分系统驱动文件( *.sys)与普通的可执行文件( *.exe, *.dll),值:1(Drive文件,系统驱动 eg:ntfs.sys)2(GUI文件,窗口应用程序 eg:notepad.exe)3(GUI文件,控制台应用程序 eg:cmd.exe)
8、NumberOfRvaAndSizes:用于指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体的最后一个成员)数组的个数
9、DataDirectory:由IMAGE_DATA_DIRECTORY结构体组成的数组,其中DataDirectory[0]= EXPORT Directory,DataDirectory[1] = IMPORT Directory,DataDirectory[2] = RESOURCE Directory(RVA与Size)
节区头定义了各节区属性,为了保证程序的安全性,PE文件中的code(代码,执行,读取权限)、data(数据,非执行,读写权限)、resource(资源,非执行,读写权限)等按照属性分类存储在不同节区。
节区头是由IMAGE_SECTION_HEADER结构体组成的数组,每个结构体对应一个节区。重要成员:
1、VirtualSize:内存中节区所占大小
2、VirtualAddress:内存中节区起始地址(RVA),不含值,由定义在IMAGE_OPTIONAL_HEADER32中的SectionAlignment确定
3、SizeOfRawData:磁盘文件中节区所占大小
4、PointerToRawData:磁盘文件中节区起始位置,不含值,由定义在IMAGE_OPTIONAL_HEADER32中的FileAlignment确定
5、Characteristics:节区属性
tips:磁盘文件中的PE与内存中的PE有不同形态,将装载到内存中的形态称为“映像(Image)”以示区别
RVA指内存地址,RAW指文件偏移
RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
本程序中
PointerToRawData = 0x00000400
VirtualAddress = 0x00001000
IAT即Import Address Table,导入地址表,是一种表格,用来记录程序正在使用哪些库中哪些函数。(Table即指数组)
DLL即Dynamic Link Librart,动态链接库,好处:
1、不需要把库包含到程序中,单独组成DLL文件,需要时调用即可
2、内存映射技术使加载后的DLL代码、资源在多个进程中实现共享
3、更新库时只要替换相关DLL文件即可
加载DLL的方式实际有“显式链接(Explicit Linking)”和“隐式链接(Implicit Linking)”
IMAGE_IMPORT_DESCRIPTOR结构体中记录着文件要导入哪些库文件。
Import:导入,向库提供服务(函数)
Export:导出,从库向其他PE文件提供服务(函数)
导入多少个库就存多少个IMAGE_IMPORT_DESCRIPTOR结构体,这些结构体构成了数组,最后以NULL结构体结束。INT与IAT都是长整型数组。
IMAGE_IMPORT_DESCRIPTOR结构体数组也被称为IMPORT Directory Table
重要成员:
1、OriginalFirstThunk:INT(Tmport Name Table)的地址(RVA)
2、Name:库名称字符串的地址(RVA)
3、FirstThunk:IAT的地址(RVA)
使用RVA to RAW可得文件偏移为6A04(7604-1000+400)
红框即为第一个元素
1、库名称(Name)
在文件中6EAC可得(RVA:7AAC -> RAW:6EAC)库名称comdlg32.dll
2、OriginalFirstThunk-INT
RVA:7990 -> RAW:6D90
3、IMAGE_IMPORT_BY_NAME
RVA:7A7A -> RAW:6E7A
最初的两个字节值000F为Ordinal,是库中函数的固有编号,函数名称为PageSetupDlgW,最后以00结尾
4、FirstThunk-IAT
RVA:12C4-> RAW:06C4
由结构体指针数组组成,NULL结尾,第一个元素被硬编码成76324906,没有实际意义,当exe文件加载到内存时,准确的地址值会取代该值。
Windows操作系统中,“库”是为了方便其他程序调用而集中包含相关函数的文件(DLL/SYS)。Win32 API是最具代表性的库,其中的kernel32.dll文件被称为最核心的库文件。
EAT是一种核心机制,它使不同的应用程序可以调用库文件中提供的函数,PE文件中的特定结构体(IMAGE_EXPORT_DIRECTORY)保存着导出信息,且PE文件中仅有一个用来说明EAT的IMAGE_EXPORT_DIRECTORY结构体
如图为kernel32.dll文件的IMAGE_OPTIONAL_HEADER32.DataDirectory[0],可得RVA=262C,所以RAW为1A2C
重要成员:
1、NumberOfFunctions:实际Export函数的个数
2、NumberOfNames:Export函数中具名的函数个数
3、AddressOfFunctions:Export函数地址数组(数组元素个数=NumberOfFunctions)
4、AddressOfNames:函数名称地址数组(数组元素个数=NumberOfNames)
5、AddressOfNameOrdinals:Ordinal地址数组(数组元素个数=NumberOfNames)
1、函数名称数组
AddressOfNames成员的值为RVA = 3538,即RAW = 2938,在Winhex中
数组元素个数为NumberOfNames = 3B9
2、查找指定函数名称
要查找的函数名称字符串为“AddAtomW”,只需要找到RVA数组中第三个元素的值即可(RVA:4BB3 -> RAW:3FB3)
3、Ordinal数组
下面查找“AddAtomW”函数的Ordinal值。AddressOfNameOrdinals成员的值为RVA:441C -> 381C,ordinal数组中的各元素大小为2个字节
4、ordinal
将2中求得得index值(2)应用到3中的Ordinal数组即可求得Ordinal(2)。
AddressOfNameOrdibals[index] = ordinal (index=2, ordinal=2)
5、函数地址数组-EAT
最后查找AddAtomW的实际函数地址,AddressOfFunctions成员的值为RVA:2654 -> RAW:1A54
如图为4字节函数地址RVA数组
6、AddAtomW函数地址
用4中求得的Ordinal用作索引,可得RVA = 000326D9
AddressOfFunctions[ordinal] = RVA (ordinal=2, RVA=326D9)
Kernel32.dll的ImageBase = 7C800000,因此AddAtomW函数的实际地址(VA)为7C8326D9,用OD验证得
无论哪种形态的文件都是由二进制组成的,经过压缩的文件若能100%恢复,则称为无损压缩(Lossless Data Compression),如ZIP,RAR;若不能恢复原状,则称为有损压缩(Loss Data Compression),如jpg,mp3,mp4。
运行时压缩器是PE文件的专用压缩器
把普通PE文件创建成运行时压缩文件的实用程序称为压缩器(Packer),经反逆向(Anti-Reversing)技术特别处理的压缩器称为保护器
目的是缩减PE文件大小和隐藏PE文件内部代码与资源,大致分为单纯压缩PE文件的压缩器如UPX,ASPACK,和对源文件进行较大变形,严重破坏PE头,常用于恶意程序的压缩器如UPack,PESpin等。
目的是防止破解,保护代码与资源,常用于保护游戏等的安全程序
整个解压缩过程由无数循环组成,注意遇到循环(Loop)时,先了解作用再跳出。
可以利用Ctrl+F8的自动步过来找到循环(按F7即可停止),然后利用F2+F9的方法跳过循环
1、UPX的特征之一是其EP代码包含在PUSHAD/POPAD指令之间,跳转到OEP代码的JMP指令紧接着出现在POPAD指令之后,所以在JMP指令处设置好断点即可找到OEP
2、硬件断点
PE文件在重定位过程中会用到基址重定位表(Base Relocation Table)
向进程的虚拟内存加载PE文件时,文件会被加载到PE头的ImageBase所指的地址处,若加载的事DLL(SYS)文件,且在ImageBase位置处已经加载了其他DLL(SYS)文件,那么PE装载器就会将其加载到其他未被占用的空间,被加载到其他地址时发生的一系列的处理行为就叫做PE重定位。
tips:用SDK或VC创建PE文件时,EXE默认ImageBase为00400000,DLL默认ImageBase为10000000,用DDK创建的SYS文件默认的ImageBase为10000。
在Windows7的ASLR机制作用下,程序每次加载到内存中的基址都不同,使硬编码在程序中的内存地址随当前加载地址变化而改变的处理过程就是PE重定位。
无法加载到ImageBase地址时,若未经过PE重定位,应用程序就不能正常运行(因发生“内存地址引用错误”,程序异常终止)
基本操作原理
1、在应用程序中查找硬编码的地址位置
2、读取值后,减去ImageBase(VA->RVA)
3、加上实际加载地址(RVA->VA)
最关键的是查找硬编码地址的位置,过程中会用到PE文件内部的Relocation Table(重定位表),它是记录硬编码地址偏移(位置)的列表(重定位表是在PE文件构建过程(编译/链接)中提供的)。
基址重定位表位于PE头的DataDirectory数组的第六个元素。
IMAGE_NT_HEADERS\IMAGE_OPTIONAL_HEADER\IMAGE_DATA_DIRECTORT[5]
基址重定位表是IMAGE_BASE_RELOCATION结构体数组,第一个成员为VirtualAddress,是一个基准地址,实际是RVA值。第二个成员为SizeOfBlock,指重定位块的大小。最后一项TypeOffset数组并不是结构体成员,而是以注释形式存在的,表示该结构体之下会出现WORD类型的数组,并且该数组元素的值就是硬编码在程序中的地址偏移。
TypeOffset成员高四位用作Type,PE文件中常用的值为3(IMAGE_REL_BASED_HIGHLOW),64位的PE+文件中常见值为A(IMAGE_REL_BASED_DIR64)。
低十二位是真正的位移,该值是基于Virtual Address的偏移,所以程序中硬编码地址的偏移换算等式为: Virtual Address + Offset = RVA