18.Upack PE文件头详细分析

Pasted image 20250402095849
UPack(UltimatePE压缩器)是一款PE文件的运行时压缩器,其特点是用一种非常独特的方
式对PE头进行变形。UPack会引起诸多现有PE分析程序错误,因此各制作者(公司)不得不重新修改、调整程序。也就是说,UPack使用了一些划时代的技术方法,详细分析UPack可以把对PE头的认识提升到一个新层次。

Upack现在用的很少了。所以这章主要是了解。不用太过深入研究

1. Upack说明

UPack是一个名叫dwing的中国人编写的PE压缩器。
网址:http://wex.cn/dwing/mycomp.htm
UPack 0.39 Final:http://wex.cn/dwing/download/Upack039.7z
UPack的制作者对PE头有深刻认识,由其对WindowsOSPE装载器的详细分析就可以推测出来。许多PE压缩器中,UPack都以对PE头的独特变形技法而闻名。初次查看UPack压缩的文件PE头时,经常会产生“这是什么啊?这能运行吗?”等疑问,其独特的变形技术可窥一斑。
UPack刚出现时,其对PE头的独特处理使各种PE实用程序(调试器、PEViewer等)无法正常运行(经常非正常退出)。这种特征使许多恶意代码制作者使用UPack压缩自己的恶意代码并发布。由于这样的恶意代码非常多,现在大部分杀毒软件干脆将所有UPack压缩的文件全部识别为恶意文件并删除(还有几个类似的在恶意代码中常用的压缩器)
理解下面所有内容后再亲自制作PEViewer或PE压缩器/Crypter,这样就能成为PE文件头的专家了,以后无论PE头如何变形都能轻松分析。

Tip

进行Upack调试时最好关闭掉杀软,因为大部分杀软都会把Upack识别为病毒

2. 使用Upack压缩notepad.exe

这里我们使用PEView是无法读取正常的PE文件头的。(没有 IMAGE_OPTIONAL_HEADERIMAGE_SECTION_HEADER 等的信息)
但是用现在的010工具是可以读取的
Pasted image 20250321133815

2.1. 比较PE头

2.1.1. Uapck压缩后的PE头

Pasted image 20250321134213

2.1.2. 原来的PE头

Pasted image 20250321134226
可以发现。 MZ与PE标签靠的很近,而且没有DOS存根 DosSub,出现了大量字符串,中间好像还夹杂着代码

3. 分析UPack的PE文件头

3.1. 重叠文件头

重叠文件头也是其他压缩器经常使用的技法,借助该方法可以把MZ文件头(IMAGE_DOSHEADER)与PE文件头(IMAGE_NT_HEADERS)巧妙重叠在一起,并可有效节约文件头空间。当然这会额外增加文件头的复杂性,给分析带来很大困难(很难再使用PE相关工具)。
下面使用010看一下Dos文件头部分。
Pasted image 20250321134816
DOS头中有两个重要成员

  • DOS签名
  • PE头偏移量
    Pasted image 20250321135106
    发现这个偏移量的值所在的地址竟然在DOS头里面。(这并不违背PE规范,只是转空子而已。这样就可以把DOS头与PE头重叠在一起了)

3.2. IMAGE_FILE_HEADER.SizeOfOptionalHeader

修改 IMAGE_FILEHEADER.SizeOfOptionalHeader 的值,可以向文件头插人解码代码。SizeOfOptionalHeader 表示PE文件头中紧接在 IMAGE_FILE_HEADER 下的 IMAGE_OPTIONAL_HEADER 结构体的长度(E 0)。UPack将该值更改为148,如图所示(图中框选的部分)。
Pasted image 20250321135519
此处会产生一个疑问。由字面意思可知,IMAGE_OPTIONALHEADER是结构体,PE32文件格式中其大小已经被确定为E0。
既然如此,PE文件格式的设计者们为何还要另外输人IMAGE_OPTIONALHEADER结构体的大小呢?原来的设计意图是,根据PE文件形态分别更换并插人其他IMAGE_OPTIONAL_HEADER形态的结构体。简言之,由于IMAGE_OPTIONAL_HEADER的种类很多,所以需要另外输入结构体的大小(比如:64位PE32+的IMAGE_OPTIONAL_HEADER结构体的大小为F0)。

SizeOfOptionalHeader的另一层含义是确定节区头(IMAGE_SECTION_HEADER)的起始偏移。
仅从PE文件头来看,紧接着IMAGE_OPTIONALHEADER的好像是IMAGE_SECTIONHEADER。但实际上(更准确地说),从IMAGE_OPTIONAL_HEADER的起始偏移加上SizeOfOptionalHeader值后的位置开始才是IMAGE_SECTION_HEADER。

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

那么在可选头与节区头之间就会有一处空闲的区域
Pasted image 20250401130851
这段数据代码并不是PE文件头中的信息,而是Upack中使用的代码。 如果吧这个识别为PE头,那么就会报错,导致程序无法正常运行

3.3. IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes

IMAGE_OPTIONAL_HEADER 结构体中可以看到,其 NumberOfRvaAndSizes 的值也发生了改变,这样做的目的也是为了向文件头插人自身代码。
Pasted image 20250401131354
Pasted image 20250401131511
可以发现这里的值是由 10h 变成了 0ah,即把后面的6个结构体数组的元素给忽略掉了
这部分被忽略掉的数据就被用来存储Upack自己的代码了

图中的这部分就是Upack忽视的区域,用来填写自己的解压代码。
Pasted image 20250401132500
使用调试器即可看到Upack自己的解压代码
Pasted image 20250401132804

3.4. IMAGE_SECTION_HEADER

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

我们已经知道了节区头结构体数组的起始位置为170h
节区头的里面的一些结构体是没有任何意义的。UPACk就会占用这部分结构体填写自己的代码进行利用

3.5. 重叠节区

UPack的主要特征之一就是可以随意重叠PE节区与文件头
Pasted image 20250401134349
从图中可以看到,其中某些部分看上去比较奇怪。首先是第一个与第三个节区的文件起始偏移(RawOffset)值都为10。偏移10是文件头区域,UPack中该位置起即为节区部分。
然后让人感到奇怪的部分是,第一个节区与第三个节区的文件起始偏移与在文件中的大小(RawSize)是完全一致的。但是,节区内存的起始RVA(VirtualOffset)项与内存大小(VirtualSize)值是彼此不同的。根据PE规范,这样做不会有什么问题(更准确地说,PE规范并未明确指出这样做是不行的)。

综合以上两点可知,UPack会对PE文件头、第一个节区、第三个节区进行重叠。
根据节区头(IMAGE_SECTION_HEADER)中定义的值,PE装载器会将文件偏移O~1FF的区域分别映射到3个不同的内存位置(文件头、第一个节区、第三个节区)。
也就是说,用相同的文件映像可以分别创建出处于不同位置的、大小不同的内存映像,请各位注意。文件的头(第一/第三个节区)区域的大小为200,其实这是非常小的。相反,第二个节区(2ndSection)尺寸(AE28)非常大,占据了文件的大部分区域,原文件(notepad.exe)即压缩于此
另外一个需要注意的部分是内存中的第一个节区区域,它的内存尺寸为14000,与原文件(notepad.exe)的Size ofImage具有相同的值。也就是说,压缩在第二个节区中的文件映像会被原样解压缩到第一个节区(notepad的内存映像)。另外,原notepad.exe拥有3个节区,它们被解压到一个节区。
Pasted image 20250401171648

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

3.6. RVA to RAW

各种PE实用程序对Upack束手无策的原因就是无法正确进行RVA一RAW的变换。UPack的制作者通过多种测试(或对PE装载器的逆向分析)发现了WindowsPE装载器的Bug(或者异常处理),并将其应用到UPack。
PE实用程序第一次遇到应用了这种技法的文件时,大部分会出现“错误的内存引用,非正常终止”(后来许多实用程序对此进行了修复)

