PE文件结构:节表

在 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此时样例程序的 VirtualSizeSectionAlignment 是如下值:

VirtualSize:        0x27334
SectionAlignment:   0x1000

当加载到内存时,实际分配的内存大小将是 VirtualSize 向上对齐到 SectionAlignment 的倍数,因此,加载后节会占用0x28000 字节的内存,而其中只有 0x27334 字节包含实际数据,剩余的部分会被填充为零。使用x32dbg打开样例程序,转到内存布局页面查看该数值:

在该页面即可看到这几个字段的对应关系:以.textbss节为例子,该节的VirtualSize0x27298,由于在内存中是以页为存储单位,所以在存储后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节的SizeOfRawData0005 8600PointerToRawData的值为0000 0400,这个时候我们就在010 Editor上定位.text节的原始数据:

①按下 Ctrl + G(或者在菜单中选择 Edit > Goto Offset)。

②数据起点为 0x400,终点为 0x58600 + 0x400 = 0x58A00(不含终点)。

那么这个区间的数据也就是.text节了。

接着我们可以从样例程序的节表中提取出来的VirtualSize\VirtualAddress\SizeOfRawData\PointerToRawData的数据:

VirtualSizeVirtualAddressSizeOfRawDataPointerToRawData
.textbss27298100000
.text583C82900058400400
.rdataB9DC82000BA0058800
.data24B08E000100064200
.idata0BA791000C0065200
.msvcjmc01429290020065E00
.00cfg1049300020066000
.rsrc43C9400060066200
.reloc29B0950002A0066800

在表中我们可以看到一个奇怪的现象,也就是.textbss这个节它VirtualAddressVirtualSize

字段有值,但是SizeOfRawDataPointerToRawData字段却为0;这就以为这这个节在内存中有对应的地址空间,但是在磁盘文件中却没有实际存储数据。这实际上是因为这个节是一个空节

空节通常指的就是 BSS(Block Started by Symbol)节,空节类似于 C 语言中的 BSS 段,这类节用于存储未初始化的全局变量或静态变量。在 PE 文件中,这些节通常在文件中没有实际数据(即 SizeOfRawData 为 0),但在加载时会占用内存(即 VirtualSize 和 VirtualAddress 有效)。

由于第一个节是空节,那么接着就以第二个节text为例子在内存中对该节的进行定位;.text在PE文件中的地址为PointerToRawData0x400,我们可以在010Editor中定位到该节,节的内容如下:

这个时候将样例程序载入x32dbg进行调试,计算出该节在内存中的地址:

内存中地址 = VirtualAddress + ImageBase 
          = 29000 + 400000 
          = 42 9000

x32dbg的内存窗口中键入ctrl+G,接着输入地址42 9000,即可定位到.text节:

6.PointerToRelocationsPointerToLinenumbersNumberOfRelocationsNumberOfLinenumbers

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(节包含代码) 

关注公众号,获取更多工具+资讯

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值