前面学习了PE的DOS部首和PE文件头,这次学习的结构为PE节表

PS:关于PE文件头中扩展PE头的数据目录项,其中包含了导入表、导出表、重定位表等等,暂且留作之后


PE节表

PE节表作用

表示Image的section头格式

PE节表结构

image-20210330204244302


PE节表结构 对应C中的结构体 说明
多个IMAGE_SECTION_HEADER 多个_IMAGE_SECTION_HEADER 每个_IMAGE_SECTION_HEADER描述后面的一个节

结构体截图

在winnt.h中找到_IMAGE_SECTION_HEADER,得到以下截图(具体查找对应C结构体方法在PE文件笔记一 PE介绍中已经说明了,这里不再赘述)

image-20210330204852909


结构体代码

 复制代码 隐藏代码#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;
#define IMAGE_SIZEOF_SECTION_HEADER 40

结构体成员分析

相比于扩展PE头,节表的成员并不算太多,但有部分成员仅针对.obj文件

下表为结构成员对应数据宽度和说明,加黑的成员为重点

成员 数据宽度 说明
Name BYTE[8]=8字节 节名称
Misc.PhysicalAddress DWORD(4字节) 节的文件地址
Misc.VirtualSize DWORD(4字节) 节的虚拟大小
VirtualAddress DWORD(4字节) 节在内存中的偏移地址
SizeOfRawData DWORD(4字节) 节在文件中对齐后的尺寸
PointerToRawData DWORD(4字节) 节区在文件中的偏移
PointerToRelocations DWORD(4字节) .obj文件有效
PointerToLinenumbers DWORD(4字节) 调试相关
NumberOfRelocations WORD(2字节) .obj文件有效
NumberOfLinenumbers WORD(2字节) 行号表中行号的数量
Characteristics DWORD(4字节) 节的属性

Name

官方翻译

一个8字节、用空填充的UTF-8字符串。如果字符串长度正好是8个字符,则没有结束空字符。对于较长的名称,该成员包含一个正斜杠(/),后面是十进制数字的ASCII表示形式,该数字是字符串表中的偏移量。可执行映像不使用字符串表,也不支持超过8个字符的节名

通俗版

ASCII字符串 可自定义 只截取8个 可以8个字节都是名字


Misc

官方翻译

Misc 双字,该字段是一个union型的数据,这是该节在没有对齐前的真实尺寸,该值可以不准确


通俗版

这是一个联合结构,可以使用下面两个值其中的任何一个,一般是取Misc.VirtualSize

Misc.PhysicalAddress

文件地址


Misc.VirtualSize

节加载到内存时的总大小,以字节为单位。如果该值大于SizeOfRawData成员,则该section将被0填充。此字段仅对可执行Image有效,对于object files应设置为0


VirtualAddress

官方翻译

section载入内存时的第一个字节的地址,相对于image base。对于object files,这是应用重定位之前的第一个字节的地址

通俗版

在内存中的偏移地址,加上ImageBase才是在内存中的真正地址(VA)

VA:Full Name Virtual Address(全名虚拟地址), is the in-memory virtual address(是内存中的虚拟地址)

VirtualAddress又被称为节区的RVA地址,RVA:Relative Virtual Offset (相对虚拟偏移)

VA = RVA(VirtualAddress) + ImageBase ,即 内存中的虚拟地址 = 虚拟地址 + 镜像基地址


SizeOfRawData

官方翻译

磁盘上初始化数据的大小,以字节为单位。这个值必须是IMAGE_OPTIONAL_HEADER结构文件对齐FileAlignment成员的倍数。如果该值小于VirtualSize成员,则节的其余部分将被填充为0。如果该节只包含未初始化的数据,则该成员为零

通俗版

节在文件中对齐后的尺寸


PointerToRawData

官方翻译

指向COFF文件中的第一页的文件指针。这个值必须是IMAGE_OPTIONAL_HEADER结构文件对齐FileAlignment成员的倍数。如果一个section只包含未初始化的数据,则将该成员设为0


