深入学习PE文件结构系列二PE-Signature-PE-File-Header-PE-Optional-Header
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 | typedef struct _IMAGE_NT_HEADERS64 { |
_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文件
0x4 PE File Header
NT Headers中的第2个成员,也叫做COFF File Header,是一个IMAGE_FILE_HEADER类型的结构体,结构体定义位于winnt.h中,定义如下
1 | typedef struct _IMAGE_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部分
0x5 PE Optional Header
NT Headers中的第3个成员,也是一个结构体,它叫PE可选头是因为有些文件类型,比如对象文件并没有这部分,但对PE文件来说,它是至关重要的,它没有一个固定大小,可以从PE File Header的成员SizeOfOptionalHeader获取PE Optional Header的大小,结构体定义如下
1 | typedef struct _IMAGE_OPTIONAL_HEADER { |
可以看到,结构体的前8个成员是COFF文件格式的标准成员(注意PE32是9个成员,PE32+是8个成员),剩下的成员是微软针对Windows PE扩展的
就像NT Headers中提到的,32位PE和64位PE差别就在于PE Optional Header,主要体现在两方面
- 成员个数:32位OptionalHeader有31个成员,64位OptionalHeader有30个成员,32位OptionalHeader中多的成员是BaseOfData,它是DWORD类型,表示相对于data节的RVA
- 一些成员的类型:有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
如图所示,可以看到字段Magic的值为20B,表示这是一个64位的PE文件
程序执行入口点的RVA是0x3D30,加上基址140000000就是140003D30,代码段起始地址是1000,加上基址140000000就是140001000,符合程序执行入口点落在代码段内,但不是代码段的起始地址
FileAlignment是0x200,我们可以检查一下,这个PE文件是否符合FileAlignment对齐的要求,首先资源节.rsrc的起始地址是0x8000
重定位节.reloc的起始地址是0x8200
可以看到从0x81DD开始就没有实际数据了,剩下的被填充为0,直到0x81FF
Data Directory部分将在下一篇文章讲述
