PE文件:导出表

0x00 导出表简述

导出表是数据目录的第一项。
导出表提供了一些函数供调用者使用。一般来说DLL提供了一些函数可以供外部使用,这些函数通过导出表被调用。
一般来说,dll都有导出表,exe都没有导出表,但是也有情况,dll没有导出表,exe有导出表。

0x01 导出表结构

typedef struct _IMAGE_EXPORT_DIRECTORY {

	DWORD Characteristics;    // 1)  保留,恒为0x00000000
	DWORD TimeDateStamp;     // 2)  时间戳,导出表创建的时间(GMT时间)
	WORD  MajorVersion;     // 3)  主版本号:导出表的主版本号
	WORD  MinorVersion;      // 4)  子版本号:导出表的子版本号
	DWORD Name;          // 5)  指向模块名称的RVA,指向模块名(导出表所在模块的名称)的ASCII字符的RVA
	DWORD Base;          // 6)  导出表用于输出导出函数序号值的基数: 导出函数序号 = 函数入口地址数组下标索引值 + 基数
	DWORD NumberOfFunctions;   // 7)  导出函数入口地址表的成员个数
	DWORD NumberOfNames;     // 8)  导出函数名称表中的成员个数
	DWORD AddressOfFunctions;  // 9)  函数入口地址表的相对虚拟地址(RVA),每一个非0的项都对应一个被导出的函数名称或导出序号(序号+基数等于导出函数序号)
	DWORD AddressOfNames;    // 10) 函数名称表的相对虚拟地址(RVA),存储着指向导出函数名称的ASCII字符的RVA
	DWORD AddressOfNameOrdinals; // 11) 存储着函数入口地址表的数组下标索引值(序号表),跟导出函数名称表的成员顺序对应
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

Characteristics:现在没有用到,一般为0。

TimeDateStamp:导出表生成的时间戳,由连接器生成。

MajorVersion,MinorVersion:看名字是版本,实际貌似没有用,都是0。

Name:模块的名字。

Base:序号的基数,按序号导出函数的序号值从Base开始递增。

NumberOfFunctions:所有导出函数的数量。

NumberOfNames:按名字导出函数的数量。

AddressOfFunctions:一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。

AddressOfNames:一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。

AddressOfNameOrdinals:一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。

在这里插入图片描述

0x02 查找导出函数入口地址

1.按函数索引导出
1.定位到PE 文件头
2.从PE 文件头中的 IMAGE_OPTIONAL_HEADER32 结构中取出数据目录表,并从第一个数据目录中得到导出表的RVA
3.从导出表的 Base 字段得到起始序号
4.将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引
5.检测索引值是否大于导出表的 NumberOfFunctions 字段的值,如果大于后者的话,说明输入的序号是无效的
6.用这个索引值在 AddressOfFunctions 字段指向的导出函数入口地址表中取出相应的项目,这就是函数入口地址的RVA 值,当函数被装入内存的时候,这个RVA 值加上模块实际装入的基地址,就得到了函数真正的入口地址

2.按函数名称导出
1.最初的步骤是一样的,那就是首先得到导出表的地址
2.从导出表的 NumberOfNames 字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环
3.从 AddressOfNames 字段指向得到的函数名称地址表的第一项开始,在循环中将每一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数
4.如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在 AddressOfNamesOrdinals 指向的数组中以同样的索引值取出数组项的值,我们这里假设这个值是x
5.最后,以 x 值作为索引值,在 AddressOfFunctions 字段指向的函数入口地址表中获取的 RVA 就是函数的入口地址

3.函数转发器导出

0x03 导出表的用处

1.知道了导出表的位置,我们可以得到导出函数的地址,进而对这些函数进行Hook。
2.dll劫持时,我们需要在自己的dll中建立一个和原dll一样的导出表。

附上代码:

void* QzGetProcessAddress(HMODULE ModuleBase, const char *Keyword)//NtQuerySystemInformation
{
	char *v1 = (char *)ModuleBase;

	PIMAGE_DOS_HEADER ImageDosHeader = (IMAGE_DOS_HEADER *)v1;
	PIMAGE_NT_HEADERS  ImageNtHeaders = (IMAGE_NT_HEADERS *)((size_t)v1 + ImageDosHeader->e_lfanew);

	PIMAGE_OPTIONAL_HEADER  ImageOptionalHeader = &ImageNtHeaders->OptionalHeader;
	PIMAGE_DATA_DIRECTORY     ImageDataDirectory    = (IMAGE_DATA_DIRECTORY *)(&ImageOptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
	PIMAGE_EXPORT_DIRECTORY ImageExportDirectory = (IMAGE_EXPORT_DIRECTORY *)((size_t)v1 + ImageDataDirectory->VirtualAddress);


	if (ImageExportDirectory->NumberOfNames == 0 || ImageExportDirectory->NumberOfFunctions == 0)
	{
		return NULL;
	}

	DWORD* AddressOfFunctions		 = (DWORD*)((size_t)v1 + ImageExportDirectory->AddressOfFunctions);
	//+		AddressOfFunctions	ntdll.dll!0x772f0a68 (加载符号以获取其他信息) {0x0002c860}	unsigned long *
	DWORD* AddressOfNames			 = (DWORD*)((size_t)v1 + ImageExportDirectory->AddressOfNames);
	//+		AddressOfNames	ntdll.dll!0x772f2f9c (加载符号以获取其他信息) {0x00106818}	unsigned long *
	WORD*   AddressOfNameOrdinals = (WORD *)((size_t)v1 + ImageExportDirectory->AddressOfNameOrdinals);
	//+		AddressOfNameOrdinals	ntdll.dll!0x772f54d0 (加载符号以获取其他信息) {0x0007}	unsigned short *

	void *FunctionAddress = NULL;
	DWORD i;

	//索引导出
	//		(ULONG_PTR)Keyword >> 16	0x0000002f	unsigned long
	if (((ULONG_PTR)Keyword >> 16) == 0) //>> 向右位移16位
	{
		/*
		#define LOWORD(l) ((WORD)((DWORD_PTR)(l) & 0xffff))
		#define HIWORD(l) ((WORD)((DWORD_PTR)(l) >> 16))
		这是windef.h头文件中对宏LOWORD和HIWORD的定义。
		作用分别是取出无符号长整型参数的高16位和低16位。
		因为一个长整型占32位,其中高低16位的值可能有不同的意义,需要通过这2个宏分别取出来使用。取出来的结果是一个无符号短整型的值。
		其原理正如定义那样,取低16位的宏LOWORD使用按位与操作符与数字0xffff运算,而数字0xffff是一个低16位全为1的数字,那么对其位与操作可以得到参数的低16位。
		而取高16位的宏HIWORD则更简单,只需将参数右移16位,剩下的就是原高16位的值了。
		*/
		//#define LOWORD(l)           ((WORD)(((DWORD_PTR)(l)) & 0xffff))
		WORD			 Ordinal = LOWORD(Keyword);
		ULONG_PTR Base	  = ImageExportDirectory->Base;//得到起始序号

		if (Ordinal < Base || Base > Base + ImageExportDirectory->NumberOfFunctions)
		{
			return NULL;
		}
		FunctionAddress = (void*)((size_t)v1 + AddressOfFunctions[Ordinal - Base]);//函数编号-起始序号=函数在AddressOfFunction中的索引号
		 //入口地址=虚拟地址+该动态链接库被导入到地址空间的基地址
	}
	else  //函数名称导出
	{
		//ImageExportDirectory->NumberOfNames = 0x0000094d
		for (i = 0; i < ImageExportDirectory->NumberOfNames; i++)
		{

			//获得函数名称
			char* FunctionName = (char*)((size_t)v1 + AddressOfNames[i]);
			                                         //v1  "MZ"+
			//FunctionName = 0x772f927f "NtQuerySystemInformation"
			if (_stricmp(Keyword, FunctionName) == 0)//FunctionName = 0x00007ffe6fcba5c7 "NtCreateThreadEx"
			{
				//获得函数地址
				FunctionAddress = (void*)((size_t)v1 + AddressOfFunctions[AddressOfNameOrdinals[i]]);
				//FunctionAddress = ntdll.dll!0x7725ac30 (加载符号以获取其他信息)
				break;
			}
		}
	}
	//函数转发器    //属于这个区域内,就是转发器,不属于这个区域的就是真正的导出函数
	if ((char *)FunctionAddress >= (char*)ImageExportDirectory && (char*)FunctionAddress < (char*)ImageExportDirectory + ImageDataDirectory->Size)
	{
		HMODULE v2 = NULL;
		//获得转发模块的名称
		//FunctionAddress =  //Dll.Sub_1........  Dll.#2

		char* v3 = _strdup((char*)FunctionAddress);//????
		//_strdup:重复的字符串。这些函数中的每个函数都返回一个指向复制字符串存储位置的指针,如果不能分配存储,则返回NULL。
		if (!v3)
		{
			return NULL;
		}

		char* FunctionName = strchr(v3, '.');//在字符串中找到一个字符。
		*FunctionName++ = 0;//++为了越过.

		FunctionAddress = NULL;

		//构建转发模块的路径
		char		 ModuleFullPath[MAX_PATH] = { 0 };
		strcpy_s(ModuleFullPath, v3);
		//
		strcat_s(ModuleFullPath, strlen(v3) + 4 + 1, ".dll");

		//判断是不是当前进程已经加载了这个转发模块
		v2 = (HMODULE)QzGetModuleHandle(ModuleFullPath);
		if (!v2)
		{
			//如果没有得到,就要重新加载这个模块
			v2 = LoadLibraryA(ModuleFullPath);
		}
		if (!v2)
		{
			return NULL;
		}
		BOOL v4 = strchr(v3, '#') == 0 ? FALSE : TRUE;
		if (v4)
		{
			//函数索引转发
			WORD FunctionOrdinal = atoi(v3 + 1);//将给定的字符串转换为整数。
			//递归自己
			FunctionAddress = QzGetProcessAddress(v2, (const char*)FunctionOrdinal);
		}
		else
		{
			//函数名称转发    递归自己
			FunctionAddress = QzGetProcessAddress(v2, FunctionName);
		}
		free(v2); 
	}
	return FunctionAddress;//没有进函数转发器
}
发布了19 篇原创文章 · 获赞 21 · 访问量 983

猜你喜欢

转载自blog.csdn.net/weixin_43742894/article/details/105252653