前言
继续学习《逆向工程核心原理》,本篇笔记是第二部分:PE文件格式,包括PE文件、运行时压缩、重定位、UPack、内嵌补丁等内容
一、PE文件
1、PE文件格式
PE(Portable Executable)文件
- windows下的32位可执行文件格式,64位称为PE+或PE32+
- 大量信息以结构体形式存储在PE头中
- 种类如下:
以notepad.exe为例,用winhex打开如下:
其加载到内存时的情形如下
- PE头:DOS头到节区头,内部信息大多以相对虚拟地址(Relative Virtual Address,RVA)形式存在
- 文件中用偏移(offset)表示位置,内存中用虚拟地址(Virtual Address,VA)表示位置
- PE头与各节区尾部都有一个NULL填充,使各个节区的起始位置都在最小单位的倍数位置上
2、PE头
(1)DOS头
考虑对DOS文件的兼容
PE文件最前面有个40字节大小的IMAGE_DOS_HEADER结构体
来扩展已有的 DOS EXE 头
两个重要成员
- e_magic:DOS签名(
4D5A
,ASCII值是MZ
),所有PE文件都有 - e_lfanew:指示NT头的偏移,指向NT头(IMAGE_NT_HEADER)所在位置
(2)DOS存根
DOS存根(stub)在DOS头下方
- 可选项
- 大小不固定
notepad的DOS存根是
在DOS环境中会输出:!This program cannot be run in DOS mode.
然后终止运行
感觉可以理解为,这部分是在DOS环境中会执行的文件(PE文件中的文件)
(3)NT头
IMAGE_NT_HEADERS有3个成员,如下图所示:
- 签名(Signature)结构体,值是
50 45 00 00
,ASCII值是PE
- 文件头
- 可选头
文件头表现文件属性(设置错误则文件无法运行),如下图所示:
- Machine码:定义在winnt.h文件中,每个CPU都有唯一的Machine码
- NumberOfSections:指出文件中存在的节区数量,且必须与实际数量相同
- SizeOfOptionalHeader:指出IMAGE_OPTIONAL_HEADER32结构体的长度(PE+是IMAGE_OPTIONAL_HEADER64)
- Characteristics:标识文件属性,是否可运行,是否为DLL等,具体如下图所示
可选头:IMAGE_OPTIONAL_HEADER32是PE头结构体中最大的,如下图所示
-
Magic:32位是
10B
,64位是20B
-
AddressOfEntryPoint:EP的RVA值,指出程序的起始地址
-
ImageBase:指出文件加载到内存时的优先装入地址,通常exe是00400000,DLL是10000000
-
SectionAlignment:指定节区在内存中的最小单位
-
FileAlignment:指定节区在磁盘文件中的最小单位
-
SizeOfImage:指定PE Image在虚拟内存中所占空间大小
-
SizeOfHeader:指出整个PE头的大小,是FileAlignment的整数倍
-
Subsystem:区分系统驱动文件(.sys)和普通可执行文件(.exe,.dll),如下图所示
-
NumberOfRvaAndSizes:指定DataDirectory数组的个数
-
DataDirectory:数组,每项定义如下图所示,需要注意0、1、2、9这几个
(4)节区头
节区头定义了各节区属性,如下图所示:
节区头由IMAGE_SECTION_HEADER结构体组成,如下图所示:
几个重要值
- VirtualSize:内存中节区大小,由SectionAlignment确定
- PointerToRawData:磁盘文件中节区大小,由FileAlignment确定
- VirtualAddress:内存中节区起始地址(RVA)
- PointerToRawData:磁盘文件中节区起始位置
- Charateristics:节区属性,具体值如下图所示
3、RVA to RAW
RVA to RAW:PE文件加载到内存时,每个节区准确完成的内存地址与文件偏移间的映射
- 查找RVA所在节区
- 计算文件偏移(RAW),公式如下:
4、IAT
导入地址表(Import Address Table,IAT)是一种表格,记录程序正在使用哪些库中的哪些函数
(1)DLL
动态链接库(Dynamic Linked Library, DLL)
- 32位时引入
- 库单独组成DLL,需要时调用
- 内存映射技术使得DLL代码在多个进程中共享
- 更新库的时候只要更新DLL文件
加载方式
- 显式链接:程序使用DLL时加载,使用完释放内存
- 隐式链接:程序开始时一同加载DLL,程序终止时释放内存
IAT就是隐式链接,几个原因:
- 不同环境,函数和DLL的位置不同
- DLL重定位问题使得无法对地址硬编码
- PE头中用的是RVA
(2)IMAGE_IMPORT_DESCRIPTOR
IMAGE_IMPORT_DESCRIPTOR结构体记录了PE文件要导入哪些库文件,导入多少个库就有多少个IMAGE_IMPORT_DESCRIPTOR结构体,形成以NULL结尾的数组。IMAGE_IMPORT_DESCRIPTOR结构体具体如下图所示:
notepad.exe的kernel32.dll的IMAGE_IMPORT_DESCRIPTOR如下:
5、EAT
EAT使不同的应用程序可以调用库文件中提供的函数,即通过EAT才能得到从相应库中导出函数的起始地址,由IMAGE_EXPORT_DIRECTORY结构体保存导出信息
可以在PE头中找到IMAGE_EXPORT_DIRECTORY结构体的位置,起始地址是IMAGE_OPTIONAL_HEADER32.DataDirectory[0].VirtualAddress值(即RVA值)
以notepad.exe为例,RVA是262C,所以文件偏移是1A2C
kernel32.dll的EAT如下图所示:
从库中获得函数地址的API为GetProcAddress(),原理如下:
6、小结
PE规范只是一种标准规范而已
书中推荐了一个工具PEView
当然吾爱破解上有个studyPE我感觉更好用
二、运行时压缩
1、数据压缩
- 无损压缩:能100%恢复,如Run-Lenth、Lempel-Ziv、Huffman等算法
- 有损压缩:损失一定信息,高压缩率,如MP3、MP4、jpg
2、运行时压缩器
运行时压缩器(Run-Time Packer)是针对可执行文件(PE文件)而言的,可执行文件内部有解压缩代码,在运行瞬间于内存中解压缩后执行
运行时压缩器(Packer)
- 缩减PE文件大小
- 隐藏PE文件内部代码与资源
- 纯粹的:UPX、ASPack等
- 不纯的:UPack、PESpin等
PE保护器(Protector)
- 防止破解,采用多种反逆向技术
- 保护代码和资源
- 商用:ASProtect、Themida、SVKP等
- 公用:UltraProtect、Morphine等
例子:用UPX压缩notepad.exe后对比如下:
- PE头大小一样
- 节区名称改变
- 第一个节区的RawDataSize=0,实际上是运行时解压到第一个节区
- EP从第一个节区变到第二个节区
- 资源节区(.rsrc)大小几乎不变
三、调试UPX压缩的notepad程序
1、notepad.exe的EP代码
在OD里看notepad.exe
- 在
010073B2
处调用了GetModuleHandleA API,获取notepad.exe的ImageBase - 在
010073B4
与010073C0
处比较MZ和PE签名
2、notepad_upx.exe的EP代码
在OD里看notepad_upx.exe,会有个警告弹窗
- EP地址
01015330
,是第二个节区的末端,即实际压缩的notepad源代码在上方 - 第二个节区的起始地址
01011000
和第一个节区的起始地址01001000
分别设置到ESI和EDI,如此同时设置ESI和EDI,可以预见ESI所指缓冲区到EDI所指缓冲区的内存发生了复制,从ESI读取数据解压后保存到EDI
3、跟踪UPX文件
数量庞大的代码通常用以下跟踪命令
基本流程:
- Ctrl+F8开始跟踪
- 在遇到循环时,可以执行F7暂停
- 然后观察循环内容
- 再F2设置断点,F9跳出
(1)循环1
内容为从EDX(01001000
)中读取放入EDI(01001001
)
(2)循环2
这是个解压循环,从ESI指的第二节区读取数据,解压,放入EDI指的第一节区
(3)循环3
这段循环恢复源代码的CALL/JMP指令的目标地址
(4)循环4
这个循环设置IAT
最终在010154BB
处跳转到notepad.exe的OEP
4、快速查找UPX OEP
UPX的EP代码在PUSHAD和POPAD之间
所以在POPAD之后的JMP就是跳转到OEP的
只要在这个JMP设置断点就能找到OEP
也可以在Dump窗口找到栈地址设置硬件断点
四、基址重定位表
1、PE重定位
在向进程的虚拟内存加载DLL/SYS文件时
若ImageBase处已经有DLL/SYS文件
就涉及PE重定位,如下图所示
(EXE文件会首先加载到内存中,无需考虑重定位)
2、PE重定位操作原理
PE重定位的基本操作原理
- 在应用程序中查找硬编码的地址
- 读取值后,减去ImageBase(VA->RVA)
- 加上实际加载地址(RVA->VA)
查找硬编码地址会用到重定位表(Relocation Table)——记录硬编码地址偏移的表
notepad.exe的重定位表如图所示:
基址重定位表是IMAGE_BASE_RELOCATION结构体数组,如下:
五、从可执行文件中删除.reloc节区
1、.reloc
节区
VC++生成的PE文件的重定位节区是.reloc
- 一般位于所有节区最后
- 删除后文件照常运行
2、示例:reloc.exe
删除.reloc
流程
- 整理
.reloc
节区头 - 删除
.reloc
节区 - 修改IMAGE_FILE_HEADER
- 修改IMAGE_OPTIONAL_HEADER
PEView打开reloc.exe,如下图所示
- 可以看到
.reloc
节区头从00000270
文件偏移开始,大小为28(270-297) - 而
.reloc
节区偏移地址从0000C000
开始
用winhex打开reloc.exe,如下图所示
- 找到
.reloc
节区头对应位置全改为0 - 找到
.reloc
节区对应位置全删了
修改IMAGE_FILE_HEADER中的Numbers of Sections如下
修改IMAGE_OPTIONAL_HEADER的Size of Image如下
如此就完成了对.reloc
节区的删除
六、UPack PE 文件头分析
1、UPack
UPack是一个叫dwing的中国人编写的
以对PE头独特的变形技法闻名
可以用Stud_PE(可以到吾爱破解下载)查看PE文件头,如下:
2、比较PE文件头
notepad.exe文件头如下:
UPack处理后的notepad.exe文件头如下:
3、分析UPack的PE文件头
(1)重叠文件头
把MZ文件头和PE文件头巧妙重叠
用Stud_PE查看如下:
PE头起始位置由e_lfanew
(在3C
文件偏移处,如上图所示)决定
一般有e_lfanew=MZ文件头大小(40)+DOS存根大小(VC++下是A0)=E0
在UPack中e_lfanew=10
,钻了规则空子,使得PE头和MZ头可以重叠
(2)IMAGE_FILE_HEADER.SizeOfOptionalHeader
SizeOfOptionalHeader
表示PE头中紧接IMAGE_FILE_HEADER
下的IMAGE_OPTIONAL_HEADER
结构体的长度(E0
),它还确定了节区头的起始偏移
UPack将这个值改为148
,比正常值大些(如上图所示),使得节区头从170
开始
这个额外空间可以加入解码代码
(3)IMAGE_OPTIONAL_HEADER.NumbreOfRvaAndSizes
这个值指出紧接着的IMAGE_DATA_DIRECTORY
结构体数组的元素个数,正常是10
UPack改为0A
,如下图所示
但是IMAGE_DATA_DIRECTORY
结构体数组个数就是10
于是UPack修改后,IMAGE_DATA_DIRECTORY
结构体后6个字节被忽略
UPack可以在这6个字节中加入自己的代码,如下图所示:
(4)IMAGE_SECTION_HEADER
IMAGE_SECTION_HEADER
结构体中,UPack会覆盖程序本身不需要的区域
书中整理了下,如下:
(5)重叠节区
UPack的特征之一:可以随意重叠PE节区和文件头
第一节区和第三节区的起始偏移和size都一致,但RVA不同,如下图所示:
为了更好的理解,书中给出了下图:
(6)RVA to RAW
常规变换之前说过,如下:
但是UPack第一个节区的PointerToRawData
是10
,而不是FileAlignment
(200
)的整数倍
于是如果按正常变换会被强制识别为0
,就报错
(7)导入表
先获取导入表地址如下:
- RVA是
271EE
- size是
14
RVA转换为RAW,如下:
找到内容如下:
这里的玄机在于
- 按照规定,导入表由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成,最后以一个内容为NULL的结构体结束
- 但是上图框出来的第一个结构体后面既不是第二个结构体,也不是内容为NULL的结束结构体
- 看似违反规定,但是偏移在
200
以下的部分不会映射到第三节区,如下图所示
映射之后,27200开始就全是NULL了,这就符合规定了
简单讲,就是映射到内存里之后,是符合规定的;但是单看文件似乎是损坏的
七、“内嵌补丁”练习
1、内嵌补丁
内嵌补丁(Inline Patch):难以直接修改指定代码时,插入并运行洞穴代码(Code Cave)后,对程序打补丁,如下图所示,很直观,很好理解
代码补丁和内嵌补丁的区别如下:
2、练习:Patchme
名为ap0x的逆向分析者制作的小程序,运行如下:
扔进OD,401007
之后都是加密的
尝试search for strings
显然是加密了的
那就调试吧
可以看到三个循环
仔细观察
第一个循环里的4010A3
调用后两个循环
对4010F5-401248
进行解密
他的代码结构大致如下
加补丁代码的方法
- 设置到文件的空白区域,补丁代码较少时选这个
- 扩展最后节区后设置
- 添加新节区后设置
修改完后的一些地方如下
结语
对PE头和两种加壳有了初步了解