首页
论坛
专栏
课程

[原创] 剖析InfinityHook原理 掀起一场更激烈的攻与防恶战

2019-7-30 11:31 8756

[原创] 剖析InfinityHook原理 掀起一场更激烈的攻与防恶战

2019-7-30 11:31
8756

0x00 前言

Hook 是一种可以改变程序执行流程的技术,巧妙利用 Hook 技术可以实现很多实用的操作,如:监控、过滤、拦截、修改等。
但在现代操作系统中,出现了诸多限制,导致无法轻松的进行底层Hook。其中典型例子就是 Win10 的 PatchGuard。
现在,我将为大家讲解一个神奇的技术:InfinityHook。

0x01 什么是 InfinityHook

InfinityHook 是一个可以Hook各种系统调用、上下文切换、页面错误、DPC 等内核事件的技术。
它目前可以与 PatchGuard 同时运行,且比常规 Hook 技术具有更好隐蔽性。

InfinityHook 并不是其名意所谓的“无限 Hook”,实际意义是可以安全的对内核底层进行 Hook。
因为安全界中攻与防的对抗,产生了一种,谁先 Hook,谁 Hook 的最底层,谁最有优势的概念。
而攻与防之间互相抢夺 Hook 优势的现象,像极了一个无尽深渊,没有终点。
微软为了保护底层系统的安全,防止被第三方程序滥用,在一系列新版本操作系统中推出和升级了 PatchGuard。
而 InfinityHook 的出现打破了微软的这一保护,拥有更安全更底层的 Hook 优势。
随着时间的推移这项技术会广为人知,这意味着一场更激烈的攻防无限 Hook 战争即将开战。
故名“InfinityHook”。

想要更深入剖析 InfinityHook 的原理,就得先了解 Windows 的 Event Tracing 机制。

0x02 什么是 Event Tracing for Windows

Windows事件跟踪(ETW)是一种高效的内核级跟踪工具,可让您将内核或应用程序定义的事件记录到日志文件中。您可以实时或从日志文件中使用事件,并使用它们来调试应用程序或确定应用程序中发生性能问题的位置。
ETW 允许您动态启用或禁用事件跟踪,允许您在生产环境中执行详细跟踪,而无需重新启动计算机或应用程序。

事件跟踪 API 分为三个不同的组件:
  • 控制器(Controllers): 用于启动和停止事件跟踪会话并启用提供程序。
  • 提供者(Providers):用于提供事件。
  • 消费者(Consumers):用于消费事件。


0x02.1 控制器(Controllers)

控制器是定义日志文件的大小和位置、启动和停止事件跟踪会话、启用提供程序以便将事件记录到会话、管理缓冲池的大小以及获取会话的执行统计信息的应用程序。

0x02.2 提供者(Providers)

提供程序是包含事件跟踪工具的应用程序。提供程序注册后,控制器可以在提供程序中启用或禁用事件跟踪。提供者定义其启用或禁用的实现。通常,启用的提供程序会生成事件,而禁用的提供程序则不会。这使您可以向应用程序添加事件跟踪,而无需始终生成事件。
虽然 ETW 模型将控制器和提供程序分离为单独的应用程序,但应用程序可以包含这两个组件。 

0x02.3 消费者(Consumers)

消费者是选择一个或多个事件跟踪会话作为事件源的应用程序。消费者可以同时从多个事件跟踪会话中请求事件;系统按时间顺序提供事件。消费者可以接收存储在日志文件中的事件,也可以接收实时传递事件的会话。处理事件时,消费者可以指定开始和结束时间,并且仅传递在指定时间范围内发生的事件。

 
(Event Tracing 事件跟踪模型图)

我们可以在 “此电脑->管理->性能->数据收集器集->事件跟踪会话” 中找到系统中所有正在运行的事件跟踪会话。

(系统中所有正在运行的事件跟踪会话)

即将讲解的 InfinityHook 就将使用第一个事件跟踪会话“Circular Kernel Context Logger”(之后简称 CKCL)进行操作。


0x03 InfinityHook 原理剖析

首先我们要知道一点,InfinityHook 因为一些特殊的操作必须要在 Ring0 层才能实现。
而 ETW 模型的3个组件是可以在 Ring3 完成。所以我们可以参考微软开放的文档例子帮助分析。

找到微软开放的“配置和启动 NT 内核记录器会话”的代码:
https://docs.microsoft.com/en-us/windows/win32/etw/configuring-and-starting-the-nt-kernel-logger-session

我从网页截取了关键代码贴了上来:
EVENT_TRACE_PROPERTIES* pSessionProperties = NULL;

BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(KERNEL_LOGGER_NAME);
pSessionProperties = (EVENT_TRACE_PROPERTIES*) malloc(BufferSize);
ZeroMemory(pSessionProperties, BufferSize);

pSessionProperties->Wnode.BufferSize = BufferSize;
pSessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
pSessionProperties->Wnode.ClientContext = 1; //QPC clock resolution
pSessionProperties->Wnode.Guid = SystemTraceControlGuid; 
pSessionProperties->EnableFlags = EVENT_TRACE_FLAG_NETWORK_TCPIP;
pSessionProperties->LogFileMode = EVENT_TRACE_FILE_MODE_CIRCULAR;
pSessionProperties->MaximumFileSize = 5;  // 5 MB
pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
pSessionProperties->LogFileNameOffset = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(KERNEL_LOGGER_NAME); 
StringCbCopy((LPWSTR)((char*)pSessionProperties + pSessionProperties->LogFileNameOffset), sizeof(LOGFILE_PATH), LOGFILE_PATH);

status = StartTrace((PTRACEHANDLE)&SessionHandle, KERNEL_LOGGER_NAME, pSessionProperties);

可以不难发现,配置会话的关键结构体是 EVENT_TRACE_PROPERTIES,启动会话的关键 API 是 StartTrace。

EVENT_TRACE_PROPERTIES 结构体的定义:
typedef struct _EVENT_TRACE_PROPERTIES {
	WNODE_HEADER Wnode;
	//
	// data provided by caller
	ULONG BufferSize;                   // buffer size for logging (kbytes)
	ULONG MinimumBuffers;               // minimum to preallocate
	ULONG MaximumBuffers;               // maximum buffers allowed
	ULONG MaximumFileSize;              // maximum logfile size (in MBytes)
	ULONG LogFileMode;                  // sequential, circular
	ULONG FlushTimer;                   // buffer flush timer, in seconds
	ULONG EnableFlags;                  // trace enable flags
	union {
		LONG  AgeLimit;                 // unused
		LONG  FlushThreshold;           // Number of buffers to fill before flushing
	} DUMMYUNIONNAME;

	// data returned to caller
	ULONG NumberOfBuffers;              // no of buffers in use
	ULONG FreeBuffers;                  // no of buffers free
	ULONG EventsLost;                   // event records lost
	ULONG BuffersWritten;               // no of buffers written to file
	ULONG LogBuffersLost;               // no of logfile write failures
	ULONG RealTimeBuffersLost;          // no of rt delivery failures
	HANDLE LoggerThreadId;              // thread id of Logger
	ULONG LogFileNameOffset;            // Offset to LogFileName
	ULONG LoggerNameOffset;             // Offset to LoggerName
} EVENT_TRACE_PROPERTIES, *PEVENT_TRACE_PROPERTIES;

typedef struct _WNODE_HEADER
{
	ULONG BufferSize;        // Size of entire buffer inclusive of this ULONG
	ULONG ProviderId;    // Provider Id of driver returning this buffer
	union
	{
		ULONG64 HistoricalContext;  // Logger use
		struct
		{
			ULONG Version;           // Reserved
			ULONG Linkage;           // Linkage field reserved for WMI
		} DUMMYSTRUCTNAME;
	} DUMMYUNIONNAME;

	union
	{
		ULONG CountLost;         // Reserved
		HANDLE KernelHandle;     // Kernel handle for data block
		LARGE_INTEGER TimeStamp; // Timestamp as returned in units of 100ns
								 // since 1/1/1601
	} DUMMYUNIONNAME2;
	GUID Guid;                  // Guid for data block returned with results
	ULONG ClientContext;
	ULONG Flags;             // Flags, see below
} WNODE_HEADER, *PWNODE_HEADER;

StartTrace 函数的声明:
EXTERN_C
ULONG
WMIAPI
StartTraceA (
    _Out_ PTRACEHANDLE TraceHandle,
    _In_ LPCSTR InstanceName,
    _Inout_ PEVENT_TRACE_PROPERTIES Properties
    );

在这段代码中有一个需要关注的点:
BufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(KERNEL_LOGGER_NAME);
pSessionProperties = (EVENT_TRACE_PROPERTIES*) malloc(BufferSize);

分配的内存长度不仅仅只是结构体的大小,还包含了两段其他的长度。

看一下 msdn 对这个 API 的描述:
https://docs.microsoft.com/en-us/windows/win32/etw/starttrace

其中同样有一点值得关注:

This function copies the session name that you provide to the offset that the LoggerNameOffset member of Properties points to.

此函数将您提供的会话名称复制到属性的LoggerNameOffset成员指向的偏移量。

这段话说明我们拷贝到结构体之后的字符串还会在 StartTrace 内部被处理。

对在 Ring3 启动跟踪会话的信息了解的差不多了。但是问题来了,如何在 Ring0 层实现这些东西呢?
所以我们得上 IDA 分析一下这个函数的内部实现。


0x03.1 StartTrace 的内部实现

从 msdn 的描述里我们可以看到:

DLL

Sechost.dll on Windows 8.1 and Windows Server 2012 R2;

Advapi32.dll on Windows 8, Windows Server 2012, Windows 7, Windows Server 2008 R2, Windows Server 2008, Windows Vista and Windows XP

所以我们将 Advapi32.dll 文件用IDA打开,下载 dll 的符号文件让 IDA 自动分析。
分析完成后,转到 StartTraceA 函数,从函数头部开始看。


首先是对参数的合法性检查。


接着是对参数2的 InstanceName 字符串内容进行比较。
如果为“NT Kernel Logger”,则将 SystemTraceControlGuid 的 Guid 赋值给 Properties->Wnode.Guid。
如果为“Circular Kernel Context Logger”, 则将 CKCLGuid 的 Guid 赋值给 Properties->Wnode.Guid。


我们看下这个 CKCLGuid 的内容,之后会用到。


首先判断“Properties->LogFileNameOffset”如果小于等于0,则跳到 LABEL_67 标签。
在图片底部可以看到 LABEL_67 标签置零了2个变量,但并没有返回退出。
说明这个函数允许 LogFileNameOffset 的值为0。

同样,我们在 msdn 查阅一下这个结构体:
https://docs.microsoft.com/en-us/windows/win32/etw/event-trace-properties

找到对这个成员的描述,截取关键内容下来:

LogFileNameOffset


Offset from the start of the structure's allocated memory to beginning of the null-terminated string that contains the log file name.

从结构的已分配内存开始到包含日志文件名的以空值终止的字符串的开头的偏移量。


If you do not want to log events to a log file (for example, you specify EVENT_TRACE_REAL_TIME_MODE only), set LogFileNameOffset to 0. If you specify only real-time logging and also provide an offset with a valid log file name, ETW will use the log file name to create a sequential log file and log events to the log file. ETW also creates the sequential log file if LogFileMode is 0 and you provide an offset with a valid log file name.

如果您不想将事件记录到日志文件中(例如,仅指定EVENT_TRACE_REAL_TIME_MODE),请将LogFileNameOffset设置为0.如果仅指定实时日志记录并提供具有有效日志文件名的偏移量,则ETW将使用日志文件名,用于创建顺序日志文件并将事件记录到日志文件中。如果LogFileMode为0,ETW还会创建顺序日志文件,并提供带有效日志文件名的偏移量。 

文章头部对 ETW 的介绍中写到:

您可以实时或从日志文件中使用事件,并使用它们来调试应用程序或确定应用程序中发生性能问题的位置。

我们可以得知, 日志文件是用于调试应用程序或定位性能问题位置的。而 InfinityHook 并不需要这些东西。
所以 LogFileNameOffset 成员可以直接置0,跳过这段继续往下分析。


接着是一些参数标志合法性检查,这些合法性检查由 Etwp 开头的 API 完成。
假定我们传递的所有参数都是正确的,所以无需跟进这些 API 进行分析。


从堆中分配一块内存空间,后面要用到。


将我们传递进来的 Properties 参数填进v27变量内。
接着我们可以看到几个字符串操作 API 。还记得刚刚在 msdn 提到的关注点吗?应该就是这里了。
第一个字符串的处理是将我们传递进来的 InstanceName 参数初始化为一个 ANSI_STRING 字符串。
再将其转换为一个 UNICODE_STRING 字符串。
转换完的 UNICODE_STRING 字符串存放在 (PUNICODE_STRING)v27 + 9 的位置。
UNICODE_STRING 结构体的大小为16,故步长为16。实际存放地址是在 v27 + 144 。
再看下 EVENT_TRACE_PROPERTIES 结构体的大小为120。
说明这个 UNICODE_STRING 字符串存放在 v27 + sizeof(EVENT_TRACE_PROPERTIES) + 24 的位置。


处理完第一个字符串后有一个判断,如果 v46 等于0,则跳到 LABEL_69 标签。
而这个 v46 就是刚刚判断 LogFileNameOffset 如果等于0所置零的变量。
所以这里可以跳过第二个字符串的处理,直接往下看。


看到了两个 Etwp 开头的API,我们先跟进第一个 API 看看。



不难看出,这个 API 对 Property 的内容影响并不大,只有两处,74和72偏移。
对比结构体定义发现,是在对 EnableFlags 成员填值。
这个成员放到后面再讲,所以这里先暂时跳过此 API。


接着来看 EtwpStartLogger 这个API。
可以很直观的看到,检查了一些成员后调用了 NtTraceControl 这个 API。
这个就是我们在 Ring0 层启动事件跟踪会话的API了。

在这 API 之下是一些与结构体内容无关的操作。所以直接跳过,返回上层继续看。


将堆内存的 Property 内容,置回到参数 Property 中。然后跳到 LABEL_58 标签。


释放堆内存,返回成功与否。

至此,StartTraceA 这个 API 逆向完成。
我们知道了部分参数的填写规则和关键 API。

下一步我们开始写 InfinityHook 的实现代码。

0x03.2 InfinityHook 的实现

首先,从刚刚分析出来的信息:
UNICODE_STRING 类型的 InstanceName 存放在 Property + sizeof(EVENT_TRACE_PROPERTIES) + 24 的位置。
所以我们定义一个名为 CKCL_TRACE_PROPERTIES 的结构体,这个结构体继承于 EVENT_TRACE_PROPERTIES。
struct CKCL_TRACE_PROPERTIES : EVENT_TRACE_PROPERTIES
{
	CHAR Padding[24];
	UNICODE_STRING InstanceName;
};

然后定义一个变量为其申请一块内存空间,并对其置零。
为了保险起见,以防底层 API 还会对结构体更后面的内存进行操作,所以申请的大小填 PAGE_SIZE。
	CKCL_TRACE_PROPERTIES *pProperty = (CKCL_TRACE_PROPERTIES *)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, ALLOC_TAG);
	if (pProperty == NULL) {
		KeBugCheckEx(HAL_MEMORY_ALLOCATION, PAGE_SIZE, 0, NULL, 0);
		return STATUS_MEMORY_NOT_ALLOCATED;
	}
	RtlZeroMemory(pProperty, PAGE_SIZE);

然后初始化一个变量名为 InstanceName 的 UNICODE_STRING 字符串。
	UNICODE_STRING InstanceName;
	RtlInitUnicodeString(&InstanceName, L"Circular Kernel Context Logger");

接着我们开始填写 pProperty 内容。

pProperty->Wnode.BufferSize:填写我们为其申请内存的大小 PAGE_SIZE。
pProperty->Wnode.ClientContext:
根据 msdn 的描述,此成员为时间戳的分辨率。可以为3个值:
  • 1:Query performance counter (QPC). QPC 计数器提供高分辨率时间戳,不受系统时钟调整的影响。存储在事件中的时间戳等同于QueryPerformanceCounter API 返回的值。
  • 2:System time(系统时间).
  • 3:CPU cycle counter(CPU 循环计数器). CPU 计数器提供最高分辨率的时间戳,并且是检索资源最少的。但是,CPU 计数器不可靠,不应在生产中使用。例如,在某些计算机上,除了在某些状态下停止之外,定时器还会因热量和功率变化而改变频率。
我们这里填写为3值(CPU 循环计数器),因为它的实现是通过一条汇编指令实现的,后面会讲。
pProperty->Wnode.Flags:根据 msdn 的描述,必须包含 WNODE_FLAG_TRACED_GUID 以指示该结构包含事件跟踪信息。
pProperty->Wnode.Guid:
从上面的 IDA 中取出来改为代码则是:
// 54dea73a-ed1f-42a4-af713e63d056f174
const GUID CkclSessionGuid = { 0x54dea73a, 0xed1f, 0x42a4, { 0xaf, 0x71, 0x3e, 0x63, 0xd0, 0x56, 0xf1, 0x74 } };

pProperty->BufferSize:每个事件的缓冲区,直接填4字节即可,因为我们不需要实际接收事件内容。
pProperty->LogFileMode:上面说过,我们不需要 ETW 写入日志文件,所以设置为 EVENT_TRACE_BUFFERING_MODE。
pProperty->MinimumBuffers:为事件跟踪会话的缓冲池分配的最小缓冲区数,最小为2,直接填2,。
pProperty->MaximumBuffers:
这个成员 msdn 描述的有点矛盾。
既说必须大于等于 MinimumBuffers,又说 LogFileMode 设置了 EVENT_TRACE_BUFFERING_MODE 可以不用设置此值。
虽然应该可以忽略这个成员,但这里为了保险起见,我们设置和 MinimumBuffers 一样的值。
pProperty->InstanceName:这个为我们刚刚扩充的成员,填写初始化的 UNICODE_STRING 字符串 InstanceName 即可。

所以Property的成员填写代码是这样的。
	pProperty->Wnode.BufferSize = PAGE_SIZE;
	pProperty->Wnode.ClientContext = 3;
	pProperty->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	pProperty->Wnode.Guid = CkclSessionGuid;
	pProperty->BufferSize = sizeof(ULONG);
	pProperty->LogFileMode = EVENT_TRACE_BUFFERING_MODE;
	pProperty->MinimumBuffers = pProperty->MaximumBuffers = 2;
	pProperty->InstanceName = InstanceName;


然后就是控制 CKCL 事件跟踪会话。

NtTraceControl,msdn 并没有给出他的声明。结合谷歌搜索、GitHub 项目和 IDA 分析,可以得到的声明为:
EXTERN_C
NTSYSCALLAPI
NTSTATUS
NTAPI
NtTraceControl(
	_In_ ULONG FunctionCode,
	_In_reads_bytes_opt_(InBufferLen) PVOID InBuffer,
	_In_ ULONG InBufferLen,
	_Out_writes_bytes_opt_(OutBufferLen) PVOID OutBuffer,
	_In_ ULONG OutBufferLen,
	_Out_ PULONG ReturnLength
);

其中参数一可以是5个常量中的任意一个:
#define EtwpStartTrace		1
#define EtwpStopTrace		2
#define EtwpQueryTrace		3
#define EtwpUpdateTrace		4
#define EtwpFlushTrace		5

这5个常量在此 API 内部对应5个 Etwp 开头的 API,分别代表:启动跟踪、停止跟踪、查询跟踪、更新跟踪、刷新跟踪。
InfinityHook 只需要:启动、停止和更新。
// 启动
NtTraceControl(EtwpStartTrace, pProperty, PAGE_SIZE, pProperty, PAGE_SIZE, &ReturnLength);
// 停止
NtTraceControl(EtwpStopTrace, pProperty, PAGE_SIZE, pProperty, PAGE_SIZE, &ReturnLength);
// 更新
NtTraceControl(EtwpUpdateTrace, pProperty, PAGE_SIZE, pProperty, PAGE_SIZE, &ReturnLength);

值得注意的是,我们需要在更新跟踪的时候,设置 pProperty->EnableFlags 标志,以此来过滤我们想要的事件。
本文将以 Hook Syscall 中的 NtOpenProcess 作为例子进行讲解。
所以这里设置 EVENT_TRACE_FLAG_SYSTEMCALL 标志,以拦截到所有的 Syscall 事件。

我们将以上内容封装为一个函数,所以整个函数的代码是这样的:
NTSTATUS EventTraceControl(CKCL_TRACE_OPERATION Operation)
{
	NTSTATUS Status = STATUS_UNSUCCESSFUL;
	ULONG ReturnLength = 0;

	CKCL_TRACE_PROPERTIES *pProperty = (CKCL_TRACE_PROPERTIES *)ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, ALLOC_TAG);
	if (pProperty == NULL) {
		KeBugCheckEx(HAL_MEMORY_ALLOCATION, PAGE_SIZE, 0, NULL, 0);
		return STATUS_MEMORY_NOT_ALLOCATED;
	}
	RtlZeroMemory(pProperty, PAGE_SIZE);

	UNICODE_STRING InstanceName;
	RtlInitUnicodeString(&InstanceName, L"Circular Kernel Context Logger");

	pProperty->Wnode.BufferSize = PAGE_SIZE;
	pProperty->Wnode.ClientContext = 3;
	pProperty->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	pProperty->Wnode.Guid = CkclSessionGuid;
	pProperty->BufferSize = sizeof(ULONG);
	pProperty->LogFileMode = EVENT_TRACE_BUFFERING_MODE;
	pProperty->MinimumBuffers = pProperty->MaximumBuffers = 2;
	pProperty->InstanceName = InstanceName;

	switch (Operation)
	{
	case CKCL_TRACE_START:
		Status = NtTraceControl(EtwpStartTrace, pProperty, PAGE_SIZE, pProperty, PAGE_SIZE, &ReturnLength);
		break;
	case CKCL_TRACE_END:
		Status = NtTraceControl(EtwpStopTrace, pProperty, PAGE_SIZE, pProperty, PAGE_SIZE, &ReturnLength);
		break;
	case CKCL_TRACE_SYSCALL:
		// 这里添加更多标志可以捕获更多事件
		pProperty->EnableFlags = EVENT_TRACE_FLAG_SYSTEMCALL;
		Status = NtTraceControl(EtwpUpdateTrace, pProperty, PAGE_SIZE, pProperty, PAGE_SIZE, &ReturnLength);
		break;
	default:
		Status = STATUS_UNSUCCESSFUL;
		break;
	}

	ExFreePool(pProperty);
	return Status;
}

接着调用这个函数,启动并更新 CKCL 事件跟踪对象:
NTSTATUS StartCkclEventTrace()
{
	NTSTATUS Status = STATUS_UNSUCCESSFUL;

	// 测试 CKCL 会话是否已经启动
	Status = EventTraceControl(CKCL_TRACE_SYSCALL);
	if (!NT_SUCCESS(Status)) {
		// 没有启动 尝试打开
		Status = EventTraceControl(CKCL_TRACE_START);
		if (!NT_SUCCESS(Status)) {
			LOG_ERROR("Start CKCL failed.", Status);
			return Status;
		}

		Status = EventTraceControl(CKCL_TRACE_SYSCALL);
		if (!NT_SUCCESS(Status)) {
			LOG_ERROR("Start CKCL failed.", Status);
			return Status;
		}
	}

	LOG_INFO("CKCL is running", 0);

	return Status;
}

至此,CKCL 已成功启动和更新。


下面,我们需要找到一些内核全局地址,来方便我们进行之后的操作。

EtwpDebuggerData,是一张存放所有 ETW 信息的表。
通过IDA搜索可以发现有这个变量符号,但是查看交叉引用却发现IDA并没有分析出哪里使用了这个变量。
所以只能通过暴搜的方法去找到这个变量的地址。

通过多个系统版本的内核文件进行 IDA 分析,可以发现两条信息。

  1. 根据相同的数值,得出 EtwpDebuggerData 的特征码是“?? ?? 2c 08 04 38 0c”。
  2. 不同系统上的 EtwpDebuggerData 可能存在于不同的区段。

那我们首先取到内核基地址,解析PE头找到“.data”和".rdata"区段的起始地址和区段大小,然后根据特征码进行暴搜。
	ULONG KernelSize = 0;
	PVOID KernelBase = System::GetKernelBase(&KernelSize);
	if (KernelBase == NULL) {
		LOG_ERROR("Get kernel base failed.", 0);
		return FALSE;
	}

	const auto fnSearchInSection = [&](CHAR *SectionName, CHAR *Pattern, CHAR *Masks)->PVOID
	{
		ULONG SizeOfSection = 0;
		PVOID SectionBase = Image::GetSection(KernelBase, SectionName, &SizeOfSection);
		if (SectionBase == NULL) {
			return NULL;
		}

		return Utils::FindPattern(SectionBase, SizeOfSection, Pattern, Masks);
	};

	PVOID EtwpDebuggerData = fnSearchInSection(".data", "\x00\x00\x2c\x08\x04\x38\x0c", "??xxxxx");
	if (EtwpDebuggerData == NULL) {
		EtwpDebuggerData = fnSearchInSection(".rdata", "\x00\x00\x2c\x08\x04\x38\x0c", "??xxxxx");
		if (EtwpDebuggerData == NULL) {
			return FALSE;
		}
	}

接着在 EtwpDebuggerData + 0x10 处取到 EtwpDebuggerDataSilo 指针。
再从 EtwpDebuggerDataSilo + 0x10 的位置取到 CkclWmiLoggerContext 指针。
这两个数据的硬编码偏移在所有系统上都一样,并没有发生改变。
EtwpDebuggerDataSilo 和 CkclWmiLoggerContext,是直接参考的 GitHub 上的项目,
因为在IDA中找了很久都没有找到关于 EtwpDebuggerDataSilo 的信息,
仅仅找到了一个可能是 CkclWmiLoggerContext 的指针,是动态分配的,
鄙人逆向能力有限,实在没能跟到 EtwpDebuggerDataSilo 在哪,
如果有大佬研究出来了,欢迎在评论区分享,谢谢!
	PVOID *EtwpDebuggerDataSilo = *(PVOID**)((ULONG_PTR)EtwpDebuggerData + 0x10);
	if (EtwpDebuggerDataSilo == NULL) {
		LOG_ERROR("EtwpDebuggerDataSilo is bad.", EtwpDebuggerDataSilo);
		return FALSE;
	}

	g.CkclWmiLoggerContext = EtwpDebuggerDataSilo[2];
	if (g.CkclWmiLoggerContext == NULL) {
		LOG_ERROR("CkclWmiLoggerContext is bad.", EtwpDebuggerDataSilo);
		return FALSE;
	}


