C/C++

探索 PE 格式系列之区块表

凡凡 · 8月20日 · 2019年 · 165次已读

1 前述

在 NT 头结束后,紧接着就是区块表,区块表包含每个块在映象中的信息,分别指向不同的区块实体。

2 区块表

区块表是一个 IMAGE_SECTION_HEADER 结构数组,这个结构包含区块的信息,比如位置、长度、属性等,区块的数目是由 NT 头中的文件头里的 NumberOfSections 给出。以下为 IMAGE_SECTION_HEADER 结构:

在上述图中,有两个字段比较重要,分别为 VirtualAddress、PointerToRawData,这两个字段用于将相对虚拟地址或虚拟地址转换为文件偏移地址,以下为 RVA 转 FOA 函数:

// RVA 转 FOA
DWORD RVAtoFOA(DWORD dwRva)
{
    // 获取区段表的数量
    DWORD dwCounts = g_NtHeader->FileHeader.NumberOfSections;

    // 获取区段表数组的首元素
    auto Sections = IMAGE_FIRST_SECTION(g_NtHeader);

    // 遍历所有的区段表找到符合要求的区段
    for (DWORD i = 0; i < dwCounts; ++i)
    {
        // 要求:RVA >= 区段的首地址 并且 RVA < 区段的结尾的地址
        if (dwRva >= Sections[i].VirtualAddress &&
            dwRva < (Sections[i].VirtualAddress + Sections[i].SizeOfRawData))
        {
            // FOA = VA - ImageBase - (所在区段的 RVA - 所在区段的 FOA)
            // FOA = RVA - 所在区段的 RVA + 所在区段的 FOA
            return dwRva - Sections[i].VirtualAddress + Sections[i].PointerToRawData;
        }
    }

    // 如果找不到就返回 -1
    return -1;
}

计算公式为:FOA = VA - ImageBase - (所在区段的 RVA - 所在区段的 FOA) 或 FOA = RVA - 所在区段的 RVA + 所在区段的 FOA

在上述代码中,有一个为 IMAGE_FIRST_SECTION,我们来看下它的定义,如下:

其实 IMAGE_FIRST_SECTION 为一个宏,它主要由三部分相加组成,作用是获取到第一个区段的首地址,参数为 NT 头。你可以把这个首地址理解成数组名,数组的首地址。在获取到了地址后,下面的 for 循环遍历所有的区段表找到符合要求的区段。这三部分内容具体如下:

IMAGE_NT_HEADERS 的起始地址
IMAGE_OPTIONAL_HEADER32 (PE 扩展头)在 IMAGE_NT_HEADERS 中的偏移
IMAGE_OPTIONAL_HEADER32 的大小

其中后两个加起来的大小恰好就是 IMAGE_NT_HEADERS 的大小,再跟第一个相加就得到区段表的地址了。看到这你可以会问,为什么不直接加上 IMAGE_NT_HEADERS 的大小呢?因为 IMAGE_OPTIONAL_HEADER32 大小不固定,32 位下该值为 0x00E0H,64 位下该值为 0x00F0H,并且用户还可以自定义其大小。

3 额外说明

扩展头大小是由文件头中 SizeOfOptionalHeader 字段给出,FIELD_OFFSET 这个是给出 OptionalHeader 在 IMAGE_NT_HEADERS 结构中的偏移,如下:

相关代码如下:

#include <windows.h>
#include <iostream>
#include <iomanip>
#include <time.h>

DWORD OpenPEFile(const char *);
BOOL IsPEFile(DWORD);
void ReadFileHeader();
void ReadOptionalHeader();
DWORD RVAtoFOA(DWORD);

DWORD g_FileAddr = 0;
PIMAGE_DOS_HEADER g_DosHeader = nullptr;
PIMAGE_NT_HEADERS g_NtHeader = nullptr;

int main()
{
	// 获取文件地址
	g_FileAddr = OpenPEFile("KDebugger.exe");

	// 判断文件是否是一个有效的 PE 文件
	if (!IsPEFile(g_FileAddr))
	{
		std::cout << "这不是一个有效的 PE 文件" << std::endl;
		system("pause");
		exit(EXIT_SUCCESS);
	}
	
	std::cout << "--------------------------------------" << std::endl;

	// 解析文件头
	ReadFileHeader();

	std::cout << "--------------------------------------" << std::endl;

	// 解析扩展头
	ReadOptionalHeader();

	system("pause");

	return 0;
}

