跳至主要內容

53-IAT 隐藏和混淆——自定义 GetProcAddress

Maldevacademy大约 7 分钟安全开发

简介

GetProcAddress WinAPI 函数从指定的模块句柄中获取导出函数的地址。如果在指定的模块句柄中未找到函数名,该函数将返回 NULL

在本模块中,将实现一个代替 GetProcAddress 的函数。新函数的原型如下所示。

FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {}

GetProcAddress 的工作原理

首先需要解决的问题是 GetProcAddress WinAPI 如何查找并获取函数的地址。

hModule 参数是已加载 DLL 的基本地址。这是在进程的地址空间中找到 DLL 模块的地址。牢记这一点,可以通过循环遍历提供的 DLL 内部导出的函数,并检查是否存在目标函数的名称来检索函数地址。如果存在有效匹配,则检索地址。

要访问导出的函数,需要访问 DLL 的导出表并在其中循环查找目标函数名称。

读取 - 导出表结构

回想一下解析 PE 标头模块,提到导出表是一个结构,定义为 IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

这个模块中结构的相关成员是最后三个。

  • AddressOfFunctions - 指定导出函数地址数组的地址。

  • AddressOfNames - 指定导出函数名称地址数组的地址。

  • AddressOfNameOrdinals - 指定导出函数序数的数组地址。

回忆 - 访问导出表

让我们回顾一下如何检索导出目录 IMAGE_EXPORT_DIRECTORY。下面的代码片段应该是熟悉的,因为它已在 解析 PE 头部 模块中进行了说明。

函数开头的变量 pBase 是代码片段中唯一新增的内容。创建此变量是为了避免在将相对虚拟地址 (RVAs) 转换为虚拟地址 (VAs) 时稍后进行类型转换。在将 PVOID 数据类型添加到值时,Visual Studio 编译器会引发错误,因此将 hModule 强制转换为 PBYTE

FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {

	// 我们这样做是为了避免每次使用 'hModule' 时进行转换
	PBYTE pBase = (PBYTE) hModule;
	
	// 获取 DOS 头并执行签名检查
	PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE) 
		return NULL;
	
	// 获取 NT 头并执行签名检查
	PIMAGE_NT_HEADERS	pImgNtHdrs	= (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) 
		return NULL;

	// 获取可选头
	IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;

	// 获取图像导出表
	// 这是导出目录
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
  
    // ...
}

访问导出函数

获取到 IMAGE_EXPORT_DIRECTORY 结构的指针后,就可以遍历导出的函数。NumberOfFunctions 成员指定了由 hModule 导出的函数数量。因此,循环的最大迭代次数应等于 NumberOfFunctions

for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
  // 搜索目标导出的函数 
}

构建搜索逻辑

下一步是为函数构建搜索逻辑。构建搜索逻辑需要使用 AddressOfFunctionsAddressOfNamesAddressOfNameOrdinals,它们都是包含 RVAs 的数组,引用导出表中的单个唯一函数。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    // ...
	// ...
    DWORD   AddressOfFunctions;     // 从映像基址开始的 RVA
    DWORD   AddressOfNames;         // 从映像基址开始的 RVA
    DWORD   AddressOfNameOrdinals;  // 从映像基址开始的 RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

由于这些元素是 RVAs,因此必须添加模块的基址 pBase 来获取 VA。前两个代码片段应该比较简单。它们分别获取函数的名称和函数的地址。第三个片段获取函数的序号,这将在下一节中详细说明。

// 获取函数名称数组指针
PDWORD FunctionNameArray 	= (PDWORD)(pBase + pImgExportDir->AddressOfNames);

