PE文件详解 - 导出表
1、导出表
- 导出表是PE文件为其他应用程序提供API的一种函数示例导出方式
- Windows下存在导出表的可执行文件以指定自身的一些变量、函数以及类,并将其导出
- 只有动态链接库才有导出表,包括静态链接库
1.1、IMAGE_EXPORT_DIRECTORY结构
- 结构体定义
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 保留,恒为0x0000000
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 主版本号,一般不赋值
WORD MinorVersion; // 子版本号, 一般不赋值
DWORD Name; // 模块名称
DWORD Base; // 索引基数
DWORD NumberOfFunctions; // 导出地址表中的成员个数
DWORD NumberOfNames; // 导出名称表中的成员个数
DWORD AddressOfFunctions; // 导出地址表(EAT)
DWORD AddressOfNames; // 导出名称表(ENT)
DWORD AddressOfNameOrdinals; // 指向导出序列号数组
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
1.2、通过代码获取导出表
#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);
printf("=================================================\n");
// 获取导入表
ExportTable(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++;
}
}
void ExportTable(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 pExportDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
// 填充导出表 返回的是文件头到导出表的偏移
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToOffset(pExportDir->VirtualAddress, buffer) + buffer);
// 计算DLL名字
char* szName = (char*)(RvaToOffset(pExport->Name,buffer),buffer);
// 判断
if (pExport->AddressOfFunctions == 0)
{
printf("当前没有导出表\n");
return;
}
printf("导出表OFFSET:%08X\n",RvaToOffset(pExportDir->VirtualAddress,buffer));
printf("特征值:%08X\n",pExport->Characteristics);
printf("基数:%08X\n",pExport->Base);
printf("名称OFFSET:%08X\n",pExport->Name);
printf("名称:%s\n",szName);
printf("函数数量:%08X\n",pExport->NumberOfFunctions);
printf("函数名数量:%08X\n",pExport->NumberOfNames);
printf("函数地址:%08X\n",pExport->AddressOfFunctions);
printf("函数名称地址:%08X\n",pExport->AddressOfNames);
printf("函数名称序号地址:%08X\n",pExport->AddressOfNameOrdinals);
// 解析里面的值
// 函数地址数量
DWORD dwNumOfFun = pExport->NumberOfFunctions;
// 函数名数量
DWORD dwNumOfNames = pExport->NumberOfNames;
// 基数
DWORD dwBase = pExport->Base;
// 导出地址表
PDWORD pEat32 = (PDWORD)(RvaToOffset(pExport->AddressOfFunctions,buffer)+buffer);
// 导出名称表
PDWORD pEat32 = (PDWORD)(RvaToOffset(pExport->AddressOfNames, buffer) + buffer);
// 导出序号表
PWORD pId = (PWORD)(RvaToOffset(pExport->AddressOfNameOrdinals, buffer) + buffer);
for (DWORD i = 0; i < dwNumOfFun; i++)
{
if (pEat32[i] == 0)
{
continue;
}
// 查找函数名是否存在
DWORD Id = 0;
for (; Id < dwNumOfNames; Id++)
{
if (pId[Id] == i)
{
break;
}
}
if (Id == dwNumOfNames)
{
printf("Id:%x Address:0x%08X Name[NULL]\n",i+dwBase,pEat32[i]);
}
else
{
char* szFunName = (char*)(RvaToOffset(pEat32[Id],buffer)+buffer);
printf("Id:%x Address:0x%08X Name[%s]\n", i + dwBase, pEat32[i],szFunName);
}
}
}
推荐阅读: