攻击者目前利用 RPC 的客户端-服务器架构来混淆其在主机上的活动,包括基于 RPC 的 COM 和 WMI。例如,许多本地 RPC 服务器会很乐意代表恶意客户端启动进程,并且如果无法将其与客户端关联,则很难将这种形式的防御规避标记为恶意。
上面的带注释的屏幕截图是 Microsoft Word 宏调用三个 COM 对象(每个对象都公开一个 ShellExecute
接口)以及 WMI Win32\_Process::Create
方法之后的逻辑进程树。WMI 调用具有专门的遥测功能,可以重建 Microsoft Word 发起的进程创建(蓝色箭头),但 COM 调用没有(红色箭头)。因此,防御者无法看到 Microsoft Word 通过 RPC 调用来生成系统其他位置的 PowerShell 的 COM 调用。
由于缺乏上下文,防御者在解释时面临挑战 - Word 生成 PowerShell 是一个危险信号,但是是 *Explorer* 生成 PowerShell 是恶意的,还是仅仅是用户行为?
RPC 通常使用 LRPC 作为进程间通信的传输。本文以进程创建为例,概述了迄今为止的规避与检测之间的军备竞赛,描述了当前某些检测方法的弱点,然后探讨了基于 LRPC 的通用规避方法。
子进程规避简史
在入侵期间生成子进程通常对攻击者非常有益。使用预安装的合法系统工具来实现目标可以节省功能开发时间,并且可以通过为活动提供合法的外观来潜在地规避安全工具。
但是,为了使活动看起来合理,父进程也需要看起来合理。一个经典的例子是 Microsoft Word 生成 PowerShell 非常反常。实际上,Elastic SIEM 包含一个预构建的规则来检测可疑的 MS Office 子进程,而 Elastic Endpoint 也会阻止恶意执行。如 Elastic 全球威胁报告中所述,可疑的父/子关系是 2022 年威胁使用的三种最常见的防御规避技术之一。
端点保护平台 (EPP) 产品可以阻止最恶劣的进程父级关系,但随着普遍的进程启动日志记录和回顾性搜索能力的发展,端点检测和响应 (EDR) 方法的兴起确立了可扩展的异常进程树检测方法。
攻击者最初转向使用 Windows Vista 中引入的 Win32 API 功能来支持用户帐户控制 (UAC) 的规避方法,该功能允许进程指定与实际调用进程不同的逻辑父进程。但是,基于 进程创建通知回调期间的调用进程上下文,端点安全仍然可以识别真实的父进程,并且很快就重新建立了检测规则覆盖范围。
作为回应,新的规避技术不断发展,目前攻击者利用的一种常见方法是通过 RPC 间接生成子进程,包括基于 RPC 的 DCOM 和 WMI。RPC 可以是主机间的,也可以只是进程间的。后者被自相矛盾地称为本地远程过程调用 (LRPC)。
其中最著名的是 Win32\_Process::Create
WMI 方法。为了检测到这一点,Microsoft 似乎在 Windows 10 1809 中显式添加了一个新的 Microsoft-Windows-WMI-Activity
ETW 事件。新事件 23 包括客户端进程 ID,这是将活动与请求客户端关联所需的缺失数据点。
不幸的是,攻击者很快就能够转向备用的进程生成进程外 RPC 服务器,例如 MMC20.Application::ExecuteShellCommand
。等待 Microsoft 将遥测添加到双用途的进程外 RPC 服务器逐个不是可行的检测方法,因此去年我们开始了一项分支任务,以将 LRPC 服务器操作与请求 LRPC 客户端进程进行通用关联。
检测 LRPC 出处
之前的大多数公共 RPC 遥测研究都侧重于主机间的横向移动,通常是在远程主机上生成进程。例如: - 使用 MMC20.Application COM 对象的横向移动- 通过 DCOM 的横向移动:第二轮- 远程服务创建和 PsExec 的端点检测 - 利用 RPC 遥测- 使用事件查询语言检测横向移动技术 - 通过 RPC 防火墙阻止横向移动
对于防御者来说,最终的建议通常是监视 RPC 网络流量中的异常情况,或者更好的是,使用 RPC 过滤器(Windows 过滤平台的一部分)或使用 RPC 防火墙等第三方工具阻止不必要的对 RPC 接口的远程访问。
不幸的是,当攻击者使用 RPC 在同一主机上的其他地方生成进程时,这些方法不起作用。在这种情况下,RPC 传输通常是 ALPC - 网络层的监控和过滤不再适用。
在主机上,检测工程师通常首先会利用内置事件跟踪(包括 EventLog)中的遥测数据。如果这证明不足,则他们可以研究自定义方法,例如用户模式函数挂钩或微型筛选器驱动程序。
在 RPC 案例中,Microsoft-Windows-RPC
ETW 事件对于识别异常行为非常有用。
特别是:- 事件 5 - RpcClientCallStart
(GUID InterfaceUuid, UInt32 ProcNum, UInt32 Protocol, UnicodeString NetworkAddress, UnicodeString Endpoint, UnicodeString Options, UInt32 AuthenticationLevel, UInt32 AuthenticationService, UInt32 ImpersonationLevel) - 事件 6 - RpcServerCallStart
(GUID InterfaceUuid, UInt32 ProcNum, UInt32 Protocol, UnicodeString NetworkAddress, UnicodeString Endpoint, UnicodeString Options, UInt32 AuthenticationLevel, UInt32 AuthenticationService, UInt32 ImpersonationLevel)
此外,RpcClientCallStart
由客户端生成,RpcServerCallStart
由服务器生成,因此 ETW 标头将分别提供客户端和服务器进程 ID。此外,端点地址和服务器进程 ID 之间存在 1:1 的映射。因此,可以从 RpcClientCallStart
事件中推断出服务器进程。
RPC 接口 UUID 和过程编号与调用者详细信息相结合,通常足以识别意图。例如,RPC 接口 UUID {367ABB81–9844–35F1-AD32–98F038001003}
是服务控制管理器远程协议,该协议公开了配置 Windows 服务的功能。此接口中的第 12 个过程是 RCreateServiceW
,众所周知,PsExec 使用此方法在远程系统上执行进程。
然而,对于端点安全供应商来说,在实现可扩展的可靠 Microsoft-Windows-RPC
检测之前,需要解决几个问题:1. RPC 事件量很大;2. 没有明显的机制可以将客户端调用与生成的服务器调用强关联;3. 没有明显的机制可以将服务器调用与生成的服务器行为强关联。
让我们逐一解决这三个问题。
LRPC 事件量
每秒有数千个 LRPC 事件——其中大多数都是无关紧要的。为了解决 LRPC 事件量的问题,我们可以将事件限制为仅那些进程间的 RPC 事件(包括跨主机的)。然而,这立即引出了第二个问题。我们需要识别每个服务器调用的客户端,以便将事件量减少到仅那些进程间的事件。
将 RPC 服务器调用与其客户端关联
现代 Windows RPC 大致有三种传输方式:- TCP/IP (nacn_ip_tcp、nacn_http、ncadg_ip_udp 和通过 SMB 的 nacn_np) - 进程间命名管道 (direct nacn_np) - 进程间 ALPC (ncalrpc)
仅靠 RpcServerCallStart
事件不足以确定调用是否是进程间的。它需要与之前的 RpcCientCallStart
事件关联,并且这种关联不幸的是很弱。最多,您可以识别一对 RpcServerCall
开始/停止事件,它们被具有相同参数的一对 RpcClientCall
事件括起来。(注意 - 出于性能原因,从不同线程生成的 ETW 事件可能会乱序到达)。这意味着您需要维护一个整体的 RPC 状态 - 这会在主机上产生存储和处理量的问题,以解决事件量的问题。
但更重要的是,RpcClientCallStart
事件是在攻击者已经获得执行权限的客户端进程中生成的,因此可以很容易地被拦截。对于如此容易规避的事情实施检测毫无意义,尤其是在有更有效的选择时。
理想情况下,RPC 服务器将访问客户端详细信息并直接记录此信息。不幸的是,ETW 事件不包含此信息 - 这并不奇怪,因为 RPC 设计目标之一是通过抽象来简化。据称,RPC 运行时可以通过组策略进行配置来执行此操作。它可以存储 RPC 状态信息,然后可以在调试期间使用这些信息来从服务器线程识别客户端调用者。不幸的是,Windows XP 时代的文档并没有立即适用于 Windows 10。
它确实提供了一个粗略的概述,描述了如何解决前两个问题:减少事件量以及将服务器调用与客户端进程关联。可以挂钩所有 RPC 服务器中的 RPC 运行时,考虑各种传输方式,然后仅记录或过滤进程间的 RPC 事件。(这可能类似于 RPC 防火墙处理网络 RPC 的方式 - 只是使用本地端点)。
将 RPC 服务器调用与生成的行为关联
下一个问题是如何将特定的服务器调用正确地归因于生成的服务器行为。在繁忙的服务器上,我们如何将不透明的调用与 ExecuteShellCommand
方法绑定到特定的进程创建事件?如果调用来自基于脚本的恶意软件,并且进一步包装在类似 IDispatch::Invoke
的方法下,又该如何处理?
我们不想检查 RPC 参数 blob 并为每个可滥用的 RPC 方法单独实现解析支持。
引入 ETW 的 ActivityId
值得庆幸的是,微软已经考虑到了这种情况,并为开发人员提供了 ETW 跟踪指南。
他们建议开发人员在相关的 ETW 事件之间生成和传播唯一的 128 位 ActivityId
,以实现端到端跟踪场景。对于在与该值存储在线程本地存储中的同一线程上生成的事件,ETW 通常会自动处理此操作。但是,开发人员必须手动将此 ID 传播到其他线程或进程执行的相关活动。只要 RPC 运行时和所有 Microsoft RPC 服务器都遵循了 ETW 跟踪最佳实践,我们最终应该可以实现我们想要的端到端关联!
是时候使用反编译器了(我们喜欢 Ghidra,但有很多选择)并检查 rpcrt4.dll。通过查看传递给 EventRegister
调用的第一个参数,我们可以看到 RPC 运行时中有三个 ETW GUID。这些 GUID 在一个连续的块中定义,并且有公共符号,这很有帮助。
这些 GUID 分别对应于 Microsoft-Windows-RPC
、Microsoft-Windows-Networking-Correlation
和 Microsoft-Windows-RPC-Events
。此外,RPC 运行时在两个地方方便地包装了对 EventWrite
的调用。
第一次调用在 McGenEventWrite\_EtwEventWriteTransfer
中,看起来像这样
`EtwEventWriteTransfer` (RegHandle, EventDescriptor, NULL, NULL, UserDataCount, UserData);
NULL 参数意味着 ActivityId
将始终是每个线程配置的 ActivityId
,并且 RelatedActivityId
将始终在此代码路径记录的事件中排除。
第二次调用在 EtwEx\_tidActivityInfoTransfer
中,看起来像这样
`EtwEventWriteTransfer` (Microsoft_Windows_Networking_CorrelationHandle, EventDescriptor, ActivityId, RelatedActivityId, UserDataCount, UserData);
这意味着 RelatedActivityId
将仅记录在 Microsoft-Windows-Networking-Correlation
事件中。RPC 运行时 ActivityId
通常在辅助函数中创建,以确保始终记录此关联。
反编译还显示,RPC 运行时通过调用 UuidCreate
来分配 ETW ActivityId
,该函数会生成一个随机的 128 位值。这在诸如 NdrAysncClientCall
和 HandleRequest
等位置完成。换句话说,客户端和服务器都各自独立分配 ActivityId
。这并不令人惊讶,因为 DCE/RPC 规范似乎没有包含事务 ID 或类似的构造,允许客户端将 ActivityId 传播到服务器。不过没关系:我们目前只缺少服务器调用和由此产生的行为之间的关联。此外,我们不希望信任任何可能被污染的客户端提供的信息。
所以现在我们确切地知道 RPC 如何关联由 RPC 调用触发的活动:通过设置每个线程的 ETW ActivityId
,并将 RPC ActivityId 关联记录到 Microsoft-Windows-Networking-Correlation
。下一个问题是,支持双重用途活动(例如进程生成)的 Microsoft RPC 接口是否能适当地传播 ActivityId
。
我们查看了初始案例研究中四个间接进程创建示例的执行跟踪。在每个示例中,RPC 请求在一个线程上接收,第二个线程处理该请求,第三个线程生成进程。除了时间之外,似乎没有可能的机制来链接这些活动。
不幸的是,虽然 RPC 子系统行为良好,但大多数 RPC 服务器并非如此——尽管这可能不完全是它们的错。ActivityId
仅在每个线程中保留,因此如果服务器使用工作线程池(根据 Microsoft 的 RPC 可伸缩性 建议),则因果关系关联会隐式中断。
此外,内核 ETW 事件似乎普遍记录 ActivityId
为 {00000000-0000-0000-0000-000000000000}
——即使该线程已配置了(用户模式)ActivityId
。可能是内核的 EtwWriteEvent
实现根本没有查询存储在用户模式线程本地存储中的 ActivityId
。
关于内核事件的这一观察对于基于 ETW 的通用方法来说是一个严重的阻碍。几乎所有有趣的服务器行为结果(进程、注册表、文件等)都由内核 ETW 事件记录。
有必要采用新的方法。在双重用途 RPC 服务器中调查单个 ETW 提供程序是不可扩展的。(尽管 Microsoft.Windows.ShellExecute
TraceLogging 提供程序看起来很有趣)。微软会怎么做?
微软会怎么做?
更具体地说,Microsoft 如何在 Microsoft-Windows-WMI-Activity
ETW 事件 23(又名 Win32_Process::Create
)中填充 ClientProcessId
?
`task_023` (UnicodeString CorrelationId, UInt32 GroupOperationId, UInt32 OperationId, UnicodeString Commandline, UInt32 CreatedProcessId, UInt64 CreatedProcessCreationTime, UnicodeString ClientMachine, UnicodeString ClientMachineFQDN, UnicodeString User, UInt32 ClientProcessId, UInt64 ClientProcessCreationTime, Boolean IsLocal)
与 RPC 不同,WMI 通过 CorrelationId
本机支持端到端跟踪,这是一个 GUID,WMI 客户端在 WMI 层传递给服务器,以便可以将 WMI 操作相关联。但是,对于安全用例,由于前面提到的原因,我们不应该盲目信任客户端提供的信息。
但是,Microsoft 如何确定要记录的进程 ID,并且它们的方法是否可以通过 RPC 服务器运行时挂钩复制到其他 RPC 服务器?
我们需要找出该字段中的数据来自哪里。ETW 方便地提供了在生成事件时记录堆栈跟踪的功能,Sealighter 工具方便地公开了此功能。Sealighter 说明了哪个进程正在调用哪个特定的 ETW Write 函数。
在这种情况下,该事件实际上是由 WMI Core Service (svchost.exe -k netsvcs -p -s Winmgmt) 中的 ntdll!EtwEventWrite
写入的,而不是在 WMI 提供程序主机 (WmiPrvSE.exe) 中。
在 PublishWin32ProcessCreation
上设置断点后,我们通过参数值检查看到 ClientProcessId
作为第 10 个参数传递(在堆栈上)。然后,我们可以查看 InspectWin32ProcessCreateExecution
以确定如何确定传入的值。
InspectWin32ProcessCreateExecution
的大致清理过的 Ghidra 反编译可能类似于这样
我们可以看到客户端进程 ID 来自 CWbemNamespace
对象。搜索对该结构字段的引用,我们发现它仅在 CWbemNamespace::Initialize
中设置。我们之前的堆栈跟踪从 wbemcore!CCoreQueue
开始,并且此初始化似乎发生在排队之前。因此,我们可以静态搜索所有发生初始化的位置,或者动态观察所采用的实际代码路径。
我们知道此活动是通过 RPC 发起的,因此一种方法是在客户端和服务器中的 RPC 发送/接收函数上设置断点。另一种方法可能是启动 Wireshark 并检查在网络上以明文形式发生整个交互时的抓包。我们在研究的后期才了解到,Microsoft 对 WMI 协议初始化 提供了出色的文档,其中解释了其中的大部分内容,并且可能节省了一些时间。
我们采用了第一种方法。InspectWin32ProcessCreateExecution
的第二个参数是一个 IWbemContext
– 它允许调用者向提供程序提供其他信息。这正是 Win32_Process::Create
的参数的传递方式。如果第一个参数与 WMI 客户端向 WMI Core 传递额外上下文有关呢?
IWbemLevel1Login::NTLMLogin
在调用跟踪中脱颖而出,是一个不错的开始寻找的地方。
紧挨着它的 COM 接口 UUID 的是 IWbemLoginClientID[Ex],它有一个非常有趣的 SetClientInfo
调用,该调用已在 MSDN 上记录。
WMI 客户端调用 wbemprox!SetClientIdentity
,它大致如下所示
IWbemLoginClientIDEx
目前未记录,但我们可以从传递的值中推断出参数。
此时,客户端进程似乎正在将 ClientMachineName
、ClientMachineFQDN
、ClientProcessId
和 ClientProcessCreationTime
传递给 WMI Core。我们可以通过更改这些值并查看 WMI Core 记录的 ETW 事件是否发生变化来确认这一点。
使用 WinDbg,我们对 WMI 客户端进程设置了几个快速补丁,然后通过 WMI 生成了一个进程
windbg> bp wbemprox!SetClientIdentity+0xff "eu @rdx \"SPOOFED....\"; gc"
windbg> bp wbemprox!SetClientIdentity+0x1c4 "r r9=0n1337; eu @r8 \"SPOOFED.COM\"; gc"
PS> ([wmiclass]"ROOT\CIMv2:Win32_Process").Create("calc.exe")
使用 SilkETW(或其他 ETW 捕获机制),我们看到服务器进程的以下事件
服务器盲目地报告客户端提供的值。这意味着此事件不能用于修复 WMI 进程来源树,因为对手可以控制客户端进程 ID。错误地报告此信息将是一种有趣的防御规避,并且很难可靠地识别。
此外,远程攻击者实际上可以传入一个等于本地主机名的 ClientMachine
名称,并且此 WMI 事件会错误地将 IsLocal 记录为 true。(请参阅前面 InspectWin32ProcessCreateExecution
的反编译)。这将使该事件看起来像是可疑的本地执行,而不是横向移动,并且代表了另一种防御规避机会。
因此,其他 RPC 服务器毕竟不应遵循这种方法。
结论
在尝试通用地解决 LRPC 来源时,我们不幸地证明了现有的一个 LRPC 来源数据点是不可靠的。这已报告给 Microsoft,并被评估为下一个版本的候选错误,将在未来的版本中进行评估。
我们热切希望最终的解决方案涉及创建一个记录在案的 API,该 API 允许服务器 LRPC 线程确定连接的客户端线程。这将为端点安全产品提供可靠的机制,以识别尝试隐藏其来源的通过 LRPC 调用代理的操作。
但更一般而言,这项研究强调了防御者需要详细了解数据来源。了解数据是由内核或服务器进程等可信来源记录是必要的,但还不够。此外,您还必须了解数据是事件固有的还是由可能不可信的客户端提供的。否则,对手将利用这些漏洞。