Windows PE数据目录表和导出表解析

起因

五一假期没有回家,是因为在四月份已经向提了离职.估计五月中旬应该就能办理离职手续,在公司呆了6年多,合同由3(2次)年换成无固定期限劳动合同.因为家庭原因,最终还是要回去的.不知不觉间北漂了8年,在公司工作期间经历我人生的大事(结婚和生子).

关于PE相关的内容,在前面也写过 学习PE 和 在解析PE遇到的问题,这里也不多的介绍了.
在看正文之前,先看看图,对下边具体要做的事情,有一个大概的认知.

关系图(省略节表部分)

从PE中解析导出表,根据导出表的地址,进行获取函数调用的地址

数据目录表

在 学习PE 这边博文中,已经对可选PE头进行了解析. 在IMAGE_OPTIONAL_HEADER结构体中这个字段DataDirectory,就是我们通常所说的数据目录表.重新看看这个可选PE头结构.
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16  //数据目标表的长度

//可选PE头
typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  //数据目录表
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32

//数据目录表结构
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;  //在内存中的相对地址
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;


先将数据目录表信息打印出来,在用PETool进行对比,看看是否是有问题.
//打印出 数据目录表 信息
void print_data_dir(char* base)
{
	PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base;
	PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((unsigned long)base + dos_header->e_lfanew);


	PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)((unsigned long)nt_header + 4);  //4为nt头中Signature DWORD
	PIMAGE_OPTIONAL_HEADER optional_header = (PIMAGE_OPTIONAL_HEADER)((unsigned long)file_header + IMAGE_SIZEOF_FILE_HEADER);

	for (int i = 0; i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES; i++)
	{
		IMAGE_DATA_DIRECTORY data_dir = optional_header->DataDirectory[i];
		printf("%d data:%08x size:%x\n", i, data_dir.VirtualAddress, data_dir.Size);
	}
}

int main(int argc, char* argv[])
{
	char* filename = "DllExportTable.dll"; //以动态库为例

	int size = file_size(filename);
	if (size > 0)
	{
		char* buf;
		file_to_memory(filename, size, &buf);

		print_data_dir(buf);
	}
	else
	{
		printf("file not found!\n");
	}

	return 0;
}

PE中的数据目录表

 0  导出表 
 1  导入表 
 2 资源表
 3 异常表
 4安全证书表 
 5重定位表 
 6调试信息表 
 7版权所有表 
 8全局指针表 
 9TLS(线程本地存储表) 
 10加载配置表 
 11绑定导入表 
 12IAT(导入地址表) 
 13延迟导入表 
 14COM表 
  15 保留(这个暂时没用) 
正常我们只需要关注重点表: 导出表/导入表/重定位/IAT

导出表

从数据目录表中,知道了第一个表是导出表,并且导出表的VirtualAddress.这个时候要了解这两个RVA和FOA知识点.
RVA(相对虚拟地址):VirtualAddress就是RVA.
FOA(文件偏移地址):
如果解析PE时候,如果不进行内存拉伸,VirtualAddress就需要用RVA转FOA的转换,这个转换是拿导出表的VirtualAddress在节表中查找每个节的VirtualAddress的区域中.然后根据节中的PointerToRelocation(文件中的偏移)
//相对虚拟地址转换为文件偏移地址
int rva_to_foa(char* base, unsigned long* va)
{
	unsigned long rva = *va;
	PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base;
	PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((unsigned long)base + dos_header->e_lfanew);


	PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)((unsigned long)nt_header + 4);
	PIMAGE_OPTIONAL_HEADER optional_header = (PIMAGE_OPTIONAL_HEADER)((unsigned long)file_header + IMAGE_SIZEOF_FILE_HEADER);

	//1. 获取节表
	PIMAGE_SECTION_HEADER section_headersection_header = (PIMAGE_SECTION_HEADER)((unsigned long)optional_header + file_header->SizeOfOptionalHeader);

	DWORD alignment = optional_header->SectionAlignment;  //内存对齐大小 4096
	//2. 获取节表的个数
	int sections = nt_header->FileHeader.NumberOfSections;
	int offset = -1;
	for (int i = 0; i < sections; i++)
	{
		PIMAGE_SECTION_HEADER psection = section_headersection_header + i;

		//3. 获取节的RVA
		DWORD section_start = psection->VirtualAddress;

		if (rva < section_start)
		{
			offset = rva;
			return offset;
		}

		int block_count = psection->SizeOfRawData / alignment;
		block_count += psection->SizeOfRawData % alignment ? 1 : 0;
		//4. 判断导出表的相对虚拟地址rva 是否在这个节中
		if (rva >= section_start && rva < section_start + block_count * alignment)
		{
			//5. 获取节的文件中偏移加上导出表的rva减去节的rva (真正在文件中的偏移地址)
			offset = psection->PointerToRawData + rva - section_start;
			return offset;
		}
	}

	return offset;
}
//打印出 数据目录表
void print_data_dir(char* base)
{
	PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base;
	PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((unsigned long)base + dos_header->e_lfanew);


	PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)((unsigned long)nt_header + 4);  //4为nt头中Signature DWORD
	PIMAGE_OPTIONAL_HEADER optional_header = (PIMAGE_OPTIONAL_HEADER)((unsigned long)file_header + IMAGE_SIZEOF_FILE_HEADER);
	
	for (int i=0;i< IMAGE_NUMBEROF_DIRECTORY_ENTRIES;i++)
	{
		IMAGE_DATA_DIRECTORY data_dir = optional_header->DataDirectory[i];
		printf("%d data:%08x size:%x\n", i, data_dir.VirtualAddress, data_dir.Size);
	}


	IMAGE_DATA_DIRECTORY export_dir = optional_header->DataDirectory[0];
	int offset = rva_to_foa(base, &export_dir.VirtualAddress);

	//根据导出表的rva,进行foa转换
	//获取导出表
	PIMAGE_EXPORT_DIRECTORY export_table = (PIMAGE_EXPORT_DIRECTORY)(base + offset);
}
//IMAGE_EXPORT_DIRECTORY结构体 主要的字段

