This short blog post is an attempt to explain the steps performed by the SyscallHook framework to hook the NtCreateFile system routine.
The main idea behind the InfinityHook is to use the Circular Kernel Context Logger for monitoring the system calls and to hook the required system routines by overwriting the
GetCpuClock function pointer. On Windows 22H2, which I was using for testing, this function pointer is no longer valid as it was converted to an index array instead.
SyscallHook does pretty much the same stuff but it overwrites the
HalpPerformanceCounter.QueryCounter function pointer instead of
GetCpuClock. This approach is valid for Windows 22H2 and other Windows versions where InfinityHook is a no-go.
SyscallHook Workflow Overview
SyscallHook performs next steps in order to hook the system calls. Each step will be discussed in the details below.
- Locate the Circular Kernel Context Logger (CKCL) session context
KeServiceDescriptorTableShadowto resolve the system routine addresses
- Update CKCL to log system calls
- Set CKCL
1to force the
- Locate the
KeQueryPerforamceCounterroutine. Locate the
HalpPerformanceCounter.QueryCountervariable and overwrite the address it is pointing to to force the execution of the custom function
tempercalls a custom function
KeQueryPerformanceCounterHookand restores the original
KeQueryPermanceCounterexecution to keep things going in the kernel
KeQueryPerformanceCounterHookexamines the current thread syscall value and "walks" the stack to locate the system routine address on the stack and to replace it with the address of the custom system routine. In the original SyscallHook version the current thread stack limits were retrieved using the
IoGetStackLimitsfunction call and the stack values within these limits were searched for the system routine address. This approach on Windows 22H2 leads to the hook not being called so guessing that the system routine address is located outside of the current thread stack limits. Walking the stack outside the current thread stack limits could lead to accessing the invalid memory and BSOD as a result of attempting to access it. Therefore, with a bit of guessing, the correct stack offset is hardcoded in the code which was
0x280in the Windows 22H2 case. There is probably a better approach to automate the stack search but ¯\_(ツ)_/¯
Step 1: Circular Kernel Context Logger
Event Tracing for Windows (ETW) is a Windows OS mechanism to log and trace events raised in user mode as well as kernel mode. The main parts of ETW are controllers, providers, and consumers. The controllers are the ones that create and set up the logging sessions, the providers create events and consumers pull the events from providers through the logging sessions. On top of regular ETW sessions, Windows supports system loggers, which allows the NT kernel to globally emit log events that are not tied to any provider and are used for performance measurement. The two supported system loggers are the NT Kernel Logger and the Circular Kernel Context Logger (CKCL). The CKCL, which is a subset of the NT Kernel Logger, provides a 2 MB circular buffer that continually tracks kernel performance statistics in memory. SyscallHook uses CKCL logger session to monitor the incoming system calls as well as hook the ones you are interested in. The Performance Monitor could be used to view the current CKCL details as well as what events it is set to log.
In the kernel, each logger session is represented by a
WMI_LOGGER_CONTEXT structure created when the logger is started and destroyed once it is stopped. The pointer to the array of
WMI_LOGGER_CONTEXT structures exists just after the
EtwpDebuggerData function which can be signature scanned for in
ntoskrnl.exe loaded module memory. This is what the function
getCKCLContex from SyscallHook does.
In WinDbg you could use the next commands to verify the CKCL logger memory address and the logger array offset from the
Step 2: System Service Dispatch Table (SSDT)
SSDT, on a x64-bit host, is an array of relative offsets to kernel routines that maps syscalls to kernel function addresses. There are two SSDTs on Windows OS:
KeServiceDescriptorTableShadow. The first one is used for generic routines and the second one is for graphical routines. It makes sense to get the second one in case you want to hook graphical routines on top of the generic ones.
To get the address of the
KeServiceDescriptorTableShadow the next steps are taken. First, the
nt!KiSystemServiceStart function is located inside the loaded
.text section using a signature scan. From there, the
nt!KiSystemServiceRepeat function is located using the hardcoded offset value which contains the reference to the
Again I used WinDbg to confirm the offsets and memory addresses. Maybe the commands below will be of use to somebody ¯\_(ツ)_/¯
SyscallHook/InfinityHook uses the
KeServiceDescriptorTableShadow to resolve system routine addresses for monitored syscalls.
Step 3 Update CKCL to log syscalls
By default, the CKCL logger is not set up to log the system call routines. Therefore, SyscallHook updates the logger context and restarts the session.
You could verify that CKCL is set to log the system calls using Performance Monitor or by examining its
WMI_LOGGET_CONTEXT structure with WinDbg.
At this point we have a way to get notified in the kernel every time there is a syscall event on the host through the ETW CKCL logger.
Step 4 Broken GetCpuClock fix
The InfinityHook was utilizing the
GetCpuClock member of the
WMI_LOGGER_CONTEXT structure to perform the actual hooking. However, on Windows 22H2,
GetCpuClock is no longer a function pointer but an index.
So it can't be used the way it is used in InfinityHook. But if this index is set to 1 it will force the
KeQueryPerformanceCounter function call.
KeQueryPerformanceCounter function references the
HalpPerformanceCounter data structure. And Anze Lesnik in his research discovered that the function pointer stored under the
HalpPerformanceCounter->QueryCounter routine could be used the same way as the now inaccessible
GetCpuClock function pointer and that overwriting it does not trigger the PatchGuard.
Therefore, by setting the
GetCpuClock to 1 we get the ability to hook the
KeQueryPerformanceCounter called from CKCL.
Step 5 HalpPerformanceCounter->QueryCounter
To find the
HalpPerformanceCounter->QueryCounter SyscallHook uses the next approach. First, it locates the
KeQueryPerformanceCounter system routine. After that, the signature scan is used to locate the
HalpPerformanceCounter address inside the
KeQueryPerformanceCounter. And finally, the
0x70 offset is used to get the function pointer that needs to be overwritten. In Windows 22H2 this address points to
Again I was using WinDbg to confirm the correct offsets and scan signature. On Windows 22H2 the signatures from the original SyscallHook repo and the infhook19041 repo were invalid and needed to be changed. The source code that worked for me is located here in case you need it. But it is not much different from the original one.
From the WinDbg output above, the address
0xfffff8016b18e1d0 is the one the SyscallHook overwrites.
Step 6 Hooked HalpPerformanceCounter->QueryCounter
Now that the
GetCpuClock issue has been fixed and the alternative has been found what are the next steps?
Well, we need a way to examine the incoming system calls but we also need a way to not break the original
The SyscallHook redirects
QueryCounter to the
checkLogger function execution. I used the
temper function instead taken from here since for some reason original Hook.asm did not work for me.
temper redirects the execution to the custom
keQueryPerformanceCounterHook function which takes the current stack pointer as an input argument through the
rcx register since its x64 and
__fastcall convention is in use. And then restores the original
KeQueryPerformanceCounter execution once the custom function has done its job.
Step 7 Actual Hooking
In the case of InfinityHook, when the CKCL is enabled to log system calls, the system function pointer is stored on the stack before logging the event and is later called after ETW returns to KiSystemCall64. InfinityHook, as well as SyscallHook, walks the stack inside the custom
keQueryPerformanceCounter function and replaces the pointer to the system function stored on the stack with a system call hook.
The original SyscallHook uses
IoGetStackLimits to safely walk the current thread stack. This does not work on Windows 22h2, the hook never gets called. In the infhook19041 version of SyscallHook the stack offset where the system routine address is stored is hardcoded and set to
0x2c0. On Windows 22H2 that offset turned out to be invalid and the one that worked was
In WinDbg you can always double-check that the hooking was successful by running the next command.
And this is it =)
The driver source code that works on Windows 22H2 Build 19045.3570 can be found here. And if you run into problems - WinDbg and DbgView should be able to help you.
I would like to mention that the kernel driver project by anquanke is a great place to start if you want to write SyscallHook from scratch or to understand it better - at least it helped me a lot.