ETW HOOK原理探析

ETW HOOK研究

文章目录

  • ETW HOOK研究
    • 前言
    • 原理探究
    • 内核开启ETW日志
    • HOOK ETW
      • 修改ETW日志上下文
      • 代理GetCpuClock函数
      • 寻找SSDT和SSDT Shadow
    • 总结
    • 参考

前言

关于ETW是什么我就不多说了,可以通过微软的相关文档了解到。据网上得知这项技术最早被披露于2345的驱动中,一位工程师将其代码逆向还原之后大白于天下。随后各大安全厂商相继使用这种技术实现监控系统调用、内存页错误等。它是一个相对于VT来说更稳定,更简单的系统监控方式。

原理探究

【环境】 W i n 10   1903   x 64 ( 19045.3208 之前) \textcolor{green}{【环境】Win10\ 1903\ x64(19045.3208之前)} 【环境】Win10 1903 x6419045.3208之前)

有逆向过系统模块的小伙伴肯定没少发现在很多函数调用中都会写入相关调用信息到事件中,例如在nt内核模块的系统调用函数 K i S y s t e m C a l l 64 \textcolor{cornflowerblue}{KiSystemCall64} KiSystemCall64

ETW HOOK原理探析_第1张图片

​ 图 2.1 KiSystemCall64

所有系统调用都会走到这个函数,然后再根据调用号派发到最终的目标函数上,而红框中的r10寄存器保存的就是最终的目标函数。我们发现r10的值是保存在栈中的,如果能够通过某种方式修改到栈中的数据,那么就可以实现动态HOOK系统调用了。请记住这一点,这将会是ETW HOOK能够监控系统调用的一个重要前提条件。

我们继续跟进 P e r f I n f o L o g S y s C a l l E n t r y \textcolor{cornflowerblue}{PerfInfoLogSysCallEntry} PerfInfoLogSysCallEntry函数去分析,当然默认情况下系统是不会走loc_14040D253这个分支的,因为第一个红框部分的条件不满足。

ETW HOOK原理探析_第2张图片

​ 图 2.2 PerfInfoLogSysCallEntry

E t w T r a c e S i l o K e r n e l E v e n t \textcolor{cornflowerblue}{EtwTraceSiloKernelEvent} EtwTraceSiloKernelEvent内部:

ETW HOOK原理探析_第3张图片

​ 图 2.3 EtwTraceSiloKernelEvent

跟进 E t w p L o g K e r n e l E v e n t \textcolor{cornflowerblue}{EtwpLogKernelEvent} EtwpLogKernelEvent函数,该函数内部会将系统调用相关的数据封装成日志,再调用 E t w p R e s e r v e T r a c e B u f f e r \textcolor{cornflowerblue}{EtwpReserveTraceBuffer} EtwpReserveTraceBuffer函数将数据写到日志缓冲区中。在 E t w p R e s e r v e T r a c e B u f f e r \textcolor{cornflowerblue}{EtwpReserveTraceBuffer} EtwpReserveTraceBuffer中有个关键的地方

ETW HOOK原理探析_第4张图片

​ 图 2.4 EtwpReserveTraceBuffer

的条件不满足时就会调用日志上下文只保存的函数指针,该结构体类型为_WMI_LOGGER_CONTEXT,在我的环境下是这样的:

1: 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
 ...

偏移0x28就是 G e t C p u C l o c k \textcolor{cornflowerblue}{GetCpuClock} GetCpuClock函数指针,别的版本的系统可能偏移不一样。

系统启动之后会初始化一些系统的ETW日志,并初始化它们的日志上下文,然后讲这些上下文存放到一个全局变量中,全局变量的结构就是_WMI_LOGGER_CONTEXT列表。它恰好位于 E t w p D e b u g g e r D a t a + 0 x 10 \textcolor{orange}{EtwpDebuggerData+0x10} EtwpDebuggerData+0x10位置处。系统ETW日志初始化工作是在函数 E t w I n i t i a l i z e S i l o S t a t e \textcolor{cornflowerblue}{EtwInitializeSiloState} EtwInitializeSiloState

ETW HOOK原理探析_第5张图片

​ 图 2.5 EtwInitializeSiloState

从这个上下文列表中就可以看到当前系统上已注册的ETW日志都有

Circular Kernel Context Logger
Eventlog-Security
AppModel
DefenderApiLogger
DefenderAuditLogger
DiagLog
Diagtrack-Listener
EventLog-Application
EventLog-System
LwtNetLog
Microsoft-Windows-Rdp-Graphics-RdpIdd-Trace
NetCore
NtfsLog
RadioMgr
UBPM
WdiContextLog
WiFiSession
PerfDiag Logger
umstartup
WUDFTrace
UserNotPresentTraceSession
COM
Terminal-Services-LSM
Terminal-Services-RCM
UserMgr
WFP-IPsec Diagnostics
MpWppTracing-20231109-203630-00000003-ffffffff
ScreenOnPowerStudyTraceSession
WindowsUpdate_trace_log
MSDTC_TRACE_SESSION
SHS-11092023-203650-7-7f
ECCB175F-1EB2-43DA-BFB5-A8D58A40A4D7
Terminal-Services-LSM-ApplicationLag-4140

并且看了这些系统日志,发现它们的 F l a g s   &   0 x 8000000 \textcolor{orange}{Flags\ \&\ 0x8000000} Flags & 0x8000000均是为0的,不仅如此,就算是用户自己开启ETW日志,其上下文的Flags字段的值同样也不满足图2.4中红框①的条件。换句话说就是,系统开启的ETW日志和我们开启的ETW日志,都会在记录日志的时候调用位于日志上下文中的 G e t C p u C l o c k \textcolor{cornflowerblue}{GetCpuClock} GetCpuClock函数指针。

回过头来再看图2.1第一个红框部分的全局标志PerfGlobalGroupMask,共16个字节,在函数 E t w p U p d a t e G l o b a l G r o u p M a s k s \textcolor{cornflowerblue}{EtwpUpdateGlobalGroupMasks} EtwpUpdateGlobalGroupMasks中会设置该标志低8个字节为0x40。而该函数会在 E t w p S t a r t L o g g e r \textcolor{cornflowerblue}{EtwpStartLogger} EtwpStartLogger中调用,但前提是满足:

ETW HOOK原理探析_第6张图片

​ 图 2.6 EtwpStartLogger判断日志类型

应用层和内核层都可以通过导出的函数 N t T r a c e C o n t r o l \textcolor{cornflowerblue}{NtTraceControl} NtTraceControl调用 E t w p S t a r t L o g g e r \textcolor{cornflowerblue}{EtwpStartLogger} EtwpStartLogger

ETW HOOK原理探析_第7张图片

​ 图 2.7 NtTraceControl

NTSTATUS NtTraceControl(
	_In_ ULONG ControlCode,
	_In_ _WMI_LOGGER_INFORMATION* pLoggerInfo,
	_In_ ULONG LoggerInfoSize,
	_Out_ _WMI_LOGGER_INFORMATION* pOutLoggerInfo,
	_In_ ULONG OutLoggerInfoSize,
	_Out_ PULONG RetSize);
  • ControlCode:控制码,取值和对应功能见下表

    取值 功能
    1 开启ETW日志追踪
    2 停止ETW日志追踪
    3 查询ETW日志追踪
    4 更新ETW日志追踪
    5 刷新ETW日志追踪
    6 增加追踪文件
  • pLoggerInfo:日志基本配置信息,_WMI_LOGGER_INFORMATION结构如下:

    typedef struct _WMI_LOGGER_INFORMATION
    {
    	WNODE_HEADER Wnode;						// WNODE_HEADER是公开的,这里就不贴上来了
    	ULONG BufferSize;
    	ULONG MinimumBuffers;
    	ULONG MaximumBuffers;
    	ULONG MaximumFileSize;
    	ULONG LogFileMode;
    	ULONG FlushTimer;
    	ULONG EnableFlags;
    	union
    	{
    		LONG AgeLimit;
    		LONG FlushThreshold;
    	};
    	ULONG Wow;
    	LONG Padding_719;
    	union
    	{
    		PVOID LogFileHandle;
    		ULONGLONG LogFileHandle64;
    	};
    	union
    	{
    		ULONG NumberOfBuffers;
    		ULONG InstanceCount;
    	};
    	union
    	{
    		ULONG FreeBuffers;
    		ULONG InstanceId;
    	};
    	union
    	{
    		ULONG EventsLost;
    		ULONG NumberOfProcessors;
    	};
    	ULONG BuffersWritten;
    	union
    	{
    		ULONG LogBuffersLost;
    		ULONG Flags;
    	};
    	ULONG RealTimeBuffersLost;
    	union
    	{
    		PVOID LoggerThreadId;
    		ULONGLONG LoggerThreadId64;
    	};
    	union
    	{
    		UNICODE_STRING LogFileName;
    		STRING64 LogFileName64;
    	};
    	union
    	{
    		UNICODE_STRING LoggerName;
    		STRING64 LoggerName64;
    	};
    	ULONG RealTimeConsumerCount;
    	ULONG SpareUlong;
    	union
    	{
    		union
    		{
    			PVOID LoggerExtension;
    			ULONGLONG LoggerExtension64;
    		};
    	}  DUMMYUNIONNAME10;
    } WMI_LOGGER_INFORMATION, * PWMI_LOGGER_INFORMATION;
    

