一、前言
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 函数计算出来的大小,而整个程序在磁碟槽里面的大小则为下面两者相加:
- DOS Header NT Headers Section Headers 的总大小对 File Alignment 对齐之后占用的大小。
- 各个 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/
锦鲤安全
一个安全技术学习与工具分享平台
点分享
点收藏
点点赞
点在看