Fun with another PG-compliant Hook

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 HAL_PRIVATE_DISPATCH, commonly referred to as HalPrivateDispatchTable, is a critical structure within the Windows operating system, specifically within its Hardware Abstraction Layer (HAL). This table plays a pivotal role in the interaction between the Windows OS and the hardware it runs on, particularly for hardware-specific functions that are not exposed through standard HAL interfaces. Put plainly, it’s just a table of function pointers, with each entry corresponding to a specific hardware function, tailored to the needs of the platform Windows is operating on. Some of these entries existence depends on the type of platform (mobile/workstation/terminal) and startup parameters, and may vary depending on Windows version. As has been noted in the paragraph prior, the use of the HalPrivateDispatchTable is limited to kernel mode, where it is accessed by drivers or other kernel components to perform low-level hardware operations.

Most of the operations range from specialized hardware initialization to advanced power management functions. It’s a crucial data structure for system stability and security; we’ll get into the significance of some of these functions, but if you’re familiar with InfinityHook, I’m sure you can guess.

An unfortunate thing about this construct is that documentation is typically sparse, as it delves into the more intricate and lower-level aspects of Windows internals. It’s primarily of interest to experienced Windows kernel developers or those working in close proximity to hardware-level programming. Most of the details extracted here were done so through a variety of resources (see references section), and independent reverse-engineering of associated components. The layout of the HalPrivateDispatchTable is given below in long form.