//DWORD   Name;
//DWORD   Base;
//IMAGE_EXPORT_DIRECTORY 导出表
//DWORD   NumberOfFunctions;   //导出函数的个数
//DWORD   NumberOfNames;       //函数名称的函数个数
//DWORD   AddressOfFunctions;     导出函数的地址表 rva   个数用NumberOfFunctions
//DWORD   AddressOfNames;         // 导出函数名称表 rva  个数用NumberOfNames
//DWORD   AddressOfNameOrdinals;  // 导出序号表 rva  个数用NumberOfNames

//函数名字 通过AddressOfNames(将地址rva到foa) 查到之后,去AddressOfNameOrdinals 获取序号,然后在根据序号去AddressOfFunctions表找到函数的地址
//序号   通过序号 减去base 得到的下标,直接去AddressOfFunctions

上面拿到导出表,通过导出表解析,可以实现GetProcAddress功能.

动态库头文件

#ifdef __cplusplus
extern "C"
{
#endif
	int add(int a, int b);

	int sub(int a, int b);

#ifdef __cplusplus
}
#endif

动态库源文件

_declspec (dllexport)  int  add(int a, int b)
{
	return a + b;
}

_declspec (dllexport) int  sub(int a, int b)
{
	return a - b;
}

看看如何实现:

//获取函数的调用地址
void* my_proc_address(char* base, char* func_name)
{
	PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base;
	PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((unsigned long)base + dos_header->e_lfanew);


	PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)((unsigned long)nt_header + 4);
	PIMAGE_OPTIONAL_HEADER optional_header = (PIMAGE_OPTIONAL_HEADER)((unsigned long)file_header + IMAGE_SIZEOF_FILE_HEADER);
	PIMAGE_SECTION_HEADER section_headersection_header = (PIMAGE_SECTION_HEADER)((unsigned long)optional_header + file_header->SizeOfOptionalHeader);


	IMAGE_DATA_DIRECTORY export_table = optional_header->DataDirectory[0];

	PIMAGE_EXPORT_DIRECTORY  export = (PIMAGE_EXPORT_DIRECTORY)(base + export_table.VirtualAddress);
	unsigned long names = export->NumberOfNames;

	unsigned long* address_names = (unsigned long*)(base + export->AddressOfNames);

	unsigned short* address_name_ordinals = (unsigned short*)(base + export->AddressOfNameOrdinals);

	unsigned long* address_fuctions = (unsigned long*)(base + export->AddressOfFunctions);

	for (int i = 0; i < names; i++)
	{
		char* name = (char*)(base + address_names[i]);

		if (strcmp(name, func_name) == 0)
		{
			int ordinal = address_name_ordinals[i];
			void* func_addr = (void*)(base + address_fuctions[ordinal]);
			return func_addr;
		}
	}
	return NULL;
}

int main(int argc, char* argv[])
{
	typedef int(*Add)(int a, int b);

	HMODULE hmodule = LoadLibraryEx("DllExportTable.dll", NULL, DONT_RESOLVE_DLL_REFERENCES);
	
	//my_proc_address 实现GetProcAddress()功能
	Add add = (Add)my_proc_address(hmodule, "add");
	int sum = add(100, 100);
	printf("sum=%d\n", sum);
}

通过导出表获取函数的调用地址,实现GetProAddress功能

上面实现GetProcAddress函数的代码,为什么没有进行RVA到FOA的转换呢? 因为LoadLibraryEx函数已经dll文件加载到内存上进行了拉伸操作.所以不需要进行转换.

通过序号查找函数的调用地址

//通过序号,查找函数在内存中地址
//1. 获取导出表在内存中的位置
//2. 序号减去导出表中起始函数序号(Base),得到的下标就是
void* my_proc_ordinal(char* base, int ordinal)
{
	if (ordinal > 0)
	{
		PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)base;
		PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)((unsigned long)base + dos_header->e_lfanew);


		PIMAGE_FILE_HEADER file_header = (PIMAGE_FILE_HEADER)((unsigned long)nt_header + 4);
		PIMAGE_OPTIONAL_HEADER optional_header = (PIMAGE_OPTIONAL_HEADER)((unsigned long)file_header + IMAGE_SIZEOF_FILE_HEADER);
		PIMAGE_SECTION_HEADER section_headersection_header = (PIMAGE_SECTION_HEADER)((unsigned long)optional_header + file_header->SizeOfOptionalHeader);


		IMAGE_DATA_DIRECTORY export_table = optional_header->DataDirectory[0];  //导出表 内存中相对地址

		PIMAGE_EXPORT_DIRECTORY  export = (PIMAGE_EXPORT_DIRECTORY)(base + export_table.VirtualAddress);  //导出表 在内存的地址

		//这里获取函数个数,要特殊处理一下
		//正常情况下,用NumberOfFunctions就能获取到
		//其他情况下,如在def文件,让个别函数没有名称或者指定序号
		unsigned long numbers = export->Base + export->NumberOfFunctions - 1;  //起始序号加个数 在减一
		if (ordinal > numbers)
		{
			return NULL;
		}

		unsigned long* address_fuctions = (unsigned long*)(base + export->AddressOfFunctions);
		unsigned long base_count = export->Base;

		return (void*)(base + address_fuctions[ordinal - base_count]);
	}
	return NULL;
}


秋风 2020-05-03