逆向工程核心原理(16-18)
简介:运行时压缩是软件逆向分析学的常见主题,为了理解好它,需要掌握有关PE文件格式,操作系统的基本知识,同时也要了解有关压缩/解压缩算法的基本内容.
数据压缩
不论哪种形态的文件,都是由二进制组成的,只要使用合适的压损算法,就能减缩其大小,若不能复原,就称为有损压缩.
无损压缩
无损压缩用来缩减文件的大小,压缩后的文件更易保管,移动.使用经过压缩的问及只能之前,需要先对文件解压缩.
有损压缩
相反,有损压缩允许压缩文件(数据)时损失一定信息,以此换取高压缩率.压缩多媒体文件,大部分都使用这种有损压缩的方式.从压缩特性来看,有损压缩的数据解压缩后不能完全恢复原始数据
运行时压缩器
顾名思义,就是针对可执行文件而言的,可执行文件内部含有解压代码,文件在运行瞬间于内存中解压缩后执行.
在程序EP代码中执行解压程序,同时在内存中解压缩后执行.
压缩器
#1,使用目的
`缩减PE文件的大小
文件尺寸是其突出的优点之一,更便于网络传输与保存.
`隐藏PE文件的内部代码于资源
#2使用现状
>>>>
保护器
PE保护器是一类保护PE文件免受代码逆向分析的实用程序.他们不像普通的压缩器一样仅对PE文件进行运行时压缩,而应用了多种防止代码逆向分析的技术(反调试,反模拟,代码混淆,多态代码,垃圾代码,调试器监控等)
目的:
(1)防止破解
(2)保护代码与资源
运行时压缩测试

PE头的大小一样,节区名称改变.第一个节区的RawDataSize = 0(文件中的大小为0)
EP位于第二个节区
资源节区几乎无变化.
第15章 调试UPX压缩的notepad程序
notepad.exe的EP代码

notepad_upx.exe的PE代码

Ep地址为01015330,该处即为第二个节区的末端部分,实际压缩的notepad源代码存在于EP地址的上方.
首先将eax-edi寄存器的值保存到栈,然后分别把第二个节区的起始地址与第一个节区的起始地址设置到esi与edi寄存器,upx文件第一个节区仅存在于内存,该处即是解压缩后保存源文件代码的地方.
调试时像这样同时设置esi与edi,就能预见从esi所指缓冲区到edi所指缓冲区的内存发生了复制.此时从source读取数据,解压缩后保存到Destination,我们的目标是跟踪全部upx ep代码,并最终找到原notepad的ep代码
跟踪upx文件
整个压缩过程由无数个循环组成。因此,只有适当跳出循环才能加快速度
ollydbg中另外提供的跟踪调试命令
如表所示

循环#1
ctrl+f8开始跟踪代码,开始后不久会遇到一个段循环。暂停跟踪(f7),仔细查看相应循环。

循环次数ecx = 36B,循环内容,从[edx]中读取一个字节写入[EDI]
edi寄存器所指的01001000地址即是第一个节区的起始地址,仅存在于内存中的节区。遇到这类循环应当跳出。
#循环2
继续跟踪遇到第二个循环,我们在循环后面下断点跳出循环。
循环*#3
继续跟踪代码就会遇到第三个循环

该段循环代码用于恢复源代码的call/jmp指令的destination地址。
继续跳出。
循环#4

upx压缩原notepad.exe文件时,它会分析其IAT,提取出程序中调用的API名称列表,形成API名称字符串。
用这些名称字符串调用图中GetProcAddress函数,获取API的起始地址,然后把API地址输入EBX寄存器所指的原notepad.exe的IAT区域。该过程会反复进行至API名称字符串结束。


可以看到这里有个popad与刚开始的pushad对应,而且jmp指令跳转到了程序OEP处.
快速查找UPX OEP的方法
在popad指令后的jmp指令处设断点
upx压缩器的特征之一是,其EP代码被包含在pushad和popad指令之间.并且,跳转到OEP代码的JMP指令紧接出现在popad指令之后.只要在JMP指令处设置好断点,运行后就能直接找到OEP.
在栈中设置硬件断点
该方法也利用UPX的pushad/popad指令的特点.
pushad指令将eax到edi寄存器的值一次存放到栈中,我们可以在栈中这个地址上设置硬件断点,在执行popad的瞬间,这个地址被访问,触发硬件断点.


