EDR对抗从入门到入狱:Hook原理与对抗

admin 2025-12-14 22:55:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章深入讲解了Windows系统中函数挂钩(InlineHook)的工作原理、实现方法和对抗技术。文章从Hook的基本概念入手,详细介绍了InlineHook的实现流程、MicrosoftDetours库的使用方法,以及EDR如何通过DLL注入来实施Hook。同时,文章还提供了检测Hook的方法和多种绕过Hook的技术,如DirectSyscalls、动态解析系统调用号SSN和重映射ntdll.dll。这些技术对于理解EDR工作原理和开发对抗技术具有重要参考价值。 综合评分: 89 文章分类: 二进制安全,红队,漏洞分析,渗透测试,安全工具


cover_image

EDR对抗从入门到入狱: Hook原理与对抗

原创

黑晶

黑晶

2025年10月20日 21:36

EDR对抗从入门到入狱: Hook对抗

应对现代 EDR 的第一步,是理解函数挂钩(Function Hooking)在用户态的运行机制。本篇文章以“FUNCTION-HOOKING DLLS”目录为纲,从最基础的概念、内存布局讲起,逐节深入实现流程、检测思路以及常见对抗技巧,力求让第一次接触该主题的读者也能搭建实验、快速验证示例代码。

HOOK是如何工作的

Hook 的动机与分类

Windows 应用在执行到系统调用时,往往需要经过多个中间层:用户代码 → Win32 API → ntdll → 内核。EDR 或调试器只要在其中任一环节设置“跳板”(Trampoline),即可在不改动应用源码的情况下观察或修改行为。根据修改位置不同,可将常见 Hook 方式分为:

| 类型 | 介入位置 | 特点 | | — | — | — | | Inline Hook | 直接修改目标函数前几个字节 | 侵入性强但适用范围广;需保存原指令 | | Import Address Table (IAT) Hook | 篡改模块导入表中的函数指针 | 对调用方透明,适合监控单个模块 | | Export Address Table (EAT) Hook | 修改 DLL 导出表 | 影响依赖该 DLL 的所有模块 |

本文示例聚焦于最常见的 inline hook。理解其原理需要掌握三个基础问题:

  1. 1. 代码段也是内存:只要调整页面保护(VirtualProtect),就能修改任何可执行模块的机器码。
  2. 2. 指令长度可变:x86/x64 的指令长度 1~15 字节不等,Hook 时必须至少覆盖一条完整指令,避免被截断后出现非法操作。
  3. 3. 需要“跳板”:我们会把原始指令保存到自建的缓冲区(Trampoline),在完成额外逻辑后再跳回原函数剩余部分。

Inline Hook 的基本流程

以 CreateFileW 为例:

  1. 1. 将目标函数所在内存页改为 PAGE_EXECUTE_READWRITE,以便写入跳转指令。
  2. 2. 复制目标开头若干字节,保存到 InlineHook::original,并在 InlineHook::trampoline 中附带一条 jmp 指令跳回原函数剩余位置。
  3. 3. 在目标函数开头写入一条无条件跳转(JMP rel32 或 MOV RAX + JMP RAX)。
  4. 4. 后续调用将先进入我们自定义的处理函数,再通过跳板继续原流程。

下面的 C++ 片段展示了原始指令的保存和跳转网桥的构建逻辑,这也是很多 Hook 框架(包括 Detours)内部的基本步骤:

#include&nbsp;<windows.h>

struct&nbsp;InlineHook&nbsp;{
&nbsp; &nbsp; BYTE original[16];
&nbsp; &nbsp; BYTE trampoline[32];
};

