ETW全称为Event Tracing for Windows,即windows事件跟踪,它是Windows提供的原生的事件跟踪日志系统。由于采用内核层面的缓冲和日志记录机制,所以ETW提供了一种非常高效的事件跟踪日志解决方案,本文基于ETW探究其攻与防的实现
ETW
事件监测(Event Instrumentation)总会包含两个基本的实体,事件的提供者(ETW Provider)和消费者(ETW Consumer),ETW框架可以视为它们的中介。ETW Provider会预先注册到ETW框架上,提供者程序在某个时刻触发事件,并将标准化定义的事件提供给ETW框架。Consumer同样需要注册到ETW框架上,在注册的时候可以设置事件的删选条件和接收处理事件的回调。对于接收到的事件,如果它满足某个注册ETW Consumer的筛选条件,ETW会调用相应的回调来处理该事件
ETW针对事件的处理是在某个会话(ETW Session)中进行的,ETW Session提供了一个接收、存储、处理和分发事件的执行上下文。ETW框架可以创建多一个会话来处理由提供者程序发送的事件,但是ETW Session并不会与某个单一的提供者绑定在一起,多个提供者程序可以向同一个ETW Session发送事件。对于接收到的事件,ETW Session可以将它保存在创建的日志文件中,也可以实时地分发给注册的消费者应用。ETW会话的开启和终止是通过 Session的开启和终止是通过ETW控制器(ETW Controller)进行管理的。除了管理ETW Session之外,ETW Controller还可以禁用或者恢复注册到某个ETW Session上的ETW Provider
在这里,我们可以看到所有已注册的ETW提供者及其对应GUID,我们还可以看到Microsoft-Windows-Threat-Intelligence突出显示的提供者及其InstrumentationManifest位于HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers\<PROVIDER_GUID>注册表项的二进制清单文件因为这是一个Manifest-based ETW提供者
1 | logman.exe query providers |

我们可以使用以下命令获取更多详细信息并了解提供程序支持的事件类型
1 | logman.exe query providers Microsoft-Windows-Threat-Intelligence |


也可以XML Manifest使用此工具检索文件,这使我们可以更详细地了解特定EtwTi事件记录的参数

使用x nt!EtwTi*来查看内核里面的所有例程

execute-assembly
cs在3.11版本实现了在非托管程序中加载.net程序集的功能,这个功能不需要向硬盘写入文件,十分隐蔽,而且现有的Powershell脚本能够很容易的转换为C#代码,十分方便,使用到的就是execute-assembly这个命令,这里我们用c#程序sharphound.exe进行演示,这个程序用来导出域内关系并可视化
1 | execute-assembly D:\Bloodhound\SharpHound.exe -c all |


首先我们来了解一下托管程序和非托管程序,说到这里就需要提一个CLR。CLR全称Common Language Runtime(公共语言运行库),是一个可由多种编程语言使用的运行环境。CLR是.NET Framework的主要执行引擎,作用之一是监视程序的运行:
- 在CLR监视之下运行的程序属于托管的代码
- 不在CLR之下,直接在裸机上运行的应用或者组件属于非托管的代码
托管程序与非托管程序的概念如下
托管代码就是Visual Basic .NET和C#编译器编译出来的代码。编译器把代码编译成中间语言(IL),而不是能直接在你的电脑上运行的机器码。中间语言被封装在一个叫程序集 (assembly)的文件中,程序集中包含了描述你所创建的类,方法和属性(例如安全需求)的所有元数据。
非托管代码就是在Visual Studio .NET 2002发布之前所创建的代码。例如Visual Basic 6, Visual C++ 6, 最糟糕的是,连那些依然残存在你的硬盘中、拥有超过15年历史的陈旧C编译器所产生的代码都是非托管代码。托管代码直接编译成目标计算机的机械码,这些代 码只能运行在编译出它们的计算机上,或者是其它相同处理器或者几乎一样处理器的计算机上。
再就是Unmanaged API,它其实是一套能将.net程序集加载到任意程序里面的API,它支持ICorRuntimeHost Interface和ICLRRuntimeHost Interface两种接口,我们看一下msdn里面的描述

其中ICorRuntimeHost Interface支持的版本有v1.0.3705, v1.1.4322, v2.0.50727,v4.0.30319,ICLRRuntimeHost Interface支持的版本有v2.0.50727,v4.0.30319,在实际的开发里面两种接口都是可以使用的
cs实现在非托管程序中加载主要是调用了ICLRRuntimeHost 的接口,主要用到以下3个接口
ICLRMetaHost
ICLRRuntimeInfo
ICLRRuntimeHost
ICLRMetaHost 提供基于版本号返回特定版本的公共语言运行时 (CLR)、列出所有已安装的 CLR、列出在指定进程中加载的所有运行时、发现用于编译程序集的 CLR 版本、退出进程的方法干净的运行时关闭,并查询旧版 API 绑定

