动态获取API函数(又称隐藏IAT)实现免杀
字数 1762 2025-08-29 08:30:12
动态获取API函数实现免杀技术详解
一、导入表基础
1.1 导入表概念
导入表(Import Directory)存储了PE文件运行时所需的API及相关DLL模块信息。安全产品(AV/EDR)常通过分析导入表中的敏感API(如VirtualAlloc、VirtualProtect、CreateThread等)来判断文件风险等级。
1.2 查看导入表工具
- studyPE+:可查看文件的导入信息
- IDA:在Import界面查看导入信息
二、动态获取API函数的三种方式
2.1 方法对比:GetModuleHandle vs LoadLibrary
| 特性 | GetModuleHandle | LoadLibrary |
|---|---|---|
| 主要作用 | 获取已加载模块句柄(不增加引用计数) | 加载模块到进程地址空间(增加引用计数) |
| 适用场景 | 模块已加载,需重复获取句柄 | 模块未加载,需显式加载 |
| 引用计数管理 | 不增加 | 增加(需配合FreeLibrary) |
| 返回值 | 模块未加载返回NULL | 模块已加载返回现有句柄(引用计数+1) |
2.2 函数指针声明
动态获取API需要声明相应的函数指针类型,格式如下:
typedef FARPROC (WINAPI *GETPROCADDR)(HMODULE hModule, LPCSTR lpProcName);
调用约定对比
| 调用约定 | 参数顺序 | 堆栈清理者 | 典型应用场景 | 变参支持 |
|---|---|---|---|---|
| __stdcall | 右→左 | 被调用者 | Windows API、COM接口 | 否 |
| __cdecl | 右→左 | 调用者 | C/C++默认、可变参数函数 | 是 |
| __fastcall | 右→左 | 被调用者 | 寄存器传参优化场景 | 否 |
2.3 方法一:GetModuleHandle+GetProcAddress组合
核心API
- GetModuleHandle:检索已加载模块的句柄
- GetProcAddress:从DLL检索导出函数或变量的地址
示例:动态调用MessageBoxW
typedef int (WINAPI *MESSAGEBOXW)(HWND hWnd, LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
int main() {
HMODULE hUser32 = GetModuleHandleW(L"user32.dll");
MESSAGEBOXW pMessageBoxW = (MESSAGEBOXW)GetProcAddress(hUser32, "MessageBoxW");
pMessageBoxW(NULL, L"Hello", L"Title", MB_OK);
return 0;
}
实战应用
- 使用加密版将shellcode加密并输出
- 将shellcode保存到shellcode.txt
- 用VS的MSVC或Cling-cl编译解密版代码
- 将编译好的exe和shellcode.txt放到目标机器同一目录
2.4 方法二:PEB + GetModuleHandle+GetProcAddress
2.4.1 获取PEB结构体地址
64位系统:
// 方法一:从TEB出发
PPEB pPeb = (PPEB)__readgsqword(0x60);
// 方法二:直接从GS寄存器
PPEB pPeb = (PPEB)__readgsqword(0x60);
32位系统:
// 方法一:从TEB出发
PPEB pPeb = (PPEB)__readfsdword(0x30);
// 方法二:直接从FS寄存器
PPEB pPeb = (PPEB)__readfsdword(0x30);
2.4.2 关键结构体
- _PEB_LDR_DATA:位于PEB结构体0x18位置
- _LIST_ENTRY:双向链表,包含Flink和Blink指针
- LDR_DATA_TABLE_ENTRY:描述已加载模块信息的关键结构
2.4.3 导出表相关数据结构
- AddressOfNames:按名称导出函数的字符串地址(RVA)
- AddressOfFunctions:导出函数的实际入口地址(RVA)
- AddressOfNameOrdinals:函数名称索引与函数地址索引的映射
2.4.4 辅助函数实现
// 自定义宽字符转小写
wchar_t my_towlower(wchar_t c) {
if (c >= L'A' && c <= L'Z')
return c + (L'a' - L'A');
return c;
}
// 不区分大小写的宽字符串比较
int MyCompareStringW(LPCWSTR str1, LPCWSTR str2) {
while (*str1 && *str2) {
if (my_towlower(*str1) != my_towlower(*str2))
return 0;
str1++;
str2++;
}
return (*str1 == L'\0' && *str2 == L'\0');
}
// ASCII字符串比较
int MyCompareStringA(LPCSTR str1, LPCSTR str2) {
while (*str1 && *str2) {
if (*str1 != *str2)
return 0;
str1++;
str2++;
}
return (*str1 == '\0' && *str2 == '\0');
}
// 提取DLL名称
LPCWSTR ExtractDllName(LPCWSTR fullPath) {
LPCWSTR lastBackslash = fullPath;
while (*fullPath) {
if (*fullPath == L'\\')
lastBackslash = fullPath + 1;
fullPath++;
}
return lastBackslash;
}
2.4.5 GetApiAddressByName实现
FARPROC GetApiAddressByName(LPCWSTR dllName, LPCSTR apiName) {
// 获取PEB地址
PPEB pPeb = (PPEB)__readgsqword(0x60);
// 获取PEB_LDR_DATA
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)pPeb->Ldr;
// 获取第一个模块
PLIST_ENTRY pListEntry = pLdr->InMemoryOrderModuleList.Flink;
while (pListEntry != &pLdr->InMemoryOrderModuleList) {
// 获取LDR_DATA_TABLE_ENTRY
PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pListEntry, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
// 检查DLL名称
if (pEntry->FullDllName.Buffer) {
LPCWSTR currentDllName = ExtractDllName(pEntry->FullDllName.Buffer);
if (MyCompareStringW(currentDllName, dllName)) {
// 解析PE头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pEntry->DllBase;
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pEntry->DllBase + pDosHeader->e_lfanew);
// 获取导出表
PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)pEntry->DllBase +
pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
// 遍历导出表
DWORD* nameArray = (DWORD*)((BYTE*)pEntry->DllBase + pExportDir->AddressOfNames);
WORD* ordinalArray = (WORD*)((BYTE*)pEntry->DllBase + pExportDir->AddressOfNameOrdinals);
DWORD* funcArray = (DWORD*)((BYTE*)pEntry->DllBase + pExportDir->AddressOfFunctions);
for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
LPCSTR currentApiName = (LPCSTR)((BYTE*)pEntry->DllBase + nameArray[i]);
if (MyCompareStringA(currentApiName, apiName)) {
return (FARPROC)((BYTE*)pEntry->DllBase + funcArray[ordinalArray[i]]);
}
}
}
}
pListEntry = pListEntry->Flink;
}
return NULL;
}
2.5 方法三:完全PEB
与方法二类似,但完全不依赖GetModuleHandle和GetProcAddress,直接通过PEB遍历获取所有需要的API函数地址。
三、免杀测试标准
- shellcode加密采用自定义XOR加密和Base64编码
- shellcode分离加载采用本地读取文本文件方式
- 修改VS的默认编译选项
- 不使用修改时间戳、加签名等手段
- 按主题采用相应技术进行免杀测试
四、补充防御规避技术
当动态获取API函数技术失效时,可结合以下技术:
- API哈希
- 系统调用(直接/间接)
- 调用栈欺骗
五、参考资料
- 什么?你连这都不会还学免杀?之「API动态解析」
- 【免杀】隐藏导入表(IAT)的六种方式
- 免杀基础-IAT隐藏
- 隐藏IAT(导入表)敏感API笔记