逆向工程核心原理-18

逆向工程核心原理-第十八章-UPackPE PE文件头分析

Upack是一个PE文件运行时压缩器(壳), 特点之一就是对PE头的变形.

18.1 UPacK说明

UPack多用于恶意程序加壳.

18.2 使用UPack压缩notepad.exe

  • 首先Upack.exe跟notepad.exe再同一目录下
  • 打开命令行输入: upack notepad.exe
  • 得到了加壳后的程序

注意加壳不是生成新文件, 而是直接修改源程序, 注意备份

使用PEView查看

Untitled

跟原书有点出入是空出的那一行为RVA = 18, Data = 0011B0BE, 描述: Time Date Stamp

可以看到左边的目录PEview没有分析处可选头和节区头等信息

18.3 使用Stud_PE工具

在google上下载的工具, 能够识别处upack加壳后的程序

Untitled

18.4 比较PE文件头

使用HED打开加了upack壳和未加upack壳的notepad.exe

18.4.1 原notepad.exe的PE文件头

Untitled

18.4.2 notepad_upack.exe的文件头

Untitled

可以看到加了upack的notepad程序把MZ文件头(DOS头)和PE文件头(NT头)重叠在了一起. 并可节约文件空间. 但这也是文件头变得更复杂(对于我们来说是坏事, 但这正是恶意代码想要的结果)

我们使用Stud_PE查看二进制试图

首先在Headers中点击下面那个红框

Untitled

我们可以看到Stud识别出了两个重要成员, 这就是他优于PEview的地方

Untitled

这两个重要成员:

1
2
e_magic:   魔术MZ
e_lfanew: 下一个exe头的地址

其余的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

Untitled

  • 疑问: 既然可选头结构体已经固定为E0, 为何多此一举在文件头中有一个SizeOfOptionalHeader来描述
  • 回答: 因为原本的意图是有多种可选头, 其大小也不同, 所以有这个SizeOfOptionalHeader

从平常的PE来看, 节区头好像必须紧挨着可选头才对. 但是它的实际位置是由成员值决定的:

1
可选头的起始地址(IMAGE_SECTION_HEADER) + 可选头大小(SizeOfOptionalHeader) = 节区头起始地址

也就是说我们修改了SizeOfOptionalHeader就可以让它们中间插入解码代码, 而不是贴贴.

在了解了上面的操作之后, 我们也要明白upack的意图是: 把PE头变形, 然后通过改变成员值来压缩或者预留空间, 并向预留空间中插入解码代码.

可选头的结束地址: D7, 节区头的起始地址: 170, 中间的就是解码代码, 我们看看

Untitled

使用调试器查看其汇编代码(但是我的OD打开不了), 但是只用知道这个是解码代码即可, 有些软件识别不出来, 以为是文件就会导致程序无法正常运行.

18.5.3 IMAGE_OPTIONAL_HEADER(可选头).NumberOfRvaAndSizes

从Stud中看到可选头的成员NumberOfRvaAndSizes也发生了改变. 该成员指出了DataDirectory结构体数组的元素个数.(重要内容有导入导出表的地址, 资源地址). 原本有0x10个, 变为了A个.

我们可以看看各个元素的描述:

Untitled

A号元素以后的元素将会被覆盖. 可见upack对文件的压缩非常巧妙.

接下来看到IMAGE_DATA_DIRECTORY, 使用HED跟Stud都可以

Untitled

十六进制视图中的蓝色区域就是DataDirectory区域, 后面的就是Upack的预留空间.

上面OD运行不了就是因为upack修改了NumberOfRvaAndSizes

18.5.4 IMAGE_SECTION_HEADER(节区头)

节区头IMAGE_SECTION_HEADER结构体中, Upack会把自身数据覆盖到运行不需要的成员中.

上面讲到过, 节区头的起始地址为: 170

我们使用HED查看

Untitled

上图为一个节区头IMAGE_SECTION_HEADER结构体, 其中有很多成员都是运行时不需要的, 可以覆盖.

18.5.5 重叠节区

除了重叠节区头, Upack还可以重叠节区

我们在Stud中查看

Untitled

发现:

  • 第一跟第三节区起始偏移相同, 都为10
  • 第一跟第三节区文件中大小相同

因为是映射关系, 所以硬盘中的偏移描述被改变, 程序并不会报错, 因为影响不了虚拟内存.