// 打开一个 PE 文件,并获取它的文件缓冲区内容返回
DWORD OpenPEFile(const char * pFilePath)
{
	// 获取文件句柄
	HANDLE hFile = CreateFileA(
		pFilePath,
		GENERIC_ALL,
		NULL,
		NULL,
		OPEN_EXISTING,
		NULL,
		NULL
	);

	// 判断获取文件句柄是否成功
	if (hFile == INVALID_HANDLE_VALUE)
	{
		std::cout << "打开文件失败!" << std::endl;
		system("pause");
		exit(EXIT_SUCCESS);
	}
	
	// 得到文件大小
	DWORD dwFileSize = GetFileSize(hFile, NULL);

	// 申请空间
	BYTE * pFileBuffer = new BYTE[dwFileSize];

	// 将文件内容读入到缓冲区
	DWORD dwRealRead = 0;
	ReadFile(hFile, pFileBuffer, dwFileSize, &dwRealRead, NULL);

	// 关闭文件句柄
	CloseHandle(hFile);

	// 返回文件缓冲区
	return (DWORD)pFileBuffer;
}

// 判断文件是否为有效的 PE 的文件
BOOL IsPEFile(DWORD dwBuffer)
{
	// 获取 DOS 头
	g_DosHeader = (PIMAGE_DOS_HEADER)dwBuffer;

	// 判断该文件的 DOS 头的第一个字段是否为 IMAGE_DOS_SIGNATRUE
	if (g_DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		// 如果不等于,则立即返回 FALSE
		return FALSE;
	}

	// 获取 NT 头
	g_NtHeader = (PIMAGE_NT_HEADERS)(dwBuffer + g_DosHeader->e_lfanew);

	// 判断该文件的 NT 头的第一个字段是否为 IMAGE_NT_SIGNATRUE
	if (g_NtHeader->Signature != IMAGE_NT_SIGNATURE)
	{
		// 如果不等于,则立马返回 FALSE
		return FALSE;
	}

	// 当上面全部通过后,则说明为一个有效的 PE 文件,返回 TRUE
	return TRUE;
}

// 解析文件头
void ReadFileHeader()
{
	// 获取文件头
	auto FileHeader = g_NtHeader->FileHeader;

	// 进行时间转换
	struct tm FileCreateTime;
	errno_t err = gmtime_s(&FileCreateTime, (time_t *)&FileHeader.TimeDateStamp);

	std::cout << "文件头解析" << std::endl;
	std::cout << "运行平台:0x" << std::hex << std::setfill('0') << std::setw(8) << FileHeader.Machine << std::endl;
	std::cout << "文件区块数目:0x" << std::hex << std::setfill('0') << std::setw(8) << FileHeader.NumberOfSections <<std::endl;
	
	if (err)
	{
		std::cout << "无效的参数" << std::endl;
	}
	else
	{
		std::cout << "文件创建日期和时间:" << std::dec << FileCreateTime.tm_year + 1900 << "-"
			<< FileCreateTime.tm_mon + 1 << "-"
			<< FileCreateTime.tm_mday << " "
			<< FileCreateTime.tm_hour + 8 << ":"
			<< FileCreateTime.tm_min << ":"
			<< FileCreateTime.tm_sec << std::endl;
	}
	
	std::cout << "指向符号表:0x" << std::hex << std::setfill('0') << std::setw(8) << FileHeader.PointerToSymbolTable << std::endl;
	std::cout << "符号表中符号个数:0x" << std::hex << std::setfill('0') << std::setw(8) << FileHeader.NumberOfSymbols << std::endl;
	std::cout << "IMAGE_OPTIONAL_HEADER 结构大小:0x" << std::hex << std::setfill('0') << std::setw(8) << FileHeader.SizeOfOptionalHeader << std::endl;
	std::cout << "文件属性:0x" << std::hex << std::setfill('0') << std::setw(8) << FileHeader.Characteristics << std::endl;
}

// 解析扩展头
void ReadOptionalHeader()
{
	// 获取扩展头
	auto OptionalHeader = g_NtHeader->OptionalHeader;

	std::cout << "扩展头解析" << std::endl;
	std::cout << "标志:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.Magic << std::endl;
	std::cout << "链接器主版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << (int)OptionalHeader.MajorLinkerVersion << std::endl;
	std::cout << "链接器次版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << (int)OptionalHeader.MinorLinkerVersion << std::endl;
	std::cout << "所有含有代码区块的总大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfCode << std::endl;
	std::cout << "所有初始化区块大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfInitializedData << std::endl;
	std::cout << "所有未初始化区块大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfUninitializedData << std::endl;
	std::cout << "程序执行入口 RVA:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.AddressOfEntryPoint << std::endl;
	std::cout << "代码区块起始 RVA:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.BaseOfCode << std::endl;
	std::cout << "数据区块起始 RVA:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.BaseOfData << std::endl;
	std::cout << "程序默认装入基地址:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.ImageBase << std::endl;
	std::cout << "内存中区块的对齐值:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SectionAlignment << std::endl;
	std::cout << "文件中区块的对齐值:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.FileAlignment << std::endl;
	std::cout << "操作系统主版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.MajorOperatingSystemVersion << std::endl;
	std::cout << "操作系统次版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.MinorOperatingSystemVersion << std::endl;
	std::cout << "用户自定义主版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.MajorImageVersion << std::endl;
	std::cout << "用户自定义次版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.MinorImageVersion << std::endl;
	std::cout << "所需子系统主版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.MajorSubsystemVersion << std::endl;
	std::cout << "所需子系统次版本号:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.MinorSubsystemVersion << std::endl;
	std::cout << "映像装入内存后的总尺寸:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfImage << std::endl;
	std::cout << "Dos头、PE头、区块表总大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfHeaders << std::endl;
	std::cout << "映像检验和:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.CheckSum<< std::endl;
	std::cout << "文件子系统:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.Subsystem << std::endl;
	std::cout << "显示DLL特性旗标:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.DllCharacteristics << std::endl;
	std::cout << "初始化堆栈大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfStackReserve << std::endl;
	std::cout << "初始化实际提交堆栈大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfStackCommit << std::endl;
	std::cout << "初始化保留堆栈大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfHeapReserve << std::endl;
	std::cout << "初始化实际保留堆栈大小:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.SizeOfHeapCommit << std::endl;
	std::cout << "数据目录表项数:0x" << std::hex << std::setfill('0') << std::setw(8) << OptionalHeader.NumberOfRvaAndSizes << std::endl;

	std::cout << "**************************************" << std::endl;
	std::cout << "目录表" << std::endl;
	std::cout << "RVA\t\t大小" << std::endl;

	for (int i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; ++i)
	{
		std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8)
			<< OptionalHeader.DataDirectory[i].VirtualAddress << "\t";
		std::cout << "0x" << std::hex << std::setfill('0') << std::setw(8)
			<< OptionalHeader.DataDirectory[i].Size << std::endl;
	}
}

// RVA 转 FOA
DWORD RVAtoFOA(DWORD dwRva)
{
	// 获取区段表的数量
	DWORD dwCounts = g_NtHeader->FileHeader.NumberOfSections;

	// 获取区段表数组的首元素
	auto Sections = IMAGE_FIRST_SECTION(g_NtHeader);

	// 遍历所有的区段表找到符合要求的区段
	for (DWORD i = 0; i < dwCounts; ++i)
	{
		// 要求:RVA >= 区段的首地址 并且 RVA < 区段的结尾的地址
		if (dwRva >= Sections[i].VirtualAddress &&
			dwRva < (Sections[i].VirtualAddress + Sections[i].SizeOfRawData))
		{
			// FOA = VA - ImageBase - (所在区段的 RVA - 所在区段的 FOA)
			// FOA = RVA - 所在区段的 RVA + 所在区段的 FOA
			return dwRva - Sections[i].VirtualAddress + Sections[i].PointerToRawData;
		}
	}

	// 如果找不到就返回 -1
	return -1;
}
0 条回应