使用ETW检测进程注入|Windows事件跟踪

使用ETW检测进程注入|Windows事件跟踪

ETW介绍

官网:
https://docs.microsoft.com/en-us/windows/win32/etw/about-event-tracing

Windows事件跟踪(ETW)是一种有效的内核级跟踪工具,可让您将内核或应用程序定义的事件记录到日志文件中。您可以实时使用事件或从日志文件使用事件,并使用它们来调试应用程序或确定应用程序中发生性能问题的位置。

ETW使您可以动态地启用或禁用事件跟踪,从而使您可以在生产环境中执行详细的跟踪而无需重新启动计算机或应用程序。

事件跟踪API分为三个不同的组件:

下图显示了事件跟踪模型

检测内存注入

要了解为什么Microsoft的“威胁情报追踪”在检测现代且通常晦涩的注入技术时有用且急需解决,首先让我们简要地提醒一下自己,过去15年中防御者和安全厂商使用的最常见的注入检测技术是什么,他们不足之处。

内存扫描

以类似于AV扫描访问文件的方式扫描进程内存中的签名,这也许是用于捕获驻留在内存中的恶意软件的最古老,最琐碎的技术。根据特定的实现,扫描可以是选择性的,并且可以在新进程启动后进行,也可以针对所有正在运行的进程进行定期(也称为持久性扫描)。

尽管从理论上讲这种方法非常有吸引力,但仍存在一些挑战:

  • 首先,内存扫描因占用大量资源臭名昭著。不断增长的平均RAM大小以及x64架构的广泛采用只会增加挑战。这意味着无论我们采用哪种实施方式,我们都需要进行准确性与性能之间的权衡。
  • 由于定期扫描程序无法在短时间内进行全内存扫描(请参见上文),因此它们通常用于查找有趣且异常的内存段元数据,例如与分配类型结合使用(这是极大的简化)。然后,它们可以触发特定进程的全内存扫描。PAGE_EXECUTE_+MEM_RESERVE | MEM_COMMIT
  • 传统的内存扫描仪缺乏上下文意识
    • 哪个进程和线程已创建/写入内存页面?
    • 这是远程还是本地进程内存分配?
    • 使用了什么顺序的WinAPI调用?传递了哪些论据等
    • 什么时候扫描?

所有这些意味着传统的(“完整”)内存扫描主要用于AV点播扫描以及事件响应期间-通常在“脱机”内存转储中。大多数(如果不是全部)主要端点安全解决方案都依赖于混合方法,沙盒执行+指纹识别以及事件触发的实时扫描。

回调和绕行

用于事件触发的进程扫描(但更常见的是AV/EDR检测)的最常用技术包括使用内核驱动程序通知/回调,以及用户模式Win32 API内联挂钩。两者都有其优点和缺点。

存在其他接口,例如AMSI(反恶意软件扫描接口),防御者可以使用它们来对各种Windows界面(例如VBA7.DLL或WMI)中的缓冲区进行事件触发的扫描。

内核驱动程序回调

Windows允许驱动程序编写器注册发生特定类型的系统事件时要调用的回调例程。例如,如果我们有兴趣监视新的流程创建事件,则可以通过调用为其注册一个回调例程。下次启动进程时,我们的驱动程序将得到通知。PsSetCreateProcessNotifyRoutine

有关驱动程序回调的一些事实:

  • 以内核模式运行
    • 已知的绕过技术需要有脆弱的签名驱动程序,或进入内核的另一种方法。SELoadDriverPrivilege
  • 回调数量是有限的,并且不存在用于内存管理/进程注入API的回调(线程创建是一个例外)。我们可以拦截的最有用的事件类型包括:
    • 流程创建
    • 图像加载
    • 档案建立
    • 线程创建
    • 注册表操作
    • Minifilter回调
  • 显然,内核回调可能会遇到竞争状况。但是,这在尝试主动阻止执行时更为重要,并且不会影响检测功能。

尽管可能仅在过程创建中使用驱动程序回调,但是线程创建和图像加载会通知我们的内存扫描,这样做并不能解决前面所述的问题。这就是为什么防御者必须求助于钩子,甚至在历史上甚至采用内核模式钩子来获得对所有内存和进程管理操作的可见性的原因。

Userland-hooking

由于有了Userland-hook,我们可以拦截实际的内存管理(和其他)API调用,并分别进行处理以提供更好的可见性,准确性和性能。而且,我们不再处于Microsoft的宽限期,只能拦截我们认为对检测有用的任何东西。

