APT之旅 - PE静态内容结构

2023-11-20 12:52:26 浏览数 (2)

一、前言

PE 是一种文件格式,在Windows操作系统上的执行可执行文件(.exe)、动态链接库(.dll)、驱动程序以及其他可执行文件类型都是 PE 格式。了解其格式对恶意分析及使用高级的攻击手法有很大的帮助,很多高级的攻击手段都需要对 PE、PEB 有详细的了解。

二、PE 结构

1. DOS Header

DOS Header 用于兼容早期的DOS系统,此结构中的 e_magic 必定等于"MZ",大小不固定,大多数结构没有用,只需要记住结构中的 e_lfranew 位指明了 NT 头的所在位置,它的作用就是获取 NT Headers 的位置起点 RVA。

RVA(Relative Virtual Address),即相对于 PE 内容起点(基址)的偏移。

2. NT Headers

NT Headers 同 DOS Header 的 e_magic 位一样有一个校验结构是否有效的位 Signature,其值必定等于"PEx00x00",NT Headers 还包含了两个结构体:File Header 和 Optional Header:

代码语言:javascript复制
typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
(1)File Header

File Header 包含了PE文件的一些基本信息,如文件类型、目标CPU等。

代码语言:javascript复制
typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;
  WORD  NumberOfSections;
  DWORD TimeDateStamp;
  DWORD PointerToSymbolTable;
  DWORD NumberOfSymbols;
  WORD  SizeOfOptionalHeader;
  WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

*表格加灰表示不重要条目。

(2)Optional Header

Optional Header 是由编译最后一阶段由编译器补上的资讯,包含了 PE 文件的一些可选信息,如程序入口点、内存对齐方式等。

代码语言:javascript复制
typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
(3)DataDirectory

在(2)中最后的 DataDirectory 数组索引取值可以是以下值之一:

#1 和 # 12 看起来比较相似,实际上 idata 整个区段都是 #12 全局导入函数地址表,而 #12 上的函数需要引用自那个 DLL 则是记录在 #1 中的 IMAGE_IMPORT_DESCRIPTOR 位。

3. Section Headers

编译过程中将源码转换为了多个 Section Data 区段,每一个 Section Data 的大小、起点位置、执行时存放的位置都不一样,因此需要用 Section Header 来记录。在 NT Headers 的结尾处就是 Section Headers 的起点,而 NT Headers 大小固定,因此从 DOS Header 的 e_magic("MZ") 位置出发很容易手工爬取到 Section Headers 位置。

Section Headers 是 Section Header (IMAGE_SECTION_HEADER) 结构数组,从 NT Headers -> File Header -> NumberOfSections 获取到了 Section Headers 数组的大小为 3,那么其占用空间就是 sizeof(IMAGE_SECTION_HEADER) * 3 这么大。

代码语言:javascript复制
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

4. PE 结构总结

整个程序从 offset=0 处即 DOS Header 到程序的最后一处 EOF 的所有块状区域经 File Alignment 对齐之后都是紧密贴合的没有任何空隙。因此,理论上最后区段即 Section Headers 数组的最后一项的 PointerToRawData SizeOfRawData 正好等于你使用 WinAPI GetFileSize 或 ftell 函数计算出来的大小,而整个程序在磁碟槽里面的大小则为下面两者相加:

  1. DOS Header NT Headers Section Headers 的总大小对 File Alignment 对齐之后占用的大小。
  2. 各个 Section Data 对 File Alignment 对齐之后占用的大小之和。

我们随便打开一个 exe 的属性页,就可以看到其大小和程序在磁碟槽里面的大小区别:

需要注意的是,对于 Section Header 中 SizeOfRawData 和 Misc.VirtualSize 两项,当程序开发时所有全局变量都没有被分配初始值,而是执行时才写入这些变量,那么 .data Data 或 .bss Data 则可能出现:静态内容没有初值,但却要在执行时分配空间的状况导致 SizeOfRawData 为 0 但是 Misc.VirtualSize 有值的情况。

(1)微软获取 Section Headers 位置的宏定义

在 winnt.h 文件中,能找到微软获取 Section Headers 位置的宏定义,引入 windows.h 后自动引入,其中使用了 FIELD_OFFSET 宏,根据微软文档,FIELD_OFFSET宏返回已知结构类型中命名字段的字节偏移量:

可以看到其算法为 NT Headers 地址 Optional Header 在 NT Headers 中的偏移 Optional Header 占用的大小,正好是 NT Headers 结构的末端,就是 Section Headers 的起址。

需要注意的是,并不能通过简单的 NT Headers 地址 sizeof(IMAGE_NT_HEADERS) 获取 Section Headers 的地址。根据C/C 语言的标准,结构体中成员的排列是按照声明的顺序进行的,但由于编译器对结构体进行了字节对齐和填充,结构体的实际大小可能比成员大小之和要大。所以,通过 NT Headers 地址 sizeof(IMAGE_NT_HEADERS) 来获取节区表的地址是不可行的,因为 IMAGE_NT_HEADERS 结构体的大小已经包含了 IMAGE_FILE_HEADER 和 IMAGE_OPTIONAL_HEADER 的大小,并且在内存中的排列情况可能会有填充字节。

三、PE 解析器编写

根据之前的内容,我们需要读取一个 PE 文件内容,其返回指针就是 DOS Header 地址,然后根据 DOS Header->e_lfanew 获取到 NT Headers 地址,然后使用微软的 NT Headers 地址 Optional Header 在 NT Headers 中的偏移 Optional Header 占用的大小的方法获取 Section Headers 数组地址。

首先,编写一个函数读取 PE 文件:

读取到 PE 文件内容并保存到 pe_content 指针中,然后直接转换为 PIMAGE_DOS_HEADER 结构就是 DOS Header 了:

通过 DOS Header->e_lfanew 获取到 NT Headers 地址:

到打印 Optional Header 的时候需要注意,IMAGE_OPTIONAL_HEADER 结构体还有 IMAGE_OPTIONAL_HEADER32 与 IMAGE_OPTIONAL_HEADER64 之分,不同区别是其中的指针变量,在 32 位下是 4 字节在 64 位下是 8 字节,你也可以用 IMAGE_OPTIONAL_HEADER32 结构体去解析 64 位,其仍然可以正常显示,因为结构体是向后兼容的,但在某些数据上可能会出错,如 ImageBase 字段。

最好通过 File Header 的 Machine 字段判断 PE 文件的架构后再调用对应的结构体进行解析:

通过微软的 IMAGE_FIRST_SECTION 宏定义加 NT Headers 地址获取到 Section Headers 数组地址,再通过 File Header 的 NumberOfSections 字段获取到数组的大小,循环遍历数组打印 Section Header 信息,并在最后一个 Section Header 打印 PointerToRawData SizeOfRawData 的值验证是否等于我们用 WinAPI GetFileSize 或 ftell 函数计算出来的大小:

执行打印出来 DOS Header、NT Headers、Section Headers 的信息,可以看到用 ftell 计算的文件大小 326656:

最后一个节点打印 PointerToRawData SizeOfRawData 的值,可以看到同样是 326656:

四、参考文献

[1] Windows APT Warfare

[2] https://learn.microsoft.com/zh-cn/windows/win32/api/winnt/

锦鲤安全

一个安全技术学习与工具分享平台

点分享

点收藏

点点赞

点在看

0 人点赞