首页
社区
课程
招聘
[原创]PE头结构分析
发表于: 2025-9-16 20:30 630

[原创]PE头结构分析

2025-9-16 20:30
630



DOS头

8 * 8 = 64 byte


DOS存根

不固定


签名结构体

PE\0\0


文件头(标准头)

5 * 4 = 20 byte

文件头末尾Characteristics

8 bit(4 byte)


可选头

24 * 8 = 224 byte(32位)

30 * 8 = 240 byte(64位)

可选头末尾DataDirectory

16 * 8 = 128 byte


节区头

每个5 * 8 = 40 byte


DOS头

共8*8=64字节。

 

typedef struct _IMAGE_DOS_HEADER {

    WORD   e_magic; // DOS签名 "MZ" (0x5A4D),重要

    WORD   e_cblp; // 字节数(最后页)

    WORD   e_cp; // 页数

    WORD   e_crlc; // 重定位项数

    WORD   e_cparhdr; // 头部段数

    WORD   e_minalloc; // 最小内存分配

    WORD   e_maxalloc; // 最大内存分配

    WORD   e_ss; // 初始SS值

    WORD   e_sp; // 初始SP值

    WORD   e_csum; // 校验和

    WORD   e_ip; // 初始IP值

    WORD   e_cs; // 初始CS值

    WORD   e_lfarlc; // 重定位表偏移

    WORD   e_ovno; // 覆盖号

    WORD   e_res[4]; // 保留字段

    WORD   e_oemid; // OEM标识符

    WORD   e_oeminfo; // OEM信息

    WORD   e_res2[10]; // 保留字段

LONG   e_lfanew; // NT头偏移(PE文件起始位置),重要

} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

 

 图 1  DOS头

DOS存根

在DOS系统下提示一句话,包含数据和代码,可以修改,但是修改不可覆盖DOS头和NT头,长度不能随意修改,如果缩短或增长需要修改地址,会很麻烦。

 

 图 2  DOS存根

 

NT头

没什么说的。

 

