windows安全:PE文件头解析

305 阅读18分钟

windows的文件格式

windows中的文件格式如下:

PE 文件
├── DOS 头 (IMAGE_DOS_HEADER)
│   ├── e_magic         // "MZ"
│   └── e_lfanew        // PE 头的偏移量
└── PE 头/NT 头 (IMAGE_NT_HEADERS)
    ├── Signature       // "PE\0\0"
    ├── 标准文件头 (IMAGE_FILE_HEADER)
    │   ├── Machine
    │   ├── NumberOfSections
    │   ├── TimeDateStamp
    │   ├── PointerToSymbolTable
    │   ├── NumberOfSymbols
    │   ├── SizeOfOptionalHeader
    │   └── Characteristics
    └── 扩展文件头 (IMAGE_OPTIONAL_HEADER)
        ├── Magic
        ├── MajorLinkerVersion
        ├── MinorLinkerVersion
        ├── SizeOfCode
        ├── SizeOfInitializedData
        ├── SizeOfUninitializedData
        ├── AddressOfEntryPoint
        ├── BaseOfCode
        ├── BaseOfData
        └── 其他字段...

DOS 头(IMAGE_DOS_HEADER)

读取文件到内存中,可以使用c++标准方法,也可以使用windows的API(这里必须将文件读取到内存中,不然后续获得的偏移地址获取不到数据)

std::ifstream file(filePath, std::ios::binary);
if (!file.is_open()) {
	std::cerr << "Failed to open the file." << std::endl;
	return;
}
// 获取文件大小
file.seekg(0, std::ios::end); 
std::streampos fileSizePos = file.tellg(); 
std::streamsize fileSize = static_cast<std::streamsize>(fileSizePos);

// 分配内存
char* buffer = new char[fileSize];

// 读取文件内容到缓冲区
file.seekg(0, std::ios::beg);
file.read(buffer, fileSize);
if (file.fail()) {
	std::cerr << "Failed to read the file." << std::endl;
	delete[] buffer;
	return;
}

也可以使用windows API来进行文件读取.

HANDLE hFile = CreateFile(szFilePath, GENERIC_ALL, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
	DWORD errorCode = GetLastError();
	printf("Error Code:%d\r\n", errorCode);
	return NULL;
}
DWORD dwFileSize = GetFileSize(hFile, NULL);
char* szBuffer = new char[dwFileSize];
memset(szBuffer, 0, dwFileSize);
DWORD dwReadSize = 0;
BOOL bRet = ReadFile(hFile, szBuffer, dwFileSize, &dwReadSize, NULL);
if (!bRet)
{
	return NULL;
}

Dos 头解析,从上述PE结构可以看出,文件头即Dos头.直接强转即可.

// 读取 DOS头
PIMAGE_DOS_HEADER pDosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(buffer);
if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
	std::cerr << "Not a valid PE file." << std::endl;
	return;
}

PE/NT 头(IMAGE_NT_HEADERS

winnt.h中定义的结构体名称为IMAGE_NT_HEADERS,所以叫NT头,内存中NT头的Signature(NT头起始位置)是个0x50 0x45,转为字符串就是PE,所以叫PE头.NT头地址偏移位于Dos头中,我们取出偏移强转即可

// 读取 PE/NT 头
PIMAGE_NT_HEADERS pNtHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(buffer + pDosHeader->e_lfanew);

if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
	std::cerr << "Not a valid PE signature." << std::endl;
	return;
}
printf("PE TAG: %08lx\r\n", pNtHeaders->Signature);

标准文件头(IMAGE_FILE_HEADER)扩展文件头(IMAGE_OPTIONAL_HEADER)

// 获取标准文件头
printf("\r\n");
IMAGE_FILE_HEADER fileHeader = pNtHeaders->FileHeader;

// 获取扩展文件头
printf("\r\n");
IMAGE_OPTIONAL_HEADER optionalHeader = pNtHeaders->OptionalHeader;