通俗版

节区在文件中的偏移,又被称为FOA:File Offset Address 文件偏移地址


PointerToRelocations

官方翻译

指向该节重定位项开始的文件指针。如果没有重新定位,则此值为零

通俗版

在”.obj”文件中使用,指向重定位表的指针


PointerToLinenumbers

官方翻译

指向section行号表开头的文件指针。如果没有COFF line numbers,该值为0


通俗版

行号表的位置(供调试用)


NumberOfRelocations

官方翻译

section重定位表项的数量。对于可执行映像,此值为0


通俗版

重定位表的个数(在OBJ文件中使用)


NumberOfLinenumbers

官方翻译

section的行号条目的数量


通俗版

行号表中行号的数量


Characteristics

官方翻译

节的特征。定义了以下值

宏定义 含义
0x00000000 保留
0x00000001 保留
0x00000002 保留
0x00000004 保留
IMAGE_SCN_TYPE_NO_PAD 0x00000008 该节不得填塞至下一边界线。这个标志过时了,被IMAGE_SCN_ALIGN_1BYTES取代
0x00000010 保留
IMAGE_SCN_CNT_CODE 0x00000020 该节包含可执行代码
IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 该节包含初始化的数据
IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 该节包含未初始化的数据
IMAGE_SCN_LNK_OTHER 0x00000100 保留
IMAGE_SCN_LNK_INFO 0x00000200 该节包含解释或其他信息。这只对object files有效
0x00000400 保留
IMAGE_SCN_LNK_REMOVE 0x00000800 该节将不会成为image的一部分。这只对object files有效。
IMAGE_SCN_LNK_COMDAT 0x00001000 该节包含COMDAT数据。这只对object files有效。
0x00002000 保留
IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 该节包含重置TLB项中的speculative异常处理位
IMAGE_SCN_GPREL 0x00008000 该节包含通过全局指针引用的数据
0x00010000 保留
IMAGE_SCN_MEM_PURGEABLE 0x00020000 保留
IMAGE_SCN_MEM_LOCKED 0x00040000 保留
IMAGE_SCN_MEM_PRELOAD 0x00080000 保留
IMAGE_SCN_ALIGN_1BYTES 0x00100000 在1字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_2BYTES 0x00200000 在2字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_4BYTES 0x00300000 在4字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_8BYTES 0x00400000 在8字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_16BYTES 0x00500000 在16字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_32BYTES 0x00600000 在32字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_64BYTES 0x00700000 在64字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_128BYTES 0x00800000 在128字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_256BYTES 0x00900000 在256字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_512BYTES 0x00A00000 在512字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 在1024字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 在2048字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 在4096字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 在8192字节的边界上对齐数据。这只对object files有效
IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 该节包含扩展的重新定位。该节的重定位计数超过了节头中为其保留的16位。如果节头中的NumberOfRelocations字段为0xffff,则实际的重定位计数存储在第一次重定位的VirtualAddress字段中。如果设置了IMAGE_SCN_LNK_NRELOC_OVFL,并且该section中的重定位值小于0xffff,则会产生错误
IMAGE_SCN_MEM_DISCARDABLE 0x02000000 该节可以根据需要丢弃
IMAGE_SCN_MEM_NOT_CACHED 0x04000000 不能缓存该节
IMAGE_SCN_MEM_NOT_PAGED 0x08000000 该节不能分页
IMAGE_SCN_MEM_SHARED 0x10000000 该节可以在内存中共享
IMAGE_SCN_MEM_EXECUTE 0x20000000 该节可以作为代码执行
IMAGE_SCN_MEM_READ 0x40000000 该节可以读
IMAGE_SCN_MEM_WRITE 0x80000000 该节可以写

通俗版

image-20210401202645060


实战分析

从先前分析的扩展PE头的结尾开始看起,选中部分为节表

image-20210401204202037


因为有多个节,这里取第一个节进行分析,按顺序依次将数据填入对应的成员得到:

