PE文件详解 - 导入表
1、导入表
- 通过PELoad查看导入表
1.1、IMAGE_IMPORT_DESCRIPTOR结构
-
它不是系统真正引导PE的结构
-
导入表基本结构
-
重要字段
- OriginalFirstThunk
- FirstThunk
- Name
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // IAT的虚拟地址
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // 输入地址表 IAT的虚拟地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
1.2、IMAGE_THUNK_DATA32结构
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD 导入函数实际的内存地址
DWORD Ordinal;
DWORD AddressOfData; // 指向这个结构PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
1.3、IMAGE_IMPORT_BY_NAME结构
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
CHAR Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
1.4、通过代码解析导入表
- 头文件
#pragma once
#include<stdio.h>
#include<Windows.h>
// 计算数据目录表起始位置到文件头的偏移
DWORD RvaToOffset(DWORD dwRva, char* buffer);
// 解析导入表的函数
void ImportTable(char * buffer);
- 实现代码
#include<stdio.h>
#include<Windows.h>
#include "Entry.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);
}
printf("=================================================\n");
ImportTable(buffer);
// 释放动态内存
free(buffer);
// 关闭文件
fclose(pFile);
return 0;
}
// dwRva 是某个数据目录表的起始位置
// buffer 读取到的PE文件缓冲区
DWORD RvaToOffset(DWORD dwRva, char* buffer)
{
// 从Dos头解析
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
// PE头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
// 区段表
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
//判断是否落在头部当中
if (dwRva < pSection[0].VirtualAddress)
{
return dwRva;
}
// VirtualAddress 起始地址
// Size 长度
// VirtualAddress + Size = 结束地址
// 判断是否落在某个区段内
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
{
if (dwRva >= pSection[i].VirtualAddress && dwRva <= pSection[i].VirtualAddress+pSection[i].Misc.VirtualSize)
{
// dwRva - pSection[i].VirtualAddress 是数据目录表起始地址到区段起始地址的偏移(OFFSET)
// pSection[i].PointerToRawData 区段到文件头的偏移
// 这一串返回的是数据目录表起始地址到文件头的偏移
return dwRva - pSection[i].VirtualAddress + pSection[i].PointerToRawData;
}
}
}
// 解析导入表
void ImportTable(char* buffer)
{
// 从DOS头开始解析
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
// PE头
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
// 定位导入表
PIMAGE_DATA_DIRECTORY pImportDir = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT);
// 填充结构
PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToOffset(pImportDir->VirtualAddress,buffer)+buffer);
// 判断导入表的动态链接库是否遍历完
while (pImport->Name != NULL)
{
// 获取名字,因为NAME是一个相对虚拟地址,所以要计算
char *szDllName = (char *)(RvaToOffset(pImport->Name, buffer) + buffer);
printf("DLL名称:%s\n",szDllName);
printf("日期时间标志:%08X\n",pImport->TimeDateStamp);
printf("ForwarderChain:%08X\n",pImport->ForwarderChain);
printf("名称OFFSET:%08X\n",pImport->Name);
printf("FirstThunk:%08X\n",pImport->FirstThunk);
printf("OriginalFirstThunk:%08X\n\n",pImport->OriginalFirstThunk);
// 解析里面的函数
// 指向导入地址表的RVA 通过OriginalFirstThunk字段来计算偏移
PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(RvaToOffset(pImport->OriginalFirstThunk, buffer) + buffer);
DWORD index = 0;
DWORD ImportOffset = 0;
// 被导入函数的序号
while (pIat->u1.Ordinal != 0)
{
printf("ThunkRva:%08X\n",pImport->OriginalFirstThunk+index);
ImportOffset = RvaToOffset(pImport->OriginalFirstThunk,buffer);
printf("ThunkOffset:%08X\n", ImportOffset + index);
index += 4;
if ((pIat->u1.Ordinal & 0x80000000) != 1)
{
PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(RvaToOffset(pIat->u1.AddressOfData,buffer)+buffer);
printf("API名称:%s\n",pName->Name);
// 序号
printf("Hint:%04X\n",pName->Hint);
// 被导入函数地址 pIat->u1.Function
printf("ThunkValue:%08X\n\n",pIat->u1.Function);
}
// 指向下一个
pIat++;
}
// 判断完后,跳到下个结构
pImport++;
}
}
推荐阅读: