继续PE系列笔记的更新
PE其它笔记索引可前往:
PE文件笔记一 PE介绍
前面学习了PE的DOS部首和PE文件头,这次学习的结构为PE节表
PS:关于PE文件头中扩展PE头的数据目录项,其中包含了导入表、导出表、重定位表等等,暂且留作之后
PE节表
PE节表作用
表示Image的section头格式
PE节表结构
PE节表结构 |
对应C中的结构体 |
说明 |
多个IMAGE_SECTION_HEADER |
多个_IMAGE_SECTION_HEADER |
每个_IMAGE_SECTION_HEADER描述后面的一个节 |
结构体截图
在winnt.h中找到_IMAGE_SECTION_HEADER,得到以下截图(具体查找对应C结构体方法在PE文件笔记一 PE介绍中已经说明了,这里不再赘述)
结构体代码
#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 |
该节可以写 |
通俗版
实战分析
从先前分析的扩展PE头的结尾开始看起,选中部分为节表
因为有多个节,这里取第一个节进行分析,按顺序依次将数据填入对应的成员得到:
成员 |
值 |
说明 |
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文件的两种状态中已经说明)
得到:
可以清楚地看到,第一个节的位置对应VA,验证完毕
Misc和SizeOfRawData
Misc:该节在没有对齐前的真实尺寸为0x001990A9
结合前面得到的VA,可以算出,该节在内存中的末尾位置为:VA+Misc=0x401000+0x001990A9=0x59A0A9
于是要转到相应的位置进行查看:
在WinHex的底部找到偏移量:XXXX,单击
在弹出的窗口中修改要跳转的VA(虚拟地址)
跳转后得到:
可以看到,已经跳转到了第一个节的末尾
SizeOfRawData:该节在文件中对齐后的尺寸为0x00199200
结合前面得到的VA,可以算出,该节在内存中的末尾位置为:VA+SizeOfRawData=0x401000+0x00199200=0x59A200
于是要转到相应的位置进行查看:
发现附近都是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
再转到相应位置去查看:
发现已经到达下一个节,和下一个节的起始位置根据VirtualAddress + ImageBase = 0x400000+0x0019b000=0x59B000相匹配
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位运行结果
64位运行结果
代码说明
代码基于上一次的笔记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文件:点我下载