标准文件头成员

  • Machine:

    • 类型WORD
    • 描述:指示生成该文件的目标机器类型。例如,IMAGE_FILE_MACHINE_X86 表示 x86 架构。
  • NumberOfSections:

    • 类型WORD
    • 描述:指示文件中段的数量。每个段包含不同类型的数据,如代码、数据、资源等。
  • TimeDateStamp:

    • 类型DWORD
    • 描述:文件的时间戳,表示文件创建或修改的时间。通常是一个 UNIX 时间戳。
  • PointerToSymbolTable:

    • 类型DWORD
    • 描述:指向符号表的指针。对于大多数 PE 文件,这个值通常为 0,因为不再使用符号表。
  • NumberOfSymbols:

    • 类型DWORD
    • 描述:符号的数量。对于大多数 PE 文件,这个值通常为 0。
  • SizeOfOptionalHeader:

    • 类型WORD
    • 描述:可选头的大小,通常是 224 或 240 字节,具体取决于是否是 32 位或 64 位可执行文件。
  • Characteristics:

    • 类型WORD
    • 描述:文件的特性标志,指示文件的属性。例如,是否是可执行文件、是否是 DLL 等。

打印出目标机器类型以及文件特性:

IMAGE_OPTIONAL_HEADER optionalHeader = pNtHeaders->OptionalHeader;
printf("入口点(EntryPoint);%08X\r\n", optionalHeader.AddressOfEntryPoint);
printf("镜像基址(ImageBase);%016llx\r\n", optionalHeader.ImageBase);
printf("镜像大小(SizeOfImage);%08X\r\n", optionalHeader.SizeOfImage);
printf("代码基址(BaseOfCode);%08X\r\n", optionalHeader.BaseOfCode);
printf("内存对齐(SectionAlignment);%08X\r\n", optionalHeader.SectionAlignment);
printf("文件对齐(FileAlignment);%08X\r\n", optionalHeader.FileAlignment);
printf("标志(Magic);%04X\r\n", optionalHeader.Magic);

// 第二列
printf("\r\n");
printf("子系统(Subsystem);%04X\r\n", optionalHeader.Subsystem);
printf("区段数量(NumberOfSections);%08X\r\n", fileHeader.NumberOfSections);
printf("日期时间标志(TimeDateStamp);%08X\r\n", fileHeader.TimeDateStamp);
printf("部首大小(SizeOfHeaders);%08X\r\n", optionalHeader.SizeOfHeaders);
printf("特征值(Characteristics);%04X\r\n", fileHeader.Characteristics);
printf("扩展头大小(SizeOfOptionalHeader);%04X\r\n", fileHeader.SizeOfOptionalHeader);
printf("RVA数及大小(NumberOfRvaAndSizes);%08X\r\n", optionalHeader.NumberOfRvaAndSizes);

// 机器类型
printf("\r\n");
printf("目标机器类型(Machine);%08X\r\n", fileHeader.Machine);
printf("\r\n");

区段数据


// 区段信息
printf("\r\n");

PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders);

for (int i = 0; i < fileHeader.NumberOfSections; i++)
{
	printf("区段名:%.8s\r\n", pSectionHeader[i].Name);
	printf("VOffset(相对虚拟地址):%08X\r\n", pSectionHeader[i].VirtualAddress);
	printf("VSize(区段大小):%08X\r\n", pSectionHeader[i].Misc.VirtualSize);
	printf("ROffset(文件偏移):%08X\r\n", pSectionHeader[i].PointerToRawData);
	printf("RSize(文件中区段的大小):%08X\r\n", pSectionHeader[i].SizeOfRawData);
	printf("标记:%08X\r\n", pSectionHeader[i].Characteristics);
	printf("\r\n");
}

数据目录表

数据目录固定为16个,宏为: IMAGE_NUMBEROF_DIRECTORY_ENTRIES.有一个是保留的,实际是15个.

IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

我们可以用一个常量数组来定义:

const char* DataDirName[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] = {
	"Export Directory(导出目录)",// IMAGE_DIRECTORY_ENTRY_EXPORT
	"Import Directory(导入目录)",// IMAGE_DIRECTORY_ENTRY_IMPORT
	"Resource Directory(资源目录)",// IMAGE_DIRECTORY_ENTRY_RESOURCE
	"Exception Directory(异常目录)",// IMAGE_DIRECTORY_ENTRY_EXCEPTION
	"Security Directory(安全目录)",// IMAGE_DIRECTORY_ENTRY_SECURITY
	"Base Relocation Table(基址重定位表)",// IMAGE_DIRECTORY_ENTRY_BASERELOC
	"Debug Directory(调试目录)",// IMAGE_DIRECTORY_ENTRY_DEBUG
	//"Copyright Directory(版权目录) X86",// IMAGE_DIRECTORY_ENTRY_COPYRIGHT (X86 usage)
	"Architecture Specific Data(架构特定数据) X64",// IMAGE_DIRECTORY_ENTRY_ARCHITECTURE
	"Global Pointer(全局指针)",// IMAGE_DIRECTORY_ENTRY_GLOBALPTR
	"TLS Directory(线程局部存储目录)",// IMAGE_DIRECTORY_ENTRY_TLS
	"Load Configuration Directory(加载配置目录)",// IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
	"Bound Import Directory(绑定导入目录)",// IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
	"Import Address Table(导入地址表)",// IMAGE_DIRECTORY_ENTRY_IAT
	"Delay Load Import Descriptors(延迟加载导入描述符)",// IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
	"COM Runtime Descriptor(COM 运行时描述符)",// IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
	"Reserved(保留)"
};

