Offensive Windows IPC Internals 3: ALPC

原文连接:

https://csandker.io/2022/05/24/Offensive-Windows-IPC-3-ALPC.html

介绍

在谈论了两种可以远程和本地使用的进程间通信(IPC)协议之后,即命名管道RPC,现在我们正在研究一种只能在本地使用的技术。虽然RPC代表Remote Procedure Call,但ALPC读出Advanced Local Procedure Call,有时也引用为A同步Local Procedure Call。特别是后面的引用(异步)是对Windows Vista时代的引用,当时ALPC被引入以取代LPC(本地过程调用),LPC是Windows Vista兴起之前使用的前身IPC机制。

关于LPC
的简短说明 本地过程调用机制是在1993-94年与原始Windows NT内核一起引入的,作为同步进程间通信工具。它的同步特性意味着客户端/服务器必须等待消息被调度并对其执行操作,然后才能继续执行。这是ALPC旨在取代的主要缺陷之一,也是ALPC被一些人称为异步LPC的原因。
ALPC在Windows Vista中曝光,至少从Windows 7开始,LPC完全从NT内核中删除。为了不破坏遗留应用程序并允许向后兼容性(Microsoft 以此而闻名),保留了用于创建 LPC 端口的函数,但函数调用被重定向到不创建 LPC,而是创建 ALPC 端口。

LPC CreatePort in Windows 7CreatePort API Call in Windows 7

由于LPC实际上已经从Windows 7开始消失了,这篇文章将只关注ALPC,所以让我们回到它。
但是,如果你像我一样喜欢阅读旧的(er)文档,了解事情是如何开始的,以及事情过去是如何工作的,这里有一篇文章详细介绍了LPC过去如何在Windows NT 3.5中工作:http://web.archive.org/web/20090220111555/http://www.windowsitlibrary.com/Content/356/08/1.html

回到ALPC
ALPC是一个快速,非常强大且在Windows操作系统(内部)内非常广泛使用的进程间通信设施,但它不打算被开发人员使用,因为对Microsoft ALPC来说是一个内部IPC设施,这意味着ALPC是无文档的,仅用作其他,有文档记录和预期供开发人员使用的消息传输协议的基础传输技术, 例如 RPC。
然而,ALPC是未记录在案的(由微软)的事实并不意味着ALPC是一个完全的黑匣子,因为像Alex Ionescu这样的聪明人已经对它的工作原理以及它有什么组件进行了逆向工程。但它的真正含义是,您不应该依赖任何ALPC行为进行任何长期生产使用,甚至更确切地说,您不应该直接使用ALPC来构建软件,因为有很多不明显的陷阱可能会导致安全性或稳定性问题。
如果你在阅读这篇文章后觉得你可以在ALPC上听到另一个声音,我强烈建议你听Alex在SyScan’14上的ALPC演讲,特别是当Alex谈论从ALPC服务器发布映射视图(这只是解决视图)所需的步骤时,请竖起耳朵,这让你在演讲的第33分钟左右。

所以我在这里说的是:

ALPC是一个非常有趣的目标,但不适用于(非微软)在生产开发中的使用。此外,您也不应该依赖本文中的所有信息是或继续是100%准确的,因为ALPC是无证的。

ALPC 内部件

好吧,让我们进入一些ALPC内部,以了解ALPC的工作原理,通信中涉及哪些移动部件以及消息的外观,以便最终从进攻性安全的角度了解为什么ALPC可能是一个有趣的目标。

基础知识

为了从地面起飞,应该注意ALPC通信的主要组件是ALPC端口对象。ALPC 端口对象是内核对象,其用法类似于使用网络套接字,其中服务器打开客户端可以连接到的套接字以交换消息。
如果您从Sysinternals Suite启动WinObj,您会发现每个Windows操作系统上运行着许多ALPC端口,可以在根路径下找到一些端口,如下所示:

WinObj 中根路径下的 ALPC 端口根路径下的 ALPC 端口

…但大多数ALPC端口都位于“RPC控制”路径下(请记住,RPC在引擎盖下使用ALPC):

WinObj_ALPC-Ports_RPC-控制.png在 \\RPC 控制下的 ALPC

要开始 ALPC 通信,服务器会打开客户端可以连接到的 ALPC 端口,该端口称为 ALPC 连接端口,但是,这并不是在 ALPC 通信流中创建的唯一 ALPC 端口(您将在下一章中看到)。另外两个 ALPC 端口是为客户端和服务器创建消息传递到的。
因此,首先要记下心理上的是:

  • ALPC 通信中总共涉及 3 个 ALPC 端口(服务器端 2 个,客户端 1 个)。
  • 您在上面的 WinObj 屏幕截图中看到的端口是 ALPC 连接端口,它们是客户端可以连接到的端口。

尽管 ALPC 通信中总共使用了 3 个 ALPC 端口,并且它们都由不同的名称(如“ALPC 连接端口”)引用,但只有一个 ALPC 端口内核对象,ALPC 通信中使用的所有三个端口都实例化了该对象。此 ALPC 内核对象的骨架如下所示:

ALPC Kernel Object_ALPC_PORT内核结构

从上面可以看出,ALPC 内核对象是一个相当复杂的内核对象,引用了各种其他对象类型。这使它成为一个有趣的研究目标,但也为错误和/或错过的攻击路径留下了一些很好的余地。

ALPC 消息流

为了更深入地了解 ALPC,我们将了解 ALPC 消息流,以了解消息的发送方式以及这些消息的外观。首先,我们已经了解到,ALPC 通信方案中涉及 3 个 ALPC 端口对象,第一个对象是由服务器进程创建且客户端可以连接到的 ALPC 连接端口(类似于网络套接字)。客户端连接到服务器的 ALPC 连接端口后,内核将创建两个新端口,称为 ALPC 服务器通信端口和 ALPC 客户端通信端口

