22 August 2025

Modern Endpoint Detection and Response (EDR) solutions often rely on inline hooking within user-mode DLLs, mainly ntdll.dll, to detect and block suspicious or malicious activity. These hooks let EDRs intercept sensitive system calls like NtOpenProcess, NtCreateThreadEx, and NtAllocateVirtualMemory.
When a process starts, the system loads ntdll.dll into its memory. EDRs often modify specific instructions in the .text section of this DLL. For example, instead of letting the original syscall instruction run directly, an EDR may replace the function prologue with a jump or call to its own monitoring routines. This redirection enables them to log, block, or alter the behavior of the call.
For red teamers, malware authors, or even developers who need clean access to system calls, these hooks create barriers. Any indirect or direct system call might be intercepted, logged, or blocked by the EDR, making stealth operations and legitimate debugging more difficult.
The Solution: Unhooking
To get around these user-mode hooks, one common technique is unhooking. This usually involves:
- Loading a clean copy of ntdll.dll from disk (not the possibly hooked one in memory).
- Extracting the .text section from the clean copy.
- Overwriting the .text section in memory with the clean one.
This effectively restores the original system call stubs (mov r10, rcx; mov eax, syscall_number; syscall) and removes the user-mode redirection set up by EDR.
Use low-level file I/O functions like CreateFile, ReadFile, or NtCreateFile to open and read the file located at: C:\Windows\System32\ntdll.dll
Do not use LoadLibrary, as it will load the already hooked version present in memory.
Reading the file manually gives you a clean, untouched copy of ntdll.dll.
Goal: Get access to the raw, unmodified bytes of ntdll.dll.
You now have two versions of ntdll.dll:
Parse both using the PE header structures:
These structures will help you locate important sections, especially .text.
Goal: Find the offset, virtual address, and size of the .text section in both versions.
The .text section contains executable code, including syscall stubs like NtOpenProcess.
From the PE headers, locate:
You need these values from both versions of ntdll.dll so you can accurately copy the clean code over the hooked one.
Goal: Identify exactly which memory region should be overwritten.
By default, the .text section is marked read-execute (RX) to prevent modification.
Use one of the following APIs to change its protection to read-write-execute (RWX):
Pass the .text region address and size to these functions.
Goal: Temporarily allow writing to the .text section of the hooked ntdll.dll.
With write access enabled, copy the .text section bytes from the clean file into the memory region of the loaded ntdll.dll.
This removes any inline hooks (e.g., JMP instructions) placed by EDR and restores the original syscall stubs.
Each syscall stub typically looks like this:
Explanation:
Goal: Replace the hooked stubs with the original system call code to bypass EDR monitoring.
Once the clean .text section is copied into memory, restore the original memory protection (RX) using the same API:
This avoids detection based on suspicious memory permissions (e.g., RWX) and returns the process to a normal state.
Goal: Finalize the patch and avoid triggering EDR based on modified memory protections.
get the code from here.
This function returns the full path to ntdll.dll inside the System32 directory.
It works by:
This function reads the raw bytes of the clean ntdll.dll from disk and returns them as a vector<BYTE>.
It works by:
If any step fails, it logs the error and returns an empty vector.
returns the offset and size of a specific section (e.g., .text) in a PE file.
It works by:
If the section is not found or headers are invalid, it logs an error and returns {0, 0}.
unhooks ntdll.dll in memory by restoring its original .text section using a clean copy from disk.
It works by:
Returns true if unhooking succeeds, otherwise logs an error and returns false.
Unhooking ntdll.dll by restoring its .text section with a clean copy from disk is an effective technique to bypass user-mode inline hooks commonly used by EDRs. It restores the original syscall stubs, allowing direct, unmonitored system calls.
However, this method has limitations. It does not bypass kernel-level hooks, ETW, or AMSI, and can still raise red flags due to memory protection changes. Advanced EDRs that rely on SSDT hooking or API emulation may still detect malicious behavior.
Use this technique as one part of a broader evasion strategy not a standalone solution.