bool&nbsp;InstallInlineHook(void* target,&nbsp;void* handler, InlineHook& hook)&nbsp;{
&nbsp; &nbsp; DWORD oldProtect;
&nbsp; &nbsp; if&nbsp;(!VirtualProtect(target,&nbsp;sizeof(hook.original), PAGE_EXECUTE_READWRITE, &oldProtect))
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;false;

&nbsp; &nbsp; memcpy(hook.original, target,&nbsp;sizeof(hook.original));

&nbsp; &nbsp; BYTE* t = hook.trampoline;
&nbsp; &nbsp; memcpy(t, hook.original,&nbsp;sizeof(hook.original));
&nbsp; &nbsp; t +=&nbsp;sizeof(hook.original);
&nbsp; &nbsp; *t++ =&nbsp;0x48;&nbsp;// mov rax, handler
&nbsp; &nbsp; *t++ =&nbsp;0xB8;
&nbsp; &nbsp; memcpy(t, &handler,&nbsp;sizeof(handler));
&nbsp; &nbsp; t +=&nbsp;sizeof(handler);
&nbsp; &nbsp; *t++ =&nbsp;0xFF;&nbsp;// jmp rax
&nbsp; &nbsp; *t++ =&nbsp;0xE0;

&nbsp; &nbsp; DWORD rel = (DWORD)((BYTE*)handler - (BYTE*)target -&nbsp;5);
&nbsp; &nbsp; BYTE patch[5] = {0xE9};
&nbsp; &nbsp; memcpy(patch +&nbsp;1, &rel,&nbsp;sizeof(rel));
&nbsp; &nbsp; memcpy(target, patch,&nbsp;sizeof(patch));

&nbsp; &nbsp; DWORD tmp;
&nbsp; &nbsp; VirtualProtect(target,&nbsp;sizeof(hook.original), oldProtect, &tmp);
&nbsp; &nbsp; return&nbsp;true;
}

Microsoft Detours实现函数HOOK

自己写 Inline Hook 容易踩坑(异常恢复、指令长度、线程安全等问题),微软开源的 Detours 库将这些细节封装成一套事务式 API。使用 Detours 时,通常遵循以下流程:

  1. 1. 开始事务DetourTransactionBegin 会冻结当前线程的执行流,避免在打补丁时发生竞态。
  2. 2. 指定受影响线程DetourUpdateThread 告诉 Detours 需要暂停哪一个线程,通常传 GetCurrentThread() 即可。
  3. 3. 附加与拆除 HookDetourAttach/DetourDetach 分别用于安装与卸载跳板。
  4. 4. 提交事务DetourTransactionCommit 会一次性写入补丁,成功后才能恢复线程执行。

与裸写机器码相比,Detours 还负责处理 WOW64、异常过滤器、线程恢复等边界情况。下面的示例拦截 CreateFileW,记录访问日志再继续执行:

#include&nbsp;<windows.h>
#include&nbsp;<detours.h>
#include&nbsp;<iostream>

static&nbsp;HANDLE(WINAPI* RealCreateFileW)(LPCWSTR, DWORD, DWORD, LPSECURITY_ATTRIBUTES, DWORD, DWORD, HANDLE)&nbsp;= CreateFileW;

HANDLE WINAPI&nbsp;HookedCreateFileW(LPCWSTR fileName,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD desiredAccess,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD shareMode,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; LPSECURITY_ATTRIBUTES securityAttributes,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD creationDisposition,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD flagsAndAttributes,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; HANDLE templateFile)&nbsp;{
&nbsp; &nbsp; std::wcout <<&nbsp;L"[Hook] CreateFileW -> "&nbsp;<< fileName << std::endl;
&nbsp; &nbsp; return&nbsp;RealCreateFileW(fileName, desiredAccess, shareMode, securityAttributes, creationDisposition, flagsAndAttributes, templateFile);
}

void&nbsp;InstallCreateFileHook()&nbsp;{
&nbsp; &nbsp; DetourTransactionBegin();
&nbsp; &nbsp; DetourUpdateThread(GetCurrentThread());
&nbsp; &nbsp; DetourAttach(&(PVOID&)RealCreateFileW, HookedCreateFileW);
&nbsp; &nbsp; DetourTransactionCommit();
}

