模块 57 - IAT 隐藏与混淆 - 编译时 API 哈希 (Compile Time API Hashing)
恶意软件开发课程 - IAT 隐藏与混淆 - 编译时 API 哈希 (Compile Time API Hashing)
模块 57 - IAT 隐藏与混淆 - 编译时 API 哈希 (Compile Time API Hashing)#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
IAT 隐藏与混淆 - 编译时 API 哈希#
简介#
在之前的 API 哈希模块中,我们需要在将函数和模块添加到代码之前手动生成它们的哈希值。遗憾的是,这非常耗时。通过使用 编译时 API 哈希 (Compile Time API Hashing),我们可以完全避免这一繁琐步骤。
此外,在之前的模块中,哈希值是硬编码的。如果不在每次实现中手动更新,安全解决方案可能会将这些固定哈希值作为 IoC(入侵指标) 进行拦截。而使用编译时 API 哈希,每次编译二进制文件时都会自动生成全新的动态哈希值。
前提条件 (Caveat)#
此方法利用了 C++ 的 constexpr 关键字,因此 仅适用于 C++ 项目。constexpr 运算符用于指示函数或变量可以在编译时求值。此外,它还能通过在编译期间执行计算来提高应用程序的运行效率,减少运行时的开销。
编译时哈希实现指南#
下面将逐步介绍实现编译时哈希所需的步骤。
1. 创建编译时哈希函数#
首先,需要使用 constexpr 运算符将现有的哈希函数转换为编译时函数。在这里,我们将修改 Djb2 算法。
#define SEED 5
// 编译时 Djb2 哈希函数 (宽字符)
constexpr DWORD HashStringDjb2W(const wchar_t* String) {
ULONG Hash = (ULONG)g_KEY;
INT c = 0;
while ((c = *String++)) {
Hash = ((Hash << SEED) + Hash) + c;
}
return Hash;
}
// 编译时 Djb2 哈希函数 (ASCII)
constexpr DWORD HashStringDjb2A(const char* String) {
ULONG Hash = (ULONG)g_KEY;
INT c = 0;
while ((c = *String++)) {
Hash = ((Hash << SEED) + Hash) + c;
}
return Hash;
}cpp其中 g_KEY 是一个全局 constexpr 变量,它是哈希计算的初始种子。为了确保每次编译产生的哈希都不同,我们将使用下文提到的 RandomCompileTimeSeed 函数生成它。
2. 生成随机种子值#
RandomCompileTimeSeed 利用 C++ 预定义的 __TIME__ 宏(格式为 HH:MM:SS)来提取当前时间的数字,并通过数学运算产生一个随机种子。由于每次编译时 __TIME__ 都在变化,因此生成的哈希也会随之改变。
// 在编译时生成一个随机键,作为哈希的初始种子
constexpr int RandomCompileTimeSeed(void)
{
return '0' * -40271 +
__TIME__[7] * 1 +
__TIME__[6] * 10 +
__TIME__[4] * 60 +
__TIME__[3] * 600 +
__TIME__[1] * 3600 +
__TIME__[0] * 36000;
};
// 编译时随机种子
constexpr auto g_KEY = RandomCompileTimeSeed() % 0xFF;cpp3. 创建辅助宏#
我们需要两组宏:
- 运行阶段宏 (
RTIME_...):在程序运行过程中,为GetProcAddressH动态计算目标函数的哈希。 - 编译阶段宏 (
CTIME_...):利用编译器直接计算出预期的哈希常量,并定义为变量。
// 运行时计算哈希的宏
#define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)
#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)
// 编译时生成常量的宏 (底层利用了 # 和 ## 运算符)
#define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);
#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);cpp💡 知识扩展:预处理器运算符
- 字符串化运算符 (
#):将宏参数包装成双引号字符串。例如#MessageBoxA会变成"MessageBoxA"。- 合并运算符 (
##):将两个标记粘合在一起。例如MessageBoxA##_Rotr32A会变成变量名MessageBoxA_Rotr32A。
宏展开演示#
调用 CTIME_HASHA(MessageBoxA) 后,编译器实际上会执行如下操作:
constexpr auto MessageBoxA_Rotr32A = HashStringDjb2A("MessageBoxA");
这将导致 MessageBoxA_Rotr32A 变量持有在编译时就已经计算好的、独特的哈希数字。

完整实现代码#
// ... 包含头文件及上述 constexpr 函数 ...
FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {
PBYTE pBase = (PBYTE)hModule;
// ... 常规 PE 解析逻辑 ...
for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
if (dwApiNameHash == RTIME_HASHA(pFunctionName)) { // 使用运行时宏比对
return (FARPROC)pFunctionAddress;
}
}
return NULL;
}
int main() {
// 1. 使用编译时宏自动生成哈希常量变量
CTIME_HASHA(MessageBoxA);
// 2. 直接使用生成的变量 MessageBoxA_Rotr32A 进行 API 地址获取
HMODULE hUser32 = GetModuleHandleA("USER32.DLL");
fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(hUser32, MessageBoxA_Rotr32A);
// ... 调用 ...
}cpp演示与验证#
特征检查#
使用 Strings.exe 搜索,发现二进制中没有任何 “MessageBox” 字符串。
使用 Dumpbin 检查 IAT,同样没有任何可疑导入。
动态哈希验证#
如下图所示,我们在同一份源码上进行两次编译。由于 __TIME__ 不同,生成的哈希值(g_KEY)完全不同。这让基于固定哈希特征的检测方案(如侦测特定的 Djb2 结果)彻底失效。


🎯 总结#
在本模块中,我们学习了:
- constexpr 的威力:理解了如何将繁重且危险的哈希计算提前到编译阶段完成。
- 动态哈希躲避:学会了利用时间戳作为种子,让每一份编译出的恶意软件都具有“独特”的数字指纹。
- 开发效率提升:不再需要任何外部脚本来计算哈希,极大地方便了大型项目的开发。
💡 关键要点
- C++ 专用:请记住,如果你坚持使用纯 C 编译器(.c 文件),
constexpr将不可用。你需要将文件后缀改为.cpp。- 灵活性:你可以随意更换
RandomCompileTimeSeed的算法,让哈希生成的规律更加难以捉摸。
📚 下一步学习
下一个模块将开启全新的篇章:API Hooking - Introduction。我们将学习如何从“躲避者”转变为“拦截者”,监控并修改系统的行为!