剩下的参数顾名思义,不再解释。

只需要关注_WMI_LOGGER_INFORMATION结构中的几个字段:

  • EnableFlags - 启用追踪标志。如果想启用syscall的追踪,需要将该标志设置为EVENT_TRACE_FLAG_SYSTEMCALL(0x80)。更多标志参见:https://learn.microsoft.com/zh-cn/windows/win32/api/evntrace/ns-evntrace-event_trace_properties

  • Wnode.Guid - 日志GUID。可使用系统定义,亦可以自己定义。系统定义的GUID和对应的LoggerName有很多,这里只列举两个:

    日志名 GUID
    Circular Kernel Context Logger {54DEA73A-ED1F-42A4-AF71-3E63D056F174}
    NT Kernel Logger {9E814AAD-3204-11D2-9A82-006008A86939}
  • LoggerName - 日志名。不可为空。

  • Wnode.Flags - 标志。必须包含 WNODE_FLAG_TRACED_GUID 0x20000),以指示结构包含事件跟踪信息。

  • Wnode.ClientContext - 记录每个事件的时间戳时要使用的时钟解析。这里是一个关键地方,系统会根据这个字段的取值来设置日志上下文中的 G e t C p u C l o c k \textcolor{cornflowerblue}{GetCpuClock} GetCpuClock函数指针。 默认值为 QPC查询性能计数器。详情参考:https://learn.microsoft.com/zh-cn/windows/win32/etw/wnode-header

  • LogFileMode - 日志文件模式。详情参考:https://learn.microsoft.com/zh-cn/windows/win32/etw/logging-mode-constants

为了了解如何正确设置上述结构以便在内核中开启ETW日志追踪,需要着重关注 E t w p S t a r t L o g g e r \textcolor{cornflowerblue}{EtwpStartLogger} EtwpStartLogger N t T r a c e C o n t r o l \textcolor{cornflowerblue}{NtTraceControl} NtTraceControl函数,这两个多数都是在检查_WMI_LOGGER_INFORMATION中的数据设置是否合法。而决定我们开启ETW系统日志成败的检查有这几个地方

在这里插入图片描述

​ 图 2.8 NtTraceControl检查LoggerInfo

ETW HOOK原理探析_第8张图片

​ 图2.9 EtwpValidateLoggerInfo

由此看出,需要设置 W n o d e . B u f f e r S i z e   > =   0 x B 0 \textcolor{orange}{Wnode.BufferSize\ >=\ 0xB0} Wnode.BufferSize >= 0xB0,而0xB0正好是_WMI_LOGGER_INFORMATION结构的大小;需要设置Wnode.Flags包含WNODE_FLAG_TRACED_GUID0x20000)。

ETW HOOK原理探析_第9张图片

​ 图 2.10 检查LogFileMode

这里涉及到LogFileMode和MaxmunFile*相关的合法性检查,但我们不关心MaxmunFile*相关的字段,所以满足条件的LogFileMode有很多,例如:0x400,0x700,0x900…而在后面的函数 E t w p I n i t L o g g e r C o n t e x t \textcolor{cornflowerblue}{EtwpInitLoggerContext} EtwpInitLoggerContext中会将LogFileMode传递到日志上下文中的LoggerMode。这又触碰到了图2.6中的检查。所以LogFileMode应该设置为 E V E N T _ T R A C E _ B U F F E R I N G _ M O D E   ∣   E V E N T _ T R A C E _ S Y S T E M _ L O G G E R _ M O D E \textcolor{orange}{EVENT\_TRACE\_BUFFERING\_MODE\ |\ EVENT\_TRACE\_SYSTEM\_LOGGER\_MODE} EVENT_TRACE_BUFFERING_MODE  EVENT_TRACE_SYSTEM_LOGGER_MODE

对于Wnode.ClientContext的选值建议从简出发,选择3。因为3对应的 G e t C p u C l o c k \textcolor{cornflowerblue}{GetCpuClock} GetCpuClock指向的函数最简单,只有一句 r d t s c \textcolor{cornflowerblue}{rdtsc} rdtsc

到这里,已经把重要字段的填值都弄明白了,可以开始进入到内核开启ETW日志的程序了。

内核开启ETW日志

Z w T r a c e C o n t r o l \textcolor{cornflowerblue}{ZwTraceControl} ZwTraceControl是内核导出的,只需在使用前定义一下原型即可。

通过前面的分析,可以将开启、更新和停止ETW日志都封装成一个函数:

NTSTATUS ChangeETWStatus(ETW_STATUS_ENUM Status, ETW_TRACE_FLAG TraceFlag)
{
	ULONG ulRetSize;
	// 填充etw logger注册信息
	WMI_LOGGER_INFORMATION LoggerInfo = { 0 };
	// 开启 Syscall的追踪
	LoggerInfo.EnableFlags = TraceFlag;
	// 某个日志对应的GUID
	LoggerInfo.Wnode.Guid = GUID_CKCL;
	// 使用CPU周期
	LoggerInfo.Wnode.ClientContext = 3;
	// 初始化日志名
	RtlInitUnicodeString(&LoggerInfo.LoggerName, CKCL_LOGGER_NAME);
	// 为了通过EtwpValidateLoggerInfo的校验
	LoggerInfo.Wnode.BufferSize = sizeof(LoggerInfo);
	LoggerInfo.Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	// 需要不满足 win10 1903 x64 nt!EtwpStartLogger+161 的条件,并且满足 win10 1903 x64 nt!EtwpStartLogger+80A 的条件
	LoggerInfo.LogFileMode = EVENT_TRACE_BUFFERING_MODE | EVENT_TRACE_SYSTEM_LOGGER_MODE;
	
	return ZwTraceControl(Status, &LoggerInfo, sizeof(LoggerInfo), &LoggerInfo, sizeof(LoggerInfo), &ulRetSize);
}

相关枚举值

typedef enum _ETW_STATUS_ENUM
{
	E_ETW_STATUS_START = 1,
	E_ETW_STATUS_STOP = 2,
	E_ETW_STATUS_UPDATE = 4
}ETW_STATUS_ENUM;

typedef enum _ETW_TRACE_FLAG
{
	E_ETW_TRACE_SYSCALL = 0x80,
	E_ETW_TRACE_ALPC = 0x00100000
}ETW_TRACE_FLAG;

注意:不建议使用 N t T r a c e C o n t r o l ,因为换成别的系统会有蓝屏的风险。内核下建议使用 Z w ∗ 系函数代替 N t ∗ 系函数。 \textcolor{BrickRed}{注意:不建议使用NtTraceControl,因为换成别的系统会有蓝屏的风险。内核下建议使用Zw*系函数代替Nt*系函数。} 注意:不建议使用NtTraceControl,因为换成别的系统会有蓝屏的风险。内核下建议使用Zw系函数代替Nt系函数。

HOOK ETW

修改ETW日志上下文

在第2小节中,我们成功控制了ETW日志状态,接下来就是HOOK我们控制的ETW日志上下文,将 G e t C p u C l o c k \textcolor{cornflowerblue}{GetCpuClock} GetCpuClock修改成我们自己的函数,然后在我们自己的函数中进行后续处理。观察ETW日志上下文列表( E t w p D e b u g g e r D a t a + 0 x 10 \textcolor{orange}{EtwpDebuggerData+0x10} EtwpDebuggerData+0x10)可以发现,对于NT Kernel Logger日志,其上下文在列表中的下标为0;对于Circular Kernel Context Logger日志,则是2,并且这个规律是固定不变的。

还有一个问题就是如何找到ETW日志上下文列表,它所在的全局变量并不导出。但幸运的是它位于 E t w p D e b u g g e r D a t a + 0 x 10 \textcolor{orange}{EtwpDebuggerData+0x10} EtwpDebuggerData+0x10,在多数64位系统上也是固定的。而EtwpDebuggerData

