在 PE(Portable Executable)文件结构中,节表(Section Table) 是 PE 文件的重要组成部分之一,位于 PE 头之后。节表记录了各节的虚拟地址、大小、节在 PE 文件中的实际大小和节属性等内容,用于定义程序在内存中的逻辑布局,操作系统加载 PE 文件时,根据节表将不同节的数据映射
到正确的内存区域;定义了各节的属性,如是否可执行、可读、可写等,操作系统会根据这些属性对相应内存区域进行保护。在调试过程中,节表可以用来定位代码和数据在文件和内存中的位置,辅助调试器解析程序结构。
节:每个节实际上是一个容器,可以包含代码、数据等等,每个节可以有独立的内存权限,比如代码节默认有读/执行权限,节的名字和数量可以自己定义,未必是上图中的三个。
数据映射
PE 文件是一种存储在磁盘上的静态格式,而程序需要在内存中以动态形式运行。将节映射到内存中可以将代码、数据、以及资源加载到合适的内存地址,以便 CPU 和操作系统能够访问并执行这些内容。
当一个 PE 文件被加载到内存中以后,我们称之为"映象"(image)。
代码节(.text):包含指令代码,必须加载到内存中供 CPU 执行。 数据节(.data, .bss):包含全局变量、静态变量等,需要在内存中分配地址以供程序访问。 资源节(.rsrc):包含图标、字符串等资源,运行时需要内存访问这些资源。
节表的数量和位置
在NT
头部中存在一个名叫NumberOfSections
的字段(如果对NT头部内容比较陌生可以看笔者的PE系列的上一篇文章《PE文件结构:NT头部》),该字段就记录了程序的节(表)数量:
以该程序为例子,我们从NT头部得知该程序包含了9个节(每个节都有自己对应的节表),这个时候可以将画面拉到NT头部结尾;这个时候就能看到紧跟在NT头部后面的9个节表(红色部分)。
接着我们就通过节表的字段解析进行进一步说明节的映射,打开Visual Studio
,在任意.cpp/.c
文件中敲入_IMAGE_SECTION_HEADER
;
接着选中该结构体类型,按下F12
进行跳转,查看节表结构。
#define IMAGE_SIZEOF_SHORT_NAME 8
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;
1.Name[IMAGE_SIZEOF_SHORT_NAME](8字节)
大小为8字节,该字段表示的是节名称,通常是一个字符串,例如,.text
表示代码节,.data
表示数据节。这个时候可以通过010 Editor打开样例程序(在笔者公众号的《PE文件结构-DOS头部&DOS stub》文章中提及)。
样例程序中出现了如下节:
.textbss 、.text、 .rdata、.data、.idata、.msvcjmc、.00cfg、.rsrc、.reloc
.textbss:
这通常是一个特定编译器或工具链生成的节,可能包含代码或未初始化数据的特殊部分。用于特定场景下优化代码和未初始化数据的存储。textbss
节只在Debug模式下有效,Release模式下默认禁用Incremental Linking,所以不会生成这个节
.text:
代码节,存放程序的可执行指令,
.rdata:
只读数据节,存放程序的只读全局变量和常量。
.data:
已初始化数据节,存放程序运行前已初始化的全局变量和静态变量。
.idata:
导入数据节,包含了程序需要从其他动态链接库(DLL)导入的函数和变量的引用。这个节帮助程序在运行时解析和加载这些外部依赖。
.msvcjmc:
这是一个特定于Microsoft Visual C++编译器的节,用于存储编译器生成的一些元数据,可能与调试信息或异常处理有关。
.00cfg:
这个节名看起来像是特定于某个编译器或程序的配置数据。它可能包含了程序的配置信息,但这不是PE文件的标准节名称,因此具体内容可能需要查看相应的文档或编译器生成的文件来确定。
.rsrc:
资源节,包含了程序的资源,如图标、菜单、对话框、字符串和其他用户界面元素。这些资源在程序运行时可以被访问和使用。
.reloc:
重定位节,包含了重定位表,用于在程序加载到内存时调整地址。这是因为PE文件可能被加载到不同的内存地址,重定位表确保所有的地址引用都是正确的。
事实上该字段是描述性字段,可以随意修改,不会影响程序运行。
2. Misc联合体(DWORD)
在PE文件结构的节表(IMAGE_SECTION_HEADER
)中,Misc
字段是一个联合体,用于描述节的大小信息。
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
联合体(union)是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。这意味着联合体可以存储多种不同的数据类型,但是任何时候只能存储其中一种。
节表的Misc
联合体字段中的PhysicalAddress
最初用于描述节的实际物理地址,但在现代 PE 文件中,这个字段已经被废弃;出于兼容性原因,仍然保留此名称,但其实际用途已被重定义。所以在现代 PE 文件中该字段中上存储的数据为VirtualSize
。
VirtualSize
表示节在内存中的大小;VirtualSize
通常需要按内存对齐值(SectionAlignment
)对齐,加载器根据 VirtualSize
和对齐要求为节分配内存。c此时样例程序的 VirtualSize
和 SectionAlignment
是如下值:
VirtualSize: 0x27334
SectionAlignment: 0x1000
当加载到内存时,实际分配的内存大小将是 VirtualSize
向上对齐到 SectionAlignment
的倍数,因此,加载后节会占用0x28000
字节的内存,而其中只有 0x27334
字节包含实际数据,剩余的部分会被填充为零。使用x32dbg
打开样例程序,转到内存布局页面查看该数值:
在该页面即可看到这几个字段的对应关系:以.textbss
节为例子,该节的VirtualSize
为0x27298
,由于在内存中是以页为存储单位,所以在存储后0x334
大小的数据时需要单独开出一页,也就是VirtualSize
需要与SectionAlignMent
进行对齐。
3.VirtualAddress
VirtualAddress
是 PE 文件中每个节的虚拟地址,描述了该节在内存中的起始位置(相对于 ImageBase
的偏移量),它指示操作系统加载器将该节映射到内存的哪个位置。节中的所有数据在运行时的虚拟地址可以通过以下公式计算:
实际虚拟地址 = ImageBase + VirtualAddress
以.textbss
为例:此时VirtualAddress
的值为00 00 10 00
,那么这个实际虚拟地址也就是0040 0000
+0000 1000
= 0040 1000
.
这个时候再来看一下x32dbg中的内存分布:
此时实际的虚拟就是我们所计算的0040 1000
。
VirtualAddress
必须与 SectionAlignment
对齐,确保节的起始地址满足内存对齐要求。VirtualSize
定义节在内存中的大小,从 VirtualAddress + ImageBase
开始计算。
4.SizeOfRawData(DWORD)
SizeOfRawData
表示节在文件中占用的大小(以字节为单位),这是节在磁盘上的对齐后的大小,该值必须是 FileAlignment
的倍数,即文件对齐值;如果节中的实际数据小于对齐后的大小,文件中会填充空字节(通常为 0x00
)以达到对齐要求。
5.PointerToRawData(DWORD)
PointerToRawData
是一个 4 字节(DWORD
)的字段,表示该节在 PE 文件中开始位置的偏移量(以字节为单位)。它是从文件开头(文件的起始地址,即文件偏移 0x0
)到该节数据在文件中的开始位置的偏移量,通过该字段结合SizeOfRawData
我们可以找到该节的原始数据。
此时样例程序的.text节的SizeOfRawData
为0005 8600
,PointerToRawData
的值为0000 0400
,这个时候我们就在010 Editor上定位.text
节的原始数据:
①按下 Ctrl + G(或者在菜单中选择 Edit > Goto Offset
)。
②数据起点为 0x400,终点为 0x58600 + 0x400 = 0x58A00(不含终点)。
那么这个区间的数据也就是.text节了。
接着我们可以从样例程序的节表中提取出来的VirtualSize\VirtualAddress\SizeOfRawData\PointerToRawData
的数据:
节 | VirtualSize | VirtualAddress | SizeOfRawData | PointerToRawData |
---|---|---|---|---|
.textbss | 27298 | 1000 | 0 | 0 |
.text | 583C8 | 29000 | 58400 | 400 |
.rdata | B9DC | 82000 | BA00 | 58800 |
.data | 24B0 | 8E000 | 1000 | 64200 |
.idata | 0BA7 | 91000 | C00 | 65200 |
.msvcjmc | 0142 | 92900 | 200 | 65E00 |
.00cfg | 104 | 93000 | 200 | 66000 |
.rsrc | 43C | 94000 | 600 | 66200 |
.reloc | 29B0 | 95000 | 2A00 | 66800 |
在表中我们可以看到一个奇怪的现象,也就是.textbss
这个节它VirtualAddress
和VirtualSize
字段有值,但是SizeOfRawData
和PointerToRawData
字段却为0;这就以为这这个节在内存中有对应的地址空间,但是在磁盘文件中却没有实际存储数据。这实际上是因为这个节是一个空节
。
空节通常指的就是 BSS(Block Started by Symbol)节,空节类似于 C 语言中的 BSS 段,这类节用于存储未初始化的全局变量或静态变量。在 PE 文件中,这些节通常在文件中没有实际数据(即 SizeOfRawData 为 0),但在加载时会占用内存(即 VirtualSize 和 VirtualAddress 有效)。
由于第一个节是空节,那么接着就以第二个节text
为例子在内存中对该节的进行定位;.text
在PE文件中的地址为PointerToRawData
:0x400
,我们可以在010Editor中定位到该节,节的内容如下:
这个时候将样例程序载入x32dbg
进行调试,计算出该节在内存中的地址:
内存中地址 = VirtualAddress + ImageBase
= 29000 + 400000
= 42 9000
在x32dbg
的内存窗口中键入ctrl+G
,接着输入地址42 9000
,即可定位到.text
节:
6.PointerToRelocations
、PointerToLinenumbers
、NumberOfRelocations
和NumberOfLinenumbers
①PointerToRelocations:
表示节中重定位表的偏移地址(以字节为单位),从文件起始位置算起。如果节包含重定位条目,该字段会指向文件中重定位数据的位置。通常在可执行文件(如 .exe
文件)中不会使用此字段,其值通常为 0,在目标文件(.obj 文件)中有意义,用于链接器处理符号的重定位。
②PointerToLinenumbers:
表示行号表的偏移地址,从文件起始位置算起。行号表用于调试,提供了源代码中行号和节中指令地址之间的映射关系。在 PE 文件中,该字段的值通常为 0,因为行号信息更多地存在于外部调试信息(如 PDB 文件)中。
③NumberOfRelocations:
表示 重定位表中的条目数,对于 .exe
文件,通常为 0,因为重定位信息已经被加载器处理。
④**NumberOfLinenumbers**:
表示 行号表中的条目数,常见于目标文件(.obj 文件),在最终生成的 PE 文件中通常为 0。
事实上这几个字段在exe
文件中基本上不用,可以随意修改,我们这边以.text
节为例子:修改这三个字段后发现程序依旧可以正常运行。
7.Characteristics(DWORD)
Characteristics
字段定义了节的属性。它以位掩码(bitmask)
的形式表示,可以指示节的存储类型、是否可执行、是否可写等信息。该字段的具体取值如下:
//
// Section characteristics.
//
// IMAGE_SCN_TYPE_REG 0x00000000 // Reserved.
// IMAGE_SCN_TYPE_DSECT 0x00000001 // Reserved.
// IMAGE_SCN_TYPE_NOLOAD 0x00000002 // Reserved.
// IMAGE_SCN_TYPE_GROUP 0x00000004 // Reserved.
#define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved.
// IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved.
#define IMAGE_SCN_CNT_CODE 0x00000020 // 节包含代码。用于 .text 节等存储程序指令的部分。
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // 节包含已初始化数据。用于 .data 节等存储已初始化全局变量的部分。
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // 节包含未初始化数据。用于 .bss 节等存储未初始化全局变量的部分。
#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // 节包含一些链接器信息(非代码或数据)。
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // 节在生成最终可执行文件时应被移除,仅在目标文件(.obj)中存在。
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // 节是 COMDAT 数据(用于消除重复的节内容)。
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // 节不支持延迟调试。
#define IMAGE_SCN_GPREL 0x00008000 // 节内容是与全局指针(GP)相对的。
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000
#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //
// Unused 0x00F00000
#define IMAGE_SCN_ALIGN_MASK 0x00F00000
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // 节是可丢弃的。例如,初始化完成后,.idata 中的导入表可以被丢弃。
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // 节不可被缓存。通常用于某些硬件相关数据。
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // 节不可被分页到硬盘上。
#define IMAGE_SCN_MEM_SHARED 0x10000000 // 节是可共享的,多个进程可以映射到同一物理内存区域。
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // 节是可执行的,通常用于存储代码。
#define IMAGE_SCN_MEM_READ 0x40000000 // 节是可读的,存储代码或数据。
#define IMAGE_SCN_MEM_WRITE 0x80000000 // 节是可写的,通常用于存储可修改数据。
这个时候在010 Eidtor中查看样例程序.text
节的Characteristics
字段:
该字段取值为:6000 0020
,通过对照上述字段取值可知.text
是可读、可执行的,并且在该节中包含代码。
0x6000 0020 = 0x40000000(可读) + 0x20000000(可执行) + 0x00000020(节包含代码)
关注公众号,获取更多工具+资讯