hwbp/CLR-Unhook
GitHub: hwbp/CLR-Unhook
通过恢复clr.dll中nLoadImage函数的原始字节,解除EDR/AV对该关键函数的Hook,从而绕过对内存中.NET程序集加载的拦截与扫描。
Stars: 213 | Forks: 24
# CLR Unhooking 工具
- 注意:为了获得干净 CLR 的效果,您需要手动将磁盘上的 DLL 映射到内存中。您不能使用 LoadLibraryA/W,因为防病毒解决方案会检测到 DLL 加载事件,并可能立即对其进行 Hook。如果您需要这种行为,可以在 GitHub 上查找现有的手动映射工具,并将其集成到您的代码库中。我没有在这里包含具体的映射工具,因为杀毒软件厂商通常不太欢迎这种做法
这是一个原生的 C++ 实用工具,通过恢复原始的 `nLoadImage` 函数实现,绕过 .NET 公共语言运行时 (CLR) 中 EDR/AV 的 Hook。
## 简要说明
该工具移除了安全产品在 CLR 的 `nLoadImage` 函数上设置的 Hook —— 这是一个关键的原生入口点,负责处理所有内存中的 .NET 程序集加载。它通过从磁盘读取干净的 `clr.dll` 并覆盖内存中被 Hook 的函数字节,恢复 CLR 的原始行为,使得 `Assembly.Load(byte[])` 可以在不受 EDR 检查或扫描的情况下执行。
## 这有什么作用?
现代安全产品(BitDefender、CrowdStrike、SentinelOne 等)通过在 `clr.dll` 内部 Hook `nLoadImage` 函数,来拦截和扫描内存中的 .NET 程序集加载。该工具通过以下步骤解除该函数的 Hook:
1. 从磁盘读取干净的 `clr.dll`
2. 找到原始的 `nLoadImage` 字节
3. 覆盖内存中被 Hook 的版本
解除 Hook 后,`Assembly.Load(byte[])` 将在没有 EDR 检查的情况下执行。
### 理解 nLoadImage
`nLoadImage` 是处理 .NET 运行时中**所有**内存中程序集加载的关键原生函数。它在托管代码中被声明为 `InternalCall`,这意味着它没有 C# 的实现 —— 相反,它是通往原生 CLR 代码的直接桥梁。
**调用链:**
```
Managed Code (C#)
↓
Assembly.Load(byte[])
↓
RuntimeAssembly.nLoadImage(...) [InternalCall - no managed body]
↓
clr.dll!AssemblyNative::LoadImage (Native C++ implementation)
↓
Assembly loaded into AppDomain
```
**为何它如此关键:**
几乎所有的内存中程序集加载都要经过 `nLoadImage`。`Assembly.Load(byte[])` 方法及其重载(包括加载带有符号字节的程序集)在底层都调用了 `nLoadImage`。当您调用 `Assembly.Load(byte[])` 时,`mscorlib.dll` 中的托管代码会将您的字节数组传递给 `RuntimeAssembly.nLoadImage()`,该方法被标记为 `[MethodImpl(MethodImplOptions.InternalCall)]` —— 这意味着它在 C# 中的方法体是空的,执行流程会立即跳转到原生 CLR 代码。
即使是动态代码生成场景 —— 在运行时发出程序集的序列化框架、XML 序列化器生成,以及像 Cobalt Strike 的 `execute-assembly` 这样的红队工具 —— 都要汇聚通过这个单一函数。
**原生实现:**
`mscorlib.dll` 中的 `nLoadImage` InternalCall 存根指向了 `clr.dll` 内部的原生 C++ 函数 `AssemblyNative::LoadImage`。该函数会:
- 解析字节数组中的 PE 头
- 验证元数据和 IL 代码
- 为程序集分配内存
- 在 AppDomain 中注册程序集
- 触发加载后事件(ETW、.NET 4.8+ 中的 AMSI 扫描)
- 处理混合模式程序集(原生 + 托管)
- 强制执行强名称验证
在 .NET Framework 4.8+ 中,每次 `nLoadImage` 调用都会在执行前自动将程序集字节传递给 Windows Defender 的 AMSI (`AmsiScanBuffer`) 进行扫描,使其成为安全产品的关键瓶颈。
**函数签名 (.NET Framework 4.7+):**
```
[MethodImpl(MethodImplOptions.InternalCall)]
static internal extern Assembly nLoadImage(
byte[] rawAssembly, // PE bytes
byte[] rawSymbolStore, // Optional PDB bytes
Evidence evidence, // CAS evidence (obsolete)
ref StackCrawlMark stackMark, // Security stack marker
bool fIntrospection, // Reflection-only flag
bool fSkipIntegrityCheck, // Skip integrity validation
SecurityContextSource securityContextSource // Security context
);
```
当您调用 `Assembly.Load(byte[])` 时,它会使用以下典型参数调用 `nLoadImage`:
```
StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
return RuntimeAssembly.nLoadImage(
rawAssembly, // Your byte array
null, // rawSymbolStore
null, // evidence
ref stackMark, // LookForMyCaller
false, // fIntrospection
SecurityContextSource.CurrentAssembly // securityContextSource
);
```
`fIntrospection` 参数控制程序集是用于执行 (`false`) 还是仅用于反射检查 (`true`)。`Assembly.ReflectionOnlyLoad(byte[])` 方法在调用 `nLoadImage` 时传入 `fIntrospection=true`,允许在不执行代码的情况下检查元数据。
**为什么 EDR 要 Hook 它:**
由于 `nLoadImage` 是所有内存中程序集加载的**单一入口点**,EDR 产品会在 `clr.dll` 的原生层对其进行 Hook。这使得它们能够:
- 在加载每个程序集之前对其进行检查
- 扫描字节数组以查找恶意模式
- 在 .NET 甚至还没开始处理程序集之前阻止执行
- 绕过 AMSI/ETW 规避技术(因为 Hook 位于这些层之下)
传统的绕过方法(AMSI 补丁、禁用 ETW)不会影响 CLR 级别的 Hook,因为它们在调用栈中处于更高的层级。Hook 发生在 CLR **内部**,在 AMSI 被调用之前。
## 用法
### 本地进程(当前进程)
```
CLRUnhook.exe
```
解除当前进程中 CLR 的 Hook。**注意:** 这仅在 CLR 已加载的情况下有效(即从 .NET 应用程序运行或手动加载 CLR 之后)。
### 远程进程(目标为另一个进程)
```
CLRUnhook.exe powershell.exe
CLRUnhook.exe 1234
```
解除远程进程中 CLR 的 Hook。
## 示例输出
### 成功的远程 Unhooking
```
=== CLR Unhooking Tool ===
[*] Mode -> Remote Process Unhooking
[*] Target -> PID 21436
[+] Found PID -> 21436
[*] Unhooking CLR->nLoadImage in remote process...
[DEBUG] Remote mode enabled
[DEBUG] Found clr.dll at 0x00007FFD38CB0000
[DEBUG] CLR path -> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
[DEBUG] CLR module size -> 10108928 bytes
[DEBUG] Read 10108928 bytes from remote process
[DEBUG] Searching for 'nLoadImage' in module (size: 10108928)
[DEBUG] Remote base address: 0x00007FFD38CB0000
[DEBUG] Scanning for string 'nLoadImage' (11 bytes)...
[DEBUG] Found string at RVA 0x7c12b8
[DEBUG] Searching for remote pointer: 0x7ffd394712b8
[DEBUG] Found pointer at offset 0x7a4340
[DEBUG] Valid function pointer found at RVA 0x5e4f30
[DEBUG] Found nLoadImage at RVA 0x00000000005E4F30
[DEBUG] Hooked function address -> 0x00007FFD39294F30
[DEBUG] Clean function at offset 0x00000000005E4F30 in disk file
[DEBUG] Reading hooked bytes before patch...
[DEBUG] First 16 bytes BEFORE unhook:
4C 8B DC 49 89 5B 08 49 89 73 10 4D 89 4B 20 57
[DEBUG] Clean bytes from disk:
8B 4B 78 E8 88 A9 EA FF C6 44 24 28 00 80 3D A4
[DEBUG] Wrote 30 bytes successfully
[DEBUG] First 16 bytes AFTER unhook:
8B 4B 78 E8 88 A9 EA FF C6 44 24 28 00 80 3D A4
[DEBUG] VERIFICATION SUCCESS: Patched bytes match clean bytes!
[+] SUCCESS -> CLR nLoadImage unhooked in remote process!
[+] EDR/AV hooks bypassed
[*] Press Enter to exit...
```
### Hook 链
```
Managed Code (C#)
↓
Assembly.Load(byte[])
↓
RuntimeAssembly.nLoadImage(...) [InternalCall]
↓
clr.dll!AssemblyNative::LoadImage
↓
[EDR HOOK] ← We bypass this
↓
Original CLR Code
```
### Unhooking 过程
1. **定位被 Hook 的函数** - 在已加载的 `clr.dll` 中查找 `nLoadImage`(当前已被 Hook)
2. **加载干净副本** - 从 `C:\Windows\Microsoft.NET\Framework64\v4.0.30319\` 读取原始 `clr.dll`
3. **提取干净字节** - 获取原始函数的前 30 个字节,.net 是 JIT 编译的,我们不希望出现问题。
4. **覆盖 Hook** - 用干净的字节修补被 Hook 的版本
### 函数发现
使用特征扫描来定位 `nLoadImage`:
1. 在模块内存中搜索 "nLoadImage" 字符串
2. 找到指向该字符串的指针
3. 定位与字符串指针相邻的函数指针
4. 验证地址是否在模块边界内
## 致谢
**技术研究:**
- [Matthew Graeber (@mattifestation)](https://exploitmonday.blogspot.com/2013/11/ReverseEngineeringInternalCallMethods.html) - 对 InternalCall 方法和 CLR 内部机制进行逆向工程
**实现:**
- **HWBP** - 通过内存恢复实现 CLR Unhooking
- **@Evilbytecode** - 帮助我进行了 Unhooking,我之前在 .net 是 JIT 方面遇到了一些问题。
## 免责声明
**仅供教育和授权的安全研究使用。**
未经授权使用此工具绕过安全控制可能会违反计算机欺诈法律(如 CFAA 及类似法规)。请仅在您拥有或获得明确书面授权进行测试的系统上使用。
## 参考
- [逆向工程 InternalCall 方法](https://exploitmonday.blogspot.com/2013/11/ReverseEngineeringInternalCallMethods.html) - Matthew Graeber
- Microsoft .NET 参考源代码
- CLR 程序集加载管道文档
标签:Assembly.Load, AV绕过, Bitdefender绕过, clr.dll, CLR卸钩, CrowdStrike绕过, C++原生开发, DNS 反向解析, EDR绕过, FastAPI, .NET安全, nLoadImage, SentinelOne绕过, TGT, 二进制安全, 免杀技术, 内存卸钩, 内存映射, 内存读写, 内联Hook恢复, 动态加载, 威胁对抗, 安全产品绕过, 安全对抗, 攻防演练, 暴力破解检测, 本地安全测试, 端点可见性, 高交互蜜罐