知识卡片
-
杀毒软件(AV)/EDR 识别恶意代码的核心手段之一就是函数调用特征匹配
-
调用混淆的本质:打乱/隐藏恶意代码的函数调用逻辑,让AV/EDR无法识别
-
技能详解
一、基础解析
为什么要混淆调用?
杀软会检测调用的API遇到高危API(如VirtualAlloc、WriteProcessMemory、CreateRemoteThread、LoadLibrary等)或者高危API调用逻辑(如OpenProcess+VirtualAllocEx+CreateRemoteThread等),混淆后程序看着"干净"。
本质就是将静态可见的调用变成运行时动态解析的
直接调用与混淆调用的区别:
直接调用时,编译器把API地址固定写进PE文件的导入地址表(IAT),API名称字符串明文也会存在.rdata节中,汇编里也是直接call [IAT偏移],静态分析器一样就能看到调用了什么。
混淆调用后,IAT里几乎没有恶意API地址,API名称被hash/加密/分割,只有调用的时候才会运行计算出来,静态看不出调用了什么。直接破坏了静态可观测的性质。

二、实战测试
直接调用
Sleep(1000);
混淆调用:动态解析(哈希遍历)
第一步:写哈希函数(这里以DJB2哈希为例)
DWORD GetApiHash(const char* str) { DWORD hash = 5381; // 初始种子(固定值) int c; while ((c = *str++)) // 遍历字符串每个字符 hash = ((hash << 5) + hash) + c; // hash = hash*33 + c return hash; }
第二步:按照哈希找API
FARPROC GetApiByHash(HMODULE hMod, DWORD targetHash) { if (!hMod) return NULL; // 解析PE头,定位导出表 PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)hMod; PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((BYTE*)hMod + pDos->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExp = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)hMod + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* pNames = (DWORD*)((BYTE*)hMod + pExp->AddressOfNames); // 函数名 RVA 数组 WORD* pOrds = (WORD*)((BYTE*)hMod + pExp->AddressOfNameOrdinals); // 序号数组 DWORD* pFuncs = (DWORD*)((BYTE*)hMod + pExp->AddressOfFunctions); // 函数地址 RVA 数组 // 遍历所有导出的函数名 for (DWORD i = 0; i < pExp->NumberOfNames; i++) { char* pName = (char*)((BYTE*)hMod + pNames[i]); // 取出函数名字符串 if (GetApiHash(pName) == targetHash) { // 哈希匹配成功 return (FARPROC)((BYTE*)hMod + pFuncs[pOrds[i]]); // 返回函数真实地址 } } return NULL; }
第三步:声明函数指针类型
typedef void (WINAPI* pSleep)(DWORD); // 注意 WINAPI 调用约定须保留,要和Sleep原型一致 // Sleep原型 VOID WINAPI Sleep( [in] DWORD dwMilliseconds );
第四步:动态解析并调用混淆后的Sleep
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll"); // 获取 kernel32 模块基址 DWORD sleepHash = GetApiHash("Sleep"); // 计算 Sleep 的哈希值 pSleep fSleep = (pSleep)GetApiByHash(hKernel32, sleepHash); // 动态获取 Sleep 地址 if (fSleep) { fSleep(1000); //和直接写 Sleep(1000) 效果完全一样 }
静态分析时完全看不到 “Sleep” 这个字符串,也看不到任何 Sleep 导入,只看到你在遍历 kernel32 的导出表和算哈希
在这个层面上再进一步的话就是将GetModuleHandleA在IAT表上去掉,毕竟GetModuleHandleA+kernel32还是暴露的,过杀软还是可能被看出来(疑似动态解析API)的恶意行为。
总结
上述动态解析仅算作中级技术,接下来在混淆阶段的提升方向如下:
Stage 1:基础动态解析 核心技术:只静态导入 LoadLibraryA + GetProcAddress,用明文字符串动态解析其他 API。 可见性:IAT 只剩 LoadLibrary/GetProcAddress,但 API 名仍明文存在。 主要对抗:静态字符串扫描 + 简单启发式。
Stage 2:API Hashing + Export Walking(上述技术) 核心技术:用 djb2/CRC32 等哈希算法 + 遍历 DLL Export Directory 匹配函数。 可见性:IAT 可能还剩 GetModuleHandle/LoadLibrary,无任何 API 明文字符串,只剩哈希值。 主要对抗:静态 IAT/YARA 规则 + 简单字符串扫描。
Stage 3:Importless PEB + LDR 手动解析 核心技术:通过 PEB → LDR InMemoryOrderModuleList 手动找 DLL 基址 + 自定义/双哈希 + 编译期字符串加密。 可见性:IAT 彻底干净(无任何 LoadLibrary/GetModuleHandle),字符串 0 明文。 主要对抗:静态全特征 + 部分行为(LoadLibrary Hook)。
Stage 4:Direct Syscall 核心技术:SysWhispers2/3 生成 stub,直接用 syscall 指令调用 Nt* 函数(绕过 ntdll.dll)。 可见性:无 ntdll 导入,连 syscall 号都硬编码在 stub 里。 主要对抗:user-mode Hook(EDR 对 ntdll 的 Hook)。
Stage 5:Indirect Syscall + 栈欺骗 核心技术:动态从 ntdll EAT 取 SSN + Indirect syscall(jmp rax) + Return Address Spoofing(调用栈伪装)。 可见性:无任何固定 syscall 号,调用栈看起来像合法线程。 主要对抗:Hook + 调用栈分析 + 行为序列。
Stage 6:多态 + 函数窃取 + 行为层对抗 核心技术:运行时复制 ntdll 机器码到 RWX + 随机哈希算法 + 全调用栈欺骗 + 轻量虚拟化。 可见性:连内存签名都随机化,几乎无固定特征。 主要对抗:全链路(静态 + 行为 + 云 + 内核)。
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://www.oneblanks.xyz/%e8%b0%83%e7%94%a8%e6%b7%b7%e6%b7%86%e6%8a%80%e6%9c%af/
共有 0 条评论