Hook Chain
Hook Chain
ACM Reference Format: Helvio Carvalho Junior. 2024. HookChain: A new perspective
for Bypassing EDR Solutions. Curitiba, PR, BRAZIL, 46 pages.
https://arxiv.org/abs/2404.16856
1. INTRODUCTION
In the current corporate scenario, where digital security is more critical
than ever, Endpoint Detection and Response (EDR) systems have emerged as
essential pillars in the defense against increasingly complex digital attacks
and threats. As the technological world becomes increasingly intricate and
digital threats evolve with impressive speed, companies have been compelled
to develop their own EDR solutions, moving billions of dollars in this vibrant
market.
In this study, I highlight the new perspective that HookChain brings to
advanced security evasion techniques, by skillfully escaping the monitoring
and control mechanisms implemented by EDRs in the user mode, specifically
in the Ntdll.dll library. This library serves as a critical point for telemetry
collection for most EDRs, operating at the last frontier before accessing the
operating kernel (ring 0).
1.2. Ethics
This study does not represent ethical violations, as all tests were
conducted in controlled environments with valid licensing. Nor does it aim to
classify the defense and EDR products demonstrated here in terms of their
effectiveness, efficacy, and quality in the process of protecting and defending
the assets where they are installed, as it is a study and presentation of a
technique focused on a single point of identification of the agents.
2. BACKGROUND
2.1. EDR Architecture
2.1.1. Overview
EDR is the acronym for Endpoint Detection and Response, whose main
function is the identification, containment, and alert of malicious behaviors.
For the purpose of this study, we will focus solely on the transition
process between user mode and kernel mode.
Note: The x86 and AMD64 (x64) architectures define four levels of
privileges (protection rings) with the aim of protecting system code and data
from erroneous or malicious changes coming from lower privilege code.
Windows uses only privilege 0 (or ring 0) for kernel mode and privilege 3 (or
ring 3) for user mode. [2, p. 17]
For security reasons, Microsoft changes the SSN of each function when
a new Service Pack or Windows Release is launched. Eventually, new functions
may be added or removed.
0:002> u ntdll!NtReadFile
ntdll!NtReadFile:
00007ffe`b258d090 4c8bd1 mov r10,rcx
00007ffe`b258d093 b806000000 mov eax,6
...
00007ffe`b258d0a2 0f05 syscall
00007ffe`b258d0a4 c3 ret
Additionally, we can observe that the value 6 was assigned to the EAX
register, so that in this release/service pack of Windows the SSN of the function
NtReadFile is decimal 6.
lkd> uf nt!ZwReadFile
nt!ZwReadFile:
fffff806`0e7f9e00 488bc4 mov rax,rsp
fffff806`0e7f9e03 fa cli
fffff806`0e7f9e04 4883ec10 sub rsp,10h
fffff806`0e7f9e08 50 push rax
fffff806`0e7f9e09 9c pushfq
fffff806`0e7f9e0a 6a10 push 10h
fffff806`0e7f9e0c 488d052d880000 lea rax,[nt!KiServiceLinkage (fffff806`0e802640)]
fffff806`0e7f9e13 50 push rax
fffff806`0e7f9e14 b806000000 mov eax,6
fffff806`0e7f9e19 e9e2710100 jmp nt!KiServiceInternal (fffff806`0e811000)
The image loader is a user-mode resident code, within Ntdll.dll and not
in a kernel library. In this way, there is a guarantee that Ntdll.dll will always
be present in the running process (Ntdll.dll is always loaded). [2, p. 232]
In Figure 5 we can observe the use of the CFF Explorer software [6] to
view the import table (Import Directory), where all the DLLs referenced by
the application are defined, as well as the referenced functions of each DLL.
During the application loading, another table called IAT (Import Address
Table) is filled with the current addresses of the function in memory. This
process is carried out dynamically to meet various requirements such as
memory reallocation, ASLR (Address Space Layout Randomization) among
others.
3. After mapping the DLL into memory, this process is repeated for this
DLL with the goal of importing the dependencies used by it.
4. After each DLL is loaded, the IAT is processed looking for the specific
functions to be imported. Usually, this process is carried out by the
function's name, however, there is a possibility of it being done by
an index number. For each imported name, the loader checks the
export table of the imported DLL and tries to locate the desired
function. If it does not find it, this operation is approached.
0:002> lm
start end module name
00007ff7`3ddf0000 00007ff7`3de28000 notepad
...
In the output above, we can observe the IAT listing of the notepad.exe
process, as well as in the output below it is observed that at the indicated
address is indeed the code of the mapped function.
0:002> u 00007ffe`b1b8b1d0
KERNEL32!GetProcAddressStub:
00007ffe`b1b8b1d0 4c8b0424 mov r8,qword ptr [rsp]
00007ffe`b1b8b1d4 48ff25a5580600 jmp qword ptr [KERNEL32!_imp_GetProcAddressForCaller
(00007ffe`b1bf0a80)]
00007ffe`b1b8b1db cc int 3
00007ffe`b1b8b1dc cc int 3
The general idea behind function interception is to insert into the control
flow of the application being monitored. The monitoring agent takes control
of the monitored function before the original code is executed, after the
desired analysis (which can be logging, telemetry, control among others) the
flow of execution is transferred to the original function. [8, p. 687]
To carry out this process, there are several approaches available, in this
article we will discuss the most used by EDRs: 1 – Use of JMP or CALL ; 2 –
Manipulation of the IAT (Import Address Table). In both strategies, the
EDR performs the desired manipulations at runtime, that is, at the moment
of the application's loading, the EDR receives the event and performs the
injection of its Hook DLL, which in turn will alter the desired code of the
application to be monitored.
This strategy is generally used to alter the code of native function calls
within ntdll.dll.
Below, we can see the original NtCreateProcess function, that is, without
the presence of a hook.
0:002> u ntdll!NtCreateProcess
ntdll!NtCreateProcess:
00007ffe`b258e700 4c8bd1 mov r10,rcx
00007ffe`b258e703 b8ba000000 mov eax,0BAh
...
00007ffe`b258e712 0f05 syscall
00007ffe`b258e714 c3 ret
00007ffe`b258e715 cd2e int 2Eh
00007ffe`b258e717 c3 ret
0:004> u ntdll!NtCreateProcess
ntdll!NtCreateProcess:
00007fff`96bee700 e9f81b1600 jmp 00007fff`96d502fd
00007fff`96bee705 cc int 3
00007fff`96bee706 cc int 3
00007fff`96bee707 cc int 3
...
00007fff`96bee712 0f05 syscall
00007fff`96bee714 c3 ret
And the destination address of the JMP is not linked to any known
module (DLL), thus being a code injected at runtime.
Usage: <unknown>
Base Address: 00007fff`96d50000
End Address: 00007fff`96d53000
Region Size: 00000000`00003000 ( 12.000 kB)
State: 00001000 MEM_COMMIT
Protect: 00000020 PAGE_EXECUTE_READ
Type: 00020000 MEM_PRIVATE
Allocation Base: 00007fff`96d50000
Allocation Protect: 00000002 PAGE_READONLY
One of the first records of the function interception process through IAT
manipulation was described by Matt Pietrek in 1995 in his book Windows 95
System Programming Secrets [8, p. 687]
Regarding the bypass of hooks performed by the EDR, there are several
possible techniques that have been publicly disclosed, but commonly they are
reduced to the following techniques:
By far, the methodology for evading hooks inserted into the functions
of Ntdll.dll is the execution of direct Syscall calls. [1, p. 25]
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, <SSN>
syscall
ret
NtAllocateVirtualMemory ENDP
• Execution chain, where the EDR expects the function call to have
come from the application, then passed through Kernel32.dll, then
through Ntdll.dll.
• 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 any prior notice.
One can alter the function's replica so that after setting the EAX, it
performs a JMP to the address of the SYSCALL instruction.
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, <SSN>
JMP 00007ffe`b258e712
ret
NtAllocateVirtualMemory ENDP
2. Enumerates all functions starting with “Zw”, as in user mode the Nt...
and Zw... functions point to the same address, thus there being no
practical difference in using Zw or Nt in this scenario.
3. Stores (in an array) the relative virtual address (RVA) and the name
of the functions enumerated in the previous step. In the
implementation of this algorithm, the author uses an EDR evasion
technique that consists of, instead of saving and using the function
name as a comparison key, a hash calculated by a proprietary
algorithm through arithmetic operations with the ROR is used.
This technique is simple and effective because the code of the Zw/Nt
functions is in a single block of sequential code as can be seen in the Figure
11.
Figure 11: Ntdll.dll Zw/Nt functions in memory and their respective SSNs
3. If these are not the bytes, the function is being monitored (in other
words, it has a Hook set), however, the neighboring functions (before
and after) may not have a Hook.
For this enumeration to be linear and reliable across all platforms, the
following premises were adopted:
For the validation of the existence of Hooks in the functions of Ntdll, the
following steps were taken:
1. Listed all functions whose names start with Zw or Nt;
2. Checked for the presence of the JMP instruction in the function code;
For the verification of the presence of Hooks in the IAT of the DLLs
loaded in the process, the following steps were carried out:
3. Checked in the IAT of all loaded DLLs for the import reference of the
Ntdll.dll, as well as the use of functions whose names start with Zw
or Nt;
2. Checked if the address present in the IAT is different from the actual
address of the function in Ntdll.
The executable and code used in this phase of the study is available on
the git of this research at commit 0b4a953 [13].
3.5. Result
The table below presents the results of the enumeration carried out
between March 1st and March 22nd, 2024.
BitDefender
CarbonBlack
Checkpoint
Cortex
CrowdStrike Falcon
Windows Defender
Elastic
ESET
Kaspersky
MalwareBytes
SentinelOne
Sophos
Symantec
Trellix
Trend
Result 1: 94% of the analyzed EDR solutions (15 out of 16) do not
present hooks in the subsystem layer above Ntdll.dll, meaning, in the
verification of all DLLs loaded in the application that reference Ntdll,
only one EDR solution showed a hook in the IAT.
Result 2: 50% of the analyzed EDR solutions (8 out of 16) show an
absence of hooks in user mode.
Note: During the final tests, the presence of hooks in the subsystem
DLLs (kernek32 and kernelbase) was observed, but within the code of critical
functions, such as CreateProcess, and not using IAT hooks. For the purpose
of this study, these cases were not considered in the above results.
4. HOOKCHAIN
4.1. Overview
2. Mapping of some base functions for use in the actions of the next
steps, such as:
a. NtAllocateReserveObject
b. NtAllocateVirtualMemory
c. NtQueryInformationProcess
d. NtProtectVirtualMemory
e. NtReadVirtualMemory
f. NtWriteVirtualMemory
3. Creation and filling of an array where each item contains the following
content:
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 action aims to change the destination
address of the native Nt/Zw calls in the IAT to internal functions of
our application. In this way, when a subsystem DLL, such as
kernel32, calls a function from Ntdll.dll, the code from the HookChain
implant will actually be executed. Thus, materializing the IAT Hook
as previously seen in item 2.3.2 of this article.
After these actions are taken, the use of APIs and subsystems continues
in a conventional manner, as the layer for flow diversion and evasion has
already been implemented, requiring no further action. Thus, the executions
of the Ntdll.dll calls will be carried out through the internal functions of our
application, but in a transparent manner for the executing PE, as it will
continue to use the subsystem APIs as demonstrated in Figure 14.
o Execution chain, where the EDR expects the function call to have
come from the application, then passed through Kernel32.dll, then
through Ntdll.dll. This is due to the fact that the HookChain implant
(interception function) passes transparently in the call stack (as
we will see in more detail later).
• Portability
As previously seen, one of the first steps is the creation of an array with
the record of various information that will be used during the execution, thus
this array uses as an item a structure called SYSCALL_INFO as follows:
Where:
• pAddress: Storage field for the virtual address (Virtual Address) of the
function within Ntdll.
The next data structure is actually a pointer to the .data section of our
application defined in Assembly as below:
.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:
• qListEntrySize : Variable that contains the size (in bytes) of each entry
in the SYSCALL_LIST-> Entries.
a. NtAllocateReserveObject
b. NtAllocateVirtualMemory
c. NtQueryInformationProcess
d. NtProtectVirtualMemory
e. NtReadVirtualMemory
f. NtWriteVirtualMemory
Thus, at the end of this process, the array is filled with all Nt/Zw
functions that present an EDR hook, as well as the 6 functions added
unconditionally for future use as can be observed inFigure 15.
Figure 15: Values of the SyscallList.Entries array
0:004> uf ntdll!NtCreateUserProcess
ntdll!NtCreateUserProcess:
00007ffe`b258e8e0 4c8bd1 mov r10,rcx
00007ffe`b258e8e3 b8c9000000 mov eax,0C9h
00007ffe`b258e8e8 f604250803fe7f01 test byte ptr [SharedUserData+0x308
(00000000`7ffe0308)],1
00007ffe`b258e8f0 7503 jne ntdll!NtCreateUserProcess+0x15 (00007ffe`b258e8f5)
Branch
ntdll!NtCreateUserProcess+0x12:
00007ffe`b258e8f2 0f05 syscall
00007ffe`b258e8f4 c3 ret
As can be seen in the image and the command result in Windbg above,
the HookChain algorithm was able to obtain the SSN of the
NtCreateUserProcess function (decimal 201, hexadecimal 0x00C9), as well as
calculate the address of the next SYSCALL instruction (00007ffe`b258e8f2).
/*
Handle non-hooked functions
}
}
return -1;
}
Once the previous step is completed, and having the array filled with
the data of the native Nt/Zw functions, it is possible to move on to the next
phase, which is the phase of modifying the IAT of all loaded DLLs.
Below is the code snippet responsible for filling the array and IAT Hook
of the kernel32 and kernelbase DLLs.
BOOL UnhookAll(_In_ HANDLE hProcess, _In_ LPCSTR imageName, _In_ BOOLEAN force);
BOOL InitApi(VOID)
{
if (!FillSyscallTable()) return FALSE;
return TRUE;
}
In this scenario of pre-loading that we are elucidating here, it would
suffice to add the desired DLLs as shown in the example below:
BOOL UnhookAll(_In_ HANDLE hProcess, _In_ LPCSTR imageName, _In_ BOOLEAN force);
BOOL InitApi(VOID)
{
if (!FillSyscallTable()) return FALSE;
return TRUE;
}
The IAT hook procedure follows the same way as detailed in section
2.3.2 of this article. In general, HookChain will perform the following
procedure for the requested DLLs through the UnhookAll function
(demonstrated above).
After completing the previous steps, all the necessary procedures for
the HookChain implantation are finalized, so that from this moment on all
calls made to the Windows subsystems will be free from interceptions and
monitoring by the EDR at the level of Ntdll.dll.
In this way, let's understand more deeply the execution flow of the
application after the completion of the HookChain implants.
Figure 16: Execution Flow After HookChain Implant
8. At this point in our flow at the top of the stack, the return address
will be contained, which will be the address of the next instruction
inserted into the stack at the moment the CreateProcessW from
kernelbase performed the CALL. Then, the Ntdll executes the syscall
instruction. And when there is a return from the kernel, the flow will
be directed to the respective return address within the
CreateProcessW.
One of the advantages of using HookChain is the fact that it does not
alter the call stack (in point of EDR view) of the calls, even though this is not
its main purpose. In this way, this test aims at the visualization and
understanding of the call stack of functions before and after the HookChain
implants. Therefore, the test code performs 3 actions:
Figure 19: Stack trace of the CreateProcessW call before the implants
Figure 22: Stack trace of the CreateProcessW call after the implants
When comparing Figure 19 and Figure 22, it can be observed that one
of our objectives was 100% achieved, in such a way that the diversion of the
application flow and the consequent presence of the hook created by
HookChain did not alter the Stack Trace, thus being able to go unnoticed by
the EDR telemetry.
Result 3 : Stack trace telemetry unchanged to the point where the flow diversion
(Hook) can go unnoticed by an EDR check in kernel-land.
0x00007FF7BE63DD30 = &SyscallList
In the text above, extracted from the application console at the time of
execution, one can see the information of the ZwCreateUserProcess function.
0:004> lm
start end module name
00007ff7`be5c0000 00007ff7`be64e000 HookChain
00007ffe`48080000 00007ffe`482a1000 ucrtbased
00007ffe`9da80000 00007ffe`9daae000 VCRUNTIME140D
00007ffe`afba0000 00007ffe`afbc7000 bcrypt
00007ffe`afbd0000 00007ffe`afec6000 KERNELBASE
00007ffe`b1680000 00007ffe`b1720000 sechost
00007ffe`b1b70000 00007ffe`b1c2d000 KERNEL32
00007ffe`b2020000 00007ffe`b2145000 RPCRT4
00007ffe`b24f0000 00007ffe`b26e8000 ntdll
Fnc0001 PROC
mov rax, SyscallExec
push rax
mov rax, 0001h
ret
nop
Fnc0001 ENDP
Fnc0002 PROC
mov rax, SyscallExec
push rax
mov rax, 0002h
ret
nop
Fnc0002 ENDP
SyscallExec PROC
sub rsp, 08h ; Address to place syscall addr and use with ret
push r12
push r9
push r8
push rdx
push rcx
push rbp
mov rbp, rsp
mov r12, rdx
mov rdx, qListEntrySize
mul rdx
mov rdx, r12
mov r12, qTableAddr
lea rax, [r12 + rax]
mov r12, [rax + 10h]
mov rax, [rax]
mov [rbp + 30h], r12 ; 0x30 = 6 * 8 = 48
mov rsp, rbp
pop rbp
pop rcx
pop rdx
pop r8
pop r9
pop r12
mov r10, rcx
ret ; jmp to the address saved at stack
SyscallExec ENDP
4.6. Testing Methodology
Other use tests have been tested. Wait until final release.
4.7. Result
NOTE: This is not the final result because this research is under construction,
and more use cases have been tested.
The table below presents the result of the enumeration carried out
between March 1, 2024, and April 3, 2024.
EXECUTED CODE
PRODUCT
Remote Process
Loading and executing a PE
Injection
BitDefender
Cortex
CrowdStrike Falcon
Windows Defender
ESET
MalwareBytes
SentinelOne
Sophos
Trellix
Trend
Where:
During the tests of running the Metasploit Open Source [15], some
blocks and alerts were observed after the establishment of the metasploit
session. But this behaviour were observed just during the execution of some
commands. So, this behavior of identification and blocking is expected, as
many of these commands execute other Windows processes, and since the
new processes (even if they are children of the HookChain process) will not
have the bypass implants performed by HookChain, the EDR will be able to
monitor these behaviors and carry out the appropriate mitigating actions.
However, with the use of the Havoc Framework [16], no block were
observed, demonstrating, in this way, that the identifications and possible
blocks are directly tied to the actions performed, and as well as the
Framework used.
Possibly with the use of other products with stealthier behavior such as
Metasploit Pro, CobaltStrike and others, most of the actions will be performed
unnoticed.
NOTE: This is not the final result because this research is under construction,
and more use cases have been tested.
6. BIBLIOGRAPHY
[1] M. Hand, Evading EDR: The Definitive Guide to Defeating Endpoint Detection
Systems, San Francisco: No Stach Press, Inc, 2024.
[2] M. Russinovich, D. A. Solomon and A. Lonescu, Windows Internals, Sixth
Edition, Part 1, Redmond, Washington: Microsoft Press, 2012.
[3] P. Yosifovich, A. Lonescu, E. M. Russinovich and A. D. Solomon, Windows
Internals Seventh Edition - Part 1, Redmond: Microsoft Press, 2017.
[4] Microsoft, "Microsoft Learn," [Online]. Available:
https://learn.microsoft.com/en-us/cpp/build/x64-calling-
convention?view=msvc-170. [Accessed 21 03 2024].
[5] Microsoft, "PE Format," [Online]. Available:
https://docs.microsoft.com/windows/win32/debug/pe-format. [Accessed 21 03
2024].
[6] NtCore, "Explorer Suite," [Online]. Available:
https://ntcore.com/?page_id=388. [Accessed 21 03 2024].
[7] R. Batra, "API Monitor," [Online]. Available:
http://www.rohitab.com/apimonitor. [Accessed 21 03 2024].
[8] M. Pietrek, Windows 95 System Programming Secrets, Foster City: IDG Books
Worldwide, Inc, 1995.
[9] @modexpblog, "Bypassing User-Mode Hooks and Direct Invocation of System
Calls for Red Teams," [Online]. Available:
https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-
invocation-of-system-calls-for-red-teams/. [Accessed 22 03 2024].
[10] am0nsec; smelly__vx;, [Online]. Available:
https://vxug.fakedoma.in/papers/VXUG/Exclusive/HellsGate.pdf. [Accessed 22
03 2024].
[11] Reenz0h from Sektor7, 23 04 2021. [Online]. Available:
https://blog.sektor7.net/#!res/2021/halosgate.md. [Accessed 22 03 2024].
[12] C. Joca, "NtGate, An implementation of Halo's Gate and indirect syscalls,"
[Online]. Available: https://github.com/hiatus/NtGate. [Accessed 22 03 2024].
[13] H. C. J. M4v3r1ck, "HookChain Research - Step 1," [Online]. Available:
https://github.com/helviojunior/hookchain/tree/0b4a953c10a18f53aa68f7588
db9818730dd7a52. [Accessed 22 03 2024].
[14] S. Fewer, "ReflectiveDLLInjection," [Online]. Available:
https://github.com/stephenfewer/ReflectiveDLLInjection/. [Accessed 22 03
2024].
[15] Rapid7, [Online]. Available: https://www.metasploit.com/. [Accessed 03 04
2024].
[16] C5pider, [Online]. Available: https://havocframework.com/. [Accessed 03 04
2024].