第16章 基址重定位表
PE重定位
向进程的虚拟内存加载PE文件时,文件会被加载到PE头的IMageBase所指的地址处.若加载的是dll文件,且在ImageBase处已经加载了其他dll文件,那么PE装载器就会将其加载到其他未被占用的空间,这就涉及PE文件重定位问题.
DLL/SYS
如图,A.DLL被加载到test.exe进程的10000000地址处,此后,B.dll试图加载到相同地址时,PE装载器将B.DLL加载到另一个尚未被占用的地址处

EXE
创建好进程后,EXE文件会首先加载到内存,所以在exe中无需考虑重定位问题.但是windows Vista之后的版本引入了ASLR安全机制,每次运行exe文件都会被加载到随即地址,这样大大增强了系统安全性.
PE重定位时执行的操作:
这里先不看书自己思考一下:
(1)查看PE文件大小
(2)根据文件大小找到一块空闲的用户内存空间
(3)修改PE文件的基址
(4)载入内存
PE重定位操作原理
(1)查找硬编码的地址位置
(2)读取值后减去ImageBase
(3)加上实际加载地址
其中最关键的就是查找硬编码值的位置.
基址重定位表
位于PE头的DataDirectory数组的第6个元素
IMAGE_BASE_RELOCATION结构体
基址重定位表中罗列了硬编码地址的偏移.

IMAGE_BASE_RELOACTION结构体的第一个成员为VirutalAddress,它是一个基准地址,实际是RVA值.第二个成员为SizeOfblock,指重定位块的大小.最后一项TypeOffset数组不是结构体成员,而是以注释形式存在的,表示在该结构之下会出现WORD类型的数组,并且该数组元素的值就是硬编码在程序中的地址偏移.
基址重定位表的分析方法

由IMAGE_BASE_RELOCATION结构体的定义可知,VirutalAddress成员的值为1000,SizeOfBlock成员的值为150
TypeOffset的值为2个字节,高4位表示type,低12位表示offset,基于VirutalAddress
硬编码地址 = VA+offset
第17章 从可执行文件中删除.reloc节区
.reloc节区
exe形式的PE文件中,"基址重定位表"项对运行没有什么影响.实际上,将其删除后程序仍然正常运行(基址重定位表对dll/sys形式的文件来说几乎是必须的)
VC++中生成的PE文件的重定位节区名为.reloc,删除该节区后文件将正常运行,且文件大小缩减.
reloc.exe
(1)整理reloc节区头.
(2)删除reloc节区
(3)修改IMAGE_FILE_HEADER
(4)修改IMAGE_OPTIONAl_HEADER
1.删除reloc节区头
使用peview打开reloc.exe

reloc节区从文件偏移270处开始,大小为28,用hex工具打开该区域,全部填充0

2.删除.reloc节区
文件.reloc节区的起始偏移量为c000,由此开始一值到文件尾
删除之后,由于尚未修改其他PE头信息,文件暂时无法运行

修改IMAGE_FILE_HEADER
首先要修改节区的个数


修改IMAGE_OPTIONAL_HEADER
删除.relocate节区之后,进程虚拟内存中整个映像就随之减少相应大小.映像大小 存储在IMAGE_OPTIONAL_HEADER-size of Image中,需要修改


可以看出sizeofImage为11000..reloc节区大小为e40,按照Section Alignment扩展后变为1000,所以应当减去1000.

所以修改为10000

第18章 Upack PE文件头详细分析
可以看到使用upack压缩之后就无法正常读取PE信息了

使用Stud_PE工具
比较PE文件头

如图可见,notepad.exe的PE头看上去不太一样,MZ与PE标签很近,并且没有DOS存根,出现了大量字符串,中间好像还夹杂着代码.
分析Upack的PE文件头
重叠文件头
重叠文件头也是其他压缩器经常使用的技法,借助该技法可把MZ文件头与PE文件头巧妙重叠在一起,并可有效节约文件头空间.
下面使用stud_PE来看一下MZ文件头部分.