ALPC 端口对象关系ALPC 端口对象关系

建立服务器和客户端通信端口后,双方都可以使用 ntdll.dll 公开的单个函数相互发送消息。
这个函数的名称听起来像是三件事 – 发送,等待和接收 – 这正是它。服务器和客户端使用此单个函数在其 ALPC 端口上等待消息、发送消息和接收消息。这听起来不必要的复杂,我不能确切地告诉你为什么它是这样构建的,但这是我的猜测:请记住,ALPC是作为一个快速且仅限内部的通信工具创建的,并且通信通道是围绕单个内核对象(ALPC端口)构建的。使用此3路函数允许在单个调用中执行多个操作,例如发送和接收消息,从而节省时间并减少用户内核-陆地切换。此外,此功能充当消息交换过程的单个门,因此允许更容易的代码更改和优化(ALPC通信用于许多不同的操作系统组件,从内核驱动程序到由不同内部团队开发的用户GUI应用程序)。最后,ALPC旨在作为仅内部IPC机制,因此Microsoft不需要主要设计用户或第三方开发人员友好。
在这个单个函数中,您还可以指定要发送的消息类型(有不同的类型具有不同的含义,我们稍后将对此进行介绍)以及要与消息一起发送的其他属性(稍后我们将在ALPC消息属性一章中再次介绍您可以与消息一起发送的内容)。NtAlpcSendWaitReceivePort