ICLRRuntimeInfo提供一些方法,这些方法可返回有关特定公共语言运行时 (CLR) 的信息,包括版本、目录和加载状态。 此接口还提供了特定于运行时的功能,而无需初始化运行时。 它包括运行时相对 LoadLibrary 方法、运行时模块特定的 GetProcAddress 方法和通过 GetInterface 方法提供的运行时提供的接口

ICLRRuntimeHost提供与 .NET Framework 版本1中提供的 ICorRuntimeHost 接口类似的功能,其中包含以下更改: 用于设置宿主控件接口的 SetHostControl 方法的添加,省略提供的某些方法 ICorRuntimeHost

硬盘加载
首先这里我们写一个Printf函数,使用Console.WriteLine接收
1 | namespace etw1 |
在服务端我们首先使用CLRCreateInstance初始化ICLRMetaHost接口
1 | CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&iMetaHost); |
然后调用GetRuntime方法获取ICLRRuntimeInfo接口
1 | iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&iRuntimeInfo); |
再使用ICLRRuntimeInfo将 CLR 加载到当前进程,返回运行时接口ICLRRuntimeHost指针
1 | iRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&iRuntimeHost); |
然后再通过ICLRRuntimeHost.EecuteInDefaultAppDomain执行指定程序
1 | iRuntimeHost->ExecuteInDefaultAppDomain |
实现效果如下

内存加载
内存加载相对于硬盘加载,首先是整个过程都会在内存执行而不会写入文件,隐蔽性较好,而且最终的payload为c#程序,调用powershell十分方便利用
那么我们来进行代码的实现,首先还是初始化CLR环境
1 | CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&iMetaHost); |
然后使用ICLRRuntimeHost获取AppDomain接口指针,然后通过AppDomain接口的QueryInterface方法来查询默认应用程序域的实例指针
1 | iRuntimeHost->GetDefaultDomain(&pAppDomain); |

使用Load_3(…)从内存中读取并加载.NET程序集
1 | saBound[0].cElements = ASSEMBLY_LENGTH; |
创建安全数组并执行入口点
1 | ZeroMemory(&vRet, sizeof(VARIANT)); |


检测execute-assembly
一般检测execute-assembly都会使用windows事件跟踪,即ETW,例如这里启动一个powershell进程,通过procexp查看可以看到被CLR托管的dll


我们可以从processhacker工具源码里面的asmpage.c源码里面查看这类工具是怎样枚举.net工具集的,这里挑出关键代码编译成etw2.exe
1 | static GUID ClrRuntimeProviderGuid = { 0xe13c0d23, 0xccbc, 0x4e12, { 0x93, 0x1b, 0xd9, 0xcc, 0x2e, 0xee, 0x27, 0xe4 } }; |
首先cs上线

然后启动我们的监控程序

在beacon里面调用SharpHound.exe,这里需要在域内且具有.net环境才能够运行成功,执行以下命令
1 | execute-assembly D:\Bloodhound\SharpHound.exe 1.2.3.4 |

这里就会在exe存放的位置生成以下三个文件

然后我们去看一下我们的监控程序,可以看到已经识别出了SharpHound的调用

这里如果想要规避检测,可以更改程序名的名字,但是这里只要修改检测方法为显示可疑方法的名称即可
1 | switch (eventDescriptor->Id) { |
这里通过select-string查找SharpHound方法

这里还是启动一下我们的SharpHound程序


可以看到还是被监控到了Sharphound2.Sharphound方法

规避ETW检测
通过查阅资料后发现ETW将 TRUE 布尔参数传递到nt!EtwpStopTrace函数中,以查找 ETW 特定结构并动态修改或修补ntdll!ETWEventWrite或advapi32!EventWrite立即返回从而停止用户模式记录器
也就是说在3环ETW是通过ntdll.dll的EtwEventWriteFull函数实现的

往下跟发现调用了EtwEventWriteFull,然后EtwEventWriteFull调用EtwpEventWriteFull

我们继续往下看EtwEventWriteFull函数,调用了NtTraceEvent

继续跟NtTraceEvent,可以发现NtTraceEvent通过syscall进入内核

这里我们可以打印一下地址

那么我们在EtwEventWriteFull直接使用0xc3即ret返回,即可达到绕过的效果,首先我们通过x64dbg和powershell验证一下
首先使用x64dbg创建一个powershell进程,这时x64dbg会在线程初始化前下一个断点
定位到ntdll!EtwEventWrite


一般windows api默认使用stdcall(x86)调用约定,这里x64默认使用fastcall,即寄存器传参,被调用者清理堆栈,所以我们直接使用ret即C3返回即可

查看CLR日志已经被清空

这里通过代码实现,定位到ntdll!EtwEventWrite函数,然后在入口处ret返回即可,使用VirtualProtectEx修改属性
1 | void bypassetw() |
实现效果如下,可以看到起了一个powershell进程,查看CLR日志也被清空

这里可能某些EDR会hookEtwEventWrite这个函数,那么我们直接往syscall进0环的函数去挂钩,代码如下
1 | unsigned char sNtTraceEvent[] = { 'N','t','T','r','a','c','e','E','v','e','n','t', 0}; |
可以看到CLR日志也被清空