文件头中有以下2个重要成员
e_magic:MZ
e_lfanew:file address of new exe header
其余成员都不怎么重要(对程序运行没有任何意义)
问题在于,根据PE文件格式规范,IMAGE_NT_HEADERS的起始位置是可变的.换言之,
一般情况下,e_flanew的值为e0,但是这里为10 也可以,就巧妙地把MZ头和PE头重叠在一起了.
IMAGE_FILE_HEADER.SizeOfOptionalHeader
修改IMAGE_FILE_HEADER.SizeOfOptionalHeader的值,可以向文件头插入解码代SizeOfOptionalHeader表示PE文件头中紧接在IMAGE_FILE_HEADER下的IMAGE_OPTIONAL_HEADER结构体的长度(e0),Upack将这个值改为148.

此处会有一个疑问,既然IMAGE_OPTIONAL_HEADER是结构体,PE32文件格式中大小已经确定为e0,为何还要另外输入大小?
因为要兼容其他比如64位的pe文件结构.
它的另一个含义是要确定节区头IMAGE_SECTION_HEADER的起始偏移.
实际上,这两个header并不是紧接着出现的,而是要通过偏移和大小精确计算,
增大这个值之后就可以在中间区域添加解码代码.

从D7到148+28就是解码代码
IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes
这个值也发生了变化,这样做的目的是为了向文件头中插入自身代码
这个值的含义是指出紧接在后面的IMAGE_DATA_DIRECTORY结构体数组的元素个数,正常文件元素个数为10,但在Upack中将其更改为A个.
所以该结构体数组的后面6个元素被忽略.这样就可以在后面6个元素的空间中复写自己的代码.

IMAGE_SECTION_HEADER
该结构体中,Upack会把自身数据记录到程序运行不需要的项目.

我们已经知道节区数是3个,将这三个节区头对应结构体如下图

框选的结构体成员对程序运行没有任何意义.
重叠节区
Upack的主要特征之一就是可以随意重叠PE节区与文件头

这里就可以看到这种特征了,第一个节区与第三个节区的文件起始偏移值都为10.
第一个节区与第三个节区的文件起始偏移与在文件中的大小是完全一致的,但是节区内存的起始RVA与内存大小又不一样.

也就是说原文件会被压缩在第2个节区,但是经过解码后会放到第一个节区.
RVA to RAW
各种PE实用程序对Upack束手无策的原因就是无法正确进行转换,Upack的制作者通过多种测试发现了windowsPE装载器的 bug,并将其应用到Upack.
什么bug呢就是在进行这个转换时会用到PointertoRawData,但是按理来说这个值应当为200的整数倍,但是就算这里填写10,也会被PE装载器强行识别为0,这样不影响执行,但是PE实用程序就无法这样了.
导入表(IMAGE_IMPORT_DESCRIPTOR)
Upack的导入表的组织结构相当独特.
首先获取导入表地址

前面4个字节为导入表的地址,后面4个字节为导入表的大小.
RAW = 271ee-27000+0 =1ee
查看1ee的数据

先看一下导入表结构体定义在分析

导入表就是多个这类结构体组成的数组,最后一个内容为null,图中所选的黑色区域为第一个结构体区域,其后既不是第二个结构体,也不是最后一个结构体.
因为第一个和第三个节区在0-1ff区域,而这里的后边就是第二个节区的内存了,故运行时偏移在200以下的部分不会映射到第三个节区.这里看着也许不符合导入表的 规范,但是当这个被映射到后面的010271ff后,以后的内容全部填充0 ,看起来不就和null结构体一样了吗.
导入地址表

首先name的RVA值为2,它属于header区域,查看内容.

IAT的值为11e8,属于第一个节区,RAW = 11e8-1000+0 = 1e8

也就是该处是name pointer数组,此外还可以看到这里导入了两个windowsAPI函数


浙公网安备 33010602011771号