在多数系统上特征码也是固定的,我们可以通过特征码定位找到ETW日志上下文列表。

EtwpDebuggerData特征码:

0x2c, 0x08, 0x04, 0x38, 0x0c 

代理GetCpuClock函数

然后在我们自己的 G e t C p u C l o c k \textcolor{cornflowerblue}{GetCpuClock} GetCpuClock函数中需要做一些事情:

  • 判断ETW事件是否为syscall类型事件:从 P e r f I n f o L o g S y s C a l l E n t r y \textcolor{cornflowerblue}{PerfInfoLogSysCallEntry} PerfInfoLogSysCallEntry来看,有两个特别的标志

    在这里插入图片描述

    ​ 图4.2.1 PerfInfoLogSysCallEntry记录系统调用到ETW日志

    这两个标志会从栈中写道日志里去,所以在我们函数里可以从栈中找到这两个值就可以确定这是一个syscall的事件,接着就可以尝试在栈中寻找syscall的目标函数了。

    • 如果我们只是检测syscall调用,到这里就可以结束了。如果要对syscall进行干预的话,我们还需要将栈中的syscall目标函数进行替换。如何从栈中找到syscall的调用目标,这需要一定的技巧。首先我们知道了栈中存放有这两个标志值,然后还有syscall的目标函数。分布如下:

      fffff888`978ae888  fffff800`36e7cdcc nt!EtwpGetPerfCounter+0xc -----------------> 当前函数返回地址
      fffff888`978ae890  00000000`00000010
      fffff888`978ae898  00000000`00000344
      fffff888`978ae8a0  fffff888`978ae8b8
      fffff888`978ae8a8  00000000`00000018
      fffff888`978ae8b0  fffff888`978ae8c0
      fffff888`978ae8b8  fffff800`36eb11ca nt!EtwpLogKernelEvent+0x28a
      fffff888`978ae8c0  00000000`00000000
      fffff888`978ae8c8  00000000`00000000
      fffff888`978ae8d0  00000000`00000000
      fffff888`978ae8d8  00000000`00000000
      fffff888`978ae8e0  00000000`00000000
      fffff888`978ae8e8  00000000`00000000
      fffff888`978ae8f0  00000000`00000000
      fffff888`978ae8f8  00000000`00000000
      fffff888`978ae900  00000008`00100000
      fffff888`978ae908  00000000`00000000
      fffff888`978ae910  00000002`0009e388
      fffff888`978ae918  fffff888`00000018
      fffff888`978ae920  00000000`00000000
      fffff888`978ae928  fffff800`36eb0f59 nt!EtwpLogKernelEvent+0x19
      fffff888`978ae930  00000000`00000010
      fffff888`978ae938  ffff8904`d8253880
      fffff888`978ae940  00000000`00000010
      fffff888`978ae948  00000000`00000018
      fffff888`978ae950  00000000`00501802
      fffff888`978ae958  fffff888`978aea18
      fffff888`978ae960  00000000`00000000
      fffff888`978ae968  00000000`00000000
      fffff888`978ae970  00000000`40000040
      fffff888`978ae978  00000000`00000000
      fffff888`978ae980  00000000`00000001
      fffff888`978ae988  fffff800`36f22afe nt!EtwTraceSiloKernelEvent+0xbe
      fffff888`978ae990  00000000`00000f33 --------------------------------------------> 标志1:0xF33
      fffff888`978ae998  ffff8904`d78ad000
      fffff888`978ae9a0  00000000`00000010
      fffff888`978ae9a8  00000000`00000001
      fffff888`978ae9b0  fffff888`978a0f33 --------------------------------------------> 标志1:0xF33
      fffff888`978ae9b8  00000000`00501802 --------------------------------------------> 标志2:0x501802
      fffff888`978ae9c0  00000000`00000000
      fffff888`978ae9c8  00000000`00000000
      fffff888`978ae9d0  00000000`00000000
      fffff888`978ae9d8  fffff800`370cda00 nt!PerfInfoLogSysCallEntry+0x70
      fffff888`978ae9e0  fffff800`373aed88 nt!NtUpdateWnfStateData --------------------> syscall调用的目标函数
      fffff888`978ae9e8  fffff888`978aeb80
      fffff888`978ae9f0  0000005d`fd37e958
      fffff888`978ae9f8  00000000`00000f33 --------------------------------------------> 标志1:0xF33
      fffff888`978aea00  00000000`00000f33 --------------------------------------------> 标志1:0xF33
      fffff888`978aea08  fffff800`00501802 --------------------------------------------> 标志2:0x501802
      fffff888`978aea10  fffff800`373aed88 nt!NtUpdateWnfStateData
      fffff888`978aea18  fffff888`978aea10
      fffff888`978aea20  00000000`00000008
      fffff888`978aea28  ffffc7c4`9fea7b16
      fffff888`978aea30  fffff888`978aeaa8
      fffff888`978aea38  fffff800`36fd85a1 nt!KiSystemServiceExitPico+0x206 -----------> KisystemCall64 + 0x8e1
      ...
      

      由上可以知道他们的大致位置。顺序是逆着栈的生长方向,先是两个标志值,随后才是syscall的目标函数,并且他们都在当前函数的返回地址之后,并且位于。于是,我们可以确定搜索的起始位置就是当前函数的返回地址在栈中的位置。同时他们又位于 n t ! K i S y s t e m S e r v i c e E x i t P i c o + 0 x 206 \textcolor{orange}{nt!KiSystemServiceExitPico+0x206} nt!KiSystemServiceExitPico+0x206以上,而这个地址处于 K i s y s t e m C a l l 64 \textcolor{cornflowerblue}{KisystemCall64} KisystemCall64函数内,所以我们就选择这个值来作为搜索的终点。如果栈中都没有两个标志位和 n t ! K i S y s t e m S e r v i c e E x i t P i c o + 0 x 206 \textcolor{orange}{nt!KiSystemServiceExitPico+0x206} nt!KiSystemServiceExitPico+0x206,则搜索的终点就定在 _ K T H R E A D . I n i t i a l S t a c k \textcolor{orange}{\_KTHREAD.InitialStack} _KTHREAD.InitialStack

      _KTHREAD结构体中有个SystemCallNumber字段,保存了当前线程最近一次syscall的调用号。可以根据这个调用号去SSDT表中取目标函数地址,然后与栈中的数据进行对比,就能找到syscall目标在栈中的位置了。

