55-IAT 隐藏和混淆 - API 哈希
介绍
在前面两个模块中,我们创建了两个自定义函数 GetProcAddressReplacement
和 GetModuleHandleReplacement
,它们取代了 GetProcAddress
和 GetModuleHandle
。对于执行 Run-Time Dynamic Linking(这是将导入的函数从 IAT 隐藏)来说,这样做已经足够了。然而,代码中使用的字符串揭示了正在使用的函数。例如,下面这行代码使用函数检索 VirtualAllocEx
。
GetProcAddressReplacement(GetModuleHandleReplacement("kernel32.dll"),"VirtualAllocEx")
安全解决方案可以轻松地检索已编译的二进制文件中的字符串并识别 VirtualAllocEx
的使用情况。为了解决这个问题,我们将对 GetProcAddressReplacement
和 GetModuleHandleReplacement
应用一个字符串哈希算法。函数将使用哈希值,而不是执行字符串比较来获取指定的模块基地址或函数地址。
实现 JenkinsOneAtATime 32 位
在这个模块中,GetProcAddressReplacement
和 GetModuleHandleReplacement
函数分别重命名为 GetProcAddressH
和 GetModuleHandleH
。这些更新后的函数利用 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;
}
示例
此示例使用 GetModuleHandleH
和 GetProcAddressH
来调用 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 工具 搜索 “MessageBox” 字符串。
可以看出,我们的二进制文件中没有相应的字符串。MessageBoxA
已成功调用,而无需将其导入 IAT 或在我们的二进制文件中作为字符串显示。这适用于 32 位和 64 位系统。