模块 55 - IAT 隐藏与混淆 - API 哈希 (API Hashing)
恶意软件开发课程 - IAT 隐藏与混淆 - API 哈希 (API Hashing)
模块 55 - IAT 隐藏与混淆 - API 哈希 (API Hashing)#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
IAT 隐藏与混淆 - API 哈希#
简介#
在涉及前两个模块中,我们创建了两个自定义函数 GetProcAddressReplacement 和 GetModuleHandleReplacement 来替换原生的 GetProcAddress 和 GetModuleHandle。这足以实现 运行时动态链接 (Run-Time Dynamic Linking),从而在 IAT 中隐藏导入函数。然而,代码中使用的字符串仍然会暴露正在使用的函数。例如,下面的代码行使用了这些函数来检索 VirtualAllocEx:
GetProcAddressReplacement(GetModuleHandleReplacement("kernel32.dll"), "VirtualAllocEx")c安全解决方案可以轻而易举地从编译后的二进制文件中提取字符串,并识别出程序正在使用 VirtualAllocEx。为了解决这个问题,我们将对 GetProcAddressReplacement 和 GetModuleHandleReplacement 应用字符串哈希算法。这些函数将不再通过字符串比较来获取指定的模块基址或函数地址,而是通过 哈希值 进行工作。
实现 API 哈希#
在本模块中,我们将上述两个函数重命名为 GetProcAddressH 和 GetModuleHandleH。这些更新后的函数使用 Jenkins One At A Time 字符串哈希算法,将函数名和模块名替换为代表它们的哈希值。请回想一下,该算法是在 字符串哈希 (String Hashing) 模块中通过 JenkinsOneAtATime32Bit 函数引入的。
获取哈希值#
为了使用本模块所示的函数,必须先获取模块名(例如 User32.dll)和函数名(例如 MessageBoxA)的哈希值。这可以通过先将哈希后的值打印到控制台来实现。请确保哈希算法使用相同的种子(Seed)。
// ...
int main(){
// 打印模块名(建议全大写以匹配后续逻辑)和函数名的哈希值
printf("[i] Hash Of \"%s\" Is : 0x%0.8X \n", "USER32.DLL", HASHA("USER32.DLL"));
printf("[i] Hash Of \"%s\" Is : 0x%0.8X \n", "MessageBoxA", HASHA("MessageBoxA"));
return 0;
}c上述 main 函数将输出:
[i] Hash Of "USER32.DLL" Is : 0x81E3778E
[i] Hash Of "MessageBoxA" Is : 0xF10E27CAtext这些哈希值现在可以用于下文的函数中。
使用方法#
除了传递哈希值而不是字符串值之外,函数的使用方式与之前相同。
// 0x81E3778E 是 USER32.DLL 的哈希值
// 0xF10E27CA 是 MessageBoxA 的哈希值
fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(GetModuleHandleH(0x81E3778E), 0xF10E27CA); cGetProcAddressH 函数#
GetProcAddressH 与之前的自定义函数类似,主要区别在于它使用 JenkinsOneAtATime32Bit 算法将导出函数名进行哈希,并与输入的哈希值进行比较。
为了保持代码简洁,我们使用宏来调用哈希函数:
HASHA:调用 ASCII 版本的哈希。HASHW:调用宽字符版本的哈希。
#define HASHA(API) (HashStringJenkinsOneAtATime32BitA((PCHAR) API))
#define HASHW(API) (HashStringJenkinsOneAtATime32BitW((PWCHAR) API))cGetProcAddressH 实现如下:
FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {
if (hModule == NULL || dwApiNameHash == NULL)
return NULL;
PBYTE pBase = (PBYTE)hModule;
// PE 头解构逻辑 (模块 50 已详细讲解)
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
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 (dwApiNameHash == HASHA(pFunctionName)) {
return (FARPROC)pFunctionAddress;
}
}
return NULL;
}cGetModuleHandleH 函数#
GetModuleHandleH 同样是之前的增强版。由于模块名大小写不一,为了统一,我们在哈希对比前先将枚举到的 DLL 名称转换为 全大写。因此,输入的 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) {
// 将枚举到的 DLL 名称转为全大写
CHAR UpperCaseDllName[MAX_PATH];
DWORD i = 0;
while (pDte->FullDllName.Buffer[i]) {
UpperCaseDllName[i] = (CHAR)toupper(pDte->FullDllName.Buffer[i]);
i++;
}
UpperCaseDllName[i] = '\0';
// 比较哈希值
if (HASHA(UpperCaseDllName) == dwModuleNameHash)
return (HMODULE)pDte->Reserved2[0];
} else {
break;
}
pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}
return NULL;
}c演示#
这个演示使用哈希值通过 GetModuleHandleH 和 GetProcAddressH 调用 MessageBoxA。
#define USER32DLL_HASH 0x81E3778E
#define MessageBoxA_HASH 0xF10E27CA
int main() {
// 加载 User32.dll 到当前进程以供查找
if (LoadLibraryA("USER32.DLL") == NULL) return 0;
// 使用哈希值获取模块和函数地址
HMODULE hUser32Module = GetModuleHandleH(USER32DLL_HASH);
fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(hUser32Module, MessageBoxA_HASH);
if (pMessageBoxA) {
pMessageBoxA(NULL, "用 Maldev 技术构建恶意软件", "成功", MB_OK | MB_ICONEXCLAMATION);
}
return 0;
}c搜索 MessageBox 字符串特征#
使用 Sysinternals 的 Strings.exe ↗ 工具搜索字符串 “MessageBox”:

可以观察到,二进制文件中 完全没有 对应的明文字符串。我们成功调用了 MessageBoxA,而它既没有出现在 IAT 中,也没有作为明文字符串暴露。这种方法在 32 位和 64 位系统中均有效。
🎯 总结#
在本模块中,我们学习了:
- 特征消除:过哈希技术彻底清除了二进制文件中的敏感 API 字符串。
- Jenkins 哈希集成:将模块 51 的算法真正应用到了 IAT 隐藏逻辑中。
- 双重隐匿:结合了“运行时动态链接”和“字符串混淆”,让防御者即便进行静态分析也难以弄清程序意图。
💡 关键要点
- 哈希冲突预防:虽然理论上存在碰撞可能,但对于 Windows API 这类有限集合,Jenkins 算法的表现非常完美。
- 自动化前奏:当前的哈希值是手动计算并填入的,后续我们将学习如何更高效地管理这些哈希值。
📚 下一步学习
下一个模块将介绍 IAT Hiding & Obfuscation - Custom Pseudo Handles。我们将探讨如何进一步优化 API 的获取流程,使其更加隐蔽和高效。