逆向工程核心原理-第十八章-UPackPE PE文件头分析
Upack是一个PE文件运行时压缩器(壳), 特点之一就是对PE头的变形.
18.1 UPacK说明
UPack多用于恶意程序加壳.
18.2 使用UPack压缩notepad.exe
- 首先Upack.exe跟notepad.exe再同一目录下
- 打开命令行输入: upack notepad.exe
- 得到了加壳后的程序
注意加壳不是生成新文件, 而是直接修改源程序, 注意备份
使用PEView查看
跟原书有点出入是空出的那一行为RVA = 18, Data = 0011B0BE, 描述: Time Date Stamp
可以看到左边的目录PEview没有分析处可选头和节区头等信息
18.3 使用Stud_PE工具
在google上下载的工具, 能够识别处upack加壳后的程序
18.4 比较PE文件头
使用HED打开加了upack壳和未加upack壳的notepad.exe
18.4.1 原notepad.exe的PE文件头
18.4.2 notepad_upack.exe的文件头
可以看到加了upack的notepad程序把MZ文件头(DOS头)和PE文件头(NT头)重叠在了一起. 并可节约文件空间. 但这也是文件头变得更复杂(对于我们来说是坏事, 但这正是恶意代码想要的结果)
我们使用Stud_PE查看二进制试图
首先在Headers中点击下面那个红框
我们可以看到Stud识别出了两个重要成员, 这就是他优于PEview的地方
这两个重要成员:
1 | e_magic: 魔术MZ |
其余的DOS头成员不重要(书中想表达的更多是, 它们是可以覆盖掉的)
正常来说e_lfanew = DOS头大小(40) + DOS存根(一般为A0) = E0
但upack中的e_lfanew指向的地址为10, 也就是DOS头中
这并不违背PE规范, 而且可以让DOS头和PE文件头重叠在一起节省空间, 并让一些软件无法识别出PE头的内容.
18.5.2 IMAGE_FILE_HEADER(文件头).SizeOfOptionalHeader(可选头的大小)
修改可选头中的成员SizeOfOptionalHeader, 可以使可选头预留出内存空间, 来向文件头中插入解码代码.
原本SizeOfOptionalHeader大小为E0, upack将其改成148
- 疑问: 既然可选头结构体已经固定为E0, 为何多此一举在文件头中有一个SizeOfOptionalHeader来描述
- 回答: 因为原本的意图是有多种可选头, 其大小也不同, 所以有这个SizeOfOptionalHeader
从平常的PE来看, 节区头好像必须紧挨着可选头才对. 但是它的实际位置是由成员值决定的:
1 | 可选头的起始地址(IMAGE_SECTION_HEADER) + 可选头大小(SizeOfOptionalHeader) = 节区头起始地址 |
也就是说我们修改了SizeOfOptionalHeader就可以让它们中间插入解码代码, 而不是贴贴.
在了解了上面的操作之后, 我们也要明白upack的意图是: 把PE头变形, 然后通过改变成员值来压缩或者预留空间, 并向预留空间中插入解码代码.
可选头的结束地址: D7, 节区头的起始地址: 170, 中间的就是解码代码, 我们看看
使用调试器查看其汇编代码(但是我的OD打开不了), 但是只用知道这个是解码代码即可, 有些软件识别不出来, 以为是文件就会导致程序无法正常运行.
18.5.3 IMAGE_OPTIONAL_HEADER(可选头).NumberOfRvaAndSizes
从Stud中看到可选头的成员NumberOfRvaAndSizes也发生了改变. 该成员指出了DataDirectory结构体数组的元素个数.(重要内容有导入导出表的地址, 资源地址). 原本有0x10个, 变为了A个.
我们可以看看各个元素的描述:
A号元素以后的元素将会被覆盖. 可见upack对文件的压缩非常巧妙.
接下来看到IMAGE_DATA_DIRECTORY, 使用HED跟Stud都可以
十六进制视图中的蓝色区域就是DataDirectory区域, 后面的就是Upack的预留空间.
上面OD运行不了就是因为upack修改了NumberOfRvaAndSizes
18.5.4 IMAGE_SECTION_HEADER(节区头)
节区头IMAGE_SECTION_HEADER结构体中, Upack会把自身数据覆盖到运行不需要的成员中.
上面讲到过, 节区头的起始地址为: 170
我们使用HED查看
上图为一个节区头IMAGE_SECTION_HEADER结构体, 其中有很多成员都是运行时不需要的, 可以覆盖.
18.5.5 重叠节区
除了重叠节区头, Upack还可以重叠节区
我们在Stud中查看
发现:
- 第一跟第三节区起始偏移相同, 都为10
- 第一跟第三节区文件中大小相同
因为是映射关系, 所以硬盘中的偏移描述被改变, 程序并不会报错, 因为影响不了虚拟内存.
下图左侧为节区信息, 右图为内存信息
根据节区头的描述, 装载器会把文件偏移0 ~ 1FF的区域分别映射到3个不同的内存位置(文件头0开始, 第一第三节区10开始)
需要注意的是第一个节区在内存中的区域大小为14000, 与文件SizeOfImage中有相同的值. 也就是说, 第二节区的文件映像会被解压缩到第一个节区,
注意, 注意, 注意: 原本的三个节区会被解压到上图中的第一节区
总结一下: 原本notepad的三个节区, 被压缩到upack的第二个(大)节区, 解压时被解压到第一个(大)节区
18.5.6 RVA to RAW
许多程序无法识别Upack的原因在于: 无法进行正确的RVA → RAW的变换, Upack的作者通过发现装载器的bug实现的.
复习一下RVA → RAW变换的常规方法(第十三章-第四小结)
1 | 换算公式: |
从可选头中的AddressOfEntryPoint成员, 我们知道了入口点的地址 = 1018
再从上图我们知道了.text节的RVA = 1000(入口点肯定在.text)
同时前面有了第一节区的文件起始地址为10
我们就可以通过公式得出RAW = 1018 - 1000 + 10 = 28
我们查看28偏移处
为一个字符串, 明显不是入口点
这是就要知道一个基址, 就是我们前面提到的粒度FileAilgnment, 节区的位置应该要向这个成员值对齐, 在upack压缩后的unotepad中, FileAilgnment = 200, 而.text起始地址PointerToRawData应该为FileAilgnment的整数被, 所以在装载器中, PointerToRawData = 10会变为PointerToRawData = 0
我们再次使用公式: RAW = 1018 - 1000 + 0 = 18
我们在该地址处得到了我们需要的汇编代码
18.5.7 导入表(IMAGE_IMPORT_DESCRIPTOR)
Upack改动了导入表(Import Table)
下面Stud是查看导入表的地址, 前四个字节为起始地址, 后四个为大小
起始地址为RVA = 0191EE, 我们需要进行变换成RAW
先得知道VirtualAddress = 19000
第三节的RawOffset跟上面的第一节区一样被化为0
所以RAW = 191EE - 19000 + 0 = 1EE
使用HED查看地址1EE
PE规范中, 导入表是IMAGE_IMPORT_DESCRIPTOR的结构体数组, 最后以NULL结构体结尾. 图中红框为第一个结构体, 但是其下方既不是IMAGE_IMPORT_DESCRIPTOR, 也不是NULL.
正常来说应该会错误, 但是在地址200处为文件第三节区的结尾. 也就是说 , 200后面的部分不会被映射到第三节区域, 当映射完成时, 第一个结构体后面就是一个NULL结构体了.
内存中剩下的第三节区部分(27200 ~ 28000)全部填充为0;
18.5.8 导入地址表
我们通过分析IAT来查看导入了哪些DLL的API
一个IMAGE_IMPORT_DESCRIPTOR为一个库, 查看其成员
- INT = 0
- Name = 2
- IAT = 11E8
首先Name的RVA = 2, 位于头区域, 所以其RAW = 2
使用HED查看
因为INT(导入名称表) = 0(RVA), 不好跟, 所以我们跟踪IAT(因为一般二者都指向的是同一个地址)
RAW = 11E8 - 1000 + 0 = 1E8
框选部分就是IAT域, 同时也作为INT使用. 也就是说, 这里的指针都指向名称字符串, 结束为NULL.
- 第一个字符串地址: 28
- 第二个字符串地址: BE