下图左侧为节区信息, 右图为内存信息

Untitled

根据节区头的描述, 装载器会把文件偏移0 ~ 1FF的区域分别映射到3个不同的内存位置(文件头0开始, 第一第三节区10开始)

需要注意的是第一个节区在内存中的区域大小为14000, 与文件SizeOfImage中有相同的值. 也就是说, 第二节区的文件映像会被解压缩到第一个节区,

注意, 注意, 注意: 原本的三个节区会被解压到上图中的第一节区

Untitled

总结一下: 原本notepad的三个节区, 被压缩到upack的第二个(大)节区, 解压时被解压到第一个(大)节区

18.5.6 RVA to RAW

许多程序无法识别Upack的原因在于: 无法进行正确的RVA → RAW的变换, Upack的作者通过发现装载器的bug实现的.

复习一下RVA → RAW变换的常规方法(第十三章-第四小结)

1
2
3
4
5
6
7
8
9
10
11
换算公式:
RAW - PointerToRawData = RVA - VirtualAddress

可推出:
Raw = RVA - VirtualAddress + PointerToRawData

RAW是内存中的地址(物理地址)
RVA是磁盘中的地址(虚拟地址)
virtualaddress是内存中节区基址(也是节区头的一个成员), 注意这个VA指的是节区头结构体的成员
而不是我们平常理解的VA(虚拟地址), 它也是用RVA来表示的.
pointertorawdata是磁盘中节区基址

从可选头中的AddressOfEntryPoint成员, 我们知道了入口点的地址 = 1018

再从上图我们知道了.text节的RVA = 1000(入口点肯定在.text)

同时前面有了第一节区的文件起始地址为10

Untitled

我们就可以通过公式得出RAW = 1018 - 1000 + 10 = 28

我们查看28偏移处

Untitled

为一个字符串, 明显不是入口点

这是就要知道一个基址, 就是我们前面提到的粒度FileAilgnment, 节区的位置应该要向这个成员值对齐, 在upack压缩后的unotepad中, FileAilgnment = 200, 而.text起始地址PointerToRawData应该为FileAilgnment的整数被, 所以在装载器中, PointerToRawData = 10会变为PointerToRawData = 0

我们再次使用公式: RAW = 1018 - 1000 + 0 = 18

我们在该地址处得到了我们需要的汇编代码

Untitled

18.5.7 导入表(IMAGE_IMPORT_DESCRIPTOR)

Upack改动了导入表(Import Table)

下面Stud是查看导入表的地址, 前四个字节为起始地址, 后四个为大小

Untitled

起始地址为RVA = 0191EE, 我们需要进行变换成RAW

先得知道VirtualAddress = 19000

Untitled

第三节的RawOffset跟上面的第一节区一样被化为0

所以RAW = 191EE - 19000 + 0 = 1EE

使用HED查看地址1EE

Untitled

PE规范中, 导入表是IMAGE_IMPORT_DESCRIPTOR的结构体数组, 最后以NULL结构体结尾. 图中红框为第一个结构体, 但是其下方既不是IMAGE_IMPORT_DESCRIPTOR, 也不是NULL.

正常来说应该会错误, 但是在地址200处为文件第三节区的结尾. 也就是说 , 200后面的部分不会被映射到第三节区域, 当映射完成时, 第一个结构体后面就是一个NULL结构体了.

Untitled

内存中剩下的第三节区部分(27200 ~ 28000)全部填充为0;

18.5.8 导入地址表

我们通过分析IAT来查看导入了哪些DLL的API

一个IMAGE_IMPORT_DESCRIPTOR为一个库, 查看其成员

  • INT = 0
  • Name = 2
  • IAT = 11E8

首先Name的RVA = 2, 位于头区域, 所以其RAW = 2

使用HED查看

Untitled

因为INT(导入名称表) = 0(RVA), 不好跟, 所以我们跟踪IAT(因为一般二者都指向的是同一个地址)

RAW = 11E8 - 1000 + 0 = 1E8

Untitled

框选部分就是IAT域, 同时也作为INT使用. 也就是说, 这里的指针都指向名称字符串, 结束为NULL.

  • 第一个字符串地址: 28
  • 第二个字符串地址: BE

Untitled

文章作者: LamのCrow
文章链接: http://example.com/2022/03/28/逆向核心工程原理-第 16542/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow