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;//没有进函数转发器
}