书接上回
节表,就是在NT_HEADER 之后紧接着的那块区域,有多个相同结构的节表,整个区域就是个结构体数组,具体有多少个这样相同的节表结构 在COFF头(FILE_HEADER)的 NumberOfSections 字段。
所以要遍历节表,只需 for 循环 NumberOfSections 次即可
节表各字段说明如下,后续有更详细的解析:
1、Name 8字节 一般以"\0"结尾的ASCII码字符串来标识的名称,内容可自定义
注意:该名称并不遵守必须以"\0"结尾的规律,如果不是以"\0"结尾,系统会截取8个字节的长度进行处理。在自写程序中读取时需注意这点
2、Misc 双字 是该节在没有对齐前的真实尺寸,该值可以不准确。
3、VirtualAddress 节区在内存中的偏移地址,加上ImageBase才是在内存中的真正地址.
4、SizeOfRawData 节在文件中对齐后的尺寸.
5、PointerToRawData 节区在文件中的偏移.
6、PointerToRelocations 在obj文件中使用 对exe无意义
7、PointerToLinenumbers 行号表的位置 调试的时候使用
8、NumberOfRelocations 在obj文件中使用 对exe无意义
9、NumberOfLinenumbers 行号表中行号的数量 调试的时候使用
10、Characteristics 节的属性
更直观的节表存储结构如下,一行16字节
节表其实就相当于书本的大章节目录,记录书本内从多少页开始,有多少页的"长度"结束。下一章节又从第几页开始,从节起始位置开始数多少页结束这一章节。目录结束后就是正文了,后面的内容就是第一个节的起始位置了。当然中间会有一段空隙
换到程序中,就是 PointerToRawData 是PE文件躺在硬盘的时候(作为文件时)的该节开始位置,注意这个起始位置是相对于文件开头的偏移,但文件在硬盘上MZ头(DOS头的magic)的位置一定是0,所以其实也是文件中的绝对位置了。
而 SizeOfRawData 就是节在PE文件中的长度,但这个长度是根据文件对齐后的长度。这个文件对齐的值在可选头 OPTIONAL_HEADER 中的 FileAlignment 字段,比如FileAlignment值为200,实际该节在文件中的长度是366,那么SizeOfRawData值为400。总之这个长度必须为FileAlignment的整数倍,实际该节在文件中的长度一定是小于或等于SizeOfRawData的。
VirtualAddress就是当文件加载进内存后(比如双击运行该文件)的节的起始地址,而且也是相对于文件MZ头起始地址的偏移,即是一个相对位置。在同一文件同一个节中在硬盘上文件的节起始位置 和 被加载进内存中 的节起始位置不一样 (即使他们都是相对于MZ头的偏移)是不是很奇怪?
而且节在文件中有节长度,那么内存中也有节长度才对,这个内存中的节长度就是Misc,也就是union形式(C语言关键字union)的Misc的其中一种形态VirtualSize。该值本意是记录放入内存后节中的真实长度(无对齐)。但VirtualSize这个值可以不准确,即瞎填都不影响加载。
这时注意到SectionAlignment内存对齐和FileAlignment文件对齐,是两个不一定相同的值(也有可能相同)那么两种对齐值都不一样,也就不难理解为什么要分为SizeOfRawData 和 VirtualAddress 了。
涉及到操作系统加载文件时的拉伸问题。即文件躺在硬盘上以一种长度对齐,但到了内存中会变为以更长的长度对齐(内存对齐的值通常 ≥ 文件对齐)所以由短变长,就是个拉伸的过程,具体见下图,至于文件从硬盘到内存是怎么变长的,会有个很直观的理解
PE加载的过程:
1、根据SizeOfImage的大小,开辟一块缓冲区 ImageBuffer(文件在内存中的形式).
2、根据SizeOfHeader的大小,将头信息从FileBuffer(文件在硬盘上的形式)拷贝到 ImageBuffer
3、根据节表中的信息循环进FileBuffer中的节拷贝到 ImageBuffer中.
图中浅绿色的部分,就是因对齐牺牲的空间,通常是无用空间,通常会填充全零
Characteristics节的属性是按位确定属性的,具体属性位置分布可到 PETool 等工具中查看。
下面就是读取节表的代码了,自己实现一遍或读懂代码可以更深刻理解节表
代码中用到的exe 可自行找一个小点的独立的exe,比如网上搜些逆向用的crackme exe程序检测自写代码用
#include "stdio.h"
#include "windows.h"
inline LPVOID ReadPEFile(LPSTR lpszFile) //LPSTR 即 CHAR *
{
FILE *pFile = NULL;
DWORD fileSize = 0;
LPVOID pFileBuffer = NULL; //LPVOID 即 void *
//打开文件
pFile = fopen(lpszFile, "rb");
if (!pFile)
{
printf(" 无法打开 EXE 文件! ");
return NULL;
}
//读取文件大小
fseek(pFile, 0, SEEK_END);
fileSize = ftell(pFile);
fseek(pFile, 0, SEEK_SET);
//分配缓冲区
pFileBuffer = malloc(fileSize); //pFileBuffer指向一段 分配的exe文件这么大的内存
if (!pFileBuffer)
{
printf(" 分配空间失败! ");
fclose(pFile);
return NULL;
}
//将文件数据读取到缓冲区
size_t n = fread(pFileBuffer, fileSize, 1, pFile); //size_t 即 unsigned int
if (!n)
{
printf(" 读取数据失败! ");
free(pFileBuffer);
fclose(pFile);
return NULL;
}
//关闭文件
fclose(pFile);
return pFileBuffer; //最后返回指向 已填充exe文件内容 的新开辟空间 的指针
}
VOID h313()
{
char FilePath[] = "CRACKME.EXE";
LPVOID pFileBuffer = NULL;
PIMAGE_DOS_HEADER pDosHeader = NULL;
PIMAGE_NT_HEADERS pNTHeader = NULL;
PIMAGE_FILE_HEADER pFileHeader = NULL;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = NULL;
PIMAGE_SECTION_HEADER pSectionHeader = NULL;
pFileBuffer = ReadPEFile(FilePath); //pFileBuffer即指向已装载到内存中的exe首部
if (!pFileBuffer)
{
printf("文件读取失败\n");
return;
}
//判断是否是有效的MZ标志
if (*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //pFileBuffer强转 WORD* 后 取指针指向的内容
{
printf("不是有效的MZ标志\n");
free(pFileBuffer);
return;
}
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; // 强转 DOS_HEADER 结构体指针
//打印DOS头
printf("********************DOS头********************\n");
printf("MZ标志:%x\n", pDosHeader->e_magic);
printf("PE偏移:%x\n", pDosHeader->e_lfanew);
//判断是否是有效的PE标志
if (*((PDWORD)((DWORD)pFileBuffer + pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE) //基址pFileBuffer + lfanew 为 NTHeader首址
{
printf("不是有效的PE标志\n");
free(pFileBuffer);
return;
}
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew);
//打印NT头
printf("*****************NT HEADERS*****************\n");
printf("NT:%x\n", pNTHeader->Signature);
pFileHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4); //NT头地址 + 4 为 FileHeader 首址
printf("****************FILE HEADER****************\n");
printf("PE:%x\n", pFileHeader->Machine);
printf("节的数量:%x\n", pFileHeader->NumberOfSections);
printf("SizeOfOptionalHeader:%x\n", pFileHeader->SizeOfOptionalHeader);
//可选PE头
pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pFileHeader + IMAGE_SIZEOF_FILE_HEADER);//SIZEOF_FILE_HEADER为固定值且不存在于PE文件字段中
printf("**************OPTIOINAL HEADER*************\n");
printf("Magic:%x\n", pOptionalHeader->Magic);
//首个节表
pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
char Name[IMAGE_SIZEOF_SHORT_NAME + 1]; //用于存放节表名,因为节表名可能被写满八个字节,所以这里+1多一个存放 '\0'
//循环遍历节表
for (int i = 1; i < pFileHeader->NumberOfSections; i++, pSectionHeader++) //注意这里i从1开始 i < NumberOfSections
{
memset(Name, 0, sizeof(Name));
printf("*************** Section Headers %d **************\n",i);
memcpy(Name, pSectionHeader->Name, IMAGE_SIZEOF_SHORT_NAME);
printf("Name:%s\n", Name);
printf("Misc:%x\n", pSectionHeader->Misc);
printf("VirtualAddress:%x\n", pSectionHeader->VirtualAddress);
printf("SizeOfRawDat:%x\n", pSectionHeader->SizeOfRawData);
printf("PointerToRawData:%x\n", pSectionHeader->PointerToRawData);
printf("Characteristics:%x\n", pSectionHeader->Characteristics);
} //出循环后pSectionHeader指向最后一个节表
//释放内存
free(pFileBuffer);
}
运行程序打印内容如下:
********************DOS头********************
MZ标志:5a4d
PE偏移:100
*****************NT HEADERS*****************
NT:4550
****************FILE HEADER****************
PE:14c
节的数量:6
SizeOfOptionalHeader:e0
**************OPTIOINAL HEADER*************
Magic:10b
*************** Section Headers 1 **************
Name:CODE
Misc:1000
VirtualAddress:1000
SizeOfRawDat:600
PointerToRawData:600
Characteristics:60000020
*************** Section Headers 2 **************
Name:DATA
Misc:1000
VirtualAddress:2000
SizeOfRawDat:200
PointerToRawData:c00
Characteristics:c0000040
*************** Section Headers 3 **************
Name:.idata
Misc:1000
VirtualAddress:3000
SizeOfRawDat:800
PointerToRawData:e00
Characteristics:c0000040
*************** Section Headers 4 **************
Name:.edata
Misc:1000
VirtualAddress:4000
SizeOfRawDat:200
PointerToRawData:1600
Characteristics:40000040
*************** Section Headers 5 **************
Name:.reloc
Misc:1000
VirtualAddress:5000
SizeOfRawDat:200
PointerToRawData:1800
Characteristics:50000040
可使用 PETool 或 LoadPE 等小软件查看相应PE文件内容。对照打印内容检测自写的程序是否正确。
这里使用 PETool 查看的上述显示值 如下