接着我们需要取到 SyscallEntry 的页面基址。
直接从 msr 中可以取出 SyscallEntry。
__readmsr(IA32_LSTAR_MSR)

但是有一个问题,还记得2018年的内核页表隔离补丁吗。
不记得没关系,大表哥之前分析过的帖子:https://bbs.pediy.com/thread-223805.htm

简单来说,在打了内核页表隔离补丁的系统上,取到的只是一个影子入口,通过影子入口内的过渡代码,最后走到真正的入口。
而 CKCL 事件是在真实入口中触发的,只拿到影子入口并不能完成之后的操作。
影子入口和过渡代码都存在于内核模块的 KVASCODE 区段内,该区段是否存在取决于有没有打内核页表隔离补丁。
如果区段存在,我们则取到这个区段的入口地址和区段大小。
再对比我们从 msr 中得到的 SyscallEntry 是否在此区段内,则可以判定内核页表隔离有没有被开启。
	/*
		通过 KVASCODE 节表是否存在,判断有没有打补丁
		也可以通过 NtQuerySystemInformation 来查询判断
	*/
	ULONG SectionSize = 0;
	PVOID SectionBase = Image::GetSection(KernelBase, "KVASCODE", &SectionSize);
	if (SectionBase == NULL) {
		return SyscallEntry;
	}

	// 判断 SyscallEntry 是否在 KVASCODE 节表内,如果不在则是真实入口直接返回
	if (!(SyscallEntry >= SectionBase && SyscallEntry < (PVOID)((ULONG_PTR)SectionBase + SectionSize))) {
		return SyscallEntry;
	}

这是没打补丁通过 msr 取到的 SyscallEntry,函数名是 KiSystemCall64。


这是打了 KB4056892 补丁后通过 msr 取到的 SyscallEntry,可以看到函数名是 KiSystemCall64Shadow。


在函数尾部有一个 jmp,跳到了真正的入口 KiSystemServiceUser,而这个真正的入口一定是在 KVASCODE 区段外。


所以我们利用 HDE 反汇编引擎,对影子入口的每条指令进行解析,找到一条跳出 KVASCODE 区段的 jmp 指令。
	hde64s Hde;
	for (PVOID ShadowPagePtr = SyscallEntry; ; ShadowPagePtr = (PVOID)((ULONG_PTR)ShadowPagePtr + Hde.len))
	{
		// 解析每条汇编指令,找到第一个 jmp(e9) 出 KVASCODE 区段的指令
		if (!hde64_disasm(ShadowPagePtr, &Hde)) {
			break;
		}

		if (Hde.opcode != 0xE9) {
			continue;
		}

		// 忽略 jmp 目标为 KVASCODE 区域内的指令
		PVOID KiSystemServiceUser = (PVOID)((ULONG_PTR)ShadowPagePtr + (INT)Hde.len + (INT)Hde.imm.imm32);
		if (KiSystemServiceUser >= SectionBase && KiSystemServiceUser < (PVOID)((ULONG_PTR)SectionBase + SectionSize)) {
			continue;
		}

		// 找到 KiSystemServiceUser
		SyscallEntry = KiSystemServiceUser;
		break;
	}

至此,定位到真实 SyscallEntry 函数地址。


再回过头来看看我们刚刚找的 CkclWmiLoggerContext。
这个变量的结构体定义是 WMI_LOGGER_CONTEXT。
通过 Windbg 输出这个结构体在 Win10 1709 上的定义:
0: 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      : Ptr64     int64 
   +0x030 LoggerThread     : Ptr64 _ETHREAD
   +0x038 LoggerStatus     : Int4B
   +0x03c FailureReason    : Uint4B
   +0x040 BufferQueue      : _ETW_BUFFER_QUEUE
   +0x050 OverflowQueue    : _ETW_BUFFER_QUEUE
   +0x060 GlobalList       : _LIST_ENTRY
   +0x070 DebugIdTrackingList : _LIST_ENTRY
   +0x080 DecodeControlList : Ptr64 _ETW_DECODE_CONTROL_ENTRY
   +0x088 DecodeControlCount : Uint4B
   +0x090 BatchedBufferList : Ptr64 _WMI_BUFFER_HEADER
   +0x090 CurrentBuffer    : _EX_FAST_REF
   +0x098 LoggerName       : _UNICODE_STRING
   +0x0a8 LogFileName      : _UNICODE_STRING
   +0x0b8 LogFilePattern   : _UNICODE_STRING
   +0x0c8 NewLogFileName   : _UNICODE_STRING
   +0x0d8 ClockType        : Uint4B
   +0x0dc LastFlushedBuffer : Uint4B
   +0x0e0 FlushTimer       : Uint4B
   +0x0e4 FlushThreshold   : Uint4B
   +0x0e8 ByteOffset       : _LARGE_INTEGER
   +0x0f0 MinimumBuffers   : Uint4B
   +0x0f4 BuffersAvailable : Int4B
   +0x0f8 NumberOfBuffers  : Int4B
   +0x0fc MaximumBuffers   : Uint4B
   +0x100 EventsLost       : Uint4B
   ......
   +0x980 BufferCompressDuration : _LARGE_INTEGER

在 WMI_LOGGER_CONTEXT + 0x28 的位置 GetCpuClock 是一个函数指针。

这个函数将在每次触发 CKCL 事件跟踪时会被调用。

