Upack的PE形式乍一看和规范相去甚远,实际上每一处都是精心设计的,在混淆人工分析的同时完美符合PE规范。主要是采取了以下多种手段。
一、重叠PE文件头
MZ文件头中对程序运行有用的只有标志位和偏移03C处NT映像头的起始位置,其余地方都是无影响可随意填充其他内容;依此可将NT映像头与MZ文件头重叠。
二、修改结构长度扩充空余空间
1.在IMAGE_FILE_HEADER结构中,SizeofOptionHeader这个参数代表可选头的大小,由于在PE32中大小是固定的E0,所以当参数大小超过E0时,可选头的剩余部分都不会被PE装载器识别,这部分空间就可以利用来存放解压代码等等。
问题一:既然大小固定,设置这个参数的意义?
为了扩展其他形式的可选头结构。
问题二:大小超过E0,紧接着IMAGE_OPTIONAL_HEADER的IMAGE_SECTION_HEADER的偏移地址不会受影响吗?
不会。其偏移地址的计算仍然是根据SizeofOptionHeader计算,不根据E0计算。
2.在IMAGE_OPTIONAL_HEADER结构中,NumberofRvaAndSize参数代表跟在后面的IMAGE_DATA_DIRECTORY结构数组的个数,同上一样,其个数正常为10个,当超过10个则被装载器忽略。
3.在IMAGE_SECTION_HEADER结构中,存在着一些不需要用到的参数,Upack会把自身数据覆盖这些参数,道理其实同重叠文件头一样。
三、重叠节区
Upack中第一第三节区在文件中的偏移位置和大小完全一致,但是内存大小和内存RVA不一样,也就是用相同的文件映像创造出不同位置、不同大小的内存映像,书中的插图可以很清晰的看出映射关系。
文件中第一三节区都比较小,第二节区占绝大部分,且第一节区的内存大小和notepad的sizeofmage大小一致,所以可以推测出源程序被压缩在第二节区,经过解压后写入第一节区。
四、PE装载器漏洞
RVA和RAW的转换公式如下:
RAW - PointertoRawData = RVA - VA
文件偏移量 - 指向节区开始的文件偏移量 = 相对虚拟地址 - 虚拟地址
按理来说PointertoRawData 指向节区开始的文件偏移量 必须是对齐粒度的整数倍,所以当Upack中这个值并不是整数倍时,PE装载器会自动四舍五入(存疑,也可能是归零)。人工分析时如果不清楚这一点就会跳转到错误的地址区。
Upack中许多地方都藏着这样的小细节。
五、导入表暗藏玄机
IMAGE_IMPORT_DESCRIPTOR结构数组是以null结尾的,但在notepad_upack中,文件里这个数组并不是以null结尾的,但却能运行。这是因为这部分从文件映射到内存中时,并不是全部映射过去。文件设置的内存实际大小较小,使得文件中IMAGE_IMPORT_DIRECTORY的数组只映射一部分,剩余部分内存都以null填充,所以在内存中这个数组确实以null结尾了。
IMAGE_IMPORT_DESCRIPTOR中对寻找调用api比较重要的参数有
Name的RVA为2,在内存中处于HEADER区域,在HEADER区域中,RVA与RAW相等,所以我们可以直接在偏移为2的地方找到导入库的名称。也就是MZ标记之后紧跟着的。
INT的RVA为0,无法取到api地址;INT和IAT一个能正常工作就可以,IAT位于11E8,根据上文提到的RVA和RAW的转换公式,注意指向节区开始的偏移是否为对齐粒度的整数倍,即可得到IAT地址,进而得到API函数地址。这里需要用到的函数只有LoadLibrary和GetProcAddress,这俩是在恢复IAT地址中最常见的。