第18章 UPack PE文件头详细分析

本文最后更新于:2022年5月19日 晚上

第18章UPack PE文件头详细分析
UPack (UltimatePE压缩器)是一款PE文件的运行时压缩器,其特点是用一种非常独特的方式对PE头进行变形。UPack会引起诸多现有PE分析程序错误,因此各制作者(公司)不得不重新修改、调整程序。也就是说,UPack使用了一些划时代的技术方法,详细分析UPack可以把对PE头的认识提升到一个新层次。本章将完全颠覆大家之前对PE头的了解,在学习更多知识的同时,进一步感受代码逆向分析的乐趣与激情。

18.1 UPack 说明

UPack是一个名叫dwing的中国入编写的PE压缩器。

网址:http://wex.cn/dwing/mycomp.htm
UPack 0.39 Final: http://wex.cn/dwing/downloadAJpack039.7z

UPack的制作者对PE头有深刻认识,由其对Windows OS PE装载器的详细分析就可以推测出来。许多PE压缩器中,UPack都以对PE头的独特变形技法而闻名。初次查看UPack压缩的文件PE头时,经常会产生“这是什么啊?这能运行吗?”等疑问,其独特的变形技术可窥一斑。

UPack刚出现时,其对PE头的独特处理使各种PE实用程序(调试器、PE Viewer等)无法正常运行(经常非正常退出)。

这种特征使许多恶意代码制作者使用UPack压缩自己的恶意代码并发布。由于这样的恶意代码非常多,现在大部分杀毒软件干脆将所有UPack压缩的文件全部识别为恶意文件并删除(还有几个类似的在恶意代码中常用的压缩器)。

理解下面所有内容后再亲自制作PE Viewer或PE压缩器/Crypter,这样就能成为PE文件头的专家了,以后无论PE头如何变形都能轻松分析。

详细分析UPack前要先关闭系统中运行的杀毒软件的实时监控功能(大部分杀毒软件会将UPack识别为病毒并删除),分析完成后再打开。

18.2使用 UPack 压缩 notepad.exe

使用 Windows XP SP3中的 notepad.exe 程序

下面使用UPack 0.39 Final版本压缩notepad.exe。首先将upack.exe与notepad.exe复制到合适的文件中(参考图18-1),然后在命令行窗口输入命令压缩文件(压缩命令带有几个参数,但这里使用默认(default)参数即可),如图18-2所示。

图18-2

UPack会直接压缩源文件本身,且不会另外备份。因此,压缩重要文件前一定要先备份。

运行时压缩完成后,文件名将变为notepad_upack.exe。接下来使用PEView查看,如图18-3所示。

图18-3

这里使用的是PEView的最新版本(0.9.8),但是仍然无法正常读取PE文件头(没有 IMAGE_OPTIONAL HEADER.IMAGE_SECTION_HEADER等的信息)。而在旧版PEView中,程序干脆会非正常终止退岀。

18.3使用Stud_PE工具

由于最强大的PE Viewer工具PEView无法正常运行,下面再向各位介绍一款类似的PE实用工具Stud_PE。

网址:http://www.cgsoftlabs.ro
Stud PE: http://www.cgsoftlabs.ro/zip/Stud_PE.zip

最新版本为2.6.0.1,在版本2.4.0.1更新说明中有一条针对UPack的说明,如图18-4所示。

图18-4

更新说明中指岀,针对Upack的RVA2RAW功能已得到修改(UPack到处制造麻烦)。图18-5是最新版Stud_PE的运行界面。

图18-5

Stud_PE的界面结构要比PEView略复杂一些,但它拥有其他工具无法比拟的众多独特优点(也能很好地显示UPack )。分析UPack文件的PE头时将对Stud_PE进行更加详细的说明。

18.4比较PE文件头

先使用Hex Editor打开2个文件(notepad.exe、notepad_upack.exe),再比较其PE头部分。

18.4.1原 notepad.exe 的 PE 文件头

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

图18-6

18.4.2 notepad_upack.exe运行时压缩的PE文件头

如图18-7所示,notepad_upx.exe的PE头看上去有些奇怪。MZ与PE签名贴得太近了,并且没有DOS存根,出现了大量字符串,中间好像还夹杂着代码。总之,整个文件不对劲的地方太多了。下面详细分析UPack中使用的这种独特的PE文件头结构。

图18-7

18.5分析UPack的PE文件头

18.5.1 重叠文件头

重叠文件头也是其他压缩器经常使用的技法,借助该方法可以把MZ文件头(IMAGE_DOS_HEADER )与PE文件(IMAGE_NT_HEADERS )巧妙重叠在一起,并可有效节约文件头空间。当然这会额外增加文件头的复杂性,给分析带来很大困难(很难再使用PE相关工具)。

下面使用Stud_PE看一下MZ文件头部分。请按Headers选项卡的Basic HEADERS tree view in hexeditor按钮,如图18-8所示。

MZ文件头(IMAGE_DOS_HEADER )中有以下2个重要成员

1
2
(offset  0) emagic : Magic number = 4D5A('MZ')
(offset 3C) 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文件头重叠在一起。

18.5.2 IMAGE_FILE_HEADER.SizeOfOptionalHeader

修改IMAGE_FILE_HEADER.SizeOfOptionalHeader的值,可以向文件头插入解码代码。

SizeOfOptionalHeader表示PE文件头中紧接在IMAGE_FILE_HEADER下的IMAGE_OPTIONAL_
HEADER结构体的长度(E0)。UPack将该值更改为148,如图18-9所示(图中框选的部分)。

图18-9

此处会产生一个疑问。由字面意思可知,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的起始偏移加上SizeOfDptionalHeadeHS后的位置幵始才是IMAGE_SECTION_HEADER。

UPack把SizeOfOptionalHeader的值设置为148,比正常值(E0或F0)要更大一些。所以IMAGE_SECTION_HEADER是从偏移 170开始的(IMAGE_OPTIONAL_HEADER的起始偏移(28)+SizeOfOptionalHeader(148)=170)。

UPack的意图是什么?为什么要改变这个值(SizeOfOptionalHeader)呢?

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

下面查看一下该区域。IMAGE_OPTIONAL_HEADER结束的位置为D7, IMAGE_SECTION_HEADER的起始位置为170。使用Hex Editor查看中间的区域,如图18-10所示。

补充:在查看时需要注意在Stud_PE中,Optional Header的范围标识并没有DATA_DIR。这里要是基础不牢很容易对D7这个位置产生怀疑

image-20211214205345372

图18-10

使用调试器查看反汇编代码,如图18-11所示。

图18-11

图18-11并不是PE文件头中的信息,而是UPack中使用的代码。若PE相关实用工具将其识别为PE文件头信息,就会引发错误,导致程序无法正常运行。

18.5.3 IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes

从IMAGE_OPTIONAL_HEADER结构体中可以看到,其NumberOfRvaAndSizes的值也发生了改变,这样做的目的也是为了向文件头插入自身代码。

NumberOfRvaAndSizes值用来指岀紧接在后面的IMAGE_DATA_DIRECTORY结构体数组的元素个数。正常文件中IMAGE_DATA_DIRECTORY数组元素的个数为10,但在UPack中将其更改为了A个(参考图18-12中的框选区域)。

图18-12

IMAGE_DATA_DIRECTORY结构体数组元素的个数已经被确定为10,但PE规范将NumberOfRvaAndSizes值作为数组元素的个数(类似于前面讲解过的SizeOfOptionalHeader )。所 以UPack中IMAGE_DATA_DIRECTORY结构体数组的后6个元素被忽略。

表18-1中已经对IMAGE_DATA_DIRECTORY结构体数组的各项进行了说明。其中粗斜体的项如果更改不正确,就会引发运行错误。

索引 内容
0 Export Directory
1 Import Directory
2 Resource Directory
3 Exception Directory
4 Security Directory
5 Base Relocation Table
6 Debug Directory
7 Architecture Specific Data
8 RVA of GP
9 TLS Directory
10 Load Configuration Directory
11 Bound Import Directory in headers
12 Import Address Table
13 Delay Load Import Descriptors
14 COM Runtime descriptor