换言之就是每次发生 Syscall 时会触发 CKCL 事件,从而调用到 GetCpuClock。


GetCpuClock 的函数指针指向就是根据我们开启事件跟踪填写的 pProperty->Wnode.ClientContext 来赋值的。
ClientContext 的:1、2、3,分别对应给GetCpuClock赋值:PpmQueryTime、EtwpGetSystemTime、EtwpGetCycleCount。
我们之前填的3,所以这个指针应该指向 EtwpGetCycleCount。

通过IDA看一下 EtwpGetCycleCount 的实现:
unsigned __int64 EtwpGetCycleCount()
{
  return __rdtsc();
}

我们在这里将这个指针替换为我们自己的函数。
PVOID ReplaceGetCpuClock(PVOID TargetAddr)
{
	PVOID *pEtwpGetCycleCount = (PVOID*)((ULONG_PTR)g.CkclWmiLoggerContext + 0x28);
	PVOID Result = *pEtwpGetCycleCount;
	*pEtwpGetCycleCount = TargetAddr;
	return Result;
}

ReplaceGetCpuClock(FakeGetCpuClock);

在 FakeGetCpuClock 函数内,先判断当前 PreviousMode 是否为 KernelMode。
当为 KernelMode 时,我们直接返回 rdtsc() ,防止意外重入。
ULONG64 FakeGetCpuClock()
{
	if (ExGetPreviousMode() == KernelMode) {
		return __rdtsc();
	}

	// ......

	return __rdtsc();
}

接着我们需要取到栈顶和栈帧,往上遍历堆栈。


在gs段寄存器中同样存放着栈顶,在其偏移量 0x1a8 的位置。

通过内联函数获取 __readgsqword 进行取值。

	PVOID *StackBase = (PVOID*)__readgsqword(0x1A8);
	PVOID *StackFrame = (PVOID*)_AddressOfReturnAddress();

虽然这里可以通过 KeQueryCurrentStackInformation,进行获取。

但还记得我们文章开头说的话吗,如果被别人 Hook 了这个 API 岂不是只能甘拜下风了?

所以我们需要尽可能的少用及不用 API。


判断两个只有 Syscall 调用才会产生的标志,和之间的指针是否有在 SyscallEntry 函数范围内,以此来确定这是否是一个 Syscall 调用事件。
	for (PVOID *StackCurrent = StackBase; StackCurrent > StackFrame; StackCurrent--)
	{
		// 检查Syscall特有标志
		if (*(ULONG*)StackCurrent != (ULONG)0x501802 || *(USHORT*)(StackCurrent - 1) != (USHORT)0xF33) {
			continue;
		}

		// 往回遍历
		for (StackCurrent--; StackCurrent < StackBase; ++StackCurrent)
		{
			PVOID CurrentPage = PAGE_ALIGN(*StackCurrent);

			// 粗略用2个页的大小判断一下是否是Syscall调用
			if (CurrentPage < g.SystemCallEntryPage || 
				CurrentPage >= (PVOID)((ULONG_PTR)g.SystemCallEntryPage + PAGE_SIZE * 2)) {
				continue;
			}

			// 到这里基本可以确定为Syscall事件了
		}
	}

此次事件的 Syscall 目标指针则相对存放在当前栈的 rsp + 72h 处,我们调用另一个函数对此 Syscall 目标指针进行派发修改。
PVOID *SyscallTarget = &StackCurrent[9];
SyscallDispatch(SyscallTarget);

我们先在驱动入口函数中中取到 NtOpenProcess 的地址保存起来。
	g.OriginalNtOpenProcess = (fn_NtOpenProcess)Utils::GetRoutineAddress(L"NtOpenProcess");
	if (g.OriginalNtOpenProcess == NULL) {
		return STATUS_UNSUCCESSFUL;
	}

再与这里的 SyscallTarget 做对比,如果地址相同则修改到我们自己的 FakeNtOpenProcess。
void SyscallDispatch(PVOID *SyscallTarget)
{
	if (*SyscallTarget == g.OriginalNtOpenProcess) {
		*SyscallTarget = FakeNtOpenProcess;
	}
}

然后在 FakeNtOpenProcess 中做相应的处理。
NTSTATUS FakeNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId)
{
	if (ClientId->UniqueProcess == (HANDLE)123) {
		LOG_INFO("Target process is being opened.", 0);
		return STATUS_ACCESS_DENIED;
	}

	return g.OriginalNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);
}

接着在驱动卸载例程中调用我们封装的 EventTraceControl 函数关闭或重启 CKCL。
因为基本在所有系统上,CKCL 事件跟踪会话是默认开启的,所以我们尽量重启它,而不是关闭。
重启之后其所有信息都会恢复默认,就不需要还原对 GetCpuClock 指针的修改了。
	if (NT_SUCCESS(EventTraceControl(CKCL_TRACE_END))) {
		EventTraceControl(CKCL_TRACE_START);
	}

至此,驱动层的代码已写完。编译驱动,在虚拟机中测试看下效果。



再看下 Windbg 的调试输出信息



0x03.2 InfinityHook 的执行流程图

(InfinityHook 驱动层执行流程图)


(InfinityHook 钩子执行流程图)

0x04 总结

相关参考资料链接:

https://github.com/everdox/InfinityHook

https://docs.microsoft.com/en-us/windows/win32/etw/about-event-tracing

https://docs.microsoft.com/en-us/windows/win32/etw/configuring-and-starting-the-nt-kernel-logger-session

