探索另一种兼容PatchGuard的Hook技术
概述
本文将介绍Nick Peterson珍爱的InfinityHook的一个有趣替代方案。这个替代方法由Aidan Khoury在Microsoft发布并随后修补EtwpGetCycleCount目标后发现,且未对原始作者进行任何致谢。该方法已从早期Windows 10到最新Windows 11 23h2进行测试。该Hook允许实现多种不同且有用的Hook。我们将重点讨论我认为最有趣的一个,用于反恶意软件/反作弊目的。
免责声明
任何尝试复制此方法的人都需要在其目标NTOS版本中定位适当的Hook/事件ID。本文不提供任何未来版本的Hook/事件ID值。本文中使用的值分别为0x0F33:0x00501802。
首次测试在Windows 10 1903(19H1)上进行;最新测试在Windows 11 23h2 22631.2506上进行。但早期Windows 10版本也已测试和验证。首次测试是在发现和概念验证开发时进行的。本文可能有部分内容较旧,我只对原始草案中的重构代码进行了更新。
Windows内核中的常见Hook点
Windows内核中最常见的Hook目标是.data指针,这些只是存储在全局表或自由浮动的函数指针。一个典型的例子是NtConvertBetweenAuxiliaryCounterAndPerformanceCounter函数。许多游戏黑客类型一直将其用作"隐蔽通信"的手段,它绝不是,但它仍然是一个受欢迎的选择,并且是规避PatchGuard对系统API保护的滥用示例。使用需要您修改HalPrivateDispatchTable中的函数指针,特别是指向xKdEnumerateDebuggingDevices的条目,然后从用户模式组件获取NTDLL中的例程,然后调用该函数。三个参数会传递到调用的HAL函数,意味着用户在使用和处理来自用户模式的信息方面具有灵活性。
1
2
3
4
5
6
|
// NtConvertBetweenAuxiliaryCounterAndPerformanceCounter 反编译摘录
//
HalpTimerConvertConvert = HalTimerConvertAuxiliaryCounterToPerformanceCounter[0];
if ( !ConvertAuxToPerf )
HalpTimerConvertConvert = HalTimerConvertPerformanceCounterToAuxiliaryCounter[0];
Result = (HalpTimerConvertConvert)(PerfCounterValue, &AuxCounter, v13);
|
上面的代码片段显示了一个指向HAL分发函数HalTimerConvertXxx的指针的初始化,具体取决于参数是否指示从辅助计数器转换为性能计数器。鉴于这些信息,有相当多的关注点在于什么可以被滥用以危害系统或实现完全控制而不触发内置的反篡改机制。因此对…的兴趣。
HalPrivateDispatchTable
HAL_PRIVATE_DISPATCH,通常称为HalPrivateDispatchTable,是Windows操作系统中的一个关键结构,特别是在其硬件抽象层(HAL)中。该表在Windows操作系统与其运行的硬件之间的交互中扮演关键角色,特别是对于未通过标准HAL接口公开的硬件特定功能。简单来说,它只是一个函数指针表,每个条目对应一个特定的硬件功能,根据Windows运行平台的需求定制。其中一些条目的存在取决于平台类型(移动设备/工作站/终端)和启动参数,并可能因Windows版本而异。如前面段落所述,HalPrivateDispatchTable的使用仅限于内核模式,由驱动程序或其他内核组件访问以执行低级硬件操作。
大多数操作范围从专门的硬件初始化到高级电源管理功能。这是一个对系统稳定性和安全性至关重要的数据结构;我们将深入了解其中一些功能的重要性,但如果您熟悉InfinityHook,我相信您可以猜到。
关于这个结构的一个不幸之处是文档通常很少,因为它涉及Windows内部的更复杂和更低级的方面。它主要引起有经验的Windows内核开发人员或那些在硬件级编程附近工作的人员的兴趣。这里提取的大部分细节是通过各种资源(参见参考资料部分)和相关组件的独立逆向工程完成的。HalPrivateDispatchTable的布局以长形式给出 below。
1
2
3
4
5
6
7
|
struct HAL_PRIVATE_DISPATCH
{
unsigned int Version;
BUS_HANDLER *(*HalHandlerForBus)(INTERFACE_TYPE, unsigned int);
// ... 大量函数指针成员 ...
unsigned __int8 (*HalIommuDmaRemappingCapable)(EXT_IOMMU_DEVICE_ID *, unsigned int *);
};
|
目标发现
有了HalPrivateDispatchTable的信息和结构,如果我们重新审视InfinityHook使用的机制,我们会看到原始版本已被修补,但存在一些有趣的函数。所有这些都发生在EtwpLogKernelEvent内部。我们可以看到其中三个似乎是良好的目标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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);
}
|
目前没有任何信息发布的是EtwpReserveWithPmcCounters。如果ETW记录器以正确的方式设置,此函数将执行,如果我们查看内部,我们会看到另一个适合修补的目标。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
signed __int64 __fastcall EtwpReserveWithPmcCounters(
WMI_LOGGER_CONTEXT *LoggerContext,
UINT16 HookId,
UINT64 AuxSize,
void *BufferHandle,
LARGE_INTEGER *TimeStamp,
UINT64 Flags)
{
// ... 变量声明 ...
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;
}
}
|
感兴趣的目标立即显而易见,HalPrivateDispatch->HalpCollectPmcCounters(…)。如果我们在实时系统上跟踪调用链,您会注意到类似这样的内容:
1
2
3
4
5
6
7
8
|
KernelBase.dll!SleepEx
|- ntdll.dll!NtDelayExecution
| |- ntoskrnl.exe!KiSystemServiceExitPico
| | |- ntoskrnl.exe!PerfInfoLogSysCallEntry
| | | |- ntoskrnl.exe!EtwTraceKernelEvent
| | | | |- ntoskrnl.exe!EtwpLogKernelEvent
| | | | | |- ntoskrnl.exe!EtwpReservePmcCounters
| | | | | | |- ntoskrnl.exe!HalpCollectPmcCounters ---> | 这些仅在ETW记录器适当配置时发生。
|
ETW配置将导致NT内核记录器具有类似这样的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
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 : *
|
这只是快速验证,以可视化我们可以以PG兼容的方式Hook SYSCALL(即它不会使机器出现bugcheck)。
DIY… 大部分
那么,我们如何利用这些信息?这相对简单,让我们列出步骤。
- 定位HalPrivateDispatchTable
- 定位系统调用处理程序
- 获取SYSCALL记录的ETW事件ID
- 以编程方式配置ETW会话
- 配置适当的事件跟踪类数据
- 交换HalPrivateDispatchTable中的指针
- 享受另一个PG兼容的Hook
定位HalPrivateDispatchTable
获取HalPrivateDispatchTable指针可以这样做:
1
2
3
4
5
|
UNICODE_STRING target = RTL_CONSTANT_STRING( L"HalPrivateDispatchTable" );
HalPrivateDispatchTable = reinterpret_cast< PHAL_PRIVATE_DISPATCH_TABLE >( MmGetSystemRoutineAddress( &target ) );
if ( !HalPrivateDispatchTable )
return STATUS_RESOURCE_UNAVAILABLE;
|
请参考上面的长形式结构获取HAL_PRIVATE_DISPATCH_TABLE的定义。
额外的ETW配置
为了使此功能正常工作,注意到除了构建跟踪属性结构并修改它以启用SYSCALL跟踪外,还必须通过ZwTraceControl配置各种跟踪控制。为此,您至少需要处理ZwTraceControl函数代码EtwStartLoggerCode、EtwStopLoggerCode和EtwUpdateLoggerCode。下面提供了概念验证的摘录。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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;
}
}
|
1
2
|
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 );
|
除此之外,我们还需要通过ZwSetSystemInformation配置一些跟踪信息块,使用SystemPerformanceTraceInformation类来设置计数器列表和分析信息,允许PMC收集例程正常工作。特别是EventTraceProfileCounterListInformation和EventTraceProfileEventListInformation。为了节省空间,代码已从本文中省略。
HalCollectPmcCounters Hook
下面给出的摘录是在SYSCALL跟踪期间系统范围内应用此Hook所需的全部内容。遍历堆栈,验证Hook ID/事件ID是否与目标事件匹配,如果匹配,则替换堆栈上包含目标系统例程地址的元素。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
void process_syscall(stack<64>& sp) {
const auto target_fn = reinterpret_cast<void**>(sp.at(9));
// 示例:将堆栈上NtQuerySystemInformation的返回地址替换为我们的hkd__nt_query_system_information,
// 允许我们以PG兼容的方式系统范围内Hook调用。
//
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)
{
// 调用原始函数以填充适当的数据结构;避免不必要的开销。
//
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兼容性
值得注意的是,这里提供的SYSCALL Hook信息相对简洁,如果您在目标Windows版本上测试,您会注意到只有ntoskrnl系统调用被捕获。要使用相同的Hook捕获win32k系统调用,需要进行一些修改。本文未涵盖这一点,但可以通过处理win32k.sys的导出、搜索__win32kstub,然后获取SYSCALL索引并将其添加到可以在运行时按地址/索引搜索的表中来实现。
由于我们需要此Hook的测试用例,我为那些有兴趣在一定程度上复制此内容的人提供了Hook及相关辅助函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
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) {
|