Every EDR on the market hooks ntdll.dll. They overwrite the first bytes of syscall stubs with a JMP into their monitoring DLL so they can inspect every API call your process makes. The fix is embarrassingly simple. A clean copy of ntdll is sitting on disk. Map it into memory, copy the .text section over the hooked one, and every hook disappears. Here is how it works and when it actually matters.
How EDR Hooking Works
When your process starts, the EDR injects a DLL into it (usually through an early-load driver callback). That DLL walks the export table of ntdll.dll and patches the syscall stubs for every function it wants to monitor. The patch replaces the first 5+ bytes of the function with a JMP to the EDR's own inspection routine.
When your code calls NtAllocateVirtualMemory, it hits the JMP instead of the syscall. The EDR inspects the arguments (what address, what size, what protection flags), decides if it looks malicious, and then either allows it to proceed or kills your process. Every call to VirtualAlloc, WriteProcessMemory, CreateRemoteThread, and friends goes through this inspection.
The Unhooking Technique
The concept is dead simple. Windows keeps the original ntdll.dll on disk at C:\Windows\System32\ntdll.dll. The EDR only hooks the copy that is already loaded in your process memory. So you read the clean copy from disk, find the .text section (where the executable code lives), and overwrite the hooked version in memory with the clean bytes.
That is it. After step 5, every syscall stub in ntdll is back to its original state. The JMP instructions are gone. Your subsequent API calls go directly to the kernel without passing through the EDR.
Variations and Trade-offs
The basic technique reads ntdll from System32, but there are multiple ways to get a clean copy. Each comes with different detection risks.
| Source | Method | Detection Risk |
|---|---|---|
| Disk (System32) | CreateFile + MapViewOfFile | MEDIUM |
| KnownDlls | Open \KnownDlls\ntdll.dll section object | MEDIUM |
| Suspended Process | Spawn suspended process, read its clean ntdll, kill it | HIGH |
| Direct Syscalls | Skip ntdll entirely, use assembly syscall stubs | LOW |
Reading ntdll from disk is the simplest but some EDRs monitor file reads to ntdll.dll specifically. The KnownDlls approach avoids touching the filesystem but still opens a known section object. The suspended process method is noisy because spawning and immediately killing a process is suspicious behaviour. Direct syscalls avoid ntdll entirely, but that is a different technique (and a different blog post).
Why EDRs Cannot Permanently Fix This
This technique has been public for years, and every EDR vendor knows about it. So why does it still work? Because the fundamental architecture makes it impossible to prevent.
When Unhooking is Not Enough
Unhooking removes user-mode visibility, but modern EDRs do not rely solely on user-mode hooks. Here is what else they use that unhooking does not touch.
Unhooking ntdll gets you past the inline hooks, but the EDR's kernel driver is still watching process creation, thread creation, memory allocation patterns, and file writes through kernel callbacks. ETW providers are logging your .NET assembly loads, your PowerShell execution, and your network connections. Unhooking is one layer. It is not a silver bullet.
The Takeaway
Unhooking ntdll works because the architecture guarantees it will always work. User-mode hooks cannot be protected from user-mode code. The clean bytes will always be accessible. But treating unhooking as the complete solution is a mistake. It removes one detection layer out of many. The operators who consistently evade modern EDRs combine unhooking with ETW patching, direct syscalls, careful process selection, and payload encryption. Unhooking is the first step, not the last.
Understand the technique. Know when to use it. But always remember that the hooks you can see are only half the story. The kernel is watching too.
EDR detection capabilities evolve constantly. Techniques described here reflect the current state of user-mode hooking architectures. Always validate against your target environment.