动态获取API函数(又称隐藏IAT)实现免杀
字数 1762 2025-08-29 08:30:12

动态获取API函数实现免杀技术详解

一、导入表基础

1.1 导入表概念

导入表(Import Directory)存储了PE文件运行时所需的API及相关DLL模块信息。安全产品(AV/EDR)常通过分析导入表中的敏感API(如VirtualAllocVirtualProtectCreateThread等)来判断文件风险等级。

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;
}

实战应用

  1. 使用加密版将shellcode加密并输出
  2. 将shellcode保存到shellcode.txt
  3. 用VS的MSVC或Cling-cl编译解密版代码
  4. 将编译好的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 关键结构体

  1. _PEB_LDR_DATA:位于PEB结构体0x18位置
  2. _LIST_ENTRY:双向链表,包含Flink和Blink指针
  3. LDR_DATA_TABLE_ENTRY:描述已加载模块信息的关键结构

2.4.3 导出表相关数据结构

  1. AddressOfNames:按名称导出函数的字符串地址(RVA)
  2. AddressOfFunctions:导出函数的实际入口地址(RVA)
  3. 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

与方法二类似,但完全不依赖GetModuleHandleGetProcAddress,直接通过PEB遍历获取所有需要的API函数地址。

三、免杀测试标准

  1. shellcode加密采用自定义XOR加密和Base64编码
  2. shellcode分离加载采用本地读取文本文件方式
  3. 修改VS的默认编译选项
  4. 不使用修改时间戳、加签名等手段
  5. 按主题采用相应技术进行免杀测试

四、补充防御规避技术

当动态获取API函数技术失效时,可结合以下技术:

  1. API哈希
  2. 系统调用(直接/间接)
  3. 调用栈欺骗

五、参考资料

  1. 什么?你连这都不会还学免杀?之「API动态解析」
  2. 【免杀】隐藏导入表(IAT)的六种方式
  3. 免杀基础-IAT隐藏
  4. 隐藏IAT(导入表)敏感API笔记
动态获取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需要声明相应的函数指针类型,格式如下: 调用约定对比 | 调用约定 | 参数顺序 | 堆栈清理者 | 典型应用场景 | 变参支持 | |---------|---------|-----------|-------------|---------| | __ stdcall | 右→左 | 被调用者 | Windows API、COM接口 | 否 | | __ cdecl | 右→左 | 调用者 | C/C++默认、可变参数函数 | 是 | | __ fastcall | 右→左 | 被调用者 | 寄存器传参优化场景 | 否 | 2.3 方法一:GetModuleHandle+GetProcAddress组合 核心API GetModuleHandle :检索已加载模块的句柄 GetProcAddress :从DLL检索导出函数或变量的地址 示例:动态调用MessageBoxW 实战应用 使用加密版将shellcode加密并输出 将shellcode保存到shellcode.txt 用VS的MSVC或Cling-cl编译解密版代码 将编译好的exe和shellcode.txt放到目标机器同一目录 2.4 方法二:PEB + GetModuleHandle+GetProcAddress 2.4.1 获取PEB结构体地址 64位系统: 32位系统: 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 辅助函数实现 2.4.5 GetApiAddressByName实现 2.5 方法三:完全PEB 与方法二类似,但完全不依赖 GetModuleHandle 和 GetProcAddress ,直接通过PEB遍历获取所有需要的API函数地址。 三、免杀测试标准 shellcode加密采用自定义XOR加密和Base64编码 shellcode分离加载采用本地读取文本文件方式 修改VS的默认编译选项 不使用修改时间戳、加签名等手段 按主题采用相应技术进行免杀测试 四、补充防御规避技术 当动态获取API函数技术失效时,可结合以下技术: API哈希 系统调用(直接/间接) 调用栈欺骗 五、参考资料 什么?你连这都不会还学免杀?之「API动态解析」 【免杀】隐藏导入表(IAT)的六种方式 免杀基础-IAT隐藏 隐藏IAT(导入表)敏感API笔记