Overview
In this article, we’ll be covering a fun alternative to the treasured InfinityHook from Nick Peterson. This alternative method was discovered by Aidan Khoury following the release and subsequent patch of the EtwpGetCycleCount target by Microsoft without any acknowledgements to the original authors. This method has been tested from early Windows 10 to latest Windows 11 23h2. The hook allows for a number of different, useful hooks. We’ll take a look at the most interesting one, in my opinion, for anti-malware/anti-cheat purposes.
Disclaimer
Anyone trying to replicate this will need to locate the appropriate hook/event IDs within their targeted NTOS version(s). I do not supply the hook/event IDs for any versions in the future within this article. The values that are used in this article are 0x0F33:0x00501802, respectively.
The first test took place on Windows 10 1903 (19H1); the latest test took place on Windows 11 23h2 22631.2506. However, earlier versions of Windows 10 have been tested and verified. The first test was just upon discovery and proof-of-concept development. There may parts of this post that are old, I have only put updates of refactored code from the original draft.
Common Hook Points in Windows Kernel
The most common targets for hooks within the Windows kernel are .data pointers, which are just function pointers stored in global tables or free-floating. A prime example of this is the NtConvertBetweenAuxiliaryCounterAndPerformanceCounter function. Many game hacking types have been using this as a means of “covert communication”, it by no means is, but it remains a popular choice and an example of the abuses to circumvent PatchGuard’s protection on system API. The usage requires you modify the function pointer from the HalPrivateDispatchTable, specifically an entry pointing to xKdEnumerateDebuggingDevices, and then from a usermode component acquire the routine from NTDLL, and then call the function. Three of the arguments get passed through to the invoked HAL function, meaning a user has flexibility in their usage and processing of information from usermode.
// NtConvertBetweenAuxiliaryCounterAndPerformanceCounter Decompilation Excerpt // HalpTimerConvertConvert = HalTimerConvertAuxiliaryCounterToPerformanceCounter[0]; if ( !ConvertAuxToPerf ) HalpTimerConvertConvert = HalTimerConvertPerformanceCounterToAuxiliaryCounter[0]; Result = (HalpTimerConvertConvert)(PerfCounterValue, &AuxCounter, v13);
The above snippet shows the initialization of a pointer to the HAL dispatch function HalTimerConvertXxx
depending on whether an argument indicates to convert from auxiliary counter to performance counter. Given this information, there is a notable amount of focus on what can be abused to compromise a system or enable complete control without triggering built-in anti-tamper. Hence the interest in…
The HalPrivateDispatchTable
The one that doesn’t have any information published on it currently is EtwpReserveWithPmcCounters. If the ETW Logger is setup in the correct manner, this function will execute, and if we look at the internals we’ll see another target ripe for patching.
signed __int64 __fastcall EtwpReserveWithPmcCounters( WMI_LOGGER_CONTEXT *LoggerContext, UINT16 HookId, UINT64 AuxSize, void *BufferHandle, LARGE_INTEGER *TimeStamp, UINT64 Flags) { volatile unsigned int CountersCount; unsigned int CtrIndex; unsigned int RequiredSize; unsigned __int8 CurrentIrql; struct_TraceBuffer *TraceBuffer; struct_TraceBuffer *pTracebuf; struct _HAL_PMC_COUNTERS *PmcEnabledForProc; _ETW_PMC_SUPPORT *PmcData; PmcData = LoggerContext->PmcData; CountersCount = PmcData->CountersCount; CtrIndex = 8 * CountersCount + 0x10; RequiredSize = CtrIndex + AuxSize; CurrentIrql = KeGetCurrentIrql(); if ( CurrentIrql < DISPATCH_LEVEL ) { KeGetCurrentIrql(); __writecr8(DISPATCH_LEVEL); } TraceBuffer = EtwpReserveTraceBuffer(&LoggerContext->LoggerId, RequiredSize, BufferHandle, TimeStamp, Flags); pTracebuf = TraceBuffer; if ( TraceBuffer ) { TraceBuffer->TimeStamp = *TimeStamp; TraceBuffer->TotalCounters = RequiredSize; TraceBuffer->HookId = HookId; TraceBuffer->Flags = Flags | (CountersCount << 8) | 0xC0110000; PmcEnabledForProc = PmcData->ProcessorCtrs[KeGetPcr()->Prcb.Number]; if ( PmcEnabledForProc ) HalPrivateDispatch->HalpCollectPmcCounters(PmcEnabledForProc, &TraceBuffer->Counters); else memset(&TraceBuffer->Counters, 0, 8 * CountersCount); if ( CurrentIrql < DISPATCH_LEVEL ) __writecr8(CurrentIrql); return pTracebuf + CtrIndex; } else { if ( CurrentIrql < DISPATCH_LEVEL ) __writecr8(CurrentIrql); return 0; } }
The target of interest is immediately obvious, HalPrivateDispatch->HalpCollectPmcCounters(...)
. If we follow the call chain on a live system you’ll notice something akin to this:
KernelBase.dll!SleepEx |- ntdll.dll!NtDelayExecution | |- ntoskrnl.exe!KiSystemServiceExitPico | | |- ntoskrnl.exe!PerfInfoLogSysCallEntry | | | |- ntoskrnl.exe!EtwTraceKernelEvent | | | | |- ntoskrnl.exe!EtwpLogKernelEvent | | | | | |- ntoskrnl.exe!EtwpReservePmcCounters | | | | | | |- ntoskrnl.exe!HalpCollectPmcCounters ---> | These only occur when the ETW logger is configured appropriately.
The ETW configuration will result in the NT Kernel Logger
having an output with information like this:
Logger Name : NT Kernel Logger Logger Id : ffff Logger Thread Id : 00000000000012A4 Buffer Size : 8192 Maximum Buffers : 118 Minimum Buffers : 96 Number of Buffers : 118 Free Buffers : 70 Buffers Written : 106898 Events Lost : 0 Log Buffers Lost : 0 Real Time Buffers Lost: 0 Flush Timer : 0 Age Limit : 0 Log File Mode : Secure PersistOnHybridShutdown SystemLogger Maximum File Size : 0 Log Filename : EdgyNameHere Trace Flags : SYSCALL PoolTagFilter : *
This is just a quick validation to visualize that we can hook SYSCALL
in a PG-compliant manner (i.e. it doesn’t bugcheck the machine).
DIY… MOSTLY
So, how do we do utilize this information? It’s relatively straightforward, let’s lay out the steps.
- Locate
HalPrivateDispatchTable
. - Locate System Call Handler.
- Acquire the ETW Event Id for SYSCALL logging.
- Configure ETW session programmatically.
- Configure the appropriate event trace class data.
- Swap pointer in the
HalPrivateDispatchTable
. - Enjoy yet another PG-compliant hook.
δ Locating HalPrivateDispatchTable
The acquisition of a pointer to the HalPrivateDispatchTable can be done like so:
UNICODE_STRING target = RTL_CONSTANT_STRING( L"HalPrivateDispatchTable" ); HalPrivateDispatchTable = reinterpret_cast< PHAL_PRIVATE_DISPATCH_TABLE >( MmGetSystemRoutineAddress( &target ) ); if ( !HalPrivateDispatchTable ) return STATUS_RESOURCE_UNAVAILABLE;
Refer to the long-form structure above for the HAL_PRIVATE_DISPATCH_TABLE
definition.
δ Additional ETW Configuration
To get this functional, it was noted that in addition to building the trace property structure and modifying it to enable tracing of SYSCALL
we would have to configure various trace controls via ZwTraceControl. For this, you will need to handle at a minimum the ZwTraceControl function codes EtwStartLoggerCode, EtwStopLoggerCode, and EtwUpdateLoggerCode. An excerpt is provided below from the proof-of-concept.
switch (operation) { case kl_trace_operation::start: { status = ZwTraceControl(EtwStartLoggerCode, pproperty, sizeof(KL_TRACE_PROPERTIES), pproperty, sizeof(KL_TRACE_PROPERTIES), &return_length); break; } case kl_trace_operation::end: { status = ZwTraceControl(EtwStopLoggerCode, pproperty, sizeof(KL_TRACE_PROPERTIES), pproperty, sizeof(KL_TRACE_PROPERTIES), &return_length); break; } case kl_trace_operation::syscall: { pproperty->EnableFlags |= EVENT_TRACE_FLAG_SYSTEMCALL; status = ZwTraceControl(EtwUpdateLoggerCode, pproperty, sizeof(KL_TRACE_PROPERTIES), pproperty, sizeof(KL_TRACE_PROPERTIES), &return_length); break; } }
const GUID session_guid = { 0x9E814AAD, 0x3204, 0x11D2, { 0x9A, 0x82, 0x0, 0x60, 0x8, 0xA8, 0x69, 0x39 } }; pproperty = nt::trace::build_property( L"NT Kernel Logger", &session_guid, EVENT_TRACE_BUFFERING_MODE );
In addition to this, we need to configure a few trace information blocks via ZwSetSystemInformation with the SystemPerformanceTraceInformation class to setup a list of counters and profiling information that allow the PMC collection routines to function. Notably, the EventTraceProfileCounterListInformation and EventTraceProfileEventListInformation. In the interest of space, the code has been omitted from this article.
δ The HalCollectPmcCounters Hook
The excerpt given below is all that is required within the hook routine to apply it system wide during the SYSCALL
tracing. Walk the stack, verify that the hook IDs / event IDs match the targeted event, if it matches then replace the element on the stack that contains the address to the target system routine.
void process_syscall(stack<64>& sp) { const auto target_fn = reinterpret_cast<void**>(sp.at(9)); // Example that replaces the return address on stack for NtQuerySystemInformation // with our hk_NtQuerySystemInformation, allowing us to hook the call system wide // in a PG-compliant manner. // if (*target_fn == o__nt_query_system_information) *target_fn = &hkd__nt_query_system_information; } void hkd__hal_collect_pmc_counters(HAL_PMC_COUNTERS* pmc_ctrs, unsigned long long* trace_buffer_end) { // Call original to populate appropriate data structures; avoid unnecessary overhead. // o__hal_collect_pmc_counters(pmc_ctrs, trace_buffer_end); if (!pmc_ctrs || !trace_buffer_end) return; const auto hook_id = *reinterpret_cast<uint32_t>(reinterpret_cast<uintptr_t>(trace_buffer_end) - 10); if (hook_id != target_value_1) return; stack<64> stk(get_pcr()->prcb->rsp_base, _AddressOfReturnAddress()); auto is_correct_event_target = [target_value_1, target_value_0](stack<64>& sp) { const auto event_id_1 = *sp.as<uint16_t*>(); const auto event_id_0 = *sp.next().as<uint32_t*>(); return event_id_1 == target_value_1 && event_id_0 == target_value_0; }; auto curr_stack = stk.find_frame( is_correct_event_target ); if (!curr_stack.valid()) return; curr_stack += 2; auto target_sp = curr_stack.find_first_within({ syscall_handler_begin, syscall_handler_end }); if(target_sp.valid()) process_syscall(target_sp); }
SYSCALL Compatibility
It’s worth noting that the SYSCALL
hook information provided here is relatively terse, and if you test on your target Windows version you will notice that only ntoskrnl system calls are captured. To capture win32k system calls using this same hook several modifications will need to be made. This is not covered in this article, but it can be achieved relatively straightforward by processing the exports of win32k.sys
, searching for __win32kstub
, and then acquiring the SYSCALL
index and adding it to a table that can be searched during runtime by address/index.
δ The NtQuerySystemInformation Hook
Since we needed a test case for this hook, I’ve provided the hook and associated helper functions for those interested in replicating this to some degree.
static NTSTATUS hkd__nt_query_system_information( SYSTEM_INFORMATION_CLASS system_information_class, PVOID system_information, ULONG system_information_length, PULONG return_length ) { NTSTATUS status = o_NtQuerySystemInformation(system_information_class, system_information, system_information_length, return_length); if (is_target_process()) { log_system_information(system_information_class, system_information, system_information_length, status); if (NT_SUCCESS(status)) { modify_system_information(system_information_class, system_information, system_information_length); } } return status; } static bool is_target_process() { return !strcmp(PsGetProcessImageFileName(PsGetCurrentProcess()), "<target>"); } static void log_system_information(SYSTEM_INFORMATION_CLASS system_information_class, PVOID system_information, ULONG system_information_length, NTSTATUS status) { const char* class_name = xnt::to_string(system_information_class); const char* process_name = PsGetProcessImageFileName(PsGetCurrentProcess()); ULONG_PTR process_id = (ULONG_PTR)PsGetProcessId(PsGetCurrentProcess()); if (class_name == nullptr) { LOG_INFO("%s (%I64d) :: NtQuerySystemInformation( %#x, 0x%p, %#x )", process_name, process_id, system_information_class, system_information, system_information_length); } else { LOG_INFO("%s (%I64d) :: NtQuerySystemInformation( %s, 0x%p, %#x )", process_name, process_id, class_name, system_information, system_information_length); } } static void modify_system_information(SYSTEM_INFORMATION_CLASS system_information_class, PVOID system_information, ULONG system_information_length) { PMDL mdl = IoAllocateMdl(system_information, system_information_length, FALSE, FALSE, NULL); if (!mdl) { LOG_INFO("Failed to allocate MDL in %s\n", __FUNCTION__); return; } __try { MmProbeAndLockPages(mdl, UserMode, IoWriteAccess); PVOID buffer = MmGetSystemAddressForMdlSafe(mdl, NormalPagePriority | MdlMappingNoExecute); if (!buffer) { goto __exit; } if (system_information_class == SystemKernelDebuggerInformation) { auto kdbg_info = reinterpret_cast<PSYSTEM_KERNEL_DEBUGGER_INFORMATION>(buffer); kdbg_info->KernelDebuggerEnabled = FALSE; kdbg_info->KernelDebuggerNotPresent = TRUE; } else if (system_information_class == SystemCodeIntegrityInformation) { auto code_integrity_info = reinterpret_cast<PSYSTEM_CODEINTEGRITY_INFORMATION>(buffer); code_integrity_info->CodeIntegrityOptions &= ~CODEINTEGRITY_OPTION_TESTSIGN; code_integrity_info->CodeIntegrityOptions |= CODEINTEGRITY_OPTION_ENABLED; } } __except (EXCEPTION_EXECUTE_HANDLER) { status = GetExceptionCode(); LOG_INFO("Exception in %s :: %#x\n", __FUNCTION__, status); IoFreeMdl(mdl); } // // ...additional handling and resource release... // }
You can find the list of NtQuerySystemInformation classes and list of IOCTLs at the linked references.
Discoverability
It’s worth noting that this hook, like all other ETW-based hooks, is straightforward to detect through the call-stack. Validation of the HalPrivateDispatchTable
is another vector that comes to mind. Call-stack spoofing is sufficient to circumvent the checks on the stack. An entire post could cover the vectors to discover the use of this hook, many of them are quite interesting, but we will refrain for now to keep things brief.
Demonstration Image
Once all the steps have been implemented, the logs of this example are given below:
Conclusion
This method of hooking SYSCALL
in a PG-compliant manner is interesting, but it’s much more versatile than meets the eye. I wrote this blog post to introduce it, and in the future I’ll cover more applications that extend beyond just SYSCALLs
. There are numerous ways to instrument operations in the kernel and stay PG-compliant, the majority of which require a lot more technical background and lack additional resources that allow for brevity in writing. One of these is planned for a future post, but it will be quite a blob. Some future research ideas involve continued investigation into the TracePoint and ETW APIs for additional indirect mechanisms that can be leveraged to control system operations.
It’s important that I credit the following people:
- Aidan Khoury for discovering this method and applications in Riot Vanguard following private disclosure.
- Everdox (N.Peterson) for the original discovery and disclosure of InfinityHook.
- iPower for coordinated research efforts with his own discoveries and future research ideas; also proof-reading this post.
If you’re interested in this sort of research, feel free to reach out to @daaximus, or @aidankhoury on twitter. As always, hope you’ve enjoyed, all the best to the readers.