Back to blog
Mar 20267 min read

Unhook Before You Walk

Restoring ntdll to factory settings in 20 lines of code

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.

CLEAN NTDLL STUB
NtAllocateVirtualMemory:
mov r10, rcx
mov eax, 0x18 ; syscall number
syscall
ret
HOOKED NTDLL STUB
NtAllocateVirtualMemory:
jmp 0x7FFE12340000 ; EDR hook
mov eax, 0x18 ; (overwritten)
syscall
ret

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.

unhook.pseudocode
NTDLL UNHOOKING FLOW
// Step 1: Map a clean copy from disk
hFile = CreateFile("C:\\Windows\\System32\\ntdll.dll")
hMapping = CreateFileMapping(hFile)
pClean = MapViewOfFile(hMapping)
// Step 2: Find the .text section in the clean copy
pDosHdr = (PIMAGE_DOS_HEADER)pClean
pNtHdr = pClean + pDosHdr->e_lfanew
pSection = find_section(".text", pNtHdr)
// Step 3: Get the hooked ntdll base in our process
pHooked = GetModuleHandle("ntdll.dll")
// Step 4: Make the hooked .text section writable
VirtualProtect(pHooked + offset, size, PAGE_EXECUTE_READWRITE)
// Step 5: Overwrite hooked bytes with clean bytes
memcpy(pHooked + offset, pClean + offset, sectionSize)
// Step 6: Restore original protection
VirtualProtect(pHooked + offset, size, PAGE_EXECUTE_READ)

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.

SourceMethodDetection Risk
Disk (System32)CreateFile + MapViewOfFileMEDIUM
KnownDllsOpen \KnownDlls\ntdll.dll section objectMEDIUM
Suspended ProcessSpawn suspended process, read its clean ntdll, kill itHIGH
Direct SyscallsSkip ntdll entirely, use assembly syscall stubsLOW

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.

1
Same Address Space
The EDR hooks live in your process memory. You have full read/write access to your own memory. Windows cannot prevent a process from modifying its own address space, because legitimate software does this constantly (JIT compilers, debuggers, self-modifying code).
2
User-Mode vs Kernel-Mode
The hooks are user-mode patches. They exist in ring 3, where your code also runs. The EDR has no privilege advantage over you. A kernel driver could protect the memory pages, but Microsoft's Kernel Patch Protection (PatchGuard) prevents third-party drivers from doing this reliably.
3
Clean Copy Always Available
The original ntdll must exist somewhere accessible. Windows needs it to start processes. You can read it from disk, from KnownDlls, from another process, or from the debug symbols. The clean bytes will always be available to anyone who looks.

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.

detection-layers.txt
WHAT UNHOOKING DOES NOT BYPASS
ETW Event Tracing for Windows, kernel-level telemetry
Callbacks Kernel callbacks (PsSetCreateProcessNotifyRoutine, etc.)
Minifilters Filesystem minifilter drivers (file write monitoring)
AMSI Antimalware Scan Interface (script content inspection)
Network DNS/TLS inspection at the network layer

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.

EvasionEDR BypassMalware Dev

EDR detection capabilities evolve constantly. Techniques described here reflect the current state of user-mode hooking architectures. Always validate against your target environment.