庖丁解牛之UPack工作原理及实例分析(2)

UPack是软件逆向工程中常见课题;上一篇已经对Runtime压缩基础知识进行了介绍。
本文主要深度剖析了UPack压缩原理,图文并茂,形象生动,对于动态调试方面会在下一篇详细表述。

经过UPack压缩后的文件,其头部和节部分的内容都发生了变化,貌似已经不再符合PE文件格式要求。

二进制文件对比

典型的PE文件头,其中数据按照IMAGE_DOS_HEADER、DOS Stub、IMAGE_NT_HEADERS、IMAGE_SECTION_HEADER顺序排列。

二进制文件

二进制文件

二进制文件2

二进制文件2

单独二进制文件对比发现,文件的大小和文件内容都发生了变化,压缩后的文件表面上看已经不在符合PE文件规范。如下图所示,更直观的对比,文件结构的很多字段都发生了变化,对于一般的实用工具或是调试工具,很难识别出压缩后文件的文件结构。

PE文件

PE文件

重灾区—压缩文件头

UPack以头部压缩著称,将PE文件头部变化的面目全非,宛如去韩国整了一次容。
有PE结构可知PE头部包括了IMAGE_DOS_HEADER、DOS Stub、IMAGE_NT_HEADERS、IMAGE_SECTION_HEADER五部分组成,其中IMAGE_NT_HEADERS有magic、IMAGE_FILE_HEADER、IMAGE_OPTIONAL_HEADER三部分组成。

3.2.1重灾区之DOS头部重叠

重叠文件头将MZ文件头(IMAGE_DOS_HEADER)与PE文件头(IMAGE_NT_HEADERS)巧妙重叠一起,节约文件头空间。

由PE文件头部结构可知(需要知道PE文件头部基础结构),IMAGE_DOS_HEADER结构中两个重要字段e_magic和e_lfanew,第一个字段一般为“MZ”是PE文件最开始的标志,第二个字段标志的是除去IMAGE_DOS_HEADER头部和DOS Stub部分后IMAGE_FILE_HEADER结构的开始位置。

一般情况下e_lfanew = IMAGE_DOS_HEADER(40)+DOS Stub(vc下A0)=0XE0,也就是说一般e_lfanew字段的值为0XE0,对比UPack压缩后的文件e_lfanew字段的值变为0X10,如下图:

PE文件1

PE文件1

二进制查看e_lfanew字段的变化

PE文件2

PE文件2

下图是一个示意图文件的DOS头部压缩前后发生的变化。文件压缩后,DOS Stub部分已经消失,被IMAGE_FILE_HEADER结构占据。

IMAGE_FILE_HEADER

IMAGE_FILE_HEADER

重灾区之IMAGE_FILE_HEADER变形

在IMAGE_FILE_HEADER有一重要字段就是SizeOfOptionHeader;SizeOfOptionHeader表示IMAGE_OPTIONAL_HEADER的大小;修改该值,改变IMAGE_OPTIONAL_HEADER头部的大小,可插入解码代码等内容。

在一般的PE文件中,IMAGE_OPTIONAL_HEADER结构的大小是固定,所以SizeOfOptionHeader字段的值也是固定的,为0XE0,如下图notepad_origin.exe所示

notepad_origin

notepad_origin

在经过UPack压缩后,文件头部的SizeOfOptionHeader变成了0x148,如下图所示

notepad_origin2

notepad_origin2

比正常值0XE0要大的多。SizeOfOptionHeader的另一层含义是确定节区头(IMAGE_SECTION_HEADER)的起始偏移,将该字段的值由0XE0扩大到0x148,无疑增大了IMAGE_OPTIONAL_HEADER结构的大小。

UPack的基本特征就是把PE文件头变形,像非洲人的头发一样拧麻花,向文件头适当插入解码需要的的代码。增大SizeOfOptionHeader的值后,就在IMAGE_OPTIONAL_HEADER与IMAGE_SECTION_HEADER之间添加了额外空间。向这个额外区域添加解码代码,是一种超越文件头常规理解的巧妙方法。

二进制查看该额外区域的数据,该区域的起始位置在IMAGE_OPTIONAL_HEADER的结束位置0XD7(为什么是D7位置,后面会介绍),该区域的结束位置是0x170

