跳至主要內容

55-IAT 隐藏和混淆 - API 哈希

Maldevacademy大约 4 分钟安全开发

介绍

在前面两个模块中,我们创建了两个自定义函数 GetProcAddressReplacementGetModuleHandleReplacement,它们取代了 GetProcAddressGetModuleHandle。对于执行 Run-Time Dynamic Linking(这是将导入的函数从 IAT 隐藏)来说,这样做已经足够了。然而,代码中使用的字符串揭示了正在使用的函数。例如,下面这行代码使用函数检索 VirtualAllocEx

GetProcAddressReplacement(GetModuleHandleReplacement("kernel32.dll"),"VirtualAllocEx")

安全解决方案可以轻松地检索已编译的二进制文件中的字符串并识别 VirtualAllocEx 的使用情况。为了解决这个问题,我们将对 GetProcAddressReplacementGetModuleHandleReplacement 应用一个字符串哈希算法。函数将使用哈希值,而不是执行字符串比较来获取指定的模块基地址或函数地址。

实现 JenkinsOneAtATime 32 位

在这个模块中,GetProcAddressReplacementGetModuleHandleReplacement 函数分别重命名为 GetProcAddressHGetModuleHandleH。这些更新后的函数利用 Jenkins One At A Time 哈希算法将函数和模块名称替换为代表它们的哈希值。回想一下,此算法是通过 字符串哈希 模块中引入的 JenkinsOneAtATime32Bit 函数来实现的。

哈希字符串

为了使用本模块中展示的函数,需要获取模块名称的哈希值(例如 User32.dll)和函数名称的哈希值(例如 MessageBoxA)。这可以通过首先将哈希值打印到控制台来完成。确保哈希算法使用相同的种子。

// ...

int main(){
	printf("[i] %s 的哈希值是:0x%0.8X\n", "USER32.DLL", HASHA("USER32.DLL")); // 大写模块名称
	printf("[i] %s 的哈希值是:0x%0.8X\n", "MessageBoxA", HASHA("MessageBoxA"));
	
  	return 0;
}

以上 main 函数将输出以下内容:

[i] USER32.DLL 的哈希值是:0x81E3778E
[i] MessageBoxA 的哈希值是:0xF10E27CA

现在可以使用这些哈希值与以下函数一起使用。

用法

除了现在传递的是哈希值而不是字符串值之外,这些函数的使用方式相同。

// 0x81E3778E 是 USER32.DLL 的哈希值
// 0xF10E27CA 是 MessageBoxA 的哈希值
proc pMessageBoxA = GetProcAddressH(GetModuleHandleH(0x81E3778E),0xF10E27CA); 

GetProcAddressH 函数

GetProcAddressH 函数相当于 GetProcAddressReplacement 函数,主要区别在于,此函数使用 JenkinsOneAtATime32Bit 字符串哈希算法的哈希值来比较导出的函数名称和输入哈希值。

值得注意的是,此代码使用了两个宏来使代码更简洁,便于日后更新。

  • HASHA - 调用 HashStringJenkinsOneAtATime32BitA(ASCII)

  • HASHW - 调用 HashStringJenkinsOneAtATime32BitW(UNICODE)

在考虑上述因素后,GetProcAddressH 如下所示。此函数带两个参数:

  • hModule - 包含该函数的 DLL 模块的句柄。

  • dwApiNameHash - 要获取其地址的函数名称的哈希值。

FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {

	if (hModule == NULL || dwApiNameHash == NULL)
		return NULL;

	PBYTE pBase = (PBYTE)hModule;

	PIMAGE_DOS_HEADER         pImgDosHdr			  = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return NULL;

	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]]);

		// 计算每个函数名称 pFunctionName 的哈希值
		// 如果两个哈希值相等,则表示我们找到了所需的函数
		if (dwApiNameHash == HASHA(pFunctionName)) {
			return pFunctionAddress;
		}
	}

	return NULL;
}

GetModuleHandleH

GetModuleHandleH 函数与 GetModuleHandleReplacement 相同,主要区别在于枚举的 DLL 名称与输入哈希值的比较将使用 JenkinsOneAtATime32Bit 字符串哈希算法的哈希值。请注意,该函数会将 FullDllName.Buffer 中的字符串转换成大写,因此,dwModuleNameHash 参数必须是 大写 模块名称的哈希值(例如 USER32.DLL)。

HMODULE GetModuleHandleH(DWORD dwModuleNameHash) {

    if (dwModuleNameHash == NULL)
        return NULL;

#ifdef _WIN64
    PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
    PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif

    PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
    PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);

    while (pDte) {

        if (pDte->FullDllName.Length != NULL && pDte->FullDllName.Length < MAX_PATH) {

            // 将 `FullDllName.Buffer` 转换为大写字符串
            CHAR UpperCaseDllName[MAX_PATH];

            DWORD i = 0;
            while (pDte->FullDllName.Buffer[i]) {
                UpperCaseDllName[i] = (CHAR)toupper(pDte->FullDllName.Buffer[i]);
                i++;
            }
            UpperCaseDllName[i] = '\0';

            // 对 `UpperCaseDllName` 进行哈希并将其哈希值与输入 `dwModuleNameHash` 进行比较
            if (HASHA(UpperCaseDllName) == dwModuleNameHash)
                return pDte->Reserved2[0];

        }
        else {
            break;
        }

        pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
    }

    return NULL;
}

示例

此示例使用 GetModuleHandleHGetProcAddressH 来调用 MessageBoxA

#define USER32DLL_HASH      0x81E3778E  // User32.dll 的散列值
#define MessageBoxA_HASH    0xF10E27CA  // MessageBoxA 函数的散列值

int main() {

    // 加载 User32.dll 以便 GetModuleHandleH 正常工作
    if (LoadLibraryA("USER32.DLL") == NULL) {
        printf("[!] LoadLibraryA 失败,错误代码:%d \n", GetLastError());
        return 0;
    }

    // 使用 GetModuleHandleH 获取 user32.dll 的句柄
    HMODULE hUser32Module = GetModuleHandleH(USER32DLL_HASH);
    if (hUser32Module == NULL){
        printf("[!] 无法获取 User32.dll 的句柄 \n");
        return -1;
    }

    // 使用 GetProcAddressH 获取 MessageBoxA 函数的地址
    fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(hUser32Module, MessageBoxA_HASH);
    if (pMessageBoxA == NULL) {
        printf("[!] 无法找到指定函数的地址 \n");
        return -1;
    }

    // 调用 MessageBoxA
    pMessageBoxA(NULL, "使用 Maldev 构建恶意软件", "哇哦", MB_OK | MB_ICONEXCLAMATION);

    printf("[#] 按 <Enter> 退出 ... ");
    getchar();

    return 0;
}

图片
图片

MessageBox 字符串搜索

使用 Strings.exe Sysinternal 工具open in new window 搜索 “MessageBox” 字符串。

image
image

可以看出,我们的二进制文件中没有相应的字符串。MessageBoxA 已成功调用,而无需将其导入 IAT 或在我们的二进制文件中作为字符串显示。这适用于 32 位和 64 位系统。