我们可以尝试计算一下Upack的EP是多少。Upack的EP是RVA 1018
Pasted image 20250401174128
然后根据公式计算出 文件偏移
使用010查看raw 28区域处。
Pasted image 20250401174255
可以发现RAW28不是代码区域,而是(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

调试器查看对应区域的代码
Pasted image 20250401174548

3.7. 导入表(IMAGE_IMPORT_DESCRIPTOR array)

UPack的导人表(ImportTable)组织结构相当独特(暗藏玄机)。
下面使用HexEditor查看 IMAGE_IMPORT_DESCRIPTOR 结构体。首先要从 DirectoryTable 中获取IDT(IMAGE_IMPORT_DESCRIPTOR结构体数组)的地址,如图所示。
Pasted image 20250401175127
图右侧框选的8个字节大小的data就是指向导人表的IMAGE_DATA_DIRECTORY结构体。前面4个字节为导人表的地址(RVA),后面4个字节是导人表的大小(Size)。从图中可以看到导人表的RVA为271EE
使用 HexEditor 查看之前,需要先进行 RVA一RAW 变换。首先确定该RVA值属于哪个节区,内存地址 271EE 在内存中是第三个节区。

进行RVA->RAW 的转换

查看文件偏移为1EE处的数据
Pasted image 20250401180036
这部分就是暗藏玄机的地方
根据PE规范,导人表是由一系列 IMAGE_IMPORT_DESCRIPTOR 结构体组成的数组,最后以一个内容为NULL的结构体结束。
图4中所选区域就是 IMAGEIMPORT_DESCRIPTOR 结构体数组(导人表)。偏移1EE~201为第一个结构体,其后既不是第二个结构体,也不是(表示导人表结束的)NULL结构体。
乍一看这种做法分明是违反PE规范的。但是请注意图中偏移200上方的粗线。该线条表示文件中第三个节区的结束。故运行时偏移在200以下的部分不会映射到第三个节区内存。下面看一下图
Pasted image 20250401180051
第三个节区加载到内存时,文件偏移0~1FF的区域映射到内存的27000~271FF区域,而(第三个节区其余的内存区域)27200~28000区域全部填充为NULL。使用调试器查看相同区域,如图所示。
准确地说,只映射到010271FF从01027200开始全部填充为NULL值。再次返回PE规范的导人表条件,01027202地址以后出现NULL结构体,这并不算违反PE规范。而这正是UPack使用节区的玄机。从文件看导人表好像是损坏了,但其实它已在内存中准确表现出来。
大部分PE实用程序从文件中读导人表时都会被这个玄机迷惑,查找错误的地址,继而引起内存引用错误,导致程序非正常终止(一句话一—这个玄机还真是妙)。

3.8. 导入地址表

UPack都输人了哪些DLL中的哪些API呢?下面通过分析IAT查看。把代码18-2的 IMAGE_IMPORT_DESCRIPTOR 结构体进行映射后,得到下表
Pasted image 20250401180406
首先Name的RVA值为2,它属于Header区域(因为第一个节区是从RVA1000开始的)。Header区域中RVA与RAW值是一样的,故使用HexEditor查看文件中偏移(RAW)为2的区域,如图所示。
Pasted image 20250401180516
在偏移为2的区域中可以看到字符串 KERNEL32.DLL。该位置原本是DOS头部分(IMAGE_DOS_HEADER),属于不使用的区域,UPack将 ImportDLL 名称写人该处。空白区域一点儿都没浪费(好节俭的UPack)。得到DLL名称后,再看一下从中导人了哪些API函数。一般而言,跟踪 OriginalFirstThunk(INT)能够发现API名称字符串,但是像UPack这样,OriginalFirstThunk(INT)为O时,跟踪FirstThunk(IAT)也无妨(只要INT、IAT其中一个有API名称字符串即可)。由图可知

IAT 的值为11E8,属于第一个节区,故RVA一RAW换算如下。

看一下文件偏移1e8处的数据
Pasted image 20250401181243
图中框选的部分就是 IAT 域,同时也作为 INT 来使用。也就是说,该处是NamePointer(RVA)数组,其结束是NULL。此外还可以看到导人了2个API,分别为 RVA 28BE。RVA位置上存在着导人函数的[ordinal+名称字符串],如图所示。由于都是header区域,所以RVA与RAW值是一样的。
Pasted image 20250401181652
可以看到导人的2个API函数,分别为 LoadLibraryAGetProcAddress,它们在形成原文件的IAT时非常方便,所以普通压缩器也常常导人使用。