Diving in SyscallHook

Intro

This short blog post is an attempt to explain the steps performed by the SyscallHook framework to hook the NtCreateFile system routine.

SyscallHook

SyscallHook is the framework developed by Anze Lesnik for monitoring and hooking system calls. It is based on the older InfinityHook framework.

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 WMI_LOGGER_CONTEXT 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.

  1. Locate the Circular Kernel Context Logger (CKCL) session context
  2. Locate KeServiceDescriptorTableShadow to resolve the system routine addresses
  3. Update CKCL to log system calls
  4. Set CKCL WMI_LOGGER_CONTEXT structure member GetCpuClock to 1 to force the KeQueryPerformanceCounter function call
  5. Locate the HalpPerformanceCounter inside the KeQueryPerforamceCounter routine. Locate  the HalpPerformanceCounter.QueryCounter variable and overwrite the address it is pointing to to force the execution of the custom function temper
  6. The temper calls a custom function KeQueryPerformanceCounterHook and restores the original KeQueryPermanceCounter execution to keep things going in the kernel
  7. The KeQueryPerformanceCounterHook examines 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 IoGetStackLimits function 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 0x280 in 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.

Fig 1. Peformance Monitor: Circular Kernel Context Logger session configuration.

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 EtwDebuggerData function:

kd> x /D  nt!EtwpDebuggerData
fffff801`6ba10e38 nt!EtwpDebuggerData = <no type information>

kd> !wmitrace.strdump
(WmiTrace) StrDump Generic
  LoggerContext Array @ 0xFFFFC58330FDEB40 [64 Elements]
   Logger Id 0x02 @ 0xFFFFC58330EF7680 Named 'Circular Kernel Context Logger'
   Logger Id 0x03 @ 0xFFFFC58330F0C040 Named 'Eventlog-Security'
Fig 2. WinDbg commands to verify EtwpDebuggerData memory address and CKCL logger context
Fig 3. Memory view of EtwpDebuggerData address. 16 bytes in is the Logger Array memory address.

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: KeServiceDescriptorTable and 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 ntoskrnl.exe .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 KeServiceDescriptorTableShadow.

Again I used WinDbg to confirm the offsets and memory addresses. Maybe the commands below will be of use to somebody ¯\_(ツ)_/¯

kd> x /D nt!kisystemservicestart 
fffff801`6b210d60 nt!KiSystemServiceStart (KiSystemServiceStart)

kd> u fffff801`6b210d60
nt!KiSystemServiceStart:
fffff801`6b210d60 4889a390000000  mov     qword ptr [rbx+90h],rsp
fffff801`6b210d67 8bf8            mov     edi,eax
fffff801`6b210d69 c1ef07          shr     edi,7
fffff801`6b210d6c 83e720          and     edi,20h
fffff801`6b210d6f 25ff0f0000      and     eax,0FFFh
nt!KiSystemServiceRepeat:
fffff801`6b210d74 4c8d15450b9f00  lea     r10,[nt!KeServiceDescriptorTable (fffff801`6bc018c0)]
fffff801`6b210d7b 4c8d1dbebc8e00  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff801`6bafca40)]
fffff801`6b210d82 f7437880000000  test    dword ptr [rbx+78h],80h

kd> x /D nt!KeServiceDescriptorTableShadow
fffff801`6bafca40 nt!KeServiceDescriptorTableShadow
Fig 4. WinDbg SSDT examination

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.

Fig 5. Performance Monitor: Updating the CKCL to log the system calls events.

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.

kd> dt nt!_WMI_LOGGER_CONTEXT
   +0x000 LoggerId         : Uint4B
   +0x004 BufferSize       : Uint4B
   +0x008 MaximumEventSize : Uint4B
   +0x00c LoggerMode       : Uint4B
   +0x010 AcceptNewEvents  : Int4B
   +0x014 EventMarker      : [2] Uint4B
   +0x01c ErrorMarker      : Uint4B
   +0x020 SizeMask         : Uint4B
   +0x028 GetCpuClock      : Uint8B   <----- 
   +0x030 LoggerThread     : Ptr64 _ETHREAD
Fig 6: WinDbg Examination of WMI_LOGGER_CONTEXT structure

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.

Now 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 nt!HalpHvCounterQueryCounter.

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.

kd> x /D nt!KeQueryPerformanceCounter
fffff801`6b0a3ab0 nt!KeQueryPerformanceCounter (void)

kd> u fffff801`6b0a3ab0
nt!KeQueryPerformanceCounter:
fffff801`6b0a3ab0 48895c2420      mov     qword ptr [rsp+20h],rbx
fffff801`6b0a3ab5 56              push    rsi
fffff801`6b0a3ab6 4883ec20        sub     rsp,20h
fffff801`6b0a3aba 48897c2430      mov     qword ptr [rsp+30h],rdi
fffff801`6b0a3abf 488bf1          mov     rsi,rcx
fffff801`6b0a3ac2 488b3de7849a00  mov     rdi,qword ptr [nt!HalpPerformanceCounter (fffff801`6ba4bfb0)]
fffff801`6b0a3ac9 4c89742440      mov     qword ptr [rsp+40h],r14
fffff801`6b0a3ace 83bfe400000005  cmp     dword ptr [rdi+0E4h],5

kd> x /D nt!HalpPerformanceCounter
fffff801`6ba4bfb0 nt!HalpPerformanceCounter = <no type information>

kd> dps poi(nt!HalpPerformanceCounter)+ 0x70 L1
fffff7d6`80015be0  fffff801`6b18e1d0 nt!HalpHvCounterQueryCounter
Fig 7. WinDbg HalpPerformanceCounter->QueryCounter function pointer search

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 KeQueryPerformanceCounter execution.

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.

extern halCounterQueryRoutine:QWORD
extern keQueryPerformanceCounterHook:PROC

.code
temper PROC
    push rcx ;write a value stored in rcx on the stack
    mov rcx,rsp ;write a current stack pointer into rcx
    call keQueryPerformanceCounterHook
    pop rcx ;restore whatever is on the stack into the register
    mov rax, halCounterQueryRoutine 
    jmp rax
temper ENDP

end
Fig 8. VisualStudio 2019 temper function

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 0x280.

In WinDbg you can always double-check that the hooking was successful by running the next command.

kd> dps poi(nt!HalpPerformanceCounter)+ 0x70 L1
fffff7d6`80015be0  fffff801`7b4a1000 HookTest5!temper
Fig 9. WinDbg Hooked HalpPerformanceCounter->QueryCounter
Fig 10. WinDbg: Monitoring check-ins from the hooked NtCreateFile

Conclusion

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.

References