UPack将IMAGE_OPTIONAL_HEAD£R.NumberOfRvaAndSizes的值更改为A,从LOAD_CONFIG项(文件偏移D8以后)开始不再使用。UPack就在这块被忽视的IMAGE_DATA_ DIRECTORY区域中覆写自己的代码。UPack真是精打细算,充分利用了文件头的每一个字节。
接下来使用Hex Editor查看IMAGE DATA DIRECTORY结构体数组区域,如图18-13所示。

图18-13

图18-13中淡色显示的部分是正常文件的IMAGE_DATA_DIRECTORY结构体数组区域,其下深色显示的是UPack忽视的部分(D8~107区域=LOAD_CONFIG Directory之后)。使用调试器查看被忽视的区域,将看到UPack自身的解码代码,如图18-11所示。

另外,NumberOfRvaAndSizes的值改变后,在OllyDbg中打开该文件就会弹岀如图18-14所示的错误消息框。

OllyDbg检查PE文件时会检查NumberOfRvaAndSizes的值是否为10,这个错误信息并不重要,可以忽略。使用其他插件也可完全删除,仅供参考。

18.5.4 IMAGE_SECTION_HEADER

IMAGE_SECTION_HEADER结构体中,Upack会把自身数据记录到程序运行不需要的项目。这与UPack向PE文件头中不使用的区域覆写自身代码与数据的方法是一样的(PE文件头中未使用的区域比想象的要多)。

在前面的学习中,我们已经知道节区数是3个,IMAGE_SECTION_HEADER结构体数组的起始位置为170。下面使用Hex Editor查看IMAGE_SECTION_HEADER结构体(偏移170~1E7的区域),如图18-15所示。

图18-15

图18-15显示的即是IMAGE_SECTION_HEADER的结构体,为便于查看,将其中数据整理如下(使用的是我亲自制作的PE Viewer )。

image-20211214210042054

代码18-1框选的结构体成员对程序运行没有任何意义。比如文件偏移1B0地址处的offset to relocations值为0100739D,它为原notepad.exe的EP值。此外,节区头中还隐藏着一些秘密(马上就会讲到)。

18.5.5重叠节区

UPack的主要特征之一就是可以随意重叠PE节区与文件头(刚刚学过PE文件头基础知识的朋友可能会对这种技法感到惊慌失措)。

通过Stud_PE提供的简略视图查看UPack的IMAGE_SECTION_HEADER。请选择Stu_PE的 “Section”选项卡,如图18-16所示。

图18-16

从图18-16中可以看到,其中某些部分看上去比较奇怪。首先是第一个与第三个节区的文件起始偏移(RawOffset)值都为10。偏移10是文件头区域,UPack中该位置起即为节区部分。

然后让入感到奇怪的部分是,第一个节区与第三个节区的文件起始偏移与在文件中的大小(RawSize )是完全一致的。但是,节区内存的起始RVA( VirtualOffset)项与内存大小(VirtualSize )值是彼此不同的。根据PE规范,这样做不会有什么问题(更准确地说,PE规范并未明确指出这样做是不行的)。

综合以上两点可知,UPack会对PE文件头、第一个节区、第三个节区进行重叠。仅从数字上很难真正理解其中的含义,为了帮助各位更好地掌握,图18-17描述了UPack重叠的情形。 图18-17左侧描述的是文件中的节区信息,右侧描述的是内存中的节区信息。

image-20211214210703591

根据节区头(IMAGE_SECTION_HEADER )中定义的值,PE装载器会将文件偏移0~IFF的区域分别映射到3个不同的内存位置(文件头、第一个节区、第三个节区)。也就是说,用相同的文件映像可以分别创建岀处于不同位置的、大小不同的内存映像,请各位注意。

文件的头(第一/第三个节区)区域的大小为200,其实这是非常小的。相反,第二个节区( 2nd Section )尺寸(AE28)非常大,占据了文件的大部分区域,原文件(notepad.exe)即压缩于此。另外一个需要注意的部分是内存中的第一个节区区域,它的内存尺寸为14000,与原文件(notepad.exe)的Size of Image具有相同的值。也就是说,压缩在第二个节区中的文件映像会被原样解压缩到第一个节区(notepad的内存映像)。另外,原notepad.exe拥有3个节区,它们被解压到一个节区。

解压缩后的第一个节区如图18-18所示。

image-20211214210833128