输出数据目录表:

printf("\r\n");
printf("数据目录表:\r\n");
for (size_t i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; i++)
{
	printf("Name:%s\r\n", DataDirName[i]);
	printf("VirtualAddress:%08X\r\n", optionalHeader.DataDirectory[i].VirtualAddress);
	printf("Size:%08X\r\n", optionalHeader.DataDirectory[i].Size);
	printf("\r\n");
}

解析导入表

导入表里面很多数据都是使用的虚拟地址,需要先将虚拟地址转为文件偏移地址.这里写一个公共的函数,进行虚拟地址转文件偏移.

虚拟地址转文件偏移函数

// 虚拟地址转为文件偏移
DWORD RvaToFoa(DWORD dwRva, PIMAGE_NT_HEADERS pNt)
{
	PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNt);
	if (dwRva < pSectionHeader[0].VirtualAddress)
	{
		// 比最小的虚拟地址还小,直接返回
		return dwRva;
	}
	// 遍历所有段
	for (size_t i = 0; i < pNt->FileHeader.NumberOfSections; i++)
	{
		DWORD startVir = pSectionHeader[i].VirtualAddress;
		DWORD virSize = pSectionHeader[i].Misc.VirtualSize;
		DWORD secAddr = pSectionHeader[i].PointerToRawData;

		if (dwRva >= startVir && dwRva <= startVir + virSize)
		{
			return dwRva - startVir + secAddr;
		}
	}
}

