逆向工程核心原理第十八章
UPack PE文件头分析
使用UPack压缩abexcm1.exe
使用WinUpack 0.39 Final版本压缩abexcm1.exe。直接拖拽abexcm1.exe到WinUpack,按以下勾选参数进行压缩,其中默认是没有清除重定位表的,这里勾选上以便复现原文。将压缩后abexcm1.exe重命名为abexcm1_upack.exe
下图为使用PE View查看的结果,这里用的是PE View 0.9.9版,这个版本仍无法正常读取PE文件
使用Stud_PE工具
下面使用Stud_PE查看abexcm1_upack.exe
可以看到Stud_PE界面要比PE View界面复杂一些,但它拥有其他工具无法比拟的众多独特优点(也能更好地显示Upack)
比较PE文件头
使用HEdit打开abexcm1.exe和abexcm1_upack.exe,再比较它们PE头部分
下图为原abexcm1.exe的PE文件头,其中数据按照IMAGE_DOS_HEADER、DOS Stub、IMAGE_NT_HEADERS、IMAGE_SECTIONS_HEADER顺序排列
下图为abexcm1_upack.exe的PE文件头,可以看到abexcm1_upack.exe的PE头看上去有些奇怪。MZ与PE签名贴得太近了,并且没有DOS存根,出现了大量字符串,中间好像还夹杂着代码
分析UPack的PE文件头
重叠文件头
重叠文件头也是其他压缩器经常使用的技法,借助该方法可以把MZ文件头(IMAGE_DOS_HEADER)与PE文件头(IMAGE_NT_HEADERS)巧妙重叠在一起,并可有效节约文件头空间。
下面使用Stud_PE看一下MZ文件头部分,在文件头选项卡中,选择"在十六进制编辑器中以树形结构查看文件头"
MZ文件头(IMAGE_DOS_HEADER)中有以下2个重要成员,其余成员都不怎么重要(对程序运行没有任何意义)
- e_magic:Magic number = 4D5A("MZ")
- e_lfanew:File address of new exe header
问题在于,根据PE文件格式规范,IMAGE_NT_HEADERS的起始位置是"可变的"。换言之,IMAGE_NT_HEADERS的起始位置是由e_lfanew的值决定的。一般在一个正常程序中,e_lfanew拥有如下所示的值(不同的构建环境会有不同)
e_lfanew = MZ文件头大小 (40) + DOS存根大小 (可变: VC++下为A0) = E0
UPack中的e_lfanew的值为10,这并不违反PE规范,只是钻了规范的空子。像这样就可以把MZ文件头和PE文件头重叠在一起
IMAGE_FILE_HEADER.SizeOfOptionalHeader
修改IMAFE_FILE_HEADER.SizeOfOptionalHeader的值,可以向文件头插入解码代码。
SizeOfOptionalHeader表示PE文件头中紧接在IMAGE_FILE_HEADER下的IMAGE_OPTIONAL_HEADER结构体的长度(E0)。如下图所示,Upack将该值更改为148
这里会产生一个疑问,IMAGE_OPTIONAL_HEADER是结构体,PE32文件格式中其大小已经被确定为E0。既然如此,PE文件格式的设计者们为何还要另外输入IMAGE_OPTIONAL_HEADER结构体的大小呢?原本的设计意图是,根据PE文件形态分别更换并插入其他IMAGE_OPTIONAL_HEADER形态的结构体。简单来说,就是由于IMAGE_OPTIONAL_HEADER的种类很多,所以需要另外输入结构体大小(例如:64位PE32+的IMAGE_OPTIONAL_HEADER结构体的大小为F0)
SizeOfOptionalHeader的另一层含义是确定节区头(IMAGE_SECTION_HEADER)的起始偏移
仅从PE文件头来看,紧接着IMAGE_OPTIONAL_HEADER的好像就是IMAGE_SECTION_HEADER。但实际上(更准确地说),从IMAGE_OPTIONAL_HEADER的起始偏移加上SizeOfOptionalHeader值后的位置开始才是IMAGE_SECTION_HEADER
Upack把SizeOfOptionalHeader的值设置为148,比正常值(E0或F0)要更大一些。所以IMAGE_SECTION_HEADER是从偏移170开始的(IMAGE_OPTIONAL_HEADER起始偏移(28)+SizeOfOptionalHeader(148)=170)
Upack这么修改的原因是:把PE文件头变形,向文件头适当插入解码需要的代码,增大逆向分析的难度
增大SizeOfOptionalHeader的值后,就在IMAGE_OPTIONAL_HEADER与IMAGE_SECTION_HEADER之间添加了额外空间,Upack就向这个区域添加解码代码。
下面查看该区域,IMAGE_OPTIONAL_HEADER结束位置为D7,IMAGE_SECTION_HEADER的起始位置为170。使用HEdit查看中间区域,如下图所示
使用Ollydbg查看反汇编代码,如下图所示,这部分信息不是PE头文件中的信息,而是UPack中使用的代码。若PE相关实用工具将其识别为PE文件头信息,就会引发错误,导致程序无法正常运行
IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes
从IMAGE_OPTIONAL_HEADER结构体中可以看到,其NumberOfRvaAndSizes的值也发生了改变,这样做的目的也是为了向文件头插入自身代码
NumberOfRvaAndSizes值用来指出紧接在后面的IMAGE_DATA_DIRECTORY结构体数组的元素个数。正常文件中IMAGE_DATA_DIRECTORY数组元素的个数为10h,但在UPack中将其更改为了Ah个。
IMAGE_DATA_DIRECTORY结构体数组元素的个数已经被确定为10,但PE规范将NumberOfRvaSizes值作为数组元素的个数。所以UPack中IMAGE_DATA_DIRECTORY结构体数组的后6个元素被忽略。
下表对IMAGE_DATA_DIRECTORY的各项进行了说明,其中粗体的项如果更改不正确就会引发运行错误
索 引 内 容 索 引 内 容 0 EXPORT Directory 8 GLOBALPTR Directory 1 IMPORT Directory 9 TLS Directory 2 RESOURCE Directory A LOAD_CONFIG Directory 3 EXCEPTION Directory B BOUND_IMPORT Directory 4 SECURITY Directory C IAT Directory 5 BASERELOC Directory D DELAY_IMPORT Directory 6 DEBUG Directory E COM_DESCRIPTOR Directory 7 COPYRIGHT Directory F Reserved Directory Upack将IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes的值更改为A,从LOAD_CONFIG Directory项(文件偏移D8以后)开始不再使用。UPack就在这块被忽视的IMAGE_DATA_DIRECTORY区域中覆写自己的代码。
接下来使用HEdit查看IMAGE_OPTIONAL_HEADER结构体数组区域
IMAGE_SECTION_HEADER
IMAGE_SECTION_HEADER结构体中,Upack会把自身数据记录到程序运行不需要的项目。这与UPack向PE文件头中不使用的区域覆写自身代码与数据的方法是一样的
下面使用HEdit查看IMAGE_SECTION_HEADER结构体,UPack把代码数据存放在IMAGE_SECTION_HEADER结构体的offset to relocations、offset to line numbers、number of relocations、number of line numbers这四个成员中,因为这四个成员本身对程序运行没有任何意义
重叠节区
UPack的主要特征之一就是可以随意重叠PE节区与文件头,下面通过Stud_PE查看UPack的IMAGE_SECTION_HEADER
从图可以看到,某些部分看上去比较奇怪。首先是第一个节区与第三个节区的文件起始偏移(RawOffset)值都为10。偏移10是文件头区域,UPack中该位置起即为节区部分。
另一奇怪的地方是,第一个节区和第三个节区的文件起始偏移与在文件中的大小(RawSize)是完全一致的。但是,节区内存的起始RVA(VirtualOffset)项与内存大小(VirtualSize)值是彼此不同的。根据PE规范,这样做不会有什么问题。
综合以上两点可知,UPack会对PE文件头、第一个节区、第三个节区进行重叠,三者重叠示意图如下。根据节区头(IMAGE_SECTION_HEADER)中的定义,PE装载器会将文件偏移0~1FF的区域分别映射到3个不同的内存位置(文件头、第一个节区、第三个节区)。也就是说,用相同的文件映像可以分别创建出处于不同位置的、不同大小的内存映像
文件的头(第一/第三节区)区域的大小为200,第二个节区大小为5A0(占据了文件的大部分区域),原文件(abexcm1.exe)就压缩于此
另一点需要注意的是内存中的第一个节区区域,它的内存尺寸为6000,与原文件(abexcm1.exe)的Size of Image具有相同的值。也就是说,压缩在第二个节区中的文件映像会被原样解压缩到第一个节区(abexcm1的内存映像)。另外,原abexcm1.exe拥有5个节区,它们被解压到一个节区。
RVA to RAW
各种PE实用程序对Upack束手无策的原因就是无法正确进行RVA=>RAW的变换。
首先复习下常规的RVA=>RAW方法
1
2
3RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
VirtualAddress、PointerToRawData是从RVA所在节区头中获取的值,它们都是已知值根据上述公式,算一下EP的文件偏移量(RAW)。
UPack的EP是RVA 1018,RVA 1018位于第一个节区,将其代入公式,换算如下
1
RAW = 1018 - 1000 + 10 = 28
用HEdit查看文件偏移28处,如下图所示,可以看到RAW 28不是代码区域,而是(ordinal:010B)"LoadLibraryA"字符串区域。
造成上面这个问题原因在于第一个节区的PointerToRawData值10。一般而言,指向节区开始的文件偏移的PointerToRawData值应该是FileAlignment的整数倍。UPack的FileAlignment为200,故PointerToRawData值应为0、200、400、600等值。PE装载器发现第一个节区的PointerToRawData(10)不是FileAlignment(200)的整数倍时,它会强制将其识别为整数倍(该情况下为0)。这使UPack文件能够正常运行,但是许多PE相关实用程序都会发生错误。正确的RVA=>RAW变换如下
1
2RAW = 1018 - 1000 + 0 = 18
PointerToRawData倍识别为0使用Ollydbg查看相应区域的代码,如下图所示
导入表(IMAGE_IMPORT_DESCRIPTOR array)
UPack的导入表(Import Table)组织结构相当独特(暗藏玄机)
下面使用HEdit查看IMAGE_IMPORT_DESCRIPTOR结构体。首先要从Directory Table中获取IDT(IMAGE_IMPORT_DESCRIPTOR结构体数组)的地址,如下图所示,图中框选的8个字节大小的数据就是指向导入表的IMAGE_IMPORT_DESCRIPTOR结构体。前四个字节为导入表地址(RVA),后面四个字节为导入表的大小(Size)。从图中可知导入表的RVA为F1EE
使用HEdit查看之前,需要先进行RVA=>RAW变换。首先确定该RVA值属于哪个节区,内存地址F1EE在内存中是第三个节区,参考下图
进行RVA=>RAW变换,如下所示
1
2RAW = RVA(F1EE) - VirtualOffset(F000) + RawOffset(0) = 1EE
这里同样地第三个节区的RawOffset不是10,而是被强制变换为0使用HEdit查看偏移1EE中的数据,如下图所示,该处就是UPack暗藏玄机的地方
首先看一下IMAGE_IMPORT_DESCRIPTOR结构体的定义,再继续分析(结构体的大小为20字节)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk; // INT(Import Name Table) address (RVA)
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // library name string address (RVA)
DWORD FirstThunk; // IAT(Import Address Table) address (RVA)
} IMAGE_IMPORT_DESCRIPTOR;根据PE规范,导入表是由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成的数组,最后以一个内容为NULL的结构体结束。
文件偏移1EE~201为第一个结构体,其后既不是第二个结构体,也不是(表示导入表结束的)NULL结构体。乍一看这种做法分明是违反PE规范的。但是偏移1FF处为第三个节区的结束,因此文件偏移在200以后的部分不会映射到第三个节区内存。
第三个节区加载到内存时,文件偏移0~1FF到区域映射到内存的F000~F1FF区域,而(第三个节区其余内存区域)F200~10000区域全部填充为NULL。使用调试器查看相同区域,如下图所示
准确地说,只映射到0040F1FF,从0040F200开始全部填充为NULL值。
再次返回PE规范的导入表条件,0040F202地址以后出现NULL结构体,这并不算违反PE规范。这就是UPack使用节区的玄机。从文件看导入表好像是损坏了,但是其实它已在内存中准确表现出来。
导入地址表
UPack都输入了哪些DLL中的哪些API呢?下面通过分析IAT查看
偏 移 成 员 RVA 1EE OriginalFirstThunk(INT) 0 1FA Name 2 1FE FirstThunk(IAT) 11E8 首先Name的RVA值为2,它属于Header区域(因为第一个节区是从RVA 1000开始的)
Header区域中RVA值与RAW值是一样的,故使用Hex Editor查看文件中偏移(RAW)为2的区域,如下图所示
在偏移为2的区域中可以看到字符串KERNEL32.DLL。该位置原本是DOS头部分(IAMGE_DOS_HEADER),属于不使用区域,UPack将ImportDLL名称写入该处。得到DLL名称后,再看一下从中导入了哪些API函数
一般而言,跟踪OriginalFirstThunk(INT)能够发现API名称字符串,但是像UPack这样,OriginalFirstThunk(INT)为0时,跟踪FirstThunk(IAT)也是可以的(只要INT、IAT其中一个有API名称字符串即可)。IAT值为11E8,属于第一个节区,故RVA=>RAW换算如下
1
RAW = RVA(11E8) - VirtualOffset(1000) + RawOffset(0) = 1E8
用HEdit查看文件偏移1E8,如下图所示,图中高亮部分就是IAT区域,同时也作为INT来使用。也就是说,该处是Name Pointer(RVA)数组,其结束是NULL。
此外可以看到导入了2个API,分别为RVA 28与BE。RVA位置上存在着导入函数的[ordinal+名称字符串],由于都是header区域,所以RVA与RAW值是一样的
从图中可以看到导入的2个API函数分别为LoadLibrary和GetProcAddress,它们在形成原文件的IAT时非常方便,所以普通压缩器也常常导入使用。
逆向工程核心原理第十九章
UPack调试 - 查找OEP
使用x64dbg调试
x64dbg是类似于Ollydbg的开源调试器,而且由于Ollydbg早已停止更新,所以x64dbg也是Ollydbg的替代品
下载最新版x64dbg,因为调试的是32位程序,所以运行时应该选择x86dbg.exe来调试
下图为x86dbg运行界面,可以看到软件界面跟Ollydbg非常相似,下面就用x86dbg来调试UPack压缩的abexcm1_upack.exe
EntryPoint
x86dbg默认停住运行的地方不是EntryPoint,但是x86dbg能识别出程序的EntryPoint(UPack的也能正确识别)并设置断点,点击断点选项卡,查看设置的断点
从图可以看到x86dbg默认在EP上设置了一个一次性的断点,并且准确识别了UPack的EP "00401018"
接着,我们F9让程序运行到EP处,可以看到程序在"00401018"处停下
解码循环
所有压缩器中都存在解码循环(Decoding Loop)。如果明白压缩/解压算法本身就是由许多条分支语句和循环构成的,那么就能理解为何解码循环看上去如此复杂。
调试这样的循环时,应该适当跳过条件分支语句以跳出某个循环。
UPack把压缩后的数据放到第二个节区,再运行解码循环将这些数据解压缩后放到第一个节区。下面从EP代码开始调试,如下图所示
前两条指令用于从004011B0处读取4个字节,然后保存到EAX寄存器。EAX拥有的值为"00401000",它就是原本abexcm1.exe的OEP(lodsd指令,相当于这2句汇编代码mov eax,[esi],esi=esi+4;该指令从esi所指的地址处读取4字节储存到EAX寄存器)。这里可以直接设置硬件断点,然后F9运行,程序就会在OEP处暂停。
我们的目标时提高调试水平,所以这里继续调试,经过一阵调试后,会出现如下图所示的函数调用代码
此时ESI的值为00407443,该地址就是decode()函数的地址,后面会反复调用执行该函数。F7跟进查看该函数代码
仅从这部分来看,还搞不清楚这段代码的用途。继续调试遇到如下图所示的代码
004075CF与004075D5地址处有"向EDI所指位置写入内容"的指令。此时EDI值指向第一个节区中的地址。也就是说,这些命令会先执行解压缩操作,然后写入实际内存。在004075D6处与004075D9地址处通过CMP/JB指令继续执行循环,直到EDI值为0040604C([ESI+34]=0040604C)
设置IAT
一般而言,压缩器执行完解码循环后会根据原文件重新组织IAT。UPack也有类似的过程,如下图所示,UPack会使用导入的2个函数(LoadLibrary和GetProcAddress),边执行循环边构建原abexcm1.exe的IAT(先获取abexcm1.exe导入函数的实际内存地址,再写入原IAT区域)。
该过程结束后,由00407627地址处的RETN命令将运行转到OEP处,如下图所示