// 获取函数地址数组指针
PDWORD FunctionAddressArray 	= (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// 获取函数序号数组指针
PWORD  FunctionOrdinalArray 	= (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

序号简介

函数的序号是整数,表示 DLL 中导出函数表中函数的位置。导出表组织为函数指针的列表(数组),每个函数根据其在表中的位置分配一个序号值。

image
image

请注意,序号值用于标识函数的地址,而不是其名称。导出表采用这种方式来处理函数名称不可用或不唯一的情况。此外,使用序号获取函数的地址比使用名称更快。因此,操作系统使用序号获取函数的地址。

例如,VirtualAlloc 的地址等于 FunctionAddressArray[VirtualAlloc 的序号], 其中 FunctionAddressArray 是从导出表获取的函数地址数组指针。

基于此,下面的代码段将打印指定模块的函数数组中每个函数的序号值。

// 获取函数名称数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	
// 获取函数地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	
// 获取函数序号数组指针
PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// 循环所有导出函数
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){

	// 获取函数的名称
	CHAR* pFunctionName		= (CHAR*)(pBase + FunctionNameArray[i]);
	
	// 获取函数的序号
	WORD wFunctionOrdinal = FunctionOrdinalArray[i];

	// 打印
	printf("[ %0.4d ] 名称: %s -\t 序号: %d\n", i, pFunctionName, wFunctionOrdinal);
}

GetProcAddressReplacement 部分演示

尽管 GetProcAddressReplacement 尚未完成,但现在它应该输出函数名称及其关联的序号。要测试迄今为止已构建的内容,请使用以下参数调用该函数:

GetProcAddressReplacement(GetModuleHandleA("ntdll.dll"), NULL);

正如预期的那样,函数名称和函数的序号被打印到控制台。

image
image

通过序数值获取地址

有了函数的序数值,可以得到该函数的地址。

// 获取函数名称数组指针
PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);

// 获取函数地址数组指针
PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);

// 获取函数序数值数组指针
PWORD FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

// 遍历所有导出的函数
for (DWORD i = 0;i < pImgExportDir->NumberOfFunctions;i++{
    // 获取函数名
    CHAR* pFunctionName = (CHAR*(pBase + FunctionNameArray[i]);

    // 获取函数序数值
    WORD wFunctionOrdinal = FunctionOrdinalArray[i];

    // 通过函数序数值获取函数地址
    PVOID pFunctionAddress = (PVOID) (pBase + FunctionAddressArray[wFunctionOrdinal]);

    printf("[ %0.4d ] 名称: %s - 地址: 0x%p - 序数值: %d\n", i, pFunctionName, pFunctionAddress, wFunctionOrdinal);
}

为验证此功能,使用 xdbg 打开 notepad.exe,然后查看 ntdll.dll 的导出信息。

image
image

上图显示 A_SHAUpdate 的地址在 xdbg 和使用 GetProcAddressReplacement 函数中均为 0x00007FFD384D2D10。不过请注意,每个进程的 Windows 加载器会生成一个新的序数值数组,因此函数的序数值不尽相同。

GetProcAddressReplacement 代码

完整的函数代码的最后一步是比较导出的函数名和目标函数名 lpApiName。这可以使用 strcmp 函数轻松完成。最后,在匹配时返回函数地址。

FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {

	// 这样做是为了避免每次使用 hModule 时进行强制转换
	PBYTE pBase = (PBYTE)hModule;
	
	// 获取 DOS 头并进行签名检查
	PIMAGE_DOS_HEADER	pImgDosHdr		= (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE) 
		return NULL;
	
	// 获取 NT 头并进行签名检查
	PIMAGE_NT_HEADERS	pImgNtHdrs		= (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) 
		return NULL;

	// 获取可选头
	IMAGE_OPTIONAL_HEADER	ImgOptHdr	= pImgNtHdrs->OptionalHeader;

	// 获取映像导出表
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

	// 获取函数名数组指针
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	
	// 获取函数地址数组指针
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	
	// 获取函数序号数组指针
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);


	// 遍历所有导出的函数
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
		
		// 获取函数名
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		
		// 通过其序号获取函数地址
		PVOID pFunctionAddress	= (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		
		// 查找指定的函数
		if (strcmp(lpApiName, pFunctionName) == 0){
			printf("[ %0.4d ] FOUND API -\t NAME: %s -\t ADDRESS: 0x%p  -\t ORDINAL: %d\n", i, pFunctionName, pFunctionAddress, FunctionOrdinalArray[i]);
			return pFunctionAddress;
		}
	}
	
	return NULL;
}

GetProcAddressReplacement 最终演示

下图展示了 GetProcAddressGetProcAddressReplacement 同时搜索 NtAllocateVirtualMemory 地址的结果。正如预期的那样,二者都获得了正确的函数地址,因此成功构建了 GetProcAddress 的自定义实现。

image
image