作为示例-我们可以绕过诸如此类的Native API并在运行时扫描传递给它的缓冲区,而不是连续扫描进程内存中是否存在恶意代码。或者,我们可以监视对的调用的子集,并根据调用参数触发扫描。NtWriteVirtualMemoryNtAllocateVirtualMemory

在许多注入技术的情况下,对传递给调用的参数的可见性可能足以执行高保真检测,而无需对目标内存段进行额外的扫描。 一个很好的例子是将APC例程放在已加载模块的外部。

这种方法也不是完美的:

  • 可以恢复功能序言(“取消钩子”)
  • 诸如基于APC的EarlyBird注入之类的技术允许在加载保护DLL之前执行代码,并花一些时间应用钩子(包括ntdll中的钩子)
  • 直接系统调用绕过用户域挂钩,自2005年以来,在x64系统上无法进行内核区域挂钩
    • 这也意味着对DOUBLEPULSAR在2017年初所展示的源自内核的注射剂没有任何了解

Mm和APC内核跟踪

在2018年末,即广泛的DOUBLEPULSAR感染使安全供应商明显需要将内核级别的钩子插入通用进程注入API之后的一年,Windows 10 build 1809添加了一个新的内核工具来执行此操作。

通过名为“ Microsoft-Windows-Threat-Intelligence ”的ETW提供程序可跟踪从内核模式内存管理和APC API截获的事件,这些事件可用于订阅以PPL-Antimalware保护级别运行的进程。实际上,这意味着只有具有适当签名证书的Microsoft认可的安全供应商才能使用此供稿。

例如,在最新的Windows 10 x64版本上,所有内存分配API最终都会到达,这是从中调用关联的日志记录功能的地方,并且-假设启用了跟踪-记录有关该调用的所有上下文信息。nt!MiAllocateVirtualMemoryEtwTiLogAllocExecVm

我们可以通过检查内核映像中的外部参照来列出所有威胁情报日志记录功能。EtwThreatIntProvRegHandle

除了确保记录内核到用户模式的注入外,这种实现方式使所有旁路尝试都非常困难,这与ntdll内联挂钩不同。而且,如果不以用户模式运行,则意味着我们不会像DotNet CLR ETW和其他用户模式会话提供程序那样被修补:

尽管已针对传感器的APC逻辑进行了一些绕过的研究并已成功尝试,但我们尚未见到任何一种类型的过程注入所需的基本内存管理操作(分配/内存写/保护掩码更改)的好方法。

http://rce4fun.blogspot.com/2019/03/examining-user-mode-apc-injection.html

https://medium.com/@philiptsukerman/bypassing-the-microsoft-windows-threat-intelligence-kernel-apc-injection-sensor-92266433e0b0

活动类型

传感器记录至少14个不同的内存和线程管理事件(分为和组),其中远程表示API调用过程不同于目标进程,而本地表示一个进程在其自己的内存空间中进行更改。LOCALREMOTE

KERNEL_THREATINT_TASK_ALLOCVM_REMOTE
KERNEL_THREATINT_TASK_PROTECTVM_REMOTE
KERNEL_THREATINT_TASK_MAPVIEW_REMOTE
KERNEL_THREATINT_TASK_QUEUEUSERAPC_REMOTE
KERNEL_THREATINT_TASK_SETTHREADCONTEXT_REMOTE
KERNEL_THREATINT_TASK_ALLOCVM_LOCAL
KERNEL_THREATINT_TASK_PROTECTVM_LOCAL
KERNEL_THREATINT_TASK_MAPVIEW_LOCAL
KERNEL_THREATINT_TASK_QUEUEUSERAPC_LOCAL
KERNEL_THREATINT_TASK_SETTHREADCONTEXT_LOCAL
KERNEL_THREATINT_TASK_READVM_LOCAL
KERNEL_THREATINT_TASK_WRITEVM_LOCAL
KERNEL_THREATINT_TASK_READVM_REMOTE
KERNEL_THREATINT_TASK_WRITEVM_REMOTE

可以想象,这些操作非常普遍而且非常嘈杂。事实如此,以至于Microsoft的人们决定甚至不将其中的大多数发送给使用默认(置零)的“ any / all”LOCAL关键字bitmask进行注册的事件使用者。

事件本身包含大量有用字段,包括源和目标进程签名信任级别或结构中的1:1详细信息。某些事件,例如,可能包括所有寄存器的当前状态,调用时指向的VAD中模块的磁盘路径等等。完整的清单清单可以在这里找到。MEMORY_BASIC_INFORMATIONSetThreadContextEIP

KERNEL_THREATINT_TASK_ALLOCVM_REMOTE (
    UInt32 CallingProcessId, 
    FILETIME CallingProcessCreateTime, 
    UInt64 CallingProcessStartKey, 
    UInt8 CallingProcessSignatureLevel, 
    UInt8 CallingProcessSectionSignatureLevel, 
    UInt8 CallingProcessProtection, 
    UInt32 CallingThreadId, 
    FILETIME CallingThreadCreateTime, 
    UInt32 TargetProcessId, 
    FILETIME TargetProcessCreateTime, 
    UInt64 TargetProcessStartKey, 
    UInt8 TargetProcessSignatureLevel, 
    UInt8 TargetProcessSectionSignatureLevel, 
    UInt8 TargetProcessProtection, 
    UInt32 OriginalProcessId, 
    FILETIME OriginalProcessCreateTime, 
    UInt64 OriginalProcessStartKey, 
    UInt8 OriginalProcessSignatureLevel, 
    UInt8 OriginalProcessSectionSignatureLevel, 
    UInt8 OriginalProcessProtection, 
    Pointer BaseAddress, 
    Pointer RegionSize, 
    UInt32 AllocationType, 
    UInt32 ProtectionMask
)

数据探索

因为NTOSKRNL只会将收集到的日志转发到受PPL保护的进程,所以获取事件本身并不是一件非常简单的任务。最简单的方法涉及使用自签名证书,配置为以开头的自定义服务可执行文件以及ELAM驱动程序。全部处于测试签名模式SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT

幸运的是,@ pathtofile已经完成并记录了此问题,因此我们可以很懒惰,并使用他的ppl_runner作为PPL启动SilkETWSealighter会话。两种工具都会将事件序列化为一系列JSON

https://blog.tofile.dev/2020/12/16/elam.html

对于第一个检测用例,我决定看一下简单的东西,但是对于将任何东西注入到外部进程中来说都是至关重要的-远程VM分配-更具体地说,仅是那些保护意味着将来执行代码的东西。

在收集事件约1小时的同时,我决定通过下载,安装和使用Microsoft Office套件,不同的Web浏览器,Slack,虚拟化软件,Spotify等来模拟一些典型的Office工作站活动。在测试结束时,我执行了8个不同的CobaltStrike和Metasploit可执行文件(分段和无分段),并执行了其他注入操作,以查看它们在数据中的突出表现方式。

针对ALLOCVM_REMOTE进行绘图显示,所有收集的合法事件均针对最小大小的内存段(4 kB)和 分配类型,而所有攻击框架注入均为有效负载分配170 kB至350 kB之间的空间,并使用选项。AllocationTypeRegionSizesMEM_COMMITMEM_COMMIT | MEM_RESERVE

在这种情况下,恶意分配也使用了保护常数,但这很容易避免。PAGE_EXECUTE_READWRITE

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

with open('output.json', 'r') as f:
    data = f.readlines()

# remove the trailing "\n" from each line
data = map(lambda x: x.rstrip(), data)

# each element of 'data' is an individual JSON object.
data_json_str = "[" + ','.join(data) + "]"

data_df = pd.read_json(data_json_str)

# keep only KERNEL_THREATINT_TASK_ALLOCVM_REMOTE events
data_df = data_df[pd.json_normalize(data_df['header'])['event_id']==1]

header_df = pd.json_normalize(data_df['header'])
data_df = pd.json_normalize(data_df['properties'])

sns.set_theme(style="whitegrid")
fig = plt.figure(figsize=(12,7))
sns.violinplot(
    data=data_df,
    x=data_df['AllocationType'],
    y=data_df['RegionSize'],
    palette='magma',
)

小数据样本没有考虑到许多边界情况,例如反作弊,IDE,编译器,防病毒和其他自定义软件,这些情况会随着时间的流逝至少产生一些误报事件。但是,这种单事件检测逻辑仍然是一个有力的指标,具有这些特征的分配由MS Defender ATP本身以及其他一些供应商(至少针对过程的子集)标记。

Windows的Microsoft Defender
F-Secure RDR

一般而言,分配也是进一步发展检测生命周期的一个很好的切入点,因此,我决定编写基于TI ETW的概念验证内存注入检测代理,以进一步研究不同的检测用例,以及了解什么。可以采用使传感器失明的技术。

建立PoC代理

新白路/ TiEtwAgent通过在GitHub上创建一个帐户来为xinbailu / TiEtwAgent开发做出贡献。github.com

该传感器是用C / C ++编写的,并且是Windows服务可执行文件,类似于设置实际EDR / AV代理的方式。

如前所述,要成功注册的事件使用者,我们至少需要以PPL进程的身份运行,而最优雅的方式是使用Microsoft验证的证书(使用代码签名EKU)对软件进行签名,或者使用测试签名模拟这种情况,如先前Microsoft-Windows-Threat-Intelligence@pathtofile的链接文章中所述。我无缘无故选择了后者; )

处理完基本服务+ ELAM驱动程序安装后,我们可以继续进行事件使用-在这里,我选择使用Microsoft的krabsetw库。会话设置非常简单:

  • 创建新的跟踪对象
  • 创建新的提供者对象
  • (可选)添加过滤器并定义任何/所有收集标志-在这里,我决定仅收集一种事件类型是因为检测代码中还没有其他事件的用例,但是,远程事件的数量足够小,因此我们可以安全地跳过此步骤,稍后再执行。 krabs::predicates::id_is((int)KERNEL_THREATINT_TASK_ALLOCVM_REMOTE)
  • 注册一个回调函数
  • 启用并启动跟踪
DWORD agent_worker()
{
    DWORD ret{ 0 };
    log_debug(L"TiEtwAgent: Started the agent worker\n");

    user_trace trace(ETS_NAME);
    provider<> provider(L"Microsoft-Windows-Threat-Intelligence");
    event_filter filter(predicates::id_is((int)KERNEL_THREATINT_TASK_ALLOCVM_REMOTE));

    try {
        log_debug(L"TiEtwAgent: Setting up the trace session\n");
        provider.add_on_event_callback(parse_generic_event);
        provider.add_filter(filter);
        trace.enable(provider);

        trace.start();
    }
    catch (...) {
        log_debug(L"TiEtwAgent: Failed to setup a trace session\n");
        trace.stop();
    }
   
    ret = GetLastError();
    return ret;
}

成功解析的事件已准备好通过作为一组独立功能实现的检测逻辑进行处理(在撰写本文时,实际上只有3个功能,但我们将在下一篇文章中进行研究)。DetectionLogic.cpp

一种检测功能的布尔输出,可以触发另一种检测功能。非常程序化,无计分等

VOID parse_generic_event(const EVENT_RECORD& record, const trace_context& trace_context) {
    schema schema(record, trace_context.schema_locator);
    parser parser(schema);  

    GenericEvent new_event;

    new_event.type = schema.event_id();

    try {
        for (property property : parser.properties()) {
            wstring wsPropertyName = property.name();
            if (new_event.fields.find(wsPropertyName) != new_event.fields.end()) {
                switch (property.type()) {
                case TDH_INTYPE_UINT32:
                    new_event.fields[wsPropertyName] = parser.parse<uint32_t>(wsPropertyName);
                    break;
                case TDH_INTYPE_POINTER:
                    new_event.fields[wsPropertyName] = parser.parse<pointer>(wsPropertyName).address;
                    break;
                }
            }
        }
    }

    catch (...) {
        log_debug(L"Error parsing the event\n");
        return;
    }

    if (new_event.fields.empty()) {
        log_debug(L"TiEtwAgent: Failed to parse an event\n");
    }
    else {
        detect_event(new_event);
    }
    return;
}
// Simple detection relying on metadata of the allocated memory page
const int ALLOC_PROTECTION{ PAGE_EXECUTE_READWRITE };
const int ALLOC_TYPE{ MEM_RESERVE | MEM_COMMIT };
const int MIN_REGION_SIZE{ 10240 };

DWORD allocvm_remote_meta_generic(GenericEvent alloc_event) {
    if (alloc_event.fields[L"RegionSize"] >= MIN_REGION_SIZE) {
        if (alloc_event.fields[L"AllocationType"] == ALLOC_TYPE) {
            if (alloc_event.fields[L"ProtectionMask"] == ALLOC_PROTECTION) {
                report_detection(ALLOCVM_REMOTE_META_GENERIC, alloc_event);
                return TRUE;
            }
        }
    }
    return FALSE;
}

这是到目前为止的效果

(应该已经在DOUBLEPULSAR示例中显示了它…)

from

Leave a Reply

您的电子邮箱地址不会被公开。