typedef struct _IMAGE_NT_HEADERS {

    DWORD Signature; // PE签名 "PE\0\0" (0x00004550)

    IMAGE_FILE_HEADER FileHeader; // 文件头

IMAGE_OPTIONAL_HEADER OptionalHeader; // 可选头(32/64位)

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

// 64位版本(IMAGE_OPTIONAL_HEADER64)

typedef struct _IMAGE_NT_HEADERS64 {

    DWORD Signature;

    IMAGE_FILE_HEADER FileHeader;

IMAGE_OPTIONAL_HEADER64 OptionalHeader;

} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

 

NT头:签名结构体(Signature)

PE文件核心标识,内容“PE\0\0”。

图 3  签名

 

NT头:文件头(FileHeader,也叫标准头,COFF头)

固定20字节。

 

typedef struct _IMAGE_FILE_HEADER {

    WORD  Machine; // 目标CPU架构(如0x014C=Intel 386)

    WORD  NumberOfSections; // 节区数量

    DWORD TimeDateStamp; // 编译时间戳

    DWORD PointerToSymbolTable; // 符号表偏移(调试用)

    DWORD NumberOfSymbols; // 符号数量

    WORD  SizeOfOptionalHeader; // 可选头大小

WORD  Characteristics; // 文件属性(如可执行/DLL)

} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

 

其中Characteristics按bit位定义,每一位含义如下:

 

Bit0 IMAGE_FILE_RELOCS_STRIPPED 0x0001 重定位信息已移除(通常是 EXE)

Bit 1 IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 文件是可执行的

Bit 2 IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 行号信息已移除(已废弃)

Bit 3 IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 符号表已移除(已废弃)

Bit 4 IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 优化工作集(已废弃)

Bit 5 IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 支持 >2GB 地址空间

Bit 7 IMAGE_FILE_BYTES_REVERSED_LO 0x0080 小端字节序(已废弃)

Bit 8 IMAGE_FILE_32BIT_MACHINE 0x0100 32 位架构(x86)

Bit 9 IMAGE_FILE_DEBUG_STRIPPED 0x0200 调试信息已移除

Bit 10 IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 可从交换设备运行(已废弃)

Bit 11 IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 可从网络运行(已废弃)

Bit 12 IMAGE_FILE_SYSTEM 0x1000 系统文件(如内核驱动)

Bit 13 IMAGE_FILE_DLL 0x2000 这是一个 DLL 文件

Bit 14 IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 仅单处理器运行(已废弃)

Bit 15 IMAGE_FILE_BYTES_REVERSED_HI 0x8000 大端字节序(已废弃)

 

将Characteristics值与上表中每一位对应的值进行按位与运算查看是否非0即可判断文件属性,比如在这张文件截图中Characteristics值为0x0102,我想要判断他是否是一个可执行文件(Bit2),使用0x0102 & 0x0002(表格Bit2对应的值)= 0x0002(非0),说明是一个可执行文件。

 

图 4  标准头

 

NT头:可选头(OptionalHeader)

长度由文件头里的 SizeOfOptionalHeader 确定,32位PE文件通常为0xE0(24*8=224字节),64位PE文件通常为0xF0(30*8=240字节)。NT头中的RVA与后面节区中的RVA区分,可以理解为NT头中的RVA是某些重要部分,比如程序入口点,实际上它也在节区中,但节区里的RVA是整个节区起始的位置,只是在NT头中专门记录了这些特殊部分的快捷访问位置。DataDirectory里的元素和这个也是一个设计思路,都是对某些特殊部分的快捷访问。

 

typedef struct _IMAGE_OPTIONAL_HEADER32 {

    // 标准字段(所有PE文件)

    WORD  Magic; // 标识:0x10B=32位,0x20B=64位

    BYTE  MajorLinkerVersion; // 链接器主版本号

    BYTE  MinorLinkerVersion; // 链接器次版本号

    DWORD SizeOfCode; // 所有代码段的总大小

    DWORD SizeOfInitializedData; // 已初始化数据的总大小

    DWORD SizeOfUninitializedData; // 未初始化数据(BSS)的总大小

    DWORD AddressOfEntryPoint; // 入口点RVA(相对于ImageBase)

    DWORD BaseOfCode; // 代码段的起始RVA

    DWORD BaseOfData; // 数据段的起始RVA(仅32位存在)

    // NT扩展字段(Windows专用)

    DWORD ImageBase; // 进程内存中的优先加载地址,重要

    DWORD SectionAlignment; // 内存中的节区对齐粒度(通常0x1000)

    DWORD FileAlignment; // 文件中的节区对齐粒度(通常0x200)

    WORD  MajorOperatingSystemVersion; // 要求的最低OS主版本

    WORD  MinorOperatingSystemVersion; // 要求的最低OS次版本

    WORD  MajorImageVersion; // 映像主版本号(用户定义)

    WORD  MinorImageVersion; // 映像次版本号(用户定义)

    WORD  MajorSubsystemVersion; // 子系统主版本(通常4=Win95)

    WORD  MinorSubsystemVersion; // 子系统次版本

    DWORD Win32VersionValue; // 保留(必须为0)

    DWORD SizeOfImage; // 映像在内存中的总大小

    DWORD SizeOfHeaders; // 所有头部的总大小(对齐后)

    DWORD CheckSum; // 校验和(驱动/DLL常用)

    WORD  Subsystem; // 子系统类型(1=Native,2=GUI,3=CUI)

    WORD  DllCharacteristics; // DLL属性(如ASLR/DEP)

    DWORD SizeOfStackReserve; // 初始保留的栈大小

    DWORD SizeOfStackCommit; // 初始提交的栈大小

    DWORD SizeOfHeapReserve; // 初始保留的堆大小

    DWORD SizeOfHeapCommit; // 初始提交的堆大小

    DWORD LoaderFlags; // 保留(已废弃)

    DWORD NumberOfRvaAndSizes; // 数据目录项数(通常16)

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

 

其中的IMAGE_DATA_DIRECTORY结构如下,是 DataDirectory 的一个条目:

typedef struct _IMAGE_DATA_DIRECTORY {

    DWORD VirtualAddress; // 数据的 RVA(相对虚拟地址)

DWORD Size; // 数据的大小(字节数)

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

DataDirectory 是数组,通常包含16个IMAGE_DATA_DIRECTORY 结构体(每个8字节),储存RVA,下面是数组每一项的含义:

 

[0] = EXPORT Directory    导出表(DLL 导出的函数列表),重要

[1] = IMPORT Directory    导入表(依赖的外部 DLL 函数),重要

[2] = RESOURCE Directory    资源表

[3] = EXCEPTION Directory    异常处理表

[4] = SECURITY Directory    数字签名

[5] = BASERELOC Directory    重定位表

[6] = DEBUG Directory    调试信息

[7] = COPYRIGHT Directory    架构特定数据

[8] = GLOBALPTR Directory    全局指针寄存器

[9] = TLS Directory    TLS表,重要

[A] = LOAD_CONFIG Directory    加载配置表

[B] = BOUND_IMPORT Directory    绑定导入表

[C] = IAT Directory    导入地址表(IAT)

[D] = DELAY_IMPORT Directory    延迟加载导入表

[E] = COM_DESCRIPTOR Directory    .NET元数据

[F] = Reserved Directory    保留,未使用

 

图 5  可选头

 

typedef struct _IMAGE_OPTIONAL_HEADER64 {

    // 标准字段(与32位类似)

    WORD  Magic; // 标识:0x20B=64位

    BYTE  MajorLinkerVersion;

    BYTE  MinorLinkerVersion;

    DWORD SizeOfCode;

    DWORD SizeOfInitializedData;

    DWORD SizeOfUninitializedData;

    DWORD AddressOfEntryPoint; // 入口点RVA

    DWORD BaseOfCode; // 代码段起始RVA

    // BaseOfData 字段在64位中不存在!

 

    // NT扩展字段

    ULONGLONG ImageBase; // 64位优先加载地址

    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; // 64位栈/堆大小

    ULONGLONG SizeOfStackCommit;

    ULONGLONG SizeOfHeapReserve;

    ULONGLONG SizeOfHeapCommit;

    DWORD LoaderFlags;

    DWORD NumberOfRvaAndSizes; // 数据目录项数(通常16)

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表

} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

 

节区头(节表)

每个节区头固定40字节。下面的实际例子里有7个节区头,对应的从上面NT头中文件头处第二个字可以看到0x07,就是说这里有7个节区头。

 

typedef struct _IMAGE_SECTION_HEADER {

    BYTE  Name[8]; // 节区名称(如 ".idata")

    DWORD VirtualSize; // 内存中节区实际大小(可能未对齐)

    DWORD VirtualAddress; // 内存中的 RVA(关键!用于计算)

    DWORD SizeOfRawData; // 文件中节区大小(对齐后)

    DWORD PointerToRawData; // 文件中的偏移(关键!用于计算)

    DWORD PointerToRelocations; // 重定位表偏移(无用,除非是OBJ文件)

    DWORD PointerToLinenumbers; // 调试信息(通常为0)

    WORD  NumberOfRelocations; // 重定位项数(无用)

    WORD  NumberOfLinenumbers; // 调试信息(通常为0)

DWORD Characteristics; // 节区属性(如可读/可写)

} IMAGE_SECTION_HEADER;

 

节区名 作用

.text 存储程序的可执行代码(机器指令)

.data 存储已初始化的全局/静态变量

.rdata 存储只读数据(如字符串常量、常量数组)

.idata 存储导入表(记录程序调用了哪些外部DLL的函数)

.edata 存储导出表(记录DLL提供了哪些函数供其他程序调用)

.rsrc 存储资源(如图标、对话框、字符串表等)

.reloc 存储重定位信息(如果程序不能加载到默认基址,需要调整某些地址)

.bss 存储未初始化的全局/静态变量(在磁盘上不占空间,内存中分配)

 

图 6  节区头

 

计算文件偏移

RAW(文件中的物理偏移) = RVA(数据在内存中的地址相对于ImageBase的偏移量) - VirtualAddress + PointorToRawData

在这个文件截图中以.text节区头为例,RVA = 0x1000,VirtualAddress = 0x1000,PointorToRawData = 0x0400,算得RAW = 0x0400。这里VirtualAddress与RVA相同,因为在节区头中VirtualAddress字段直接存储的是RVA值。但实际上他们的关系是所有VirtualAddress都是 RVA,但并非所有 RVA都是VirtualAddres。在通用公式中,VirtualAddress即取值的是节区头中那个VirtualAddress,但RVA取值需要取决于各区域的VirtualAddress(例如导入表、导出表),只是在节区头中,RVA需要取自己区域里的VirtualAddress值,所以在这里计算时他们的值相等。

程序运行时CPU和操作系统访问的都是VA(虚拟地址)。RAW、RVA、VA三者转换关系为RAW(磁盘中的物理偏移,与内存无关)-> RVA(加载到内存,是PE文件内部的相对偏移,用于静态分析) -> VA(运行时访问)

 

RAW:磁盘里的物理偏移,用Hex软件打开看到的地址,这里偏移就是相对于文件开头部分,前面有多少字节RAW就是多少。

RVA:相对于ImageBase(PE文件映射到内存后的起始点)的偏移,.exe文件一般不重定位,.dll文件可能重定位。

VA:虚拟地址,等于ImageBase + RVA(一般情况)。和物理地址(实际地址)区分,举个例子,就好像DosBox虚拟机可以模拟硬件操控,但是实际上在当前操作系统下并不能操作那些系统配置文件,这时并不是实际操控,而是操作系统划定了一片本来可用的区域作为虚拟空间使用,那些空间原本的位置就是实际地址,在虚拟机中的地址(比如那些模拟可操控的配置文件)就是虚拟地址。加载文件时也是一个道理,虚拟地址不是实际地址。

 

磁盘文件:文件地址 = 文件起始位置(0x00) + 偏移量(RAW)

内存映射:VA = ImageBase + RVA,这里ImageBase是NT头IMAGE_OPTIONAL_HEADER里的。

注意:ImageBase是内存的优先基地址,系统在装载文件时在能按ImageBase装载文件的情况下,计算VA使用上面的公式。但是不排除有时系统不按ImageBase装载文件(比如多个.dll文件ImageBase地址重叠导致地址占用),此时会进行重定位,选择一个新的加载地址ActualBase,计算时应使用VA = ActualBase + RVA。获得ActualBase通过系统API或调试器(如OLLYDBG)。

实际运行:VA = 实际加载基址(随机化)+ RVA


  一点个人学习整理笔记,欢迎批评指正。


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2025-9-16 20:42 被Calparrot编辑 ,原因: 修改格式。
收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回