重新归纳整理一下,压缩的notepad在内存的第二个节区,解压缩的同时被记录到第一个节区。重要的是,notepad.exe (原文件)的内存映像会被整体解压,所以程序能够正常运行(地址变得准确而一致)。

18.5.6 RVA to RAW

各种PE实用程序对Upack束手无策的原因就是无法正确进行RVA→RAW的变换。UPack的制作者通过多种测试(或对PE装载器的逆向分析)发现了Windows PE装载器的Bug (或者异常处理),并将其应用到UPack。

PE实用程序第一次遇到应用了这种技法的文件时,大部分会出现“错误的内存引用,非正常终止”(后来许多实用程序对此进行了修复)。

首先复习一下RVA→RAW变换的常规方法。

RAW - PointerToRawData = RVA - VirtualAddress
RAW = RVA - VirtualAddress + PointerToRawData
VirtualAddress, PointerToRawData是从RVA所在的节区头中获取的值,它们都是已知值(known value)。

根据上述公式,算一下EP的文件偏移量(RAW)。UPack的EP是RVA 1018(参考图18-19)。

RAW = 1018 - 1000 + 10 = 28

1st Section的VirtualAddress为 1000, PointerToRawData为 10。

使用Hex Editor打开RAW 28区域査看,如图18-20所示。

图18-20

RAW 28不是代码区域,而是(ordinal:010B) “LoadLibraryA”字符串区域。现在UPack的这种 把戏欺骗了我们(实际上,OllyDbg的早期版本并不能找岀UPack的EP)。秘密就在于第一个节区的PointerToRawData值10。

一般而言,指向节区开始的文件偏移的PointerToRawData值应该是FileAlignment的整数倍。UPack的FileAlignment为200,故PointerToRawData值应为0、200、400、600等值。PE装载器发现第一个节区的PointerToRawData ( 10 )不是FileAlignment ( 200 )的整数倍时,它会强制将其识别为整数倍(该情况下为0 )。这使UPack文件能够正常运行,但是许多PE相关实用程序都会发生错误。

正常的RVA→RAW变换如下。

RAW = 1018 - 1000 + 0 = 18

PointerToRawData被识别为0。

使用调试器查看相应区域的代码,如图18-21所示

图18-21

现在各位应该能够对UPack文件进行正常的RVA→RAW换算了。

18.5.7 导入表(IMAGEJMPORT—DESCRIPTOR array)

UPack的导入表(Import Table )组织结构相当独特(暗藏玄机)。

下面使用Hex Editor查看IMAGE_IMPORT_DESCRIPTOR结构体。首先要从Directory Table中获取IDT (IMAGE_IMPORT_DESCRIPTOR结构体数组)的地址,如图18-22所示。

图18-22

图18-22右侧框选的8个字节大小的data就是指向导入表的IMAGE_DATA_DIRECTORY结构体。前面4个字节为导入表的地址(RVA),后面4个字节是导入表的大小(Size)。从图中可以看到导入表的RVA为271EE。

使用Hex Editor查看之前,需要先进行RVA→RAW变换。首先确定该RVA值属于哪个节区,内存地址271EE在内存中是第三个节区(参考图18-23)。

图18-23

进行RVA→RAW变换,如下所示。

RAW = RVA(271EE) - VirtualOffset(27000) + RawOffset(0)= 1EE

注意: 3rd Section的RawOffset值不是10,而会被强制变换为0。

使用Hex Editor查看文件偏移1EE中的数据,如图18-24所示。

图18-24

该处就是使用UPack节区隐藏玄机的地方。

首先看一下代码18-2中IMAGE_IMPORT_DESCRIPTOR结构体的定义,再继续分析(结构体的大小为14h字节)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//@[comment("MVI_tracked")]
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
// In other words, INT(Import Name Table)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
// IAT(Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

根据PE规范,导入表是由一系列IMAGE_IMPORT_DESCRIPTOR结构体组成的数组,最后以一个内容为NULL的结构体结束。 图18-24中所选区域就是IMAGE_IMPORT_DESCRIPTOR结构体数组(导入表)。偏移1EE~201为第一个结构体,其后既不是第二个结构也不是(表示导入表结束的)NULL结构体。