成员 说明
Name 2E 74 65 78 74 00 00 00 对应ASCII为 .text,即节名
Misc 0x001990A9
VirtualAddress 0x00001000 RVA=0x1000
SizeOfRawData 0x00199200 节在文件中对齐后的尺寸为0x00199200
PointerToRawData 0x00000400 FOA=0x400
PointerToRelocations 0x00000000
PointerToLinenumbers 0x00000000
NumberOfRelocations 0x0000
NumberOfLinenumbers 0x0000
Characteristics 0x60000020 详见下方

Name

该节的节名为 .text


VirtualAddress

节的RVA为0x1000

通过RVA可以得到VA=RVA+ImageBase(在内存中虚拟的地址 = 虚拟地址 + 镜像基地址)

ImageBase在前面的扩展PE头中已经得知是0x400000,不清楚的可以回顾PE文件笔记五 PE文件头之扩展PE头,这里不再赘述

所以得到VA=0x1000+0x400000=0x401000

为了验证这一点,将程序启动,使其加载到内存后再用Winhex查看其状态(具体流程在PE文件笔记二 PE文件的两种状态中已经说明)

得到:

image-20210401210954485


可以清楚地看到,第一个节的位置对应VA,验证完毕


Misc和SizeOfRawData

Misc:该节在没有对齐前的真实尺寸为0x001990A9

结合前面得到的VA,可以算出,该节在内存中的末尾位置为:VA+Misc=0x401000+0x001990A9=0x59A0A9

于是要转到相应的位置进行查看:

在WinHex的底部找到偏移量:XXXX,单击

image-20210401211633311


在弹出的窗口中修改要跳转的VA(虚拟地址)

image-20210401213744534


跳转后得到:

image-20210401213810759

可以看到,已经跳转到了第一个节的末尾


SizeOfRawData:该节在文件中对齐后的尺寸为0x00199200

结合前面得到的VA,可以算出,该节在内存中的末尾位置为:VA+SizeOfRawData=0x401000+0x00199200=0x59A200

于是要转到相应的位置进行查看:

image-20210401213943737


发现附近都是00,因为该地址为节在内存中的首地址+文件对齐后的地址,在内存中(运行态)无效

并且此时会发现SizeOfRawData=0x00199200=(Misc ÷ FileAlignment)向上取整×FileAlignment

即SizeOfRawData=0x00199200=(0x001990A9整除0x200+1)×0x200=(0xCC8+1)×0x200 = 0xCC9 × 0x200 = 0x00199200

满足SizeOfRawData的定义

其实SizeOfRawData在PE文件笔记二 PE文件的两种状态中也已经说明了,这里主要是强调SizeOfRawData在运行态时无效,且验证了SizeOfRawData在运行态时的来源,有关文件对齐和内存对齐等的知识可以前往回顾


在内存中(运行态时),实际上 该节在内存中的末尾位置应该为:VA+内存对齐后的大小

但是在节的属性中并没有表示内存对齐后大小的成员,内存对齐后的大小是如何得来的?

内存对齐后的大小

决定内存对齐后的大小的因素有2个:

  • 内存对齐:即SectionAlignment
  • Max{Misc,SizeOfRawData}:Misc和SizeOfRawData的最大值

内存对齐后的大小 = (Max{Misc,SizeOfRawData} ÷ SectionAlignment) 向上取整 × SectionAlignment


取SizeOfRawData很容易理解,但为什么还和Misc有关?

Misc表示的是实际大小难道不是一定小于文件对齐后的大小吗?

并不是,实际大小也可能要比文件对其后的大小要大,就拿全局变量为例

全局变量可以分为两种:有初始值的和没有初始值的

有初始值的全局变量在文件中就已经为其分配了空间,而没有初始值的全局变量只有到程序加载到内存中(运行态)后才会为其分配空间

假设当前存储的全局变量都是没有有初始值的,即在文件中没有为其分配空间,也就是在文件中大小为0,这也就导致SizeOfRawData为0,因此,此时的Misc > SizeOfRawData