到目前为止,这听起来非常简单:服务器打开一个端口,一个客户端连接到它,两者都接收一个通信端口的句柄,并通过单个函数发送消息……容易。
我们将在高层次上轻松,但您肯定来这里了解详细信息,帖子的标题说“内部”,所以让我们系好安全带仔细看看:NtAlpcSendWaitReceivePort

  1. 服务器进程使用选定的ALPC端口名称(例如“CSALPCPort”)调用NtAlpcCreatePort,并可以选择使用SecurityDescriptor来指定谁可以连接到它。
    内核创建一个 ALPC 端口对象,并将此对象的句柄返回给服务器,此端口称为 ALPC 连接端口
  2. 服务器调用 NtAlpcSendWaitReceivePort,将句柄传递到其先前创建的连接端口,以等待客户端连接
  3. 然后,客户端可以使用以下命令调用 NtAlpcConnectPort
    • 服务器的 ALPC 端口 (CSALPCPort) 的名称)
    • (可选)给服务器的消息(例如,发送魔术关键字或其他任何内容)
    • (可选)服务器的 SID,以确保客户端连接到目标服务器
    • (可选)要与客户端的连接请求
      一起发送的消息属性(消息属性将在 ALPC 消息属性一章中详细介绍)
  4. 然后,此连接请求将传递到服务器,服务器调用 NtAlpcAcceptConnectPort 以接受或拒绝客户端的连接请求。
    (是的,尽管该函数名为 NtAlpcAccept…此函数还可用于拒绝客户端连接。此函数 last 参数是一个布尔值,它指定是接受连接(如果设置为 true)还是拒绝连接(如果设置为 false)。
    服务器还可以:
    • (可选)向客户端返回一条消息,其中包含接受或拒绝连接请求和/或…
    • (可选)将消息属性添加到该消息和/或 ..
    • (可选)分配一个自定义结构,例如一个唯一的ID,该结构连接到服务器的通信端口以识别客户端
      — 如果服务器接受连接请求,则服务器和客户端各自接收一个通信端口的句柄 —
  5. 客户端服务器现在可以通过NtAlpcSendWaitReceivePort相互发送和接收消息,其中:
    • 客户端侦听新消息并将其发送到其通信端口
    • 服务器侦听新消息并将其发送到其连接端口
    • 客户端服务器都可以指定它们在侦听新消息时要接收的消息属性(我们稍后将介绍 tht)

…等一会。。。为什么服务器在连接端口而不是其通信端口上发送/接收数据,因为它具有专用的通信端口?…这是在ALPC上让我感到困惑的众多事情之一,我没有做所有繁重的逆转工作来弄清楚这一点,而是作弊并联系了Alex Ionescu,简单地询问了专家。我把答案放在本文末尾的附录A中,因为我不想在这一点上离消息流太远……对不起悬崖衣架…

无论如何,从上面回顾消息流,我们可以推断出客户端和服务器正在使用各种函数调用来创建ALPC端口,然后通过单个函数发送和接收消息。虽然这包含有关消息流的大量信息,但重要的是要始终注意服务器和客户端没有直接的对等连接,而是通过内核路由所有消息,内核负责将消息放在消息队列上,通知每一方收到的消息以及其他事情,例如验证消息和消息属性。为了正确看待这一点,我在这张图片中添加了一些内核调用:NtAlpcSendWaitReceivePort

ALPC 消息流ALPC 消息流

我不得不承认,乍一看,这是图表不是超级直观,但我保证事情会变得更加清晰,忍受我。
为了更全面地了解 ALPC 在引擎盖下的样子,我们需要更深入地了解 ALPC 消息的实现部分,我将在下一节中介绍这些内容。

ALPC 消息详细信息

好的,首先,让我们澄清一下ALPC消息的结构。ALPC 消息始终包含所谓的PORT_HEADERPORT_MESSAGE,后跟要发送的实际消息,例如一些文本、二进制内容或其他任何内容。

ALPC_Kernel_PortMessage_Structure.png_PORT_MESSAGE 内核结构

在普通的旧C++我们可以使用以下两个结构定义ALPC消息:

为了发送消息,我们所要做的就是:

此代码段将向我们连接到的服务器发送一条带有“Hello World!”正文的 ALPC 消息。我们将消息指定为具有标志的同步消息,这意味着此调用将等待(阻止),直到在客户端的通信端口上收到消息。
当然,我们不必等到新消息出现,而是将时间用于其他任务(请记住,ALPC的构建是异步的,快速的和高效的)。为了便于 ALPC 提供三种不同的消息类型:ALPC_MSGFLG_SYNC_REQUEST

  • 同步请求:如上所述,同步消息块直到新消息传入(作为逻辑结果,在使用同步消息调用时必须指定接收ALPC消息缓冲区)NtAlpcSendWaitReceivePort
  • 异步请求:异步消息发送消息,但不等待任何收到的消息或对任何接收的消息执行操作。
  • 数据报请求:数据报请求类似于UDP数据包,它们不需要回复,因此内核在发送数据报请求时不会阻止等待收到的消息。

因此,基本上,您可以选择发送需要回复的消息或不需要回复的消息,当您选择前者时,您还可以选择等到回复进来,或者不要等待,同时使用宝贵的CPU时间做其他事情。这给你留下了一个问题,即如果您选择了最后一个选项而不是在函数调用中等待(异步请求),如何接收回复?
再一次,您有3个选择:NtAlpcSendWaitReceivePort

  • 您可以使用 ALPC 完成列表,在这种情况下,内核不会通知您(作为接收方)已收到新数据,而只是将数据复制到进程内存中。由您(作为接收者)来了解这些新数据的存在。例如,这可以通过使用您与 ALPC 服务器¹ 之间共享的通知事件来实现。一旦服务器发出事件信号,您就知道新数据已到达。
    ¹Taken 自 Windows Internals, Part 2, 7th Edition
  • 您可以使用 I/O 完成端口,这是一个有文档记录的同步工具。
  • 您可以接收内核回调来获取回复 – 但仅当您的进程位于内核环境中时才允许这样做。

由于您可以选择不直接接收消息,因此不太可能有多个消息进入并等待被获取。为了处理不同状态下的多条消息,ALPC 使用队列来处理和管理为服务器堆积的大量消息。消息有五个不同的队列,为了区分它们,我将直接从Windows Internals,Part 2,7th Edition的第8章中引用(因为没有更好的方法来用这几个词来表达这一点):

  • 主队列:已发送消息,客户端正在处理该消息。
  • 挂起队列:已发送消息,呼叫者正在等待答复,但尚未发送答复。
  • 大型消息队列:已发送消息,但调用方的缓冲区较小以接收它。调用方再次有机会分配更大的缓冲区并再次请求消息负载。
  • 已取消队列:已发送到端口但此后已被取消的消息。
  • 直接队列:附加了直接事件的情况下发送的消息。

在这一点上,我不打算深入研究消息同步选项和不同的队列 – 我必须在某个地方做一个切入 – 但是如果有人有兴趣在这些代码区域中查找错误,我强烈建议您查看令人惊叹的Windows内部组件第8章,第2部分,第7版。我从这本书中学到了很多东西,不能称赞它!

最后,关于ALPC的消息传递细节,还有最后一件事尚未详细说明,即消息如何从客户端传输到服务器。已经提到了可以发送什么样的消息,消息的结构如何,存在什么机制来同步和停止消息,但到目前为止,还没有详细说明消息如何从一个进程到达另一个进程。
您有两种选择:

  • 双缓冲机制:在此方法中,在发送方和接收方的(虚拟)内存空间中分配消息缓冲区,并将消息从发送方的(虚拟)内存复制到内核的(虚拟)内存中,然后从那里复制到接收方的(虚拟)内存中。它被称为双缓冲区,因为包含消息的缓冲区被分配和复制两次(sender -> kernel & kernel -> receiver)。
  • Section 对象机制:客户端和服务器还可以分配一个共享内存部分,由双方访问,映射该部分的视图 – 这基本上意味着引用该分配部分的特定区域 – 将消息复制到映射视图中,最后将此视图作为消息属性(在下一章中讨论)发送给接收方。接收方可以提取指向发送方通过 view 消息属性使用的同一视图的指针,并从此视图中读取数据。

使用“section对象机制”的主要原因是发送大型消息,因为通过“双缓冲机制”发送的消息长度具有65535字节的硬编码大小限制。如果在消息缓冲区中超过此限制,则会引发错误。该函数可用于获取最大消息缓冲区大小,该大小在将来版本的 Windows 中可能会更改。
这种“双缓冲机制”就是在上面的代码片段中使用的。回头看一下发送的消息缓冲区,并且已通过前三行代码隐式分配了已接收的消息:AlpcMaxAllowedMessageLength()

然后,此消息缓冲区在调用 中传递给内核,内核将发送缓冲区复制到另一端的接收缓冲区中。
我们还可以深入研究内核,找出 ALPC 消息(通过消息缓冲区发送)的实际情况。反转 将我们引向核函数 ,它最终调用 – 对于我们的代码路径 – 进入 ,其中发生了缓冲区的复制。
附注:如果您对我在这里遗漏的所有逆转细节感兴趣,请查看我的后续帖子(我将在下周发布):…下周即将推出…NtAlpcSendWaitReceivePortNtAlpcSendWaitReceivePortAlpcpReceiveMessageAlpcpReadMessageData

在这条路的尽头,你会发现一个简单的RtlCopyMemory调用 – 它只是memcpy的一个宏 – 它将一堆字节从一个内存空间复制到另一个内存空间 – 它并不像人们想象的那么花哨,但这就是它̄\(ツ)/ ̄。

反编译函数:AlpcpReadMessageData (Ghidra)AlpcpReadMessageData 在 Ghidra 中反编译

为了在操作中看到这一点,我已经将断点放入上面显示的ALPC服务器进程的函数中。一旦我的 ALPC 客户端连接并向服务器发送初始消息,就会触发断点。客户端发送的消息是: 。带注释的调试输出如下所示:AlpcpReadMessageDataHello Server

ALPC_Message_View.svg可视化的双缓冲消息传递机制

这些调试屏幕显示 ALPC 消息通过消息缓冲区发送的内容…只是进程内存中的字节。
另请注意,上述屏幕是“双缓冲机制”在其第二个缓冲区复制阶段的可视表示形式,其中消息从内核内存空间复制到接收器的进程内存空间中。尚未跟踪从发送方到内核空间的复制操作,因为仅为接收方进程设置了断点。

ALPC 消息属性

好吧,在将它们放在一起之前,还需要详细说明最后一部分,即ALPC消息属性。我之前已经多次提到消息属性,所以这就是它的含义。
发送和接收消息时,via 客户端和服务器都可以指定它们要发送和/或接收的一组属性。要发送的这些属性集和要接收的属性集在两个额外的参数中传递到,如下所示:NtAlpcSendWaitReceivePortNtAlpcSendWaitReceivePort

功能:NtAlpcSendWaitReceivePortNtAlpcSendWaitReceivePort 函数签名

这里的想法是,作为发送者,您可以将其他信息传递给接收者,而另一端的接收者可以指定他想要获得的属性集,这意味着不一定所有发送的额外信息也暴露给接收者。
可以发送和/或接收以下消息属性:

  • 安全属性:安全属性保存安全上下文信息,例如,这些信息可用于模拟邮件的发件人(详见“模拟”部分)。此信息由内核控制和验证。此属性的结构如下所示:
  • 视图属性:如“消息传递详细信息”一章末尾所述,此属性可用于传递指向共享内存部分的指针,接收方可以使用该指针从此内存部分读取数据。此属性的结构如下所示:
  • 上下文属性:上下文属性存储指向已分配给特定客户端(通信端口)或特定消息的用户指定的上下文结构的指针。上下文结构可以是任意结构,例如唯一编号,用于标识客户端。服务器可以提取和引用端口结构,以唯一标识发送消息的客户端。我使用的端口结构示例可以在这里找到。内核将设置序列号、消息 ID 和回调 ID,以启用结构化消息处理(类似于 TCP)。此消息属性始终可以由消息的接收方提取,发送方不必指定此属性,也无法阻止接收方访问此属性。此属性的结构如下所示:
  • 句柄属性:句柄属性可用于将句柄传递给特定对象,例如文件。接收方可以使用此句柄来引用对象,例如在调用 ReadFile 时。内核将验证传递的句柄是否有效,否则将引发和错误。此属性的结构如下所示:
  • 令牌属性:令牌属性可用于传递有关发送方令牌的有限信息。此属性的结构如下所示:
  • 直接属性:直接属性可用于将创建的事件与消息相关联。接收方可以检索发送方创建的事件,并向其发出信号,让发送方知道已收到发送消息(对于数据报请求尤其有用)。此属性的结构如下所示:
  • 代表工作属性:此属性可用于发送与发件人关联的工作票证。我还没有玩过这个,所以我不能再详细了。此属性的结构如下所示:


消息属性,这些如何初始化和发送是编写示例ALPC服务器和客户端时困扰我的另一件事。因此,您不会崩溃,因为我在这里遇到的问题是我了解ALPC消息属性的秘密:

要开始使用,必须知道ALPC消息属性的结构如下:

看着这个,我最初认为你调用函数AlpcInitializeMessageAttribute,给它一个对上述结构和要发送的消息属性的标志的引用(所有属性都由一个标志值引用,这是我代码中的列表),然后内核为你设置它。然后,将引用的结构放入 NtAlpcSendWaitReceivePort 中,对要发送的每条消息重复该过程并完成所有操作。
事实并非如此,似乎在多个层面上都是错误的。直到我找到了2020年的这篇Twitter帖子,并再次重看了Alex的SyScan’14演讲(我在研究期间至少重看了20次),我才找到了我目前认为是正确的轨道。让我先发现我最初信念中的错误,然后再捆绑正确的行动方案:

  • AlpcInitializeMessageAttribute不会为你做任何事情,它实际上只清除标志并根据你指定的消息属性设置标志(所以根本没有内核魔术填充数据)。
    我不得不承认,我很早就从对函数进行逆向工程中发现了这一点,但是有一段时间,我仍然希望它能做得更多,因为函数的名称非常有前途。ValidAttributesAllocatedAttributes
  • 要实际正确设置消息属性,您必须分配相应的消息结构,并将其放在ALPC_MESSAGE_ATTRIBUTES结构之后的缓冲区中。因此,这类似于ALPC_MESSAGE,其中实际消息需要放在PORT_MESSAGE结构之后的缓冲区中。
  • 它不是为ALPC_MESSAGE_ATTRIBUTES结构设置 ValidAttributes 属性的内核,您必须自己设置。我通过玩弄结构来解决这个问题,有一段时间我认为这只是一个奇怪的解决方法,因为为什么我需要设置这个字段?就我而言,我的属性始终有效,检查它们是否有效不应该是内核的任务。
    我又看了一轮Alex的SyScan’14演讲来理解这一点。ValidAttributes
  • 您不需要为每次调用 NtAlpcSendWaitReceivePort 设置消息属性,只需设置一次所有消息属性,并在调用 NtAlpcSendWaitReceivePort 之前使用 ValidAttributes 标志来指定您现在发送的所有设置属性中的哪一个对您现在发送的消息有效。

为了将其捆绑到有用的知识中,以下是发送消息属性的工作原理(在我目前的理解中):

  • 首先,您有两个缓冲区:一个用于要接收的消息属性的缓冲区(在我的代码中名为:)和一个用于要发送的消息属性的缓冲区(在我的代码中名为:)。MsgAttrReceivedMsgAttrSend
  • 对于缓冲区,您只需要分配一个足够大的缓冲区来容纳ALPC_MESSAGE_ATTRIBUTES结构以及要接收的所有消息属性。分配此缓冲区后,将属性设置为相应的属性标志值。对于您收到的每封邮件,可以更改此值。
    对于我的示例服务器和客户端应用程序,我只想始终接收内核可以提供给我的所有属性,因此我在代码开头设置一次接收属性的缓冲区,如下所示:MsgAttrReceivedAllocatedAttributesAllocatedAttributes

[法典]

  • 对于缓冲区,还涉及两个步骤。您必须分配一个足够大的缓冲区来容纳ALPC_MESSAGE_ATTRIBUTES结构以及要发送的所有消息属性(就像以前一样)。您必须设置属性(就像以前一样),但随后还必须初始化要发送的消息属性(意味着创建必要的结构并用有效值填充这些属性),然后最终设置属性。在我的代码中,我想在不同的消息中发送不同的属性,所以这是我是如何做到的:MsgAttrSendAllocatedAttributesValidAttributes

[法典]

  • 发送属性缓冲区还有一个额外的捕获:您不必分配或初始化上下文属性或令牌属性。内核将始终准备这些属性,并且接收方始终可以请求它们。
  • 如果要发送多个消息属性,您将有一个以ALPC_MESSAGE_ATTRIBUTES开头的缓冲区,后跟所需的所有消息属性的初始化结构。
    那么内核如何知道哪个属性结构是哪个呢?答案:您必须将消息属性按预定义的顺序放置,这可以从其消息属性标志的值(从最高最低)中猜测,也可以在_KALPC_MESSAGE_ATTRIBUTES内核结构中找到:KALPC_MESSAGE_ATTRIBUTES结构_KALPC_MESSAGE_ATTRIBUTES内核结构
  • 您可能已经注意到,在此结构中不跟踪上下文令牌属性,这是因为内核将始终为任何消息提供这些属性,因此会独立跟踪它们的消息。
  • 发送后,内核将验证所有消息属性,填写值(例如序列号)或清除无效的属性,然后再将其提供给接收方。
  • 最后,内核会将接收方指定的属性复制到接收方的缓冲区中,接收方可以从中获取这些属性。AllocatedAttributesMsgAttrReceived

希望上述所有内容都可以更清晰一些,如果你仔细阅读我的代码并将这些语句与我使用消息属性的位置和方式进行匹配。

到目前为止,我们已经介绍了 ALPC 的各种组件来描述 ALPC 消息传递系统的工作原理以及 ALPC 消息的外观。最后,让我通过透视其中的一些组件来结束本章。ALPC消息的上述描述和结构描述了ALPC消息对发送方和接收方的外观,但应该意识到内核正在向此消息添加更多信息 – 实际上它采用提供的部分并将它们放在更大的内核消息结构中 – 如下所示:

KALPC_MESSAGE结构_KALPC_MESSAGE内核结构

所以这里的信息是:我们已经很好地理解了,但是在引擎盖下还有很多我们没有触及的东西。

将各个部分组合在一起:示例应用程序

我编写了一个示例 ALPC 客户端和服务器应用程序作为游乐场,以了解不同的 ALPC 组件。随意浏览和更改代码,以获得您对ALPC的感觉。关于此代码的一些公平警告:

  • 该代码不打算很好地扩展/增长。该代码旨在易于阅读,并指导您完成发送/接收ALPC消息的主要步骤。
  • 这段代码绝对没有接近性能,资源或其他任何优化的东西。这是为了学习。
  • 我没有费心去释放缓冲区,消息或任何其他资源(它带有直接攻击路径,如Unfreed Message Objects一节中所述)。

虽然要浏览的文件不多,但让我指出几行值得注意的代码:

  • 您可以在此处找到我如何设置示例消息属性。
  • 您可以在此处找到发送和接收消息的调用。NtAlpcSendWaitReceivePort
  • 您可以在此处找到 ALPC 端口标志、消息属性标志、消息和连接标志。

最后是它的样子:

ALPC 客户端和服务器应用程序示例ALPC 客户端和服务器应用程序示例

攻击面

在深入研究ALPC通信通道的攻击面之前,我想指出ALPC通信的一个有趣的概念弱点,下面的攻击路径建立在以下攻击路径的基础上,应该牢记这一点以找到进一步的利用潜力。

回顾 ALPC 消息流部分,我们可以回顾一下,为了允许 ALPC 通信的发生,服务器必须打开一个 ALPC(连接)端口,等待传入的消息,然后接受或拒绝这些消息。尽管 ALPC 端口是一个安全对象,因此可以使用安全描述符创建,该安全描述符定义了谁可以访问和连接到它,但大多数情况下,创建 ALPC 服务器进程不能(或希望)根据被叫方的 SID 限制访问。如果您无法(或希望)限制 SID 对 ALPC 端口的访问,则唯一的选择是允许所有人连接到您的端口,并在客户端连接并向您发送消息后做出接受/拒绝决定。这反过来意味着许多内置的ALPC服务器确实允许每个人连接并向服务器发送消息。即使服务器立即拒绝客户端,发送初始消息和一些消息属性以及该消息可能也足以利用漏洞。

由于这种通信架构和ALPC的无处不在,利用ALPC也是逃离沙箱的有趣媒介。

确定目标

映射攻击面的第一步是识别目标,在我们的例子中是ALPC客户端或服务器进程。
对于如何识别这些过程,我通常想到了三种途径:

  1. 识别 ALPC 端口对象并将其映射到所属进程
  2. 检查流程并确定其中是否使用了 ALPC
  3. 使用 Windows 事件跟踪 (ETW) 列出 ALPC 事件

所有这些方式都可能很有趣,所以让我们来看看它们……

查找 ALPC 端口对象
我们已经在本文开头看到了识别 ALPC 端口对象的最直接方法,即启动 WinObj 并通过“类型”列发现 ALPC 对象。WinObj 无法向我们提供更多详细信息,因此我们转到 WinDbg 内核调试器来检查此 ALPC 端口对象:

Inspect_AlpcPortObject_WinDbg使用 WinDbg 检查 ALPC 端口对象

在上面的命令中,我们使用 Windbg 的 !object 命令在对象管理器中查询指定路径中的命名对象。这已经隐含地告诉我们,这个ALPC端口必须是ALPC连接端口,因为通信端口没有被命名。反过来,我们可以得出结论,我们可以仅使用WinObj来查找ALPC连接端口,并通过这些ALPC服务器进程。
说到服务器进程:如上所示,可以使用WinDbg的未记录命令来显示有关我们刚刚识别的ALPC端口的信息。输出包括 – 以及许多其他有用的信息,端口的所属服务器进程,在本例中是svchost.exe
现在我们知道了 ALPC Port 对象的地址,我们可以再次使用该命令来显示此 ALPC 连接端口的活动连接:!alpc!alpc

WinDbg_alpc_show_connections在 WinDbg 中显示 ALPC 端口连接

附注:!alpc Windbg 命令未记录,但 LPC 时代存在的过时的 !lpc 命令记录在此处,并具有 2021 年 12 月的时间戳。本文档页面确实提到 !lpc 命令已过时,应改用 !alpc 命令,但 !alpc 命令语法和选项完全不同。但为了公平起见,如果您输入任何无效的 !alpc 命令,则 !alpc 命令语法将显示在 WinDbg 中:

WinDbg_alpc_command_syntaxWinDbg 中的 ALPC 命令

感谢 James Forshaw 和他在 .NET 中的 NtObjectManager,我们还可以很容易地查询 PowerShell 中的 NtObjectManager 来搜索 ALPC 端口对象,甚至更好的是,James 已经通过 Get-AccessibleAlpcPort 为此提供了包装器功能。

Get-AccesseAlpcPortGet-AccesseAlpcPort 命令输出

查找工艺中使用的 ALPC

与往常一样,有多种方法可以在进程中找到ALPC端口使用情况,以下是想到的一些方法:

  • 与之前文章(此处)中的方法类似,可以使用转储箱.exe实用程序列出可执行文件的导入函数,并在其中搜索ALPC特定的函数调用。
  • 由于上述方法适用于磁盘上的可执行文件,但不适用于正在运行的进程,因此可以传输转储箱使用的方法.exe并解析正在运行的进程的导入地址表(IAT)以查找ALPC特定的函数调用。
  • 可以附加到正在运行的进程,查询此进程的打开句柄,并筛选指向 ALPC 端口的句柄。

安装转储.exe后(例如,Visual Studio C++开发套件附带),可以使用以下两个 PowerShell 单行代码来查找创建 ALPC 端口或连接到 ALPC 端口的.exe.dll文件:

AlpcProcesses_via_Dumpbin使用 ALPC 功能的可执行文件

我没有编写第二个选项(解析IAT) – 如果你知道一个这样做的工具,让我知道,但是有一种简单但非常慢的方法可以使用下面的WinDbg命令来处理选项3(在进程中找到ALPC句柄):!handle 0 2 0 ALPC Port

Identify_ALPCPorts_via_WindbgHandle使用 WinDbg 识别 ALPC 端口对象的句柄

请注意,这非常慢,可能需要几个小时才能完成(我在10分钟后停止,只有大约18个句柄)。
但是,再次感谢James Forshaw和他的NtApiDotNet,有更简单的方法可以自己编码并加快这个过程,而且我们还可以获得一些有趣的ALPC统计数据……
你可以在这里找到这个工具

AlpcProcessHandles.svg使用 NtApiDotNet 识别 ALPC 端口对象的句柄

请注意,此程序不在内核环境中运行,因此我希望使用WinDbg命令获得更好的结果,但它可以列出各种进程使用的一些ALPC端口。通过迭代我们可以访问的所有进程,我们还可以计算有关ALPC使用情况的一些基本统计信息,如上所示。这些数字不是100%准确的,但是平均而言,每个进程使用大约14个ALPC通信端口句柄,我们可以肯定地得出结论,ALPC在Windows中经常使用。

一旦你确定了一个听起来像一个有趣的目标的过程,WinDbg可以再次用于更深入地挖掘……

AlpcProcess_via_Windbg使用 WinDbg 查找进程中的 ALPC 端口对象

对窗口使用事件跟踪

尽管 ALPC 未记录,但一些 ALPCs 事件公开为 Windows 事件,这些事件可以通过 Windows 事件跟踪 (ETW) 捕获。帮助ALPC事件的工具之一是zodiaconProcMonXv2

ALPC via ProcMonXv2使用 ProcMonXv2 识别 ALPC 与 ETW 的通信

经过几秒钟对五个暴露的ALPC事件的过滤后,我们得到了超过1000个事件,这再次表明ALPC的使用非常频繁。但除此之外,ETW在深入了解ALPC通信渠道方面没有提供太多,但无论如何,它做了它打算做的事情:确定ALPC目标。

模拟和非模拟

与该系列的上一篇文章(请参阅此处此处)一样,一个有趣的攻击媒介是冒充另一方。
与上次一样,我不打算再次介绍模拟,但您将在命名管道帖子的模拟部分中找到所需的所有解释。
对于 ALPC 通信,模拟例程绑定到消息,这意味着客户端和服务器(即每个通信方)都可以模拟另一端的用户。但是,为了允许模拟,模拟通信伙伴必须允许模拟发生,并且模拟通信伙伴需要持有SeImpersonate特权(它仍然是一个安全的通信通道,对吧?…
查看代码,似乎有两个选项可以满足第一个条件,即允许被模拟:

  • 第一个选项:通过 ,例如,如下所示:PortAttributes
  • 第二个选项:通过消息属性ALPC_MESSAGE_SECURITY_ATTRIBUTE

如果你不是很熟悉VC ++/ALPC代码,这些片段可能不会告诉你任何事情,这完全没问题。这里的要点是:理论上有两个选项可以指定允许模拟。
但是,有一个问题:

  • 如果服务器(具有连接端口句柄的服务器)想要模拟客户端,则当客户端指定了第一个选项或第二个选项(或两者,但一个选项就足够了)时,允许模拟。
  • 但是,如果客户端想要模拟服务器,则服务器必须提供第二个选项。换句话说:服务器必须发送 以允许客户端模拟服务器。ALPC_MESSAGE_SECURITY_ATTRIBUTE

我已经研究了这两种路由:模拟客户端的服务器和模拟服务器的客户端。
我的第一条路径是查找尝试连接到不存在的服务器端口的客户端,以便检查模拟条件。我尝试了各种方法,但到目前为止,我还没有找到一种很好的方式来识别这些客户。我设法在内核中使用断点来手动发现一些情况,但到目前为止,找不到任何允许客户端模拟的有趣情况。下面是“ApplicationFrameHost.exe”尝试连接到不存在的ALPC端口的示例,我可以使用示例服务器捕获该端口,但是,该过程不允许模拟(并且应用程序以当前用户身份运行)…

客户端模拟尝试客户端模拟尝试

这不是一个成功的冒充尝试,但至少它证明了这个想法。

在另一条路径上:如前所述,我使用 Get-AccesseAlpcPort 找到了一堆 ALPC 连接端口,并指示我的 ALPC 客户端连接到这些端口,以验证这些端口是否 a) 允许我连接,b) 向我发送任何实际消息,以及 c) 发送模拟消息属性以及消息。对于我检查的所有ALPC连接端口,我最多只能收到一些简短的初始化消息,其中包含ALPC_MESSAGE_CONTEXT_ATTRIBUTE,这对于模拟没有用,但至少它再次展示了这里的想法:

服务器模拟尝试服务器模拟尝试

服务器非模拟

在本系列的 RPC 部分中,我提到连接到服务器也可能很有趣,该服务器确实使用模拟将其线程的安全上下文更改为调用客户端的安全上下文,但不检查模拟是成功还是失败。在这种情况下,服务器可能会被诱骗使用自己的(可能提升的)安全上下文执行任务。正如有关RPC的帖子中所详述的那样,查找此类情况归结为对您正在查看的特定ALPC服务器进程的逐个案例分析。为此,您需要的是:

  • 打开客户端可以连接到的 ALPC 端口的服务器进程
  • 服务器必须接受连接消息,并且必须尝试模拟服务器
  • 服务器不得检查模拟是成功还是失败
  • (对于相关情况,服务器必须在与客户端不同的安全上下文中运行,即不同的用户或不同的完整性级别)

到目前为止,我想不出一种自动化或半自动化寻找这些目标的过程的好方法。想到的唯一选择是找到ALPC连接端口并反转托管过程。
如果我在这个方向上偶然发现任何有趣的东西,我会更新这篇文章,但对于主要部分,我想重新迭代失败的模拟尝试的攻击路径。

未压缩的消息对象

如 ALPC 消息属性部分所述,客户端或服务器可以随消息一起发送多个消息属性。其中之一是ALPC_DATA_VIEW_ATTR属性,可用于将有关映射视图的信息发送到另一方。
回想一下:例如,这可用于在共享视图中存储较大的消息或数据,并将该共享视图的句柄发送给另一方,而不是使用双缓冲消息传递机制将数据从一个内存空间复制到另一个内存空间。
这里有趣的是,当在ALPC_DATA_VIEW_ATTR属性中引用共享视图(或其在 Windows 中调用的部分)时,该视图(或在 Windows 中调用的部分)将映射到接收器的进程空间中。然后,接收方可以对此部分执行某些操作(如果他们知道它被映射),但最终消息的接收方必须确保映射的视图从其自己的内存空间中释放出来,这需要一定数量的步骤,这些步骤可能无法正确遵循。如果接收方未能释放映射视图,例如,因为它从一开始就没有期望接收视图,则发送方可以使用任意数据发送越来越多的视图,以用任意数据的视图填充接收方的内存空间,这归结为堆喷攻击。

我只是通过(再次)听Alex Ionescu的SyScan ALPC Talk来了解这个ALPC攻击向量,我认为没有办法更好地表达和展示这个攻击向量是如何工作的,而不是他在这次演讲中所做的,所以我不打算复制他的内容和文字,只是指向他演讲的第32分钟, 他开始解释这次袭击。你还想看看他演讲的第53分钟,演示他的堆喷雾攻击。https://www.youtube.com/embed/UNpL5csYC1E?start=3180

相同的逻辑适用于其他 ALPC 消息属性,例如,通过 ALPC 句柄属性以ALPC_MESSAGE_HANDLE_INFORMATION发送的句柄。

再次,为此类攻击寻找易受攻击的目标是一个逐案调查的过程,其中必须:

  • 使用 ALPC 通信查找(感兴趣的)进程
  • 确定目标进程如何处理 ALPC 消息属性,尤其是在释放 ALPC 消息属性时
  • 对滥用非释放资源的选项进行创意,其中明显的PoC选项是耗尽进程内存空间

当然,另一种有效的方法是选择一个目标,然后用视图(作为示例)淹没它,以检查结果是否在目标的地址空间中分配了大量共享内存区域。检查进程内存区域的一个有用工具是来自 Sysinternals 套件的 VMMap,这是我在下面用作 PoC 的工具。
作为一个例子,我用20kb的视图淹没了我的ALPC示例服务器,如下所示:

ALPC_Unfreed_Views.png内存喷涂易受攻击的 ALPC 应用程序

这确实有效,因为我没有费心去释放我的示例ALPC服务器中的任何分配属性
我还随机挑选了一些 – 比如四五个 – 微软的ALPC流程(我使用上面显示的技术识别),但我选择的那些似乎并没有犯同样的错误。
老实说,为此检查更多进程可能是有价值的,但据我所知,除了使进程崩溃之外,我对这种错误没有任何用处,如果进程足够严重,也可能会使操作系统崩溃(拒绝服务)。

有趣的旁注
在他的演讲中,Alex Ionescu提到Windows内存管理器在64kb边界上分配内存区域,这意味着每当您分配内存时,内存管理器都会将此内存放在下一个可用的64kb块的开头。作为攻击者,它允许您创建和映射任意大小的视图(最好小于64kb以使内存耗尽效率),操作系统将映射服务器内存中的视图并将64kb-YourViewSize标记为不可用的内存,因为它需要将所有内存分配对齐到64kb边界。你想看到亚历克斯演讲的第54分钟,以获得这种效果的视觉和口头解释。
Raymond Chen在这里解释了64kb粒度背后的原因。

在一天结束时,内存耗尽攻击当然不是使用内存/堆喷雾原语的唯一可行选择,比我聪明的人可以将其变成漏洞利用路径……

凝聚

ALPC是无文档记录的,并且相当复杂,但作为一个激励性的好处:ALPC内部的漏洞可以变得非常强大,因为ALPC在Windows操作系统中无处不在,所有内置的高特权进程都使用ALPC,并且由于其通信架构,即使从沙箱的角度来看,它也是一个有吸引力的目标。

ALPC的内容比我在这篇文章中介绍的要多得多。也许可以写一本关于ALPC的书,但我希望至少能触及基础知识,让你开始对ALPC感兴趣。

为了获得第一个“ALPC在我的PC中的位置和数量”的印象,我建议在主机上启动ProcMonXv2(通过zodiacon),以便在几秒钟内看到数千个ALPC事件触发。

ALPC via ProcMonXv2使用 ProcMonXv2 识别 ALPC 通信

从那里继续,您可能会发现我的ALPC客户端和服务器代码对于使用ALPC进程以及识别和利用ALPC中的漏洞非常有用。如果您发现自己正在编码和/或调查ALPC,请务必查看参考部分,以获取有关其他人如何处理ALPC的意见。

最后,作为最后一句话,从一开始就结束我的建议:如果你觉得你可以听到另一种声音和对ALPC的看法,我强烈建议你再喝一杯饮料,享受接下来一个小时的Alex Ionescu谈论LPC,RPC和ALPC:https://www.youtube.com/embed/UNpL5csYC1E

附录A:连接和通信端口的使用

在研究ALPC时,我最初认为服务器侦听其通信端口,当它通过NtAlpcConnectPort接受客户端连接时,它会接收该端口。这是有道理的,因为它被称为通信端口。但是,侦听服务器通信端口上的传入消息会导致对 NtAlpcSendWaitReceivePort 的阻塞调用,该调用从未返回消息。
因此,我对服务器的ALPC通信端口的假设肯定是错误的,这让我感到困惑,因为另一端的客户端确实在他的通信端口上获取消息。我在这个问题上坚持了一段时间,直到我联系了Alex Ionescu询问他这个问题,我了解到我的假设确实是不正确的,但更确切地说,随着时间的推移,它已经变得不正确:Alex向我解释说,我的想法(服务器在其通信端口上收听和发送消息)是LPC(ALPC的前身)设计的工作方式。但是,这种设计将迫使您与服务器接受的每个新客户端一起监听越来越多的通信端口。想象一下,一台服务器有100个客户端与它通信,然后服务器需要侦听100个通信端口以获取客户端消息,这通常会导致创建100个线程,其中每个线程将与不同的客户端进行通信。这被认为是低效的,一个更有效的解决方案是在服务器的连接端口上让单个线程监听(和发送),其中所有消息都发送到此连接端口。
这反过来意味着:服务器接受客户端连接,接收客户端通信端口的句柄,但在调用NtAlpcSendWaitReceivePort时仍然使用服务器的连接端口句柄,以便从所有连接的客户端发送和接收消息。

这是否意味着服务器的通信端口已经过时了(这是我向Alex提出的后续问题)?他的回答再次完全有意义,并清除了我对ALPC的理解:操作系统在内部使用服务器的每个客户端通信端口将特定客户端发送的消息绑定到该客户端的特定通信端口。这允许操作系统将特殊的上下文结构绑定到可用于标识客户端的每个客户端通信端口。这种特殊的上下文结构是 PortContext,它可以是任意结构,可以传递给 NtAlpcAcceptConnectPort,以后可以从具有 ALPC_CONTEXT_ATTR 消息属性的 any 消息中提取。
这意味着:当服务器侦听其连接端口时,它会从所有客户端接收消息,但是如果它想知道哪个客户端发送消息,服务器可以获取端口上下文结构(通过ALPC_CONTEXT_ATTR消息属性),该结构在接受连接时分配给此客户端,操作系统将从内部保留的客户端通信端口获取该上下文结构。

到目前为止,我们可以得出结论,服务器的每客户端通信端口对于操作系统仍然很重要,并且在ALPC通信结构中仍然具有其位置和作用。但是,这并没有回答为什么服务器实际上需要每个客户端通信端口的句柄的问题(因为可以从消息中提取客户端的PortContext,该消息是通过使用连接端口句柄接收的)。
这里的答案是冒充。当服务器想要模拟客户端时,它需要将客户端的通信端口传递到NtAlpcImpersonateClientOfPort。这样做的原因是,执行模拟所需的安全上下文信息已绑定(如果客户端允许)到客户端的通信端口。将这些信息附加到连接端口是没有意义的,因为所有客户端都使用此连接端口,而每个客户端对于每个服务器都有自己唯一的通信端口。
因此:如果要模拟客户端,则需要保留每个客户端的通信端口句柄。

引用

以下是我发现有助于学习和深入研究ALPC的一些资源。

利用 ALPC 的参考项目

对 ALPC 实施细节的引用

谈论ALPC

LPC 参考

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部