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恢复, 动态加载, 威胁对抗, 安全产品绕过, 安全对抗, 攻防演练, 暴力破解检测, 本地安全测试, 端点可见性, 高交互蜜罐