所以内存对齐后的大小要综合Misc和SizeOfRawData决定


于是按照上面的公式计算:

内存对齐后的大小 = (Max{0x001990A9,0x00199200} ÷ 0x1000) 向上取整 × 0x1000 = (0x199200 ÷ 0x1000)向上取整 × 0x1000 = 0x19A000

该节在内存中的末尾位置应该为:VA+内存对齐后的大小 = 0x401000+0x19A000=0x59B000

再转到相应位置去查看:

image-20210404131954044


发现已经到达下一个节,和下一个节的起始位置根据VirtualAddress + ImageBase = 0x400000+0x0019b000=0x59B000相匹配

image-20210404131821611


PointerToRawData

节区在文件中的偏移为0x400,这里引入了一个概念:FOA,即文件偏移地址

所以PointerToRawData=FOA=文件偏移地址,同样在运行态无效,在PE文件笔记二 PE文件的两种状态也可以回顾


Characteristics

此时Characteristics=0x60000020=0x40000000+0x20000000+0x00000020

对照前面的表格:

宏定义 含义
IMAGE_SCN_CNT_CODE 0x00000020 该节包含可执行代码
IMAGE_SCN_MEM_EXECUTE 0x20000000 该节可以作为代码执行
IMAGE_SCN_MEM_READ 0x40000000 该节可以读

所以得到了该节的属性为:包含可执行代码、可以作为代码执行、可以读


自写代码解析节表

在先前代码的基础上,进一步改进

 复制代码 隐藏代码// PE.cpp : Defines the entry point for the console application.
//
#include <stdio.h>
#include <malloc.h>
#include <windows.h>
#include <winnt.h>
//在VC6这个比较旧的环境里,没有定义64位的这个宏,需要自己定义,在VS2019中无需自己定义
#define IMAGE_FILE_MACHINE_AMD64 0x8664
int main(int argc, char* argv[])
{
//创建DOS对应的结构体指针
_IMAGE_DOS_HEADER* dos;
//读取文件,返回文件句柄
HANDLE hFile = CreateFileA("C:\\Users\\lyl610abc\\Desktop\\dbgview64.exe", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0);
//根据文件句柄创建映射
HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY, 0, 0, 0);
//映射内容
LPVOID pFile = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
//类型转换,用结构体的方式来读取
dos = (_IMAGE_DOS_HEADER*)pFile;
//输出dos->e_magic,以十六进制输出
printf("dos->e_magic:%X\n", dos->e_magic);

//创建指向PE文件头标志的指针
DWORD* peId;
//让PE文件头标志指针指向其对应的地址=DOS首地址+偏移
peId = (DWORD*)((UINT)dos + dos->e_lfanew);
//输出PE文件头标志,其值应为4550,否则不是PE文件
printf("peId:%X\n", *peId);