导入表解析

  1. 导入表,通过数据目录表地址偏移获取到导入表的地址pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT
  2. 然后通过RVA2Fov将虚拟地址转为文件偏移地址,来获取到第一个模块地址(PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(pImportDir->VirtualAddress, pNtHeaders) + buffer)
  3. 模块是一个数组,所以后续可以通过指针自增来实现模块的遍历.
  4. 通过模块的OriginalFirstThunk虚拟地址,找到函数的文件偏移地址. `(PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk, pNtHeaders) + buffer);
  5. 函数PIMAGE_THUNK_DATA分为32位版本与64位版本,OriginalFirstThunk指向的是第一个函数地址(RVA),实际地址可以通过PIMAGE_THUNK_DATA的自增来获取,虚拟地址则通过OriginalFirstThunk地址偏移结构体大小来获取,文件偏移地址则通过RVA转Foa来获取.
  6. 函数的导入有通过名称导入的,也有通过序号导入的.通过名称导入的thunkData->u1.Ordinal的最高位为0即(thunkData->u1.Ordinal & 0x80000000) == 0,Function 最高位也是一样的.
  7. 为名称导入的,thunkData->u1.AddressOfData地址存储的是PIMAGE_IMPORT_BY_NAME结构的RVA地址,转Foa后进行基址相加得到实际地址.
  8. 序号导入的,则可以获取序号thunkData->u1.Function - 0x80000000 (这里需要减去最高位的1)

整体树状关系

PIMAGE_IMPORT_DESCRIPTOR
├── Name                  // 导入模块的名称(RVA)
├── TimeDateStamp        // 时间戳
├── ForwarderChain       // 转发链(通常为0)
├── OriginalFirstThunk   // 指向原始导入函数的指针(RVA)
│   └── PIMAGE_THUNK_DATA // 原始导入项
│       ├── Ordinal       // 序号或指向名称的地址
│       └── AddressOfData // 指向 PIMAGE_IMPORT_BY_NAME
│           └── PIMAGE_IMPORT_BY_NAME // 指向实际的导入名称
│               ├── Hint  // 函数提示(如果有)
│               └── Name  // 函数名称
└── FirstThunk           // 指向实际导入函数的指针(RVA)
    └── PIMAGE_THUNK_DATA // 实际导入项
        ├── Function      // 实际函数地址(RVA)
        └── Ordinal       // 序号(如果是序号导入)

解析代码

void ImportTableParser(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
	// 导入表目录(通过地址偏移找到导入表的地址)
	PIMAGE_DATA_DIRECTORY pImportDir = pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT;
	// 导入表
	PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(pImportDir->VirtualAddress, pNtHeaders) + buffer);
	while (pImport->Name != NULL)
	{
		// 导入模块读取
		char* szModuleName = (char*)(RvaToFoa(pImport->Name, pNtHeaders) + buffer);
		printf("模块名称:%s\r\n", szModuleName);
		printf("日期时间标志:%08X\r\n", pImport->TimeDateStamp);
		printf("ForwarderChain:%08X\r\n", pImport->ForwarderChain);
		printf("模块名称Offset:%08X\r\n", pImport->Name);
		printf("\r\n");
		printf("FirstThunk(模块运行时的地址(虚拟)):%08X\r\n", pImport->FirstThunk);
		printf("OriginalFirstThunk(模块加载时的地址(虚拟)):%08X\r\n", pImport->OriginalFirstThunk);

		// API 解析
		PIMAGE_THUNK_DATA thunkData = (PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk, pNtHeaders) + buffer);
		DWORD dwIndex = 0;
		DWORD dwImportOffset = 0;
		while (thunkData->u1.Ordinal != 0)
		{
			printf("\r\n");
			// 从第一个ThunkData 开始,每次步进PIMAGE_THUNK_DATA大小(这里是找的RVA虚拟地址)
			printf("ThunkRva:%08X\r\n", pImport->OriginalFirstThunk + dwIndex);
			dwImportOffset = RvaToFoa(pImport->OriginalFirstThunk, pNtHeaders);
			// 这里是文件偏移地址(未加上文件基址)
			printf("ThunkOffset:%08X\r\n", dwImportOffset + dwIndex);

			// 实际函数地址1
			printf("ThunkOffset2:%016llX\r\n", (thunkData));
			// 实际函数地址2 两者的值是一样的,一个是自增,一个是通过RVA自增然后转换
			printf("ThunkOffset2:%016llX\r\n", ((RvaToFoa(pImport->OriginalFirstThunk + dwIndex, pNtHeaders) + buffer)));


			// 目标是32位的IMAGE_THUNK_DATA32,64位的用IMAGE_THUNK_DATA64
			dwIndex += sizeof(IMAGE_THUNK_DATA32);
			// 最高位 为 1 时,为序号导入,为0时,为名称导入
			if ((thunkData->u1.Ordinal & 0x80000000) == 0)
			{
				PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(thunkData->u1.AddressOfData, pNtHeaders) + buffer);
				__try
				{
					printf("API名称1:%s\r\n", pName->Name);
					printf("HINT:%04X\r\n", pName->Hint);
				}
				__except (EXCEPTION_EXECUTE_HANDLER)
				{
					// 按序号导入的API
					int nOun = thunkData->u1.Function - 0x80000000;
					char szNameBuffer[MAX_PATH] = { 0 };
					sprintf_s(szNameBuffer, MAX_PATH, "序号:%Xh %dd", nOun, nOun);
					printf("API名称1-2:%s\r\n", szNameBuffer);
					printf("HINT(提示):-\r\n");
				}
				printf("ThunkValue:%016llX\r\n\r\n", thunkData->u1.Function);
			}
			else
			{
				// 序号导入
				int nOun = thunkData->u1.Function - 0x80000000;
				char szNameBuffer[MAX_PATH] = { 0 };
				sprintf_s(szNameBuffer, MAX_PATH, "序号:%Xh %dd", nOun, nOun);
				printf("API名称2:%s\r\n", szNameBuffer);
				printf("HINT:-\r\n");
			}
			thunkData++;
		}
		pImport++;
	}
}

导出表

  1. 导出表通过pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT 地址偏移即可获取到导出表的地址.
  2. 通过(PIMAGE_EXPORT_DIRECTORY)(RvaToFoa(pExportDir->VirtualAddress, pNtHeaders) + buffer) 即可找到导出表的实际内存.
  3. AddressOfFunctions存储的函数地址,AddressOfNames 存储函数名称地址.AddressOfNameOrdinals 存储函数名称与序号映射关系地址.

解析代码如下:

void ExportTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
	PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;
	PIMAGE_DATA_DIRECTORY pExportDir = pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
	PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToFoa(pExportDir->VirtualAddress, pNtHeaders) + buffer);
	char* szName = (char*)(RvaToFoa(pExport->Name, pNtHeaders) + buffer);
	if (pExport->AddressOfFunctions == 0)
	{
		printf("当前没有导出表!\r\n");
		return;
	}
	printf("名称:%s\r\n", szName);
	printf("导出表OFFSET%08X\r\n", RvaToFoa(pExportDir->VirtualAddress, pNtHeaders));
	printf("特征值:%08X\r\n", pExport->Characteristics);
	printf("起始序号:%08X\r\n", pExport->Base);
	printf("名称OFFSET:%08X\r\n", pExport->Name);
	printf("导出函数总数:%08X\r\n", pExport->NumberOfFunctions);
	printf("具有函数名的导出函数总数:%08X\r\n", pExport->NumberOfNames);
	printf("函数地址:%08X\r\n", pExport->AddressOfFunctions);
	printf("函数序号地址:%08X\r\n", pExport->AddressOfNameOrdinals);
	printf("函数名称地址:%08X\r\n", pExport->AddressOfNames);

	DWORD dwFunCount = pExport->NumberOfFunctions;
	DWORD dwNameCount = pExport->NumberOfNames;
	DWORD dwBase = pExport->Base;

	PDWORD pEat = (PDWORD)(RvaToFoa(pExport->AddressOfFunctions, pNtHeaders) + buffer);
	PDWORD pEnt = (PDWORD)(RvaToFoa(pExport->AddressOfNames, pNtHeaders) + buffer);
	PWORD pEot = (PWORD)(RvaToFoa(pExport->AddressOfNameOrdinals, pNtHeaders) + buffer);

	for (DWORD i = 0; i < dwFunCount; i++)
	{
		if (pEat[i] == 0)
		{
			continue;
		}
		// 找出函数名称对应的序号
		DWORD dwOrdinals = 0;
		for (; dwOrdinals < dwNameCount; dwOrdinals++)
		{
			// 名称序号 == 函数序号,则找到名称
			if (pEot[dwOrdinals] == i)
			{
				break;
			}
		}
		// 没有找到序号对应的名称
		if (dwOrdinals == dwNameCount)
		{
			printf("Ordinals:%x Addresss:0x%08X Name[NULL]\r\n", dwBase + i, pEat[i]);
		}
		else
		{
			char* szFuncName = (char*)(RvaToFoa(pEnt[dwOrdinals], pNtHeaders) + buffer);
			printf("Ordinals:%x Addresss:0x%08X Name[%s]\r\n", dwBase + i, pEat[i], szFuncName);
		}
	}
}

资源表

  1. 首先通过数据目录表,偏移获取到资源地址:PIMAGE_DATA_DIRECTORY pResDir = pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_RESOURCE
  2. 然后获取资源表的文件偏移基址: (RvaToFoa(pResDir->VirtualAddress, pNtHeaders) + buffer)
  3. 该基址指向的即为结构IMAGE_RESOURCE_DIRECTORY的地址,即PIMAGE_RESOURCE_DIRECTORY pFirst = (PIMAGE_RESOURCE_DIRECTORY)resourceBaseAddr 这里获取资源目录的信息,在目录信息后,紧跟随的是一个目录下的数据列表(数组)
  4. 取出资源数据列表的对象,这里用指针.(PIMAGE_RESOURCE_DIRECTORY_ENTRY)(pFirst + 1) 这里+1表示步进一个IMAGE_RESOURCE_DIRECTORY大小. 也可以转为字符指针,步进IMAGE_RESOURCE_DIRECTORY结构大小.
  5. 递归展开

整体结构如下:

PIMAGE_RESOURCE_DIRECTORY
├── NumberOfNamedEntries   // 命名条目数量
├── NumberOfIdEntries      // ID条目数量
├── PIMAGE_RESOURCE_DIRECTORY_ENTRY (Entry 1)
│   ├── Name (字符串或ID)
│   ├── OffsetToData (指向数据或子目录的偏移)
│   ├── (如果是字符串名称)
│   │   └── PIMAGE_RESOURCE_DIR_STRING_U (包含字符串长度和内容)
│   └── (如果是子目录)
│       └── PIMAGE_RESOURCE_DIRECTORY (指向子目录)
│   └── (如果是资源数据)
│       └── PIMAGE_RESOURCE_DATA_ENTRY (包含资源的 RVA 和大小)
│           ├── RVA           // 资源的相对虚拟地址
│           ├── Size          // 资源的大小
│           └── CodePage      // 代码页
├── PIMAGE_RESOURCE_DIRECTORY_ENTRY (Entry 2)
│   ├── Name (字符串或ID)
│   ├── OffsetToData (指向数据或子目录的偏移)
│   ├── (如果是字符串名称)
│   │   └── PIMAGE_RESOURCE_DIR_STRING_U
│   └── (如果是子目录)
│       └── PIMAGE_RESOURCE_DIRECTORY
│   └── (如果是资源数据)
│       └── PIMAGE_RESOURCE_DATA_ENTRY
│           ├── RVA
│           ├── Size
│           └── CodePage
└── ...

解析代码如下:


const char* g_ResType[0x11] = {
"NULL",
"鼠标指针",// RT_CURSOR        
"位图", // RT_BITMAP
"图标",
"菜单",
"对话框",
"字符串列表",
"字体目录",
"字体",
"快捷键",
"非格式化资源",
"消息列表",
"鼠标指针组",
"NULL",
"图标组",
"NULL",
"版本信息",
};

void ResourceTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{

	PIMAGE_OPTIONAL_HEADER pOptionalHeader = &pNtHeaders->OptionalHeader;

	PIMAGE_DATA_DIRECTORY pResDir = pOptionalHeader->DataDirectory + IMAGE_DIRECTORY_ENTRY_RESOURCE;
	char* resourceBaseAddr = (RvaToFoa(pResDir->VirtualAddress, pNtHeaders) + buffer);

	PIMAGE_RESOURCE_DIRECTORY pFirst = (PIMAGE_RESOURCE_DIRECTORY)resourceBaseAddr;

	SubDirResourceTable(pNtHeaders, buffer, resourceBaseAddr, pFirst);

}

// 资源目录解析
void SubDirResourceTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer, char* resourceBaseAddr, PIMAGE_RESOURCE_DIRECTORY dir)
{
	printf("\r\n");
	printf("\r\n");

	PIMAGE_RESOURCE_DIRECTORY_ENTRY entry = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)(dir + 1);
	printf("目录名称条目:%04X\r\n", dir->NumberOfNamedEntries);
	printf("目录ID条目:%04X\r\n", dir->NumberOfIdEntries);

	// 总条目数
	DWORD dwResNum = dir->NumberOfIdEntries + dir->NumberOfNamedEntries;

	for (int i = 0; i < dwResNum; ++i)
	{
		// 节点类型解析
		if (entry->NameIsString != 1)
		{
			if (entry->Id < 0x11)
			{
				printf("ResType:%s\r\n", g_ResType[entry->Id]);
			}
			else
			{
				printf("ResType:%d\r\n", entry->Id);
			}
		}
		// 资源名称
		if (entry->NameIsString != 1)
		{
			// 没有名称
			printf("Name(Id):%d\r\n", entry->Id);
		}
		else
		{
			PIMAGE_RESOURCE_DIR_STRING_U pResName = (PIMAGE_RESOURCE_DIR_STRING_U)(resourceBaseAddr + entry->NameOffset);

			wprintf(L"Resource Name: %.*s\n", pResName->Length, pResName->NameString);
		}
		// 判断是否是目录,如果是目录继续递归
		if (entry->DataIsDirectory == 1)
		{
			// 递归读取目录
			PIMAGE_RESOURCE_DIRECTORY nextDir = (PIMAGE_RESOURCE_DIRECTORY)(resourceBaseAddr + entry->OffsetToDirectory);
			SubDirResourceTable(pNtHeaders, buffer, resourceBaseAddr, nextDir);
			continue;
		}
		// 资源数据解析
		PIMAGE_RESOURCE_DATA_ENTRY pStcData = (PIMAGE_RESOURCE_DATA_ENTRY)(resourceBaseAddr + entry->OffsetToData);
		DWORD StcDataOffset = RvaToFoa(pStcData->OffsetToData, pNtHeaders);
		printf("Rva:%08X\r\n", pStcData->OffsetToData);
		printf("Foa:%08X\r\n", StcDataOffset);
		printf("Size:%08X\r\n", pStcData->Size);



		// 下一个资源
		entry++;
	}


}

重定位表解析

  1. 通过数据目录表偏移获取到重定位数据目录表pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_BASERELOC
  2. 根据重定位目录表的VirtualAddress转文件偏移获取到第一个重定位块. (因为重定位块的大小是不一致的,所以查找下一个重定位块的时候,不能通过指针自增来获取,而应该通过大小计算之后的偏移来获取.)
  3. 获取下一个重定位块: (PIMAGE_BASE_RELOCATION)((char*)pReloc + pReloc->SizeOfBlock)
  4. 根据重定位块VirtualAddressRVA地址可以找到对应的块地址,然后获取对应块的名称.
  5. 在重定位块IMAGE_BASE_RELOCATION后紧随的是重定位项RELOCATION_INFO的起始地址.这个重定位项其实就是一个16位(2字节)的结构,前12位表示偏移,后4位表示偏移类型.(32.64都是2字节)
  6. RELOCATION_INFO 结构需要字节定义.
typedef struct _RELOCATION_INFO {
	WORD Offset : 12;  // 12 位偏移
	WORD Type : 4;     // 4 位重定位类型
} RELOCATION_INFO, * PRELOCATION_INFO;
  1. 通过DWORD dwItems = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(RELOCATION_INFO) 即可计算出重定位项的数量
  2. 重定位类型如下:
  • IMAGE_REL_BASED_ABSOLUTE (0) :

    • 描述:不进行重定位。
    • 解析:原始地址不变。
  • IMAGE_REL_BASED_HIGH (1) :

    • 描述:高 8 位重定位。
    • 解析:将高 8 位的偏移量加到目标地址的高 16 位。
  • IMAGE_REL_BASED_LOW (2) :

    • 描述:低 8 位重定位。
    • 解析:将低 8 位的偏移量加到目标地址的低 16 位。
  • IMAGE_REL_BASED_HIGHLOW (3) :

    • 描述:32 位重定位。
    • 解析:调整整个 32 位地址。
  • IMAGE_REL_BASED_HIGHADJ (4) :

    • 描述:高位调整重定位,用于进行高 16 位和低 16 位的调整。
    • 解析:调整高 16 位,并可能影响后续低 16 位的计算。
  • IMAGE_REL_BASED_MACHINE_SPECIFIC_5 (5) :

    • 描述:机器特定的重定位,具体实现依赖于架构。
    • 解析:根据特定机器的需求进行处理。
  • IMAGE_REL_BASED_RESERVED (6) :

    • 描述:保留类型,未定义。
    • 解析:不应用任何重定位。
  • IMAGE_REL_BASED_MACHINE_SPECIFIC_7 (7) :

    • 描述:另一个机器特定的重定位。
    • 解析:根据特定机器的需求进行处理。
  • IMAGE_REL_BASED_MACHINE_SPECIFIC_8 (8) :

    • 描述:另一个机器特定的重定位。
    • 解析:根据特定机器的需求进行处理。
  • IMAGE_REL_BASED_MACHINE_SPECIFIC_9 (9) :

    • 描述:另一个机器特定的重定位。
    • 解析:根据特定机器的需求进行处理。
  • IMAGE_REL_BASED_DIR64 (10) :

    • 描述:用于 64 位重定位。
    • 解析:调整 64 位地址。

重定位项的具体解析代码如下:

void ProcessRelocation(PRELOCATION_INFO relocInfo, DWORD baseAddress, PIMAGE_NT_HEADERS pNtHeaders, char* buffer) {
	DWORD offset = relocInfo->Offset; // 获取偏移
	DWORD dwRva = 0; // 初始化新虚拟地址

	std::string str;

	// 根据重定位类型处理不同的情况
	switch (relocInfo->Type) {
	case IMAGE_REL_BASED_ABSOLUTE:
		// 不做任何调整,原始地址保持不变
		str = "ABSOLUTE";
		break;

	case IMAGE_REL_BASED_HIGH:
		// 高 8 位重定位:将高 8 位的偏移加入到基地址的高 16 位
		dwRva = (baseAddress & 0xFFFF0000) | (offset << 12);
		str = "HIGH";
		break;

	case IMAGE_REL_BASED_LOW:
		// 低 8 位重定位:将低 8 位的偏移加入到基地址的低 16 位
		dwRva = (baseAddress & 0xFFFF0000) | (offset & 0x0FFF);
		str = "LOW";
		break;

	case IMAGE_REL_BASED_HIGHLOW:
		// 32 位重定位:直接将偏移量加到基地址
		dwRva = baseAddress + offset;
		str = "HIGHLOW";
		break;

	case IMAGE_REL_BASED_HIGHADJ:
		// 高位调整重定位:需要特殊处理
		str = "HIGHADJ";
		break;

	case IMAGE_REL_BASED_MACHINE_SPECIFIC_5:
		str = "SPECIFIC_5";
		break;
	case IMAGE_REL_BASED_MACHINE_SPECIFIC_7:
		str = "SPECIFIC_7";
		break;
	case IMAGE_REL_BASED_MACHINE_SPECIFIC_8:
		str = "SPECIFIC_8";
		break;
	case IMAGE_REL_BASED_MACHINE_SPECIFIC_9:
		// 机器特定的重定位:根据特定机器的需求进行处理
		str = "SPECIFIC_9";
		break;

	case IMAGE_REL_BASED_RESERVED:
		// 保留类型:不进行任何调整
		str = "RESERVED";
		break;

	case IMAGE_REL_BASED_DIR64:
		// 64 位重定位:需要进行 64 位地址的调整
		dwRva = baseAddress + offset;
		str = "DIR64";
		break;

	default:
		// 未知的重定位类型:输出错误信息
		printf("Unknown relocation type: %d\n", relocInfo->Type);
		str = "Unknown";
		break;
	}

	printf("RVA(%s): 0x%lX \r\n", str.c_str(), dwRva);


	if (dwRva != 0)
	{
		DWORD dwFoa = RvaToFoa(dwRva, pNtHeaders);
		printf("FOA(%s): 0x%lX\r\n", str.c_str(), dwFoa);

		DWORD64 dwFarCall = *(DWORD64*)(dwFoa + buffer);
		printf("FarCall(%s): 0x%016llX\r\n\n", str.c_str(), dwFarCall);

		// printf("data(%s): 0x%lX\r\n\n", szName, *(char*)dwFarCall);
	}
}

整体结构如下:

PIMAGE_DATA_DIRECTORY
├── VirtualAddress         // 数据目录的虚拟地址
├── Size                   // 数据目录的大小
└── (后续的 PIMAGE_BASE_RELOCATION)  // 基址重定位表的条目
    ├── PIMAGE_BASE_RELOCATION (Entry 1)
    │   ├── VirtualAddress  // 需要重定位的虚拟地址
    │   ├── SizeOfBlock     // 该块的大小
    │   └── (后续的重定位信息)
    │       ├── RELOCATION_INFO (Entry 1)
    │       │   ├── Offset   // 12 位偏移
    │       │   └── Type     // 4 位重定位类型
    │       └── RELOCATION_INFO (Entry 2)
    │           ├── Offset
    │           └── Type
    └── PIMAGE_BASE_RELOCATION (Entry 2)
        ├── VirtualAddress
        ├── SizeOfBlock
        └── (后续的重定位信息)
            ├── RELOCATION_INFO (Entry 1)
            └── RELOCATION_INFO (Entry 2)

解析代码如下:

void RelocTable(PIMAGE_NT_HEADERS pNtHeaders, char* buffer)
{
	DWORD dwRva = 0;
	PWORD pRelocData = NULL;

	// 获取重定位表目录
	PIMAGE_DATA_DIRECTORY pRelocDir = pNtHeaders->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_BASERELOC;
	PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)(RvaToFoa(pRelocDir->VirtualAddress, pNtHeaders) + buffer);

	while (pReloc->VirtualAddress != 0 && pReloc->SizeOfBlock != 0)
	{
		printf("Rva: %08X\r\n", pReloc->VirtualAddress);

		char* szSectionName = GetSectionName(pReloc->VirtualAddress, pNtHeaders);
		if (szSectionName) {
			printf("Section: %s\r\n", szSectionName);
			delete[] szSectionName; // 释放内存
		}
		else {
			printf("Section: Not found\r\n");
		}

		// 获取重定位项的首地址
		PRELOCATION_INFO pRelocation = (PRELOCATION_INFO)((char*)pReloc + sizeof(IMAGE_BASE_RELOCATION));

		// 计算重定位项数量,每一项占用2字节(16位,前12位表示偏移,4位表示重定位类型)
		DWORD dwItems = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(RELOCATION_INFO);

		printf("Items: %X h / %d d\r\n", dwItems, dwItems);

		for (DWORD i = 0; i < dwItems; i++) {

			ProcessRelocation(pRelocation, pReloc->VirtualAddress, pNtHeaders, buffer);
			pRelocation++;
		}
		// 找出下一个重定位块
		pReloc = (PIMAGE_BASE_RELOCATION)((char*)pReloc + pReloc->SizeOfBlock);

	}
}

未完待续....