0x1 前言

上一篇文章探讨了DOS Header以及逆向了DOS Stub,本篇文章谈论NT Headers

开始之前我们需要先明确一个重要概念,RVA(Relative Virtual Address,相对虚拟地址),它是相对于基址的偏移地址,其中映像基址是PE文件被载入内存后的起始地址,也就是说实际地址 = 相对虚拟地址 + 映像基址

回顾一下上一篇文章中提到的,PE文件结构从上到下依次是:DOS Header -> DOS Stub -> Rich Header -> NT Headers -> Section Header -> Section1 Section2 … SectionN

其中,NT Headers从上到下依次是:PE Signature -> PE File Header -> PE Optional Header,本篇文章就是详细讲述这部分内容

0x2 NT Headers

NT Headers是一个结构体,名为IMAGE_NT_HEADERS,结构体定义位于winnt.h中,定义如下

1
2
3
4
5
6
7
8
9
10
11
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

_IMAGE_NT_HEADERS64是针对64位PE,_IMAGE_NT_HEADERS是针对32位PE,可以看到,它有3个成员:

  • DWORD类型的Signature
  • IMAGE_FILE_HEADER结构体类型的FileHeader
  • IMAGE_OPTIONAL_HEADER64(IMAGE_OPTIONAL_HEADER32)结构体类型的OptionalHeader

0x3 PE Signature

NT Headers中的第1个成员,变量名是Signature,DWORD类型表示它是一个4字节无符号整型,它里面是一个固定值0x00004550,x86架构使用小端序,所以在内存中的顺序为50 45 00 00,翻译成ASCII是P E \0 \0,我们用PE-bear查看一个实际PE文件
image

0x4 PE File Header

NT Headers中的第2个成员,也叫做COFF File Header,是一个IMAGE_FILE_HEADER类型的结构体,结构体定义位于winnt.h中,定义如下

1
2
3
4
5
6
7
8
9
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;

它包含7个成员

  • Machine:这个字段表示可执行文件是运行在x86还是x86-64上,值为0x14c表示x86,值为0x8864表示x86-64,还有其他值,但是我们不关心,具体可以参考:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
  • NumberOfSections:这个字段表示节的数量(节的数量也就是节头的数量)
  • TimeDateStamp:这个字段是一个unix时间戳,表明文件被创建的时间
  • PointerToSymbolTable和NumberOfSymbols:当有COFF符号表的时候,PointerToSymbolTable表示相对COFF符号表的文件偏移,NumberOfSymbols表示符号表中项的数量,但是由于不鼓励使用COFF调试信息,所以并没有COFF符号表,这两项通常被设置为0
  • SizeOfOptionalHeader:这个字段表示PE Optional Header的大小
  • Characteristics:表明文件属性的标志,文件可以是可执行文件,也可以是系统文件,等等,具体可以参考:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format

下图是一个实际PE文件的PE File Header部分
image

0x5 PE Optional Header

NT Headers中的第3个成员,也是一个结构体,它叫PE可选头是因为有些文件类型,比如对象文件并没有这部分,但对PE文件来说,它是至关重要的,它没有一个固定大小,可以从PE File Header的成员SizeOfOptionalHeader获取PE Optional Header的大小,结构体定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;

//
// NT additional fields.
//
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;



typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG 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;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

可以看到,结构体的前8个成员是COFF文件格式的标准成员(注意PE32是9个成员,PE32+是8个成员),剩下的成员是微软针对Windows PE扩展的

就像NT Headers中提到的,32位PE和64位PE差别就在于PE Optional Header,主要体现在两方面

  1. 成员个数:32位OptionalHeader有31个成员,64位OptionalHeader有30个成员,32位OptionalHeader中多的成员是BaseOfData,它是DWORD类型,表示相对于data节的RVA
  2. 一些成员的类型:有5个成员在32位OptionalHeader中是DWORD类型,在64位OptionalHeader中是ULONGLONG类型,分别是:ImageBase、SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit

