Hookchain Technique Introduction by Helvio Júnior (M4v3r1ck)
This technique was created by Helvio Júnior (M4v3r1ck). Here are his social media links:
GitHub: https://github.com/helviojunior
Hookchain Project: https://github.com/helviojunior/hookchain
Website: https://sec4us.com.br/
YouTube Channel: https://www.youtube.com/c/sec4us
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 aroundntdll.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:
Subsequently, in the C++ application, create the function definition:
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 throughntdll.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...
andZw...
.
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:
Remapping
ntdll.dll
: The first step involves remapping a clean copy ofntdll.dll
into the process. This copy is unhooked and free from the hooks introduced by EDR solutions.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.
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:
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:
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.Customizable: Indirect syscall techniques can be customized to accommodate various syscalls, making them versatile for different applications.
Lower Detection Rates: Because the call does not follow the typical pattern expected by monitoring tools, indirect syscalls can bypass signature-based detection.
Challenges:
Complexity: Implementing indirect syscalls requires a deep understanding of the Windows internals and the syscall mechanisms.
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:
Locates the current address of the desired function within
ntdll.dll
.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
).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.
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:
Use of dynamic mapping techniques for the SSN, such as Halo's Gate.
Mapping of some base functions for the next steps, such as:
NtAllocateReserveObject
NtAllocateVirtualMemory
NtQueryInformationProcess
NtProtectVirtualMemory
NtReadVirtualMemory
NtWriteVirtualMemory
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 inntdll.dll
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
.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.
Modification of the IAT of key DLLs that use calls to
ntdll.dll
such askernel32
,kernelbase
,bcrypt
,bcryptPrimitives
,gdi32
,mswsock
,netutils
, andurlmon
. This step changes the destination address of the nativeNt/Zw
calls in the IAT to internal functions of our application. This means that when a subsystem DLL (e.g.,kernel32
) calls a function fromntdll.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 thenntdll.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:
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 withinntdll.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.
References and Indexes
The next data structure is a pointer to the .data
section of our application, defined in Assembly as follows:
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
, andZwDelayExecution
.
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:
In this pre-loading scenario, it would suffice to add the desired DLLs as shown below:
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:
Listing all DLL dependencies in the IAT.
Checking for references to
ntdll.dll
.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 theSyscallList.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:
The application wants to create a new process using the
CreateProcessW
function available in thekernel32.dll
API/subsystem.Since this specific function is implemented in
kernelbase.dll
,kernel32.dll
simply redirects the execution flow tokernelbase
.Within the
CreateProcessW
code inkernelbase.dll
, after some parameter checks, it will reach the point of calling theZwCreateUserProcess
function fromntdll.dll
.Instead of the original address of
ZwCreateUserProcess
, thekernelbase
IAT now contains the address of the HookChain-implanted function.After obtaining the address of the deployed function, the
CreateProcessW
code calls this address instead ofZwCreateUserProcess
inntdll.dll
, redirecting execution to HookChain's implanted function.HookChain's interception function searches in the
SyscallList.Entries
array for the information previously stored, such as the SSN and the address of thesyscall
instruction inntdll.dll
.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 thesyscall
instruction inntdll.dll
.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
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 usesExecAddr()
to process function addresses in several DLLs, such askernel32
,kernelbase
, anduser32
.
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
, andReadProcessMemory
.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 nextsyscall
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
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 inntdll.dll
.pSyscallRet
: Address of the associatedsyscall
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
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 toNtAllocateVirtualMemory
to the hooked function.NtOpenProcessStub
: Stub function that redirects calls toNtOpenProcess
.ExecAddr()
: Assembly function that executes the redirected code after the hook is triggered.
File: main.c
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 andNtWriteVirtualMemory
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
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
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:
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
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
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:
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:
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:
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
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 iswindows/x64/meterpreter/reverse_tcp
.The user sets the local host (
LHOST
) toeth0
and the local port (LPORT
) to4231
.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 toREAD_EXECUTE
, and finally executes the shellcode viaCreateRemoteThreadEx
.The process completes successfully, confirming that the shellcode has been executed with a "Shellcode OK!" message.
Complete main.c
Last updated