struct HAL_PRIVATE_DISPATCH
{
  unsigned int Version;
  BUS_HANDLER *(*HalHandlerForBus)(INTERFACE_TYPE, unsigned int);
  BUS_HANDLER *(*HalHandlerForConfigSpace)(BUS_DATA_TYPE, unsigned int);
  void (*HalLocateHiberRanges)(void *);
  int (*HalRegisterBusHandler)(INTERFACE_TYPE, BUS_DATA_TYPE, unsigned int, INTERFACE_TYPE, unsigned int, unsigned int, int (*)(BUS_HANDLER *), BUS_HANDLER **);
  void (*HalSetWakeEnable)(unsigned __int8);
  int (*HalSetWakeAlarm)(unsigned __int64, unsigned __int64);
  unsigned __int8 (*HalPciTranslateBusAddress)(INTERFACE_TYPE, unsigned int, LARGE_INTEGER, unsigned int *, LARGE_INTEGER *);
  int (*HalPciAssignSlotResources)(UNICODE_STRING *, UNICODE_STRING *, DRIVER_OBJECT *, DEVICE_OBJECT *, INTERFACE_TYPE, unsigned int, unsigned int, CM_RESOURCE_LIST **);
  void (*HalHaltSystem)();
  unsigned __int8 (*HalFindBusAddressTranslation)(LARGE_INTEGER, unsigned int *, LARGE_INTEGER *, unsigned __int64 *, unsigned __int8);
  unsigned __int8 (*HalResetDisplay)();
  int (*HalAllocateMapRegisters)(_ADAPTER_OBJECT *, unsigned int, unsigned int, MAP_REGISTER_ENTRY *);
  int (*KdSetupPciDeviceForDebugging)(void *, DEBUG_DEVICE_DESCRIPTOR *);
  int (*KdReleasePciDeviceForDebugging)(DEBUG_DEVICE_DESCRIPTOR *);
  void *(*KdGetAcpiTablePhase0)(LOADER_PARAMETER_BLOCK *, unsigned int);
  void (*KdCheckPowerButton)();
  unsigned __int8 (*HalVectorToIDTEntry)(unsigned int);
  void *(*KdMapPhysicalMemory64)(LARGE_INTEGER, unsigned int, unsigned __int8);
  void (*KdUnmapVirtualAddress)(void *, unsigned int, unsigned __int8);
  unsigned int (*KdGetPciDataByOffset)(unsigned int, unsigned int, void *, unsigned int, unsigned int);
  unsigned int (*KdSetPciDataByOffset)(unsigned int, unsigned int, void *, unsigned int, unsigned int);
  unsigned int (*HalGetInterruptVectorOverride)(INTERFACE_TYPE, unsigned int, unsigned int, unsigned int, unsigned __int8 *, unsigned __int64 *);
  int (*HalGetVectorInputOverride)(unsigned int, GROUP_AFFINITY *, unsigned int *, KINTERRUPT_POLARITY *, INTERRUPT_REMAPPING_INFO *);
  int (*HalLoadMicrocode)(void *);
  int (*HalUnloadMicrocode)();
  int (*HalPostMicrocodeUpdate)();
  int (*HalAllocateMessageTargetOverride)(DEVICE_OBJECT *, GROUP_AFFINITY *, unsigned int, KINTERRUPT_MODE, unsigned __int8, unsigned int *, unsigned __int8 *, unsigned int *);
  void (*HalFreeMessageTargetOverride)(DEVICE_OBJECT *, unsigned int, GROUP_AFFINITY *);
  int (*HalDpReplaceBegin)(HAL_DP_REPLACE_PARAMETERS *, void **);
  void (*HalDpReplaceTarget)(void *);
  int (*HalDpReplaceControl)(unsigned int, void *);
  void (*HalDpReplaceEnd)(void *);
  void (*HalPrepareForBugcheck)(unsigned int);
  unsigned __int8 (*HalQueryWakeTime)(unsigned __int64 *, unsigned __int64 *);
  void (*HalReportIdleStateUsage)(unsigned __int8, KAFFINITY_EX *);
  void (*HalTscSynchronization)(unsigned __int8, unsigned int *);
  int (*HalWheaInitProcessorGenericSection)(WHEA_ERROR_RECORD_SECTION_DESCRIPTOR *, WHEA_PROCESSOR_GENERIC_ERROR_SECTION *);
  void (*HalStopLegacyUsbInterrupts)(SYSTEM_POWER_STATE);
  int (*HalReadWheaPhysicalMemory)(LARGE_INTEGER, unsigned int, void *);
  int (*HalWriteWheaPhysicalMemory)(LARGE_INTEGER, unsigned int, void *);
  int (*HalDpMaskLevelTriggeredInterrupts)();
  int (*HalDpUnmaskLevelTriggeredInterrupts)();
  int (*HalDpGetInterruptReplayState)(void *, void **);
  int (*HalDpReplayInterrupts)(void *);
  unsigned __int8 (*HalQueryIoPortAccessSupported)();
  int (*KdSetupIntegratedDeviceForDebugging)(void *, DEBUG_DEVICE_DESCRIPTOR *);
  int (*KdReleaseIntegratedDeviceForDebugging)(DEBUG_DEVICE_DESCRIPTOR *);
  void (*HalGetEnlightenmentInformation)(HAL_INTEL_ENLIGHTENMENT_INFORMATION *);
  void *(*HalAllocateEarlyPages)(LOADER_PARAMETER_BLOCK *, unsigned int, unsigned __int64 *, unsigned int);
  void *(*HalMapEarlyPages)(unsigned __int64, unsigned int, unsigned int);
  void *Dummy1;
  void *Dummy2;
  void (*HalNotifyProcessorFreeze)(unsigned __int8, unsigned __int8);
  int (*HalPrepareProcessorForIdle)(unsigned int);
  void (*HalRegisterLogRoutine)(HAL_LOG_REGISTER_CONTEXT *);
  void (*HalResumeProcessorFromIdle)();
  void *Dummy;
  unsigned int (*HalVectorToIDTEntryEx)(unsigned int);
  int (*HalSecondaryInterruptQueryPrimaryInformation)(INTERRUPT_VECTOR_DATA *, unsigned int *);
  int (*HalMaskInterrupt)(unsigned int, unsigned int);
  int (*HalUnmaskInterrupt)(unsigned int, unsigned int);
  unsigned __int8 (*HalIsInterruptTypeSecondary)(unsigned int, unsigned int);
  int (*HalAllocateGsivForSecondaryInterrupt)(char *, unsigned __int16, unsigned int *);
  int (*HalAddInterruptRemapping)(unsigned int, unsigned int, PCI_BUSMASTER_DESCRIPTOR *, unsigned __int8, INTERRUPT_VECTOR_DATA *, unsigned int);
  void (*HalRemoveInterruptRemapping)(unsigned int, unsigned int, PCI_BUSMASTER_DESCRIPTOR *, unsigned __int8, INTERRUPT_VECTOR_DATA *, unsigned int);
  void (*HalSaveAndDisableHvEnlightenment)();
  void (*HalRestoreHvEnlightenment)();
  void (*HalFlushIoBuffersExternalCache)(MDL *, unsigned __int8);
  void (*HalFlushExternalCache)(unsigned __int8);
  int (*HalPciEarlyRestore)(_SYSTEM_POWER_STATE);
  int (*HalGetProcessorId)(unsigned int, unsigned int *, unsigned int *);
  int (*HalAllocatePmcCounterSet)(unsigned int, _KPROFILE_SOURCE *, unsigned int, struct _HAL_PMC_COUNTERS **);
  void (*HalCollectPmcCounters)(struct HAL_PMC_COUNTERS *, unsigned __int64 *);
  void (*HalFreePmcCounterSet)(struct HAL_PMC_COUNTERS *);
  int (*HalProcessorHalt)(unsigned int, void *, int (*)(void *));
  unsigned __int64 (*HalTimerQueryCycleCounter)(unsigned __int64 *);
  void *Dummy3;
  void (*HalPciMarkHiberPhase)();
  int (*HalQueryProcessorRestartEntryPoint)(LARGE_INTEGER *);
  int (*HalRequestInterrupt)(unsigned int);
  int (*HalEnumerateUnmaskedInterrupts)(unsigned __int8 (*)(void *, HAL_UNMASKED_INTERRUPT_INFORMATION *), void *, HAL_UNMASKED_INTERRUPT_INFORMATION *);
  void (*HalFlushAndInvalidatePageExternalCache)(LARGE_INTEGER);
  int (*KdEnumerateDebuggingDevices)(void *, DEBUG_DEVICE_DESCRIPTOR *, KD_CALLBACK_ACTION (*)(DEBUG_DEVICE_DESCRIPTOR *));
  void (*HalFlushIoRectangleExternalCache)(_MDL *, unsigned int, unsigned int, unsigned int, unsigned int, unsigned __int8);
  void (*HalPowerEarlyRestore)(unsigned int);
  int (*HalQueryCapsuleCapabilities)(void *, unsigned int, unsigned __int64 *, unsigned int *);
  int (*HalUpdateCapsule)(void *, unsigned int, LARGE_INTEGER);
  unsigned __int8 (*HalPciMultiStageResumeCapable)();
  void (*HalDmaFreeCrashDumpRegisters)(unsigned int);
  unsigned __int8 (*HalAcpiAoacCapable)();
  int (*HalInterruptSetDestination)(INTERRUPT_VECTOR_DATA *, GROUP_AFFINITY *, unsigned int *);
  void (*HalGetClockConfiguration)(HAL_CLOCK_TIMER_CONFIGURATION *);
  void (*HalClockTimerActivate)(unsigned __int8);
  void (*HalClockTimerInitialize)();
  void (*HalClockTimerStop)();
  int (*HalClockTimerArm)(_HAL_CLOCK_TIMER_MODE, unsigned __int64, unsigned __int64 *);
  unsigned __int8 (*HalTimerOnlyClockInterruptPending)();
  void *(*HalAcpiGetMultiNode)();
  void (*(*HalPowerSetRebootHandler)(void (*)(unsigned int, volatile int *)))(unsigned int, volatile int *);
  void (*HalIommuRegisterDispatchTable)(HAL_IOMMU_DISPATCH *);
  void (*HalTimerWatchdogStart)();
  void (*HalTimerWatchdogResetCountdown)();
  void (*HalTimerWatchdogStop)();
  unsigned __int8 (*HalTimerWatchdogGeneratedLastReset)();
  int (*HalTimerWatchdogTriggerSystemReset)(unsigned __int8);
  int (*HalInterruptVectorDataToGsiv)(INTERRUPT_VECTOR_DATA *, unsigned int *);
  int (*HalInterruptGetHighestPriorityInterrupt)(unsigned int *, unsigned __int8 *);
  int (*HalProcessorOn)(unsigned int);
  int (*HalProcessorOff)();
  int (*HalProcessorFreeze)();
  int (*HalDmaLinkDeviceObjectByToken)(unsigned __int64, DEVICE_OBJECT *);
  int (*HalDmaCheckAdapterToken)(unsigned __int64);
  void *Dummy4;
  int (*HalTimerConvertPerformanceCounterToAuxiliaryCounter)(unsigned __int64, unsigned __int64 *, unsigned __int64 *);
  int (*HalTimerConvertAuxiliaryCounterToPerformanceCounter)(unsigned __int64, unsigned __int64 *, unsigned __int64 *);
  int (*HalTimerQueryAuxiliaryCounterFrequency)(unsigned __int64 *);
  int (*HalConnectThermalInterrupt)(unsigned __int8 (*)(KINTERRUPT *, void *));
  unsigned __int8 (*HalIsEFIRuntimeActive)();
  unsigned __int8 (*HalTimerQueryAndResetRtcErrors)(unsigned __int8);
  void (*HalAcpiLateRestore)();
  int (*KdWatchdogDelayExpiration)(unsigned __int64 *);
  int (*HalGetProcessorStats)(HAL_PROCESSOR_STAT_TYPE, unsigned int, unsigned int, unsigned __int64 *);
  unsigned __int64 (*HalTimerWatchdogQueryDueTime)(unsigned __int8);
  int (*HalConnectSyntheticInterrupt)(unsigned __int8 (*)(KINTERRUPT *, void *));
  void (*HalPreprocessNmi)(unsigned int);
  int (*HalEnumerateEnvironmentVariablesWithFilter)(unsigned int, unsigned __int8 (*)(const _GUID *, const wchar_t *), void *, unsigned int *);
  int (*HalCaptureLastBranchRecordStack)(unsigned int, HAL_LBR_ENTRY *, unsigned int *);
  unsigned __int8 (*HalClearLastBranchRecordStack)();
  int (*HalConfigureLastBranchRecord)(unsigned int, unsigned int);
  unsigned __int8 (*HalGetLastBranchInformation)(unsigned int *, unsigned int *);
  void (*HalResumeLastBranchRecord)(unsigned __int8);
  int (*HalStartLastBranchRecord)(unsigned int, unsigned int *);
  int (*HalStopLastBranchRecord)(unsigned int);
  int (*HalIommuBlockDevice)(void *);
  int (*HalIommuUnblockDevice)(EXT_IOMMU_DEVICE_ID *, void **);
  int (*HalGetIommuInterface)(unsigned int, DMA_IOMMU_INTERFACE *);
  int (*HalRequestGenericErrorRecovery)(void *, unsigned int *);
  int (*HalTimerQueryHostPerformanceCounter)(unsigned __int64 *);
  int (*HalTopologyQueryProcessorRelationships)(unsigned int, unsigned int, unsigned __int8 *, unsigned __int8 *, unsigned __int8 *, unsigned int *, unsigned int *);
  void (*HalInitPlatformDebugTriggers)();
  void (*HalRunPlatformDebugTriggers)(unsigned __int8);
  void *(*HalTimerGetReferencePage)();
  int (*HalGetHiddenProcessorPowerInterface)(HIDDEN_PROCESSOR_POWER_INTERFACE *);
  unsigned int (*HalGetHiddenProcessorPackageId)(unsigned int);
  unsigned int (*HalGetHiddenPackageProcessorCount)(unsigned int);
  int (*HalGetHiddenProcessorApicIdByIndex)(unsigned int, unsigned int *);
  int (*HalRegisterHiddenProcessorIdleState)(unsigned int, unsigned __int64);
  void (*HalIommuReportIommuFault)(unsigned __int64, FAULT_INFORMATION *);
  unsigned __int8 (*HalIommuDmaRemappingCapable)(EXT_IOMMU_DEVICE_ID *, unsigned int *);
};

Target Discovery

Armed with the information and structure of HalPrivateDispatchTable, if we revisit the mechanisms that InfinityHook used we will see the original is patched, but there exists some functions of interest. All of these occur within EtwpLogKernelEvent. We can see three of which appear to be good targets.

if( /* etw init conditions */ )
{
  v33 = EtwpReserveTraceBuffer(v14, v15 + 0x10, &v59, &v55, a6);
  //
  // ... [etc]
  //
  goto LABEL_19;
}
if ( /* etw init conditions */  )
{
  v34 = EtwpReserveWithPebsIndex(v14, 0x524, v15, &v59, &v55, a6);
}
else
{
  //
  // ... [etc]
  //
  v34 = EtwpReserveWithPmcCounters(v14, a5, v15, &v59, &v55, a6);
}

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.

  1. Locate HalPrivateDispatchTable.
  2. Locate System Call Handler.
  3. Acquire the ETW Event Id for SYSCALL logging.
  4. Configure ETW session programmatically.
  5. Configure the appropriate event trace class data.
  6. Swap pointer in the HalPrivateDispatchTable.
  7. 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.

Authors

Leave a Reply