二进制0

二进制0

利用调试器查看该区域的部分反汇编代码如下图

二进制1

二进制1

下图是IMAGE_FILE_HEADER压缩前后的格式对比,压缩后的文件头部增大。

IMAGE_FILE1

IMAGE_FILE1

3.2.3 重灾区之IMAGE_OPTIONAL_HEADER变形

对比压缩前后的文件可知,IMAGE_OPTIONAL_HEADER结构中的NumberOfRvaAndSizes字段的值也变化,该字段表示的是数据目录的大小,改变该值也是为了向文件头插入自身代码。正常文件该字段的大小是0x10,也就是IMAGE_OPTIONAL_HEADER结构中最后一部分有有0x10个IMAGE_DATA_DIRECTORY数组,但是在UPack压缩有该值变成了0x0A,也就是说少了6个IMAGE_DATA_DIRECTORY结构数组,每个数组占8个字节。从下图可以看出IMAGE_OPTIONAL_HEADER结构的后半部分IMAGE_DATA_DIRECTORY结构刚好到0Xd7的位置,也即是说从0XD8一直到0x170之间都是作为空闲区域插入代码。如下图插入了代码

notepad_origin3

notepad_origin3

下图是IMAGE_FILE_HEADER压缩前后的格式对比,压缩后的DATA_DIRECTORY结构变小。

IMAGE_FILE2

IMAGE_FILE2

3.2.4 重灾区之IMAGE_SECTION_HEADER重叠变形

前面介绍了文件图的IMAGE_DOS_HEADER、DOS Stub、IMAGE_FILE_HEADER、IMAGE_OPTIONAL_HEADER、DATA_DIRECTORY的变形,接下来就是IMAGE_SECTION_HEADER的变形。

IMAGE_SECTION_HEADER是文件头部的最后的一部分。节区的变化包括了节区区域的增加,节区区域的减少,节区数量的增加等。UPack对PE文件的节区也做了变化,下图是原始notepad文件与压缩后的notepad文件节区对比

notepad

notepad

对比两幅图,变化前后节区的名称,虚拟大小,偏移,文件大小,文件起始偏移等都发生了变化。

查看压缩后的notepad文件的节区,发现第一个节区与第三节区的文件起始偏移与在文件中的大小完全一致。但是节区内存的起始RVA(VirtualOffset)项与内存大小(VirtualSize)值确不同。根据PE规范,这样做不会有什么问题(更准确的说,PE规范并未明确指出这样做不行)。UPck会对PE文件头、第一节区、第三节区进行重叠。

根据节区头中定义的值,PE装载器会将文件偏移0x10-0x200的区域分别映射到内存中的第一、第三节区。也就是说用相同的文件分别创建出不同位置的、大小不同的内存映像。

观察压缩的节区,文件的头部区域大小为200字节。相反,第二个节区大小(AD6C)非常大,原文件就压缩与此。另外需要注意的部分是内存中的第一个节区区域,它的内存尺寸为13000,与原文件的sizeofImage相同,如下图

notepad_origin4

notepad_origin4

文件节区压缩示意图

IMAGE_FILE3

IMAGE_FILE3

也就是说,压缩在第二个节区的文件映像很可能会解压缩到第一个节区。文件开始装入内存后的示意图如下图。

IMAGE_FILE4

IMAGE_FILE4

3.3 暗藏玄机之地址转换

地址转换一直是PE文件学习中一个重点,也是难点。而UPack在利用地址转换变幻时更是暗藏了更深一层的玄机。 首先来复习一下RVA->RAW变换的常规方法。

RAW - PointerToRawData = RVA – VirtualAddress
RAW = RVA – VirtualAddress + PointerToRawData

其中VirtualAddress、PointerToRawData是从RVA所在的节区头中获取的值,是已知值。 以下图为例,

IMAGE_FILE5

IMAGE_FILE5

左边是文件存在于磁盘中,右边文件被装载入内存。在磁盘中文件的各个节区是以FileAlignment字段的值也就是0x200对齐的(每个区大小大于等与0x200,一定是0x200整数倍),在内存中各个节区是以SectionAlignment的值也就是0x1000字节对齐(每个区大小大于等与0x1000,一定是0x1000整数倍)。