//创建指向可选PE头的第一个成员magic的指针
WORD* magic;
//让magic指针指向其对应的地址=PE文件头标志地址+PE文件头标志大小+标准PE头大小
magic = (WORD*)((UINT)peId + sizeof(DWORD) + sizeof(_IMAGE_FILE_HEADER));
//输出magic,其值为0x10b代表32位程序,其值为0x20b代表64位程序
printf("magic:%X\n", *magic);
//根据magic判断为32位程序还是64位程序
switch (*magic) {
case IMAGE_NT_OPTIONAL_HDR32_MAGIC:
{
printf("32位程序\n");
//确定为32位程序后,就可以使用_IMAGE_NT_HEADERS来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS* nt;
//让PE文件头指针指向其对应的地址
nt = (_IMAGE_NT_HEADERS*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while(cnt< nt->FileHeader.NumberOfSections){
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER)*cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}

break;
}

case IMAGE_NT_OPTIONAL_HDR64_MAGIC:
{
printf("64位程序\n");
//确定为64位程序后,就可以使用_IMAGE_NT_HEADERS64来接收数据了
//创建指向PE文件头的指针
_IMAGE_NT_HEADERS64* nt;
nt = (_IMAGE_NT_HEADERS64*)peId;
printf("Machine:%X\n", nt->FileHeader.Machine);
printf("Magic:%X\n", nt->OptionalHeader.Magic);

//创建一个指针数组,该指针数组用来存储所有的节表指针
//这里相当于_IMAGE_SECTION_HEADER* sectionArr[nt->FileHeader.NumberOfSections],声明了一个动态数组
_IMAGE_SECTION_HEADER** sectionArr = (_IMAGE_SECTION_HEADER**)malloc(sizeof(_IMAGE_SECTION_HEADER*) * nt->FileHeader.NumberOfSections);

//创建指向块表的指针
_IMAGE_SECTION_HEADER* sectionHeader;
//让块表的指针指向其对应的地址,区别在于这里加上的偏移为_IMAGE_NT_HEADERS64
sectionHeader = (_IMAGE_SECTION_HEADER*)((UINT)nt + sizeof(_IMAGE_NT_HEADERS64));
//计数,用来计算块表地址
int cnt = 0;
//比较 计数 和 块表的个数,即遍历所有块表
while (cnt < nt->FileHeader.NumberOfSections) {
//创建指向块表的指针
_IMAGE_SECTION_HEADER* section;
//让块表的指针指向其对应的地址=第一个块表地址+计数*块表的大小
section = (_IMAGE_SECTION_HEADER*)((UINT)sectionHeader + sizeof(_IMAGE_SECTION_HEADER) * cnt);
//将得到的块表指针存入数组
sectionArr[cnt++] = section;
//输出块表名称
printf("%s\n", section->Name);
}
break;
}

default:
{
printf("error!\n");
break;
}

}
return 0;
}

运行结果

32位运行结果

image-20210401230600646


64位运行结果

image-20210401230508810


代码说明

代码基于上一次的笔记PE文件笔记五 PE文件头之扩展PE头增加了对块表的解析

此次代码中用到了动态声明数组,只不过声明的数组为指针数组

关于指针数组可以回顾逆向基础笔记二十三 汇编 指针(四)

这里补充一下动态数组的声明:

 复制代码 隐藏代码#include <stdio.h>
#include <malloc.h>
int main() {
//普通的数组声明
int arr[5];
//动态的数组声明
int num = 5;
int* arr2 =(int*) malloc(sizeof(int) * 5);
return 0;
}

普通的数组声明转动态的数组声明

 复制代码 隐藏代码#include <stdio.h>
#include <malloc.h>
int main() {
//普通的数组声明
类型 arr[5];
//动态的数组声明
int num = 5;
类型* arr2 =(类型*) malloc(sizeof(类型) * 5);
return 0;
}

对于前面的代码无非就是类型为_IMAGE_SECTION_HEADER*的情况

代入可得

 复制代码 隐藏代码#include <stdio.h>
#include <malloc.h>
int main() {
//普通的数组声明
_IMAGE_SECTION_HEADER* arr[5];
//动态的数组声明
int num = 5;
_IMAGE_SECTION_HEADER** arr2 =(_IMAGE_SECTION_HEADER**) malloc(sizeof(_IMAGE_SECTION_HEADER*) * 5);
return 0;
}

说明

大致将PE文件的各个结构都说明了一遍,数据目录项之后也会补充,掌握了结构后,就要开始操作这些结构了

在节表中比较重要的成员就是和内存对齐或文件对齐有关的那几个成员,这次的笔记也稍微提到了FOA、RVA、VA的概念,为后续作个铺垫

PE文件的学习可能有些枯燥,但只有在了解了其结构以后才能更好地搞事情( ̄▽ ̄)*

现在看似只是做个类似弱化的PEID,只能读取PE文件的相关数据貌似没有什么意思

后续有了PE的知识,就可以结合知识,来修改程序使其呈现我们想要的内容以及自己写个保护壳等等,尽请期待

附件

附上本笔记中分析的EverEdit文件:点我下载

转载自吾爱破解上的一个大佬