int&nbsp;main()&nbsp;{
&nbsp; &nbsp; InstallCreateFileHook();
&nbsp; &nbsp; HANDLE h =&nbsp;CreateFileW(L"C\\temp\\demo.txt", GENERIC_READ, FILE_SHARE_READ,&nbsp;nullptr, OPEN_EXISTING,&nbsp;0,&nbsp;nullptr);
&nbsp; &nbsp; if&nbsp;(h != INVALID_HANDLE_VALUE)&nbsp;CloseHandle(h);
}

EDR的防护手段之dll注入

Hook 代码往往与目标进程不同源,因此第一步是让我们的 DLL 驻留在目标的地址空间。早期(Windows 8 之前),许多商业安全软件使用 AppInit_DLLs 注册表项:只要某个交互式进程加载 user32.dll,系统就会自动把配置列表中的 DLL 注入进去。该方案实现简单、覆盖范围广,但也被恶意软件滥用进行持久化,并且会显著拖慢系统启动。自 Windows 8 起,Microsoft 在启用 Secure Boot 的系统上完全禁用了该机制,行业逐渐转向更可控的注入手段。

现代 EDR 常借助两类思路:

  1. 1. 用户态远程线程注入OpenProcess → VirtualAllocEx → WriteProcessMemory → CreateRemoteThread,适合在已有权限的情况下向任意进程加载 DLL。
  2. 2. 内核态 KAPC 注入:驱动程序订阅进程创建通知,分配目标进程内存并排队一个 Kernel Asynchronous Procedure Call(KAPC)。当目标线程下次恢复执行时,Windows 会先跑我们排队的 APC 例程,它再调用 LdrLoadDll/LoadLibraryW 来加载 Hook DLL。由于逻辑运行在内核态,EDR 能够在用户启动任何程序的瞬间插入监控逻辑。

下图展示了用户态远程线程注入的关键步骤:

  1. 1. 拿到句柄:通过 OpenProcess 获取目标进程的可读写句柄。
  2. 2. 写入 DLL 路径:在目标进程分配一段内存,把 DLL 的完整路径写进去。
  3. 3. 远程创建线程:令远程线程执行 LoadLibraryW,其参数即为步骤 2 中写入的路径。
  4. 4. 等待加载完成:线程结束后 DLL 已经映射到目标进程,可执行其中的 DllMain 或自定义初始化逻辑。

如果目标开启了防注入策略(如 PROCESS_MITIGATION_DYNAMIC_CODE_POLICY),上述方式可能失败。这也是为什么内核态 KAPC 注入在商业产品中更常见的原因:驱动可以在进程初始化的最早阶段插入 APC,并通过更高权限规避用户态的阻拦。下面的示例实现了最基础的 CreateRemoteThread 注入,方便读者在实验环境中理解用户态链路:

#include&nbsp;<windows.h>
#include&nbsp;<string>