尽管文件在磁盘和内存中存放的地址会改变,节区之间的距离也会改变,但是每个节区的大小不会改变,以上图中的data节区为例来解释公式中的几个变量:

RAW=1932;PonterToRawData = 1600;RVA = 1008332;VirtualAddress = 1008000
所以RAW - PointerToRawData = RVA – VirtualAddress
          1932 – 1600 = 1008332 – 1008000
                  RAW = 1008332 – 1008000+1600

回到压缩的notepad文件,如下图,第一节区的PointerToRawData值为0x10,计算一下文件的EP的文件偏移(RAW),

notepad_origin5

notepad_origin5

图中可知EP的在内存中的RVA是1018,同时由下图可知1000对齐地址需要装载到内存的第一节区,那么按照上面的公式计算RAW为:

RAW = 1018-1000+0x10 = 0x28

查看文件偏移0X28处数据是不是EP呢,如下图:

notepad_origin6

notepad_origin6

从图中看出偏移0x28处是字符串“loadlibraryA”的地址,并不是代码数据。UPack彻底戏弄了我们,秘密在于第一个节区的PointerToRawData值为10.

如果PE装载器发现第一个节区的PointerToRawData(0x10)不是FileAlignment(0x200)整数倍时,会强制将其识别为整数倍(该情况下是0)。这使UPack文件能够正常运行。

notepad_origin7

notepad_origin7

正常的RVAraw的变化如下:

RAW = 1018 – 1000 + 0 = 0x18
装载器会将PointerToRawData识别为0.

也就是说文件偏移0x18的EP代码数据将被加载到内存第一个节区的1018处,众所周知,代码在加载到内存时有一个基地址,默认是加载到了0x1000000,那么EP在内存中的真实地址就是0x1001018,如下图所示:

PointerToRawData

PointerToRawData

3.4 暗藏玄机之导入表

在UPack加壳压缩后,notepad.exe的导入表位置,大小,内容也都发生了变化,从下图可以看出,压缩有的notepad.exe的导入表只显示出了两个导入函数。

notepad_origin8

notepad_origin8

在头部DATA_DIRECTORY数组中保存了导入表结构数组IMAGE_IMPORT_DESCRIPTOR的地址,如下图所示

notepad_origin9

notepad_origin9

其中前四个字节表示导入表结果数组的地址,后四个字节表示它的大小。从图中看出导入表的RVA=261EE。 通过RVA-RAW转换,计算该结构在文件中的位置,由地址261EE可知,该内存段属于第三节区,根据上节的原理

RAW = 261EE-26000+0 = 1EE
第三节区的RAWOFFSET值是10,会被强制变换为了0。

在文件中的偏移地址就是1EE,此处是UPack节区的隐藏玄机的地方。

所以导入表的在文件的入口在0x1EE出,IMAGE_IMPORT_DESCRIPTORE结构体大小为0x14个字节,最后以一个内容为NULL的结构体结束。如下图所示

notepad_origin10

notepad_origin10

偏移1EE-201的位置是第一个结构体,映射到第三节区,但是由前面的节区图我们知道,文件偏移200开始的位置被映射到了第二个节区。效果图如下

IMAGE_FILE6

IMAGE_FILE6

也就是说IMAGE_IMPORT_DESCRIPTORE结构体只有一个结构,文件中地址200之后内容已经不属于IMAGE_IMPORT_DESCRIPTORE结构体。这种做法看似已经违反PE规范。明明结构体数量是20个,但是实际只给出了一个结构体。文件200地址之后的内容被映射到了第二节区。这就是此处UPack的玄机之处,这样并不违反PE规范。文件200地址之后的内容被映射到了第二节区,并且第三节区中26000-261FF是映射文件0-1FF的内容,26200开始的数据全部为0,内存中数据内容如下图所示

二进制2

二进制2

未完待续,相关内容链接:
庖丁解牛之UPack工作原理及实例分析(1)
庖丁解牛之UPack工作原理及实例分析(3)

如果您需要了解更多内容,可以
加入QQ群:486207500
直接询问:010-68438880-8669

Spread the word. Share this post!

Meet The Author

Leave Comment