https://bbs.pediy.com/thread-223805.htm

InfinityHook 说到底也算是 PatchGuard 的一个疏忽,不出意外的话在不久的将来应该会被微软修复。
但至少这短暂的时间也够看一场戏了。

毕竟攻防无绝对,技术无黑白。
没有攻与防之间的战斗,哪里来绝对的安全呢?

如果发现文章内容有任何不正之处,欢迎评论区指出。
感谢阅读!


[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-11-8 03:33 被Sprite雪碧编辑 ,原因:
最新回复 (42)
xiaofu 8 2019-7-30 11:39
2
0
nice~支持
nevinhappy 2 2019-7-30 11:41
3
0
好文,学习!
pxhb 2 2019-7-30 12:01
4
0
 毕竟攻防无绝对,技术无黑白。
没有攻与防之间的战斗,哪里来绝对的安全
whathhh 2019-7-30 13:50
5
0
666666
刘铠文 2019-7-30 15:17
6
0
火钳刘明
黑洛 1 2019-7-30 16:02
7
0
19xx可能来不及了,下一个版本有望被patchguard修复。如果影响力不大的话可能会放过(怎么可能)
Justgoon 1 2019-7-30 17:09
8
0
mark
万剑归宗 1 2019-7-30 17:20
9
0
lei了lei了
wmslecz 2019-7-30 18:05
10
0
无敌啊。
HadesW 1 2019-7-30 19:49
11
0
发出来了,可以的,先回复一下,之后再看
实都 2019-7-30 20:48
12
0
既然发出来了  就等着被修复吧
HadesW 1 2019-7-31 00:12
13
0
等大佬们搞其他事件的Hook
malokch 2 2019-7-31 00:39
14
0
类似的技术我已经用很久了,终于有人发出来了。这套技术还可以用于注入代码到进程运行。
如斯咩咩咩 2019-7-31 02:22
15
0
lei了lei了,mark
BkHumor 2019-7-31 05:13
16
0
friendanx 2019-7-31 07:35
17
0
分析很到位,赞
mlgbwoai 1 2019-7-31 07:50
18
0
有毒 2 2019-7-31 10:40
19
0
这个流批!讲的很详细
囧囧 2019-7-31 12:00
20
0
牛逼666
miWusn 2019-7-31 12:29
21
0
牛逼
EvilKnight 4 2019-7-31 12:52
22
0
nice,支持,思路不错!
shuyangzjg 2019-7-31 13:58
23
0
牛逼
coolboyme 2 2019-7-31 15:19
24
1
这个bug的产生应该是部门之间的协作出现了问题:PatchGuard 不让hook,改检查的地方都检查了,内核数据结构,代码段,做好了放下了。现在ETW要上,要实现功能,还要灵活配置,就留下了这样的接口,然而并没有考虑到是否充当了保护罩让hook通过了PatchGuard 检查。或者想到了,没有通知PatchGuard 。或者通知了PatchGuard ,却无力推动PatchGuard 修复。我所在的一个2000人开发的公司都推不动,何况微软呢。
coolboyme 2 2019-7-31 15:24
25
1
这也给挖洞提供了一个思路。杀软不让病毒执行却为了管理方便,留了一个后门给自己自动更新等等
小艾 1 2019-7-31 20:51
26
0
牛批
niuzuoquan 2019-8-1 12:06
27
0
mark
ncsi 2019-8-1 23:01
28
0
好文,学习!
ugvjewxf 2019-8-2 14:40
29
0
好东西,还能这么操作,
柒小苦瓜 2019-8-2 16:33
30
0
mark
Sprite雪碧 1 2019-8-4 14:46
31
0

山某人又来做秀了
最后于 2019-8-5 12:38 被Sprite雪碧编辑 ,原因:
5ilent 2019-8-5 21:31
32
0
感谢分享,不久就要被补了,
appview 2019-8-6 19:09
33
0
牛逼了
Sprite雪碧 1 2019-8-7 01:10
34
0
malokch 类似的技术我已经用很久了,终于有人发出来了。这套技术还可以用于注入代码到进程运行。
坐等大佬分享 嘻嘻
木志本柯 2019-8-7 03:37
35
0
骚操作。。。。。。。。。。
X-Blades 2019-9-3 19:54
36
0
牛皮
cralsen 2019-9-4 18:46
37
0
malokch 类似的技术我已经用很久了,终于有人发出来了。这套技术还可以用于注入代码到进程运行。
如何做到呢?能否提示一二
mb_ddciclgc 2019-9-26 13:07
38
1
都说前人栽树,后人乘凉。现在是前人栽树,乘完凉后把树砍了,后人只能对着树桩发呆。
killpy 2 2019-10-7 17:50
39
0
mb_ddciclgc 都说前人栽树,后人乘凉。现在是前人栽树,乘完凉后把树砍了,后人只能对着树桩发呆。
现在是 栽树的人 统一被物业公司管理 能进入小区住的 可以乘凉 否则你连看都看不到
killpy 2 2019-10-7 18:53
40
0
mb_ddciclgc 都说前人栽树,后人乘凉。现在是前人栽树,乘完凉后把树砍了,后人只能对着树桩发呆。
我就想知道 如何一劳永逸 禁止 ETW
莫灰灰 8 2019-10-11 19:06
41
0
好详细,学习了~
xuanzee 2019-10-15 22:32
42
0
学习了!
淡然他徒弟 2019-11-1 18:55
43
0
mark
游客
登录 | 注册 方可回帖
返回