示例代码:

ULONG GetCpuClock_Proxy()
{
	// 忽略内核模式的调用,防重入
	if (ExGetPreviousMode() == KernelMode)
	{
		return __rdtsc();
	}
	
	ULONG_PTR pfnCall = 0;
	PCHAR pThread = (PCHAR)PsGetCurrentThread();
	BOOLEAN bFoundMagicNum1 = FALSE;
	BOOLEAN bFoundMagicNum2 = FALSE;

	__try
	{
		ULONG ulSyscallNumber = *(PULONG)(pThread + SYSTEMCALL_NUMBER_OFFSET_IN_KTHREDA);
		ULONG ulThreadFlags = *(PULONG)(pThread + THREAD_FLAGS_OFFSET_IN_KTHREAD);
		if (FlagOn(ulThreadFlags, GUI_THREAD))
		{
			pfnCall = gKeServiceDescriptorTableShadow[ulSyscallNumber];
		}
		else
		{
			pfnCall = gKeServiceDescriptorTable[ulSyscallNumber];
		}
		
		ULONG_PTR ulRetAddressInStack = (ULONG_PTR)_AddressOfReturnAddress();

		ULONG_PTR ulInitialStack = *(PULONG_PTR)(pThread + INITIAL_STACK_OFFSET_IN_KPRCB);

		for (ULONG_PTR ulCurrentStack = ulRetAddressInStack; ulCurrentStack < ulInitialStack; ulCurrentStack += sizeof(PVOID))
		{
			ULONG_PTR ulValue = *(PULONG_PTR)ulCurrentStack;
			if ( (ulValue >= gKiSystemCall64Entry &&
					ulValue < (gKiSystemCall64Entry + 2 * PAGE_SIZE)))
			{
				break;
			}

			if ((ulValue & 0xFFFF) == MAGIC_NUMBER1)
			{
				bFoundMagicNum1 = TRUE;
			}
			else if ((ulValue & 0xFFFFFFFF) == MAGIC_NUMBER2)
			{
				bFoundMagicNum2 = TRUE;
			}
			else if (ulValue == pfnCall && bFoundMagicNum1 && bFoundMagicNum2)
			{
				// Hook
				// *(PULONG_PTR)ulCurrentStack = pfnYours;
				break;
			}
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER)
	{

	}
	
	return __rdtsc();
}

寻找SSDT和SSDT Shadow

纵观Win7 x64到Win10 x64,SSDT和SSDT Shadow在 K i S y s t e m C a l l 64 \textcolor{cornflowerblue}{KiSystemCall64} KiSystemCall64中按以下方式进行引用:

.text:0000000140073FF2 4C 8D 15 47 A9 23 00                                            lea     r10, KeServiceDescriptorTable
.text:0000000140073FF9 4C 8D 1D 80 A9 23 00                                            lea     r11, KeServiceDescriptorTableShadow

K i S y s t e m C a l l 64 \textcolor{cornflowerblue}{KiSystemCall64} KiSystemCall64地址可以通过 _ _ r e a d m s r ( I A 32 _ L S T A R _ M S R ) \textcolor{orange}{\_\_readmsr(IA32\_LSTAR\_MSR)} __readmsr(IA32_LSTAR_MSR)取得。不过在打了页表隔离补丁之后,这种方式返回的是位于 .KVASCODE 段内的 K i S y s t e m C a l l 64 S h a d o w \textcolor{cornflowerblue}{KiSystemCall64Shadow} KiSystemCall64Shadow,在其尾部会跳转到 K i S y s t e m C a l l 64 \textcolor{cornflowerblue}{KiSystemCall64} KiSystemCall64内部:

KiSystemCall64+23A                                KiSystemServiceUser:                    ; CODE XREF: KiSystemService+231↑j
KiSystemCall64+23A                                                                        ; KiSystemCall64Shadow+252↓j
KiSystemCall64+23A  C6 45 AB 02                                   mov     byte ptr [rbp-55h], 2
KiSystemCall64+23E  65 48 8B 1C 25 88 01 00 00                    mov     rbx, gs:188h
----------------------------------------------------------------------------------------------------------------------------------------------
KiSystemCall64Shadow+246                                loc_140A173C6:                          ; CODE XREF: KiSystemCall64Shadow+11B↑j
KiSystemCall64Shadow+246  0F AE E8                                      lfence
KiSystemCall64Shadow+249  65 C6 04 25 53 08 00 00 00                    mov     byte ptr gs:853h, 0
KiSystemCall64Shadow+252  E9 63 54 9F FF                                jmp     KiSystemServiceUser

所以这种情况还需要通过解析jmp到的地址,从而取得 K i S y s t e m C a l l 64 \textcolor{cornflowerblue}{KiSystemCall64} KiSystemCall64附近的地址。方法也很简单,就是从 K i S y s t e m C a l l 64 S h a d o w \textcolor{cornflowerblue}{KiSystemCall64Shadow} KiSystemCall64Shadow起始位置,搜索所有jmp指令,跳转的目标地址不在 .KVASCODE 段内即是我们想要的 K i S y s t e m C a l l 64 \textcolor{cornflowerblue}{KiSystemCall64} KiSystemCall64附近的地址。进而找到SSDT和SSDT Shadow。

我的代码就不发了,参考文献中有比较完整的项目。其他版本的系统需要自己去调整里面涉及到的偏移值。

总结

实现ETW HOOK的步骤:

  • 调用 Z w T r a c e C o n t r o l \textcolor{cornflowerblue}{ZwTraceControl} ZwTraceControl启用ETW日志(Win8只有 N t T r a c e C o n t r o l \textcolor{cornflowerblue}{NtTraceControl} NtTraceControl,可以在调用前修改一下先前模式为KernelMode),可以是任意的日志,只是要保证能找到对应的日志上下文即可。
  • 修改对应日志上下文中的GetCpuClock指针为自己的代理函数。
  • 在代理函数中,从栈上寻找syscall调用的目标函数,并修改之。

ETW日志可以在用户层关闭,所以需要对我们自己的ETW日志做一些保护,防止失效。

参考

[1] https://github.com/everdox/InfinityHook

[2] https://bbs.kanxue.com/thread-253450.htm

[3] https://bbs.kanxue.com/thread-258352.htm

你可能感兴趣的:(黑客编程,逆向,总结,网络安全,windows,ETW)