API Unhooking — Restoring ntdll to a Clean State

What EDR Hooks Are

When an EDR is installed on a Windows system, its kernel driver registers a callback that is invoked whenever a new process is created. At that moment — before the process's main code executes — the EDR injects its DLL into the process address space and overwrites the first bytes of critical functions in ntdll.dll (and sometimes in other DLLs like kernel32.dll, kernelbase.dll) with jumps to its own inspection code.

This mechanism is called inline hooking and is the foundation of most modern EDRs' userland visibility.

┌──────────────────────────────────────────────────────────────────────┐
│                 EDR Hooking Process                                  │
│                                                                      │
│  1. Process created (suspended)                                      │
│  2. EDR driver receives PsSetLoadImageNotifyRoutine callback         │
│  3. EDR injects its DLL via section mapping                          │
│  4. EDR DLL walks ntdll.dll exports in memory                        │
│  5. For each target function:                                        │
│                                                                      │
│  BEFORE hook:                                                        │
│  ntdll!NtCreateFile:                                                 │
│    4C 8B D1     mov r10, rcx                                         │
│    B8 55 00 00  mov eax, 0x55                                        │
│    0F 05        syscall                                              │
│    C3           ret                                                  │
│                                                                      │
│  AFTER hook (EDR installed):                                         │
│  ntdll!NtCreateFile:                                                 │
│    E9 XX XX XX  jmp <EDR_DLL+offset>   ← 5 bytes overwritten        │
│    XX 00 00 00                                                        │
│    0F 05        syscall                 ← rest intact                │
│    C3           ret                                                  │
└──────────────────────────────────────────────────────────────────────┘

Unhooking is the technique of restoring those modified bytes back to their original values, effectively removing the EDR's ability to inspect API calls.


Technique 1: Overwrite ntdll with a Copy from Disk

The most reliable approach: read the original image of ntdll.dll directly from disk (which was not modified by the EDR) and overwrite the .text section of the in-memory copy with the original bytes.

Why Disk is Trustworthy

The EDR hooks the DLL in memory, in the process address space. The file on disk remains intact. By mapping a fresh copy of the DLL from disk and copying the code section over the in-memory version, we restore all original bytes.


Technique 2: Mapping via NtMapViewOfSection (without CreateFileMapping)

To avoid detections based on calls to CreateFileMapping or MapViewOfFile (which some EDRs monitor), we can use native NT calls to map the section:


Technique 3: Selective Unhooking per Function

Instead of restoring the entire .text section, we identify which functions are hooked and restore only those. This is more surgical and less noisy:


Technique 4: Unhooking via Duplicate Module (Loading a Second ntdll)

An interesting variant: instead of using the file on disk directly, we load a second instance of ntdll.dll into the process under a different name. Since the loader does not apply hooks to the second instance (the EDR only monitors the initial loading), the second instance can be used directly.

Limitation: Some EDRs monitor LoadLibrary and check whether the loaded module has hooks applied. Additionally, the temporary file name may draw attention in file activity logs.


Unhooking Detection


Combined Approach in Real Engagements

In practice, unhooking is almost always combined with other techniques:

  1. Unhook first, execute later: Remove hooks before any malicious activity to ensure API calls are not inspected.

  2. Combined with direct/indirect syscalls: For the unhooking operations themselves (VirtualProtect), use direct syscalls to avoid the unhooking process itself being detected.

  3. Selective unhooking: Instead of restoring all of ntdll (which might trigger the EDR's integrity scan), restore only the functions needed for the current operation.


References

  • Aleksandra Doniec (hasherezade), "PE-sieve — Scanning for Hooks" — github.com/hasherezade/pe-sieve

  • ired.team, "Unhooking the Entire ntdll.dll" — ired.team/offensive-security/defense-evasion/

  • am0nsec, "Freshycalls: Syscall-Based API Unhooking" — github.com/am0nsec

  • Sektor7, "Malware Dev: API Unhooking Techniques" — sektor7.net

  • Kyle Avery, "NtMapViewOfSection Injection and Unhooking" — kyleleavery.com (2021)

  • Josh Lospinoso, "Offensive Security with Go" — Chapter on Unhooking (2022)

  • WithSecure Labs, "NTDLL Unhooking from a Different Process" — labs.withsecure.com (2022)

  • Reenz0h (Sektor7), "Malware Development: Evasion: API Unhooking" — sektor7.net

Last updated