然后分别看一下每个成员

  • Magic:这个字段表示当前的PE文件是一个32位PE,还是一个64位PE,值为0x10B表示这是一个32位PE,值为0x20B表示这是一个64位PE,IMAGE_FILE_HEADER.Machine是被Windows PE Loader忽略的
  • MajorLinkerVersion和MinorLinkerVersion:这两个字段表示链接器的主版本号和次版本号
  • SizeOfCode:这个字段表示text段(代码段)的大小,如果有多个代码段,表示全部代码段大小的总和
  • SizeOfInitializedData:这个字段表示data段(初始化数据段)的大小,如果有多个初始化数据段,表示全部大小的总和
  • SizeOfUninitializedData:这个字段表示bss段(未初始化数据段)的大小,如果有多个未初始化数据段,表示全部大小的总和
  • AddressOfEntryPoint:程序被载入内存时入口点的RVA,当载入PE文件时,入口点是程序起始地址的RVA,当载入驱动程序时,入口点是初始化函数的RVA,对于DLL来说,入口点是可选的,当没有入口时,这个字段的值为0
  • BaseOfCode:PE文件被载入内存后,text段起始地址的RVA
  • BaseOfData:只存在32位PE中,PE文件被载入内存后,data段起始地址的RVA
  • ImageBase:这个字段表示PE文件被载入内存后第一个字节的地址,这个地址必须是64K的整数倍,实际上由于KASLR等内存保护措施,几乎不会使用这个地址作为PE的内存基址,PE Loader会选择一个未使用的内存地址作为PE的内存基址,然后开始地址修复,修改所有使用这个地址作为常量的地址,这个过程也叫重定位,这些常量位于一个特殊的节,称为reloc节(重定位节)
  • SectionAlignment:这个字段表示内存中节对齐的最小单位,默认值是特定架构(x86或x86-64)的内存页大小,微软规定它的值不能小于FileAlignment
  • FileAlignment:这个字段表示文件中节对齐的最小单位,不足的填充0,微软规定它的值应该是512到64 * 1024之间2的整次幂,如果SectionAlignment的值小于内存页的大小,那么FileAlignment的值和SectionAlignment的值必须一致
  • MajorOperatingSystemVersion、MinorOperatingSystemVersion、MajorImageVersion、MinorImageVersion、MajorSubsystemVersion、MinorSubsystemVersion:MajorOperatingSystemVersion和MinorOperatingSystemVersion指定了所需操作系统的主版本号和次版本号,后四个字段指定了PE文件的主版本号和次版本号,子系统的主版本号和次版本号
  • Win32VersionValue:一个保留字段,值为0
  • SizeOfImage:这个字段表示PE文件的大小(包含全部的头),单位是字节,当PE文件被载入到内存时,PE Loader会使用这个字段的值,所以值需要是SectionAlignment的整数倍
  • SizeOfHeaders:这个字段表示全部头的大小,包括从DOS Header的第一个字节到Section Header的最后一个字节
  • CheckSum:PE文件的校验值,当PE文件被载入到内存时用到
  • Subsystem:这个字段指定了运行当前PE文件需要的Windows子系统(如果有的话),完整的值可以参考:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
  • DLLCharacteristics:这个字段指定了PE文件的一些特征,比如是否兼容DEP机制、再比如能否在运行时进行重定位,完整的值可以参考:https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
  • SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit:这几个字段指定了要保留的栈大小、要提交的栈大小、要保留的堆大小、要提交的堆大小
  • LoaderFlags:一个保留的字段,值为0
  • NumberOfRvaAndSizes:DataDirectory数组的大小
  • DataDirectory:IMAGE_DATA_DIRECTORY数组(在下一篇文章中会详细讲述它)

我们看一个实际PE文件的PE Optional Header
image

如图所示,可以看到字段Magic的值为20B,表示这是一个64位的PE文件

程序执行入口点的RVA是0x3D30,加上基址140000000就是140003D30,代码段起始地址是1000,加上基址140000000就是140001000,符合程序执行入口点落在代码段内,但不是代码段的起始地址

FileAlignment是0x200,我们可以检查一下,这个PE文件是否符合FileAlignment对齐的要求,首先资源节.rsrc的起始地址是0x8000
image

重定位节.reloc的起始地址是0x8200
image

可以看到从0x81DD开始就没有实际数据了,剩下的被填充为0,直到0x81FF
image

Data Directory部分将在下一篇文章讲述

参考链接

https://0xrick.github.io/win-internals/pe4/