乍一看这种做法分明是违反PE规范的。但是请注意图18-24中偏移200上方的粗线。该线条表示文件中第三个节区的结束(参考图18-23)。故运行时偏移在200以下的部分不会映射到第三个节区内存。下面看一下图18-25。

图18-25

第三个节区加载到内存时,文件偏移0~IFF的区域映射到内存的27000~271FF区域,而(第三个节区其余的内存区域)27200~28000区域全部填充为NULL。使用调试器查看相同区域,如图18-26所示。

图18-26

准确地说,只映射到010271FF,从01027200开始全部填充为NULL值。再次返回PE规范的导入表条件,01027202地址以后出现NULL结构体,这并不算违反PE规范。而这正是UPack使用节区的玄机。从文件看导入表好像是损坏了,但其实它已在内存中准确表现出来。

大部分PE实用程序从文件中读导入表时都会被这个玄机迷惑,査找错误的地址,继而引起内存引用错误,导致程序非正常终止(一句话—这个玄机还真是妙)。

问题:剩下的IMAGE_IMPORT_DESCRIPTOR呢?

18.5.8 导入地址表

UPack都输入了哪些DLL中的哪些API呢?下面通过分析IAT查看。把代码18-2的IMAGE_IMPORT_DESCRIPTOR结构体与图18-24进行映射后,得到下表18-2。

表18-2 UPack的 IMAGE_MPORT_DESCRIPTOR结构体的重要成员

偏移 成员 RVA
IEE OriginalFirstThunk(INT) 0
IFA Name 2
IFE FirstThunk(IAT) 11E8

首先Name的RVA值为2,它属于Header区域(因为第一个节区是从RVA 1000开始的)。Header区域中RVA与RAW值是一样的,故使用Hex Editor查看文件中偏移(RAW)为2的区域,如图18-27所示。

图18-27

在偏移为2的区域中可以看到字符串KERNEL32.DLL。该位置原本是DOS头部分(IMAGE_DOS_HEADER ),属于不使用的区域,UPack将Import DLL名称写入该处。空白区域一点儿都没浪费(好节俭的UPack)。得到DLL名称后,再看一下从中导入了哪些API函数。

一般而言,跟踪OriginalFirstThunk (INT)能够发现API名称字符串,但是像UPack这样,OriginalFirstThunk (INT)为0时,跟踪FirstThunk (IAT)也无妨(只要INT、IAT其中一个有API名称字符串即可)。IAT的值为11E8,由图18-23可知,属于第一个节区,故RVA→RAW换算如下。

RAW = RVA(11E8) - VirtualOffset(1000) + RawOffset(0) = 1E8

注意:1st Section的RawOffset值不是10,而是被强制转换为0。

IAT的文件偏移1E8显示在图18-28中。

图18-28

图18-28中框选的部分就是IAT域,同时也作为INT来使用。也就是说,该处是Name Pointer(RVA)数组,其结束是NULL。此外还可以看到导入了2个API,分别为RVA 28与BE。

RVA位置上存在着导入函数的[ordinal名称字符串],如图18-29所示。由于都是header区域,所以RVA与RAW值是一样的。

从图18-29中可以看到导入的2个API函数,分另为LoadLibraryA与GetProcAddress,它们在形成原文件的IAT时非常方便,所以普通压缩器也常常导入使用。

18.6小结

本章详细讲解了UPack的独特PE文件头相关知识。学习PE文件格式时虽然未涉及各结构体的所有成员,但分析UPack压缩的可执行文件的PE文件头(PE文件头变形得很厉害),会进一步加深大家对PE文件格式的了解。这些内容虽然对初学者有些难,但是如果多努力去理解并掌握这些内容,以后无论遇到什么样的PE文件头都能轻松分析。

Q. UPack压缩器是病毒文件吗?
A. UPack压缩器本身不是恶意程序。但是许多恶意代码制作者用UPack来压缩自己的恶意代
码,使文件变得畸形,所以许多杀毒软件将UPack压缩的文件全部识别为病毒文件并删除。

参考

《逆向工程核心原理》 第18章


第18章 UPack PE文件头详细分析
https://m0ck1ng-b1rd.github.io/1999/02/16/逆向工程核心原理/第18章 UPack PE文件头详细分析/
作者
何语灵
发布于
1999年2月16日
许可协议