bool&nbsp;InjectDll(DWORD pid,&nbsp;const&nbsp;std::wstring& path)&nbsp;{
&nbsp; &nbsp; HANDLE process =&nbsp;OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
&nbsp; &nbsp; if&nbsp;(!process)&nbsp;return&nbsp;false;

&nbsp; &nbsp; SIZE_T size = (path.size() +&nbsp;1) *&nbsp;sizeof(wchar_t);
&nbsp; &nbsp; LPVOID remoteMem =&nbsp;VirtualAllocEx(process,&nbsp;nullptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
&nbsp; &nbsp; if&nbsp;(!remoteMem) {
&nbsp; &nbsp; &nbsp; &nbsp; CloseHandle(process);
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;false;
&nbsp; &nbsp; }

&nbsp; &nbsp; WriteProcessMemory(process, remoteMem, path.c_str(), size,&nbsp;nullptr);
&nbsp; &nbsp; HANDLE thread =&nbsp;CreateRemoteThread(process,&nbsp;nullptr,&nbsp;0,
&nbsp; &nbsp; &nbsp; &nbsp; (LPTHREAD_START_ROUTINE)LoadLibraryW, remoteMem,&nbsp;0,&nbsp;nullptr);
&nbsp; &nbsp; bool&nbsp;success = thread !=&nbsp;nullptr;

&nbsp; &nbsp; if&nbsp;(thread) {
&nbsp; &nbsp; &nbsp; &nbsp; WaitForSingleObject(thread, INFINITE);
&nbsp; &nbsp; &nbsp; &nbsp; CloseHandle(thread);
&nbsp; &nbsp; }
&nbsp; &nbsp; VirtualFreeEx(process, remoteMem,&nbsp;0, MEM_RELEASE);
&nbsp; &nbsp; CloseHandle(process);
&nbsp; &nbsp; return&nbsp;success;
}

检测函数挂钩

如果你负责构建安全产品或者要确保样本在受控环境下运行,检测 Hook 既是防守方的必要步骤,也是进攻方反取证的常规动作。常见策略包括:

  1. 1. 字节比较:读取函数开头的几个字节,与磁盘上原始映像进行对比;若存在跳转指令、无意义填充,则高度可疑。
  2. 2. 完整性校验:将函数所在页面计算哈希或签名,检测是否被改写。
  3. 3. 导入表验证:遍历 IAT,确认函数指针是否仍指向预期 DLL;如果指向未知模块,很可能被 IAT Hook。
  4. 4. 内存映像对比:加载同一 DLL 的只读副本,与进程内已加载的 DLL 字节级比较,定位差异。

下面的示例演示了最基础的字节比较。它通过把磁盘状态的函数入口与内存状态对比,快速判断是否被改写:

#include&nbsp;<windows.h>
#include&nbsp;<wincrypt.h>
#include&nbsp;<vector>

bool&nbsp;IsFunctionPatched(const&nbsp;wchar_t* modulePath,&nbsp;const&nbsp;char* exportName,&nbsp;void* inMemory)&nbsp;{
&nbsp; &nbsp; HMODULE diskModule =&nbsp;LoadLibraryExW(modulePath,&nbsp;nullptr, DONT_RESOLVE_DLL_REFERENCES | LOAD_LIBRARY_AS_IMAGE_RESOURCE);
&nbsp; &nbsp; if&nbsp;(!diskModule)&nbsp;return&nbsp;false;

&nbsp; &nbsp; FARPROC diskProc =&nbsp;GetProcAddress(diskModule, exportName);
&nbsp; &nbsp; if&nbsp;(!diskProc) {
&nbsp; &nbsp; &nbsp; &nbsp; FreeLibrary(diskModule);
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;false;
&nbsp; &nbsp; }

&nbsp; &nbsp; BYTE diskBytes[16];
&nbsp; &nbsp; memcpy(diskBytes, diskProc,&nbsp;sizeof(diskBytes));

&nbsp; &nbsp; BYTE memBytes[16];
&nbsp; &nbsp; memcpy(memBytes, inMemory,&nbsp;sizeof(memBytes));

&nbsp; &nbsp; bool&nbsp;patched =&nbsp;memcmp(diskBytes, memBytes,&nbsp;sizeof(memBytes)) !=&nbsp;0;
&nbsp; &nbsp; FreeLibrary(diskModule);
&nbsp; &nbsp; return&nbsp;patched;
}

Evading Function Hooks

如果 EDR 已经在用户态挂钩关键 API,攻击者需要想办法绕过这些拦截点。以下技术兼具学习价值与实战意义,但也要意识到它们容易触发安全产品的高危告警,需要在授权环境中实验。

Direct Syscalls

平时调用 WriteFile 等 Win32 API,实际上最终会落到 ntdll.dll 的 NtWriteFile,通过 syscall 指令触发内核服务。如果我们直接调用后者,就能绕开对 WriteFile 的用户态 Hook。关键点包括:

  • • 解析系统调用号:在 x64 下,ntdll 中的 stub 会将 syscall 号写入 EAX,再执行 syscall
  • • 维持调用契约:参数、调用约定、异常处理需与官方实现一致,否则可能导致蓝屏或崩溃。

Syscall Stub 中的汇编

使用调试器查看 ntdll!NtWriteFile,可以看到类似如下的汇编:

mov r10, rcx &nbsp; &nbsp; &nbsp; &nbsp;; Windows x64 调用约定要求 syscall 前把 RCX 备份
mov eax, 0x0055 &nbsp; &nbsp; ; 0x55 是 NtWriteFile 的系统调用号(不同版本可能变化)
syscall &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ; 触发内核态服务
ret

指令含义:

  1. 1. mov r10, rcx:用户态调用使用微软 x64 调用约定,首个参数在 RCX;而内核层需要 RCX 保存指向 KTHREAD 的指针,因此在进入内核之前,Windows 规定 stub 必须把 RCX 备份到 R10,内核返回后会恢复。
  2. 2. mov eax, imm32EAX 存放系统调用号,内核根据该值分发到相应服务入口。该编号与操作系统版本耦合,对应表可在 ntdll.dll 中查到。
  3. 3. syscall:x86_64 ISA 提供的快速陷入指令,会切换到内核态执行,并将 RCXR11 等寄存器保存到栈帧。
  4. 4. ret:系统调用完成后跳回用户态,RAX 中携带 NTSTATUS 返回值。

如果我们自己构造一次系统调用,只要模仿上述序列即可。为了避免调用被 Hook 的 stub,可以将磁盘 ntdll 中的原始字节复制到自定义缓冲区,再通过函数指针跳转过去。

以下示例调用 NtWriteFile 写入文件,同时绕过 WriteFile 的用户态 Hook:

#include&nbsp;<windows.h>

extern&nbsp;"C"&nbsp;NTSTATUS&nbsp;NtWriteFile(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, PULONG);

typedef&nbsp;NTSTATUS(NTAPI* NtWriteFile_t)(HANDLE, HANDLE, PIO_APC_ROUTINE, PVOID, PIO_STATUS_BLOCK, PVOID, ULONG, PLARGE_INTEGER, PULONG);

NtWriteFile_t&nbsp;ResolveNtWriteFile()&nbsp;{
&nbsp; &nbsp; HMODULE ntdll =&nbsp;GetModuleHandleW(L"ntdll.dll");
&nbsp; &nbsp; return&nbsp;reinterpret_cast<NtWriteFile_t>(GetProcAddress(ntdll,&nbsp;"NtWriteFile"));
}

bool&nbsp;DirectSyscallWrite(HANDLE hFile,&nbsp;const&nbsp;void* buffer, ULONG size)&nbsp;{
&nbsp; &nbsp; auto&nbsp;syscall =&nbsp;ResolveNtWriteFile();
&nbsp; &nbsp; IO_STATUS_BLOCK ios = {};
&nbsp; &nbsp; NTSTATUS status =&nbsp;syscall(hFile,&nbsp;nullptr,&nbsp;nullptr,&nbsp;nullptr, &ios,&nbsp;const_cast<void*>(buffer), size,&nbsp;nullptr,&nbsp;nullptr);
&nbsp; &nbsp; return&nbsp;status >=&nbsp;0;
}

动态解析系统调用号SSN

如果 ntdll.dll 本身被打了 Hook(常见于用户态 API 监控),直接 GetProcAddress 可能得到被篡改的 stub。为了拿到真正的 syscall 入口,可以:

  1. 1. 从磁盘加载一个只读副本,不执行其中代码。
  2. 2. 手动解析其 PE 结构,找到导出表中的函数 RVA。
  3. 3. 将该 RVA 映射到本进程新申请的可执行内存,得到干净的 syscall stub。

以下代码读取磁盘 ntdll 并返回导出地址指针,供后续复制或分析:

#include&nbsp;<windows.h>
#include&nbsp;<vector>

void*&nbsp;ResolveExportFromDisk(const&nbsp;wchar_t* path,&nbsp;const&nbsp;char* exportName)&nbsp;{
&nbsp; &nbsp; HANDLE hFile =&nbsp;CreateFileW(path, GENERIC_READ, FILE_SHARE_READ,&nbsp;nullptr, OPEN_EXISTING,&nbsp;0,&nbsp;nullptr);
&nbsp; &nbsp; if&nbsp;(hFile == INVALID_HANDLE_VALUE)&nbsp;return&nbsp;nullptr;

&nbsp; &nbsp; HANDLE hMap =&nbsp;CreateFileMappingW(hFile,&nbsp;nullptr, PAGE_READONLY,&nbsp;0,&nbsp;0,&nbsp;nullptr);
&nbsp; &nbsp; if&nbsp;(!hMap) {
&nbsp; &nbsp; &nbsp; &nbsp; CloseHandle(hFile);
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;nullptr;
&nbsp; &nbsp; }

&nbsp; &nbsp; BYTE* base = (BYTE*)MapViewOfFile(hMap, FILE_MAP_READ,&nbsp;0,&nbsp;0,&nbsp;0);
&nbsp; &nbsp; if&nbsp;(!base) {
&nbsp; &nbsp; &nbsp; &nbsp; CloseHandle(hMap);
&nbsp; &nbsp; &nbsp; &nbsp; CloseHandle(hFile);
&nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;nullptr;
&nbsp; &nbsp; }

&nbsp; &nbsp; auto&nbsp;dos = (IMAGE_DOS_HEADER*)base;
&nbsp; &nbsp; auto&nbsp;nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
&nbsp; &nbsp; auto&nbsp;exports = (IMAGE_EXPORT_DIRECTORY*)(base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
&nbsp; &nbsp; DWORD* names = (DWORD*)(base + exports->AddressOfNames);
&nbsp; &nbsp; WORD* ordinals = (WORD*)(base + exports->AddressOfNameOrdinals);
&nbsp; &nbsp; DWORD* functions = (DWORD*)(base + exports->AddressOfFunctions);

&nbsp; &nbsp; void* result =&nbsp;nullptr;
&nbsp; &nbsp; for&nbsp;(DWORD i =&nbsp;0; i < exports->NumberOfNames; ++i) {
&nbsp; &nbsp; &nbsp; &nbsp; const&nbsp;char* name = (char*)(base + names[i]);
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(strcmp(name, exportName) ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD rva = functions[ordinals[i]];
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result = base + rva;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp; UnmapViewOfFile(base);
&nbsp; &nbsp; CloseHandle(hMap);
&nbsp; &nbsp; CloseHandle(hFile);
&nbsp; &nbsp; return&nbsp;result;
}

重映射ntdll.dll

如果对手在现有 ntdll 上布满 Hook,可以重新映射一份“干净”的 ntdll 并自行解析导出。这一技术对理解 PE 装载流程非常有帮助,核心步骤如下:

  1. 1. 建立镜像:把磁盘上的 ntdll.dll 映射为 SEC_IMAGE,自动处理节对齐。
  2. 2. 复制到可写区域:为执行准备,可将镜像拷贝到 PAGE_EXECUTE_READWRITE 的内存中。
  3. 3. 重定位修复(简化版可以忽略):如果新基址与原始基址不同,需要按重定位表更新各个指针。
  4. 4. 重定向调用:获取目标函数在新镜像中的地址,并替换原函数指针。

下面的示例展示了镜像复制和导出解析的最小实现,为了易读性省略了重定位和依赖修复,适合作为实验骨架:

struct&nbsp;RemappedModule&nbsp;{
&nbsp; &nbsp; BYTE* base;
&nbsp; &nbsp; SIZE_T size;
};

RemappedModule&nbsp;RemapNtdll()&nbsp;{
&nbsp; &nbsp; wchar_t&nbsp;systemDir[MAX_PATH];
&nbsp; &nbsp; GetSystemDirectoryW(systemDir, MAX_PATH);
&nbsp; &nbsp; std::wstring path = std::wstring(systemDir) +&nbsp;L"\\ntdll.dll";

&nbsp; &nbsp; HANDLE file =&nbsp;CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ,&nbsp;nullptr, OPEN_EXISTING,&nbsp;0,&nbsp;nullptr);
&nbsp; &nbsp; HANDLE map =&nbsp;CreateFileMappingW(file,&nbsp;nullptr, PAGE_READONLY | SEC_IMAGE,&nbsp;0,&nbsp;0,&nbsp;nullptr);
&nbsp; &nbsp; BYTE* image = (BYTE*)MapViewOfFile(map, FILE_MAP_READ,&nbsp;0,&nbsp;0,&nbsp;0);

&nbsp; &nbsp; auto&nbsp;dos = (IMAGE_DOS_HEADER*)image;
&nbsp; &nbsp; auto&nbsp;nt = (IMAGE_NT_HEADERS*)(image + dos->e_lfanew);
&nbsp; &nbsp; SIZE_T imageSize = nt->OptionalHeader.SizeOfImage;

&nbsp; &nbsp; BYTE* region = (BYTE*)VirtualAlloc(nullptr, imageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
&nbsp; &nbsp; memcpy(region, image, imageSize);

&nbsp; &nbsp; RemappedModule mod{region, imageSize};
&nbsp; &nbsp; UnmapViewOfFile(image);
&nbsp; &nbsp; CloseHandle(map);
&nbsp; &nbsp; CloseHandle(file);
&nbsp; &nbsp; return&nbsp;mod;
}

template&nbsp;<typename&nbsp;T>
T&nbsp;ResolveFromRemap(const&nbsp;RemappedModule& mod,&nbsp;const&nbsp;char* name)&nbsp;{
&nbsp; &nbsp; auto&nbsp;dos = (IMAGE_DOS_HEADER*)mod.base;
&nbsp; &nbsp; auto&nbsp;nt = (IMAGE_NT_HEADERS*)(mod.base + dos->e_lfanew);
&nbsp; &nbsp; auto&nbsp;exportDir = (IMAGE_EXPORT_DIRECTORY*)(mod.base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

&nbsp; &nbsp; DWORD* names = (DWORD*)(mod.base + exportDir->AddressOfNames);
&nbsp; &nbsp; WORD* ordinals = (WORD*)(mod.base + exportDir->AddressOfNameOrdinals);
&nbsp; &nbsp; DWORD* funcs = (DWORD*)(mod.base + exportDir->AddressOfFunctions);

&nbsp; &nbsp; for&nbsp;(DWORD i =&nbsp;0; i < exportDir->NumberOfNames; ++i) {
&nbsp; &nbsp; &nbsp; &nbsp; if&nbsp;(strcmp(name, (char*)(mod.base + names[i])) ==&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; return&nbsp;reinterpret_cast<T>(mod.base + funcs[ordinals[i]]);
&nbsp; &nbsp; }
&nbsp; &nbsp; return&nbsp;nullptr;
}

Conclusion

本文围绕函数挂钩 DLL 的实现、注入、检测与规避展开,通过 C++ 示例展示了核心步骤,并尽量从零解释关键原理。对防守方而言,理解 Hook 的工作细节有助于构建更可靠的检测策略;对于研究 offensive 技术的读者,则可借助直接 syscall、动态解析及 remapping 等技巧提升对抗强度。未来的研究方向还包括内核态 Hook、基于硬件的控制流监控以及自动化反 Hook 分析工具。通过不断实验与迭代,开发者可以在攻防两端获得更深入的洞察。

好书推荐,EDR原理与对抗必读书目:《Evading EDR: The Definitive Guide to Defeating Endpoint Detection Systems》 👇👇👇获取:

这是一个纯粹,开放,前沿的技术交流社区,成员主要有互联网大厂安全部门任职的成员,乙方红队专家,以及正在学习入门的小白等,目前主题主要以红队研发为主(有经验的都知道是什么意思),以及其他涉及到红队进攻侵入性技术,如果你想学习技术,认识不同的人或者寻求一个机会之类的,可以来看看👇👇👇****

欢迎加入交流圈

扫码获取更多精彩


weinxin
版权声明
本站原创文章转载请注明文章出处及链接,谢谢合作!
评论:0   参与:  0