PE文件详解 - 区段表
1、区段表
- 数据目录表后就是区段表
- 区段表:用来描述位于其后各个区段的各种属性,PE文件最少要有一个区段才能被加载运行
- 区段表:由数个首尾相连的IMAGE_SECTION_HEADER结构体数组组成
- 可以使用IMAGE_FIRST_SECTION32 来找到第一个区段表所在的位置
- 示例
#include <Windows.h>
// 获取PE文件中第一个节(section)的指针
PIMAGE_SECTION_HEADER GetFirstSection(PIMAGE_NT_HEADERS pNTHeaders) {
// 使用IMAGE_FIRST_SECTION宏计算第一个节的指针
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNTHeaders);
// 返回第一个节的指针
return pSectionHeader;
}
1.1、IMAGE_SECTION_HEADER结构
- IMAGE_SECTION_HEADER : 包含了详细描述该区段属性的字段信息,如区段名,长度,属性等
- winnt.h中
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //区段名
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; // 区段大小
DWORD SizeOfRawData; // 区段的RAV地址
DWORD PointerToRawData;
DWORD PointerToRelocations; // 区段在文件中的偏移
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics; // 区段的属性
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
1.2、区段对齐
- PE文件中,每个区段与结构的起始位置都要遵守对齐机制,在32位的平台中,一个分页的大小是4KB,所以无论是内存还是文件中的区段对齐,都为4KB的倍数
- PE头文件的FileAlignment(文件区段对齐)、SectionAlignment(内存对齐),由这两个字段来描述
- 假如现在有一个体积为0x1201的区段,此映像文件的内存对齐与文件对齐分别为0x1000与0x200,那么这个区段在内存中实际占用的空间将为0x2000,其中后面因对齐而空出的大小为0x799字节的空间会用0x00填充,同理,由文件对齐大小为0x200可知,这个区段在文件中实际占用的空间为0x1400,后面空出的0x199大小的空间同样用0填充
- 内存中的实际占用空间:
内存对齐是0x1000,意味着在内存中,区段的起始地址必须是0x1000的倍数。由于区段大小为0x1201,不满足0x1000的倍数关系,因此需要向上取整到下一个0x1000的倍数。下一个0x1000的倍数是0x2000,所以区段在内存中实际占用的空间是0x2000。 - 文件中的实际占用空间:
文件对齐是0x200,意味着在文件中,区段的起始位置必须是0x200的倍数。由于区段在内存中占用的空间是0x2000,不满足0x200的倍数关系,因此需要向上取整到下一个0x200的倍数。下一个0x200的倍数是0x1400,所以区段在文件中实际占用的空间是0x1400。
- 内存中的实际占用空间:
1.3、通过代码读取区段表
- c++
#include<stdio.h>
#include<Windows.h>
int main()
{
FILE* pFile = NULL;
char* buffer;
int nFileLength = 0;
errno_t err = fopen_s(&pFile, "E:\\C_code\\MyDemo\\Debug\\MyDemo.exe", "rb");
if (err != 0)
{
// 处理文件打开错误
printf("Failed to open file.\n");
return 1;
}
// fseek()设置文件指针的位置。这里将文件指针移动到文件末尾
// SEEK_END 指定偏移量为0
fseek(pFile, 0, SEEK_END);
// ftell() 获取文件指针的当前位置,也就是文件的长度
nFileLength = ftell(pFile);
// rewind() 将文件指针重置到文件开头
rewind(pFile);
// 存储缓冲区大小
// 计算方式:文件长度乘以sizeof(char)(sizeof(char)表示char类型的大小,通常是1字节),再加1字节用于存储字符串结束符\0
int imageLength = nFileLength * sizeof(char) + 1;
// 动态分配内存 : malloc函数动态分配了一块内存,大小为imageLength字节
buffer = (char*)malloc(imageLength);
// memset() :将分配的内存初始化为0。
memset(buffer, 0, nFileLength * sizeof(char) + 1);
// fread函数用于从文件中读取数据。这里将文件中的内容读取到之前分配的缓冲区buffer中,每次读取1字节,总共读取imageLength个字节。
fread(buffer, 1, imageLength, pFile);
// 读取DOS头
// 定义指向 PIMAGE_DOS_HEADER的指针 ReadDosHeader
PIMAGE_DOS_HEADER ReadDosHeader;
// 因为buffer是char类型,所以要转成指针类型
// 目的是将缓冲区中的数据解释为IMAGE_DOS_HEADER结构
ReadDosHeader = (PIMAGE_DOS_HEADER)buffer;
// 打印指针中的数据
printf("MS-DOS Info:\n");
printf("MZ标志位:%x\n", ReadDosHeader->e_magic);
printf("PE头位置:%x\n", ReadDosHeader->e_lfanew);
// PE头
// 要注意你读取的EXE是32位还是64位的,64位要使用 PIMAGE_NT_HEADERS64
printf("PE Header Info:\n");
PIMAGE_NT_HEADERS ReadNTHeaders;
// 因为要跳过DOS头,读取到PE开头,所以要加e_lfanew
ReadNTHeaders = (PIMAGE_NT_HEADERS)(buffer + ReadDosHeader->e_lfanew);
printf("PE标志位:%x\n", ReadNTHeaders->Signature);
// 打印标准PE头里的东西
printf("运行平台:%x\n", ReadNTHeaders->FileHeader.Machine);
// 打印拓展PE头里的东西
printf("ImageBase: %x\n",ReadNTHeaders->OptionalHeader.ImageBase);
// 区段解析遍历
printf("Section Header Info:\n");
// Windows定义了宏来解析区段,使用IMAGE_FIRST_SECTION
// 定位到区段表
PIMAGE_SECTION_HEADER ReadSectionHeader = IMAGE_FIRST_SECTION(ReadNTHeaders);
// 在标准PE头中有个字段可以遍历区段的数量,在 PIMAGE_FILE_HEADER中的NumberOfSections字段
PIMAGE_FILE_HEADER pFileHeader = &ReadNTHeaders->FileHeader;
for (int i = 0; i < pFileHeader->NumberOfSections; i++)
{
printf("Name(区段名称):%s\n", ReadSectionHeader[i].Name);
printf("VOffset(起始相对虚拟地址):%08X\n", ReadSectionHeader[i].VirtualAddress);
printf("VSize(区段大小):%08X\n", ReadSectionHeader[i].SizeOfRawData);
printf("ROffset(文件偏移):%08X\n", ReadSectionHeader[i].PointerToRawData);
printf("RSize(文件中区段大小):%08X\n", ReadSectionHeader[i].Misc.VirtualSize);
printf("标记(区段的属性):%08X\n\n", ReadSectionHeader[i].Characteristics);
}
// 释放动态内存
free(buffer);
// 关闭文件
fclose(pFile);
return 0;
}
推荐阅读: