Offensive Windows IPC Internals 1: Named Pipes

原文链接:

https://csandker.io/2021/01/10/Offensive-Windows-IPC-1-NamedPipes.html

介绍

这篇文章标志着一系列关于各种基于Windows的Inter-P rocess-C ommunication(IPC)技术组件的内部和有趣部分的帖子的开始。最初,本系列将涵盖以下主题:

  • 命名管道
  • 断续器
  • 阿尔帕克
  • 断续器

因此,一些IPC技术组件被遗漏了,但我可能会在某个时候附加本系列,例如包括其中一些:

  • 窗口消息
  • DDE(基于窗口消息)
  • 视窗插座
  • 邮件插槽

好吧,让我们开始吧,命名管道…

虽然名称听起来有点奇怪,但管道是一种非常基本和简单的技术,用于在两个进程之间进行通信和共享数据,其中术语管道仅描述了这两个进程使用的共享内存的一部分。
为了从一开始就正确地描述这一点,我们正在谈论的IPC技术被称为“管道”,有两种类型的管道:

  • 命名管道
  • 匿名管道

大多数时候,在谈论管道时,您可能指的是命名管道,因为它们提供了完整的功能集,其中匿名管道主要用于子父通信。这也意味着:管道通信可以在同一系统上的两个进程之间(使用命名管道和匿名管道),但也可以跨计算机边界进行(只有命名管道才能跨计算机边界进行通信)。由于命名管道最相关并支持完整的功能集,因此本文将仅重点介绍命名管道。

要为命名管道添加一些历史背景:命名管道起源于 OS/2 次。确定命名管道被引入Windows的确切发布日期是很困难的,但至少可以说它一定是在1992年的Windows 3.1中支持的 – 正如Windows / DOS Developer’s Journal Volume 4中所述,因此可以公平地假设命名管道已在1990年代初添加到Windows中。

在我们深入研究命名管道内部之前,请注意,后面将有一些代码片段取自我的公共命名管道示例实现。每当您觉得需要围绕代码段的更多上下文时,请前往代码存储库并查看更大的图景。

命名管道消息传递

好吧,让我们分解一下,以获得命名管道内部。当你从未听说过命名管道之前,将这种通信技术想象成一个真正的钢管 – 你有一个两端的空心条,如果你在一端喊出什么东西,听众就会在另一端听到你的话。这就是命名管道所做的一切,它将信息从一端传输到另一端。
如果你是Unix用户,你肯定以前使用过管道(因为这不是一种纯粹的Windows技术),如下所示:.输出 的内容的命令,但不是将输出显示到 STDOUT(可能是您的终端窗口),而是将输出重定向(“管道化”)到第二个命令的输入,从而计算文件的行数。这是一个匿名管道的例子。cat file.txt | wc -lfile.txtwc -l

基于 Windows 的命名管道与上述示例一样易于理解。为了使我们能够使用管道的完整功能集,我们将摆脱匿名管道,并创建一个相互通信的服务器和客户端。
命名管道只是一个对象,更具体地说是一个FILE_OBJECT,它由一个特殊的文件系统,命名管道文件系统 (NPFS) 管理

命名管道内核对象

当您创建命名管道时,假设我们将其称为“fpipe”,在引擎盖下,您正在一个名为“fpipe”(因此:命名管道)的特殊设备驱动器上创建一个FILE_OBJECT,该驱动器名为“管道”。
让我们把它包装成一个实用的东西。命名管道是通过调用 WinAPI 函数 CreateNamedPipe 创建的,例如使用以下 [Source]:

HANDLE serverPipe = CreateNamedPipe(
    L"\\\\.\\pipe\\fpipe",	// name of our pipe, must be in the form of \\.\pipe\<NAME>
    PIPE_ACCESS_DUPLEX, // open mode, specifying a duplex pipe so server and client can send and receive data
    PIPE_TYPE_MESSAGE,	// MESSAGE mode to send/receive messages in discrete units (instead of a byte stream)
    1,			// number of instanced for this pipe, 1 is enough for our use case
    2048,		// output buffer size
    2048,		// input buffer size
    0,			// default timeout value, equal to 50 milliseconds
    NULL		// use default security attributes
);

目前,此调用中最有趣的部分是 .
C++需要转义斜杠,因此与语言无关,这等于 。前导的“\.”是指计算机的全局根目录,其中术语“管道”是指向 NamedPipe 设备的符号链接。\\\\.\\pipe\\fpipe\\.\pipe\fpipe

NamedPipe SymbolicLink

由于命名管道对象是FILE_OBJECT,因此访问我们刚刚创建的命名管道等于访问“普通”文件。
因此,从客户端连接到命名管道就像调用 [Source] 一样简单:CreateFile

HANDLE hPipeFile = CreateFile(L"\\\\127.0.0.1\\pipe\\fpipe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

连接后,只需调用 [Source] 即可从管道读取:ReadFile

ReadFile(hPipeFile, pReadBuf, MESSAGE_SIZE, pdwBytesRead, NULL);

在从管道中读取一些数据之前,您希望服务器向其写入一些数据(您可以读取)。这是通过打电话 – 谁会猜到它 – [来源]来完成的:WriteFile

WriteFile(serverPipe, message, messageLenght, &bytesWritten, NULL);

但是,当您“写入”管道时,实际会发生什么?
客户端连接到服务器管道后,您创建的管道将不再处于侦听状态,并且可以向其写入数据。用户 land 调用被调度到内核 land,在调用位置,它确定有关写入操作的所有位和片段,例如,哪个设备对象与给定文件关联,写入操作是否应同步(请参阅重叠管道 I/O、阻塞模式和传入/输出缓冲区一节),设置 I/O 请求数据包 (IRP) 并最终实现 NtWriteFile 注意您的数据已写入文件。在我们的例子中,指定的数据不会写入磁盘上的实际文件,而是写入由文件句柄引用的共享内存部分。WriteFileNtWriteFileCreateNamedPipe

最后 – 如简介中所述 – 命名管道也可以跨系统边界通过网络连接使用。
调用远程命名管道服务器不需要其他实现,只需确保对 CreateFile 的调用指定了 IP 或主机名(如上面的示例所示)。
让我们猜猜一猜:调用远程管道服务器时将使用什么网络协议?….鼓滚…绝对不足为奇的是,它是SMB
与远程服务器建立 SMB 连接,默认情况下,远程服务器由协商请求初始化,以确定网络身份验证协议。与其他IPC机制(例如RPC)不同,您作为服务器开发人员无法控制网络身份验证协议,因为这始终是通过SMB协商的。由于 Kerberos 是自 Windows 2000 以来的首选身份验证方案,因此如果可能的话,将协商 Kerberos。
注: 从客户端的角度来看,您可以通过选择连接到主机名或 IP 来有效地选择身份验证协议。由于 Kerberos 的设计,它不能很好地处理 IP,因此,如果您选择连接到 IP 地址,协商的结果将始终为 NTLM(v2)。然而,当您连接到主机名时,您很可能最终总是使用Kerberos。

确定身份验证后,客户端和服务器要执行的操作再次只是经典文件操作,由 SMB 处理,就像任何其他文件操作一样,例如,通过启动“创建请求文件”请求,如下所示:

NamedPipe RemoteRead

数据传输模式

命名管道提供两种基本通信模式:字节模式和消息模式。

字节模式下,消息在客户端和服务器之间作为连续的字节流传输。这意味着客户端应用程序和服务器应用程序不确切知道在任何给定时刻从管道读取或写入多少字节。因此,一端的写入并不总是导致另一侧的相同大小的读取。这允许客户端和服务器在不关心数据大小的情况下传输数据。

消息模式下,客户端和服务器以离散单元发送和接收数据。每次在管道上发送消息时,都必须将其作为完整消息读取。如果在消息模式下从服务器管道读取,但读取缓冲区太小而无法容纳所有数据,则缓冲区中适合的数据部分将被复制到该缓冲区,其余数据保留在服务器的共享内存部分中,您将收到错误 234(0xEA, ERROR_MORE_DATA) 表示有更多数据要获取。

消息模式的直观比较如下所示,取自“Microsoft Windows的网络编程”(1999):命名管道消息模式

重叠管道 I/O、阻塞模式和进出缓冲器

从安全角度来看,重叠的 I/O、阻塞模式和入/出缓冲区并不是特别重要,但要意识到这些缓冲区的存在以及它们的含义,可以帮助理解、通信、构建和调试命名管道。因此,我将在这里简要添加这些概念。

重叠 I/O
多个与命名管道相关的函数(如 ReadFileWriteFileTransactNamedPipe 和 ConnectNamedPipe)可以同步执行管道操作,这意味着执行线程在继续之前等待操作完成,或者异步执行,这意味着执行线程将触发操作并继续而不等待其完成。
请务必注意,通过在 CreateNamedPipe 调用中设置FILE_FLAG_OVERLAPPED,只能在允许重叠 I/O 的管道(服务器)上进行异步管道操作。

可以通过指定重叠结构作为上述每个“标准”管道操作的最后一个参数来进行异步调用。如 ReadFile,或通过将COMPLETION_ROUTINE指定为“扩展”管道操作(如 ReadFileEx)的最后一个参数。前者是重叠结构,方法是基于事件的,这意味着必须创建一个事件对象,并在操作完成后发出信号,而COMPLETION_ROUTINE方法是基于回调的,这意味着回调例程被传递给执行线程,该线程在发出信号后排队并执行。有关此内容的更多详细信息,请参阅此处其中包含 Microsoft 在此处的示例实现。

阻塞模式

在使用 CreateNamedPipe 设置命名管道服务器时,通过在 dwPipeMode 参数中使用(或省略)标志来定义阻塞模式行为。以下两个 dwPipeMode 标志定义了服务器的阻塞模式:

  • PIPE_WAIT(默认):已启用阻止模式。使用命名管道操作(如在启用了阻塞模式的管道上的 ReadFile)时,该操作将等待完成。这意味着在这样的管道上进行读取操作将等到有数据要读取,写入操作将等到所有数据都写入。这当然会导致操作在某些情况下无限期等待。
  • PIPE_NOWAIT:禁用阻止模式。命名管道操作(如 ReadFile)会立即返回。您需要例程(如重叠 I/O)来确保读取或写入所有数据。

输入/输出缓冲器

通过输入/输出缓冲区,我指的是您在调用 CreateNamedPipe 时创建的命名管道服务器的输入和输出缓冲区,更准确地说是指 nInBufferSize 和 nOutBufferSize 参数中这些缓冲区的大小。
执行读取和写入操作时,命名管道服务器使用非分页内存(即物理内存)临时存储要读取或写入的数据。允许影响所创建服务器的这些值的攻击者可以滥用这些值,通过选择大型缓冲区来可能导致系统崩溃,或者通过选择小缓冲区来延迟管道操作(例如 0):

  • 大缓冲区:由于输入/输出缓冲区未分页,如果选择太大,服务器将耗尽内存。但是,nInBufferSize 和 nOutBufferSize 参数不会被系统“盲目”接受。上限由系统依赖常数定义;我找不到有关此常量的超级准确信息(并且没有挖掘标题);这篇文章表明,对于x64 Windows7系统来说,它大约是4GB。
  • 小缓冲区:缓冲区大小为 0 对于 nInBufferSize 和 nOutBufferSize 绝对有效。如果系统严格执行它被告知的内容,您将无法将任何内容写入管道,则导致大小为0的缓冲区是…好吧,一个不存在的缓冲区。很高兴系统足够智能,可以理解您要求最小缓冲区,因此会将实际分配的缓冲区扩展到它接收的大小,但这会对性能产生影响。缓冲区大小为 0 表示在将新数据写入缓冲区之前,管道另一端的进程必须读取每个字节(从而清除缓冲区)。对于 nInBufferSize 和 nOutBufferSize 来说,情况都是如此。因此,大小为 0 的缓冲区可能会导致服务器延迟。

命名管道安全性

再一次,我们可以使关于如何设置和控制命名管道安全性的本章变得相当短,但重要的是要知道这是如何完成的。

当您想要保护命名管道设置时,唯一可以转动的齿轮是将命名管道服务器的安全描述符设置为 CreateNamedPipe 调用的最后一个参数 (lpSecurityAttributes)。

如果您想了解安全描述符是什么,如何使用它以及它的外观,您可以在我的帖子中找到答案 Windows授权指南

设置此安全描述符是可选的;可以通过为 lpSecurityAttributes 参数指定 NULL 来设置默认安全描述符。
Windows 文档定义了默认安全描述符对命名管道服务器的作用:

命名管道的默认安全描述符中的 ACL 向 LocalSystem 帐户、管理员和创建者所有者授予完全控制权限。它们还向 Everyone 组的成员和匿名帐户授予读取访问权限。
来源:CreateNamedPipe > Paremter > lpSecurityAttributes

因此,默认情况下,如果未指定安全描述符,则无论读取客户端是否在同一台计算机上,“每个人都可以从命名管道服务器读取”。
如果在未设置安全描述符的情况下连接到命名管道服务器,但仍收到“拒绝访问错误”(错误代码:5),请确保仅指定了读取访问权限(请注意,上面的示例指定了“读取”和“写入”访问权限)。GENERIC_READ | GENERIC_WRITE

对于远程连接,请再次注意(如命名管道消息传递一章末尾所述),网络身份验证协议是通过 SMB 协议在客户端和服务器之间协商的。无法以编程方式强制使用更强的 Kerberos 协议(只能在服务器主机上禁用 NTLM)。

模仿

模拟是一个简单的概念,我们将在下一节中讨论具有命名管道的攻击向量。
如果您熟悉模拟,请随时跳过本节;模拟并非特定于命名管道

如果您还没有在 Windows 环境中遇到模拟,让我为您快速总结一下这个概念:

模拟是线程在与拥有线程的进程的安全上下文不同的安全上下文中执行的能力。模拟通常适用于客户端-服务器体系结构,其中客户端连接到服务器,服务器可以(如果需要)模拟客户端。模拟使服务器(线程)能够代表客户端执行操作,但在客户端访问权限的限制内。
一个典型的场景是服务器希望访问某些记录(例如在数据库中),但只允许客户端访问自己的记录。服务器现在可以回复客户端,要求获取记录本身并将其发送到服务器,或者服务器可以使用授权协议来证明客户端允许服务器访问记录,或者 – 这就是模拟 – 客户端向服务器发送一些标识信息并允许服务器切换到客户端的角色.有点像客户端将其驾驶执照授予服务器,并允许使用该许可证来识别其他方,例如网关守卫(或更技术性的数据库服务器)。

标识信息(如指定客户端是谁的信息(如 SID))打包在称为安全上下文的结构中。此结构深深地嵌入到操作系统的内部,是进程间通信所需的信息。因此,客户端无法在没有安全上下文的情况下进行IPC调用,但它需要一种方法来指定它允许服务器了解的内容以及如何处理其标识。控制微软创建了所谓的模拟级别
SECURITY_IMPERSONATION_LEVEL枚举结构定义了四个模拟级别,这些级别确定服务器可以在客户端上下文中执行的操作。

SECURITY_IMPERSONATION_LEVEL描述
安全性匿名服务器无法模拟或标识客户端。
安全性识别服务器可以获取客户端的标识和权限,但不能模拟客户端。
安全性个性化服务器可以在本地系统上模拟客户端的安全上下文。
安全委派服务器可以在远程系统上模拟客户端的安全上下文。

有关模拟的更多背景信息,请阅读 Microsoft 的客户端模拟文档。
有关模拟的一些上下文,请查看访问令牌和我关于 Windows 授权的帖子中的以下模拟部分。

模拟命名管道客户端

好吧,当我们谈论这个话题时,以防您还没有完全无聊。让我们快速了解一下如果服务器模拟客户端,后台实际发生的情况。
如果您对如何实现这一点更感兴趣,可以在此处的示例实现中找到答案。

  • 步骤 1:服务器等待来自客户端的传入连接,然后调用模拟命名管道客户端函数。
  • 步骤 2:此调用会导致调用 NtCreateEvent(以创建回调事件)和 NtFsControlFile(执行模拟的函数)。
  • 步骤 3NtFsControlFile 是一个通用函数,其操作由参数指定,在本例中,参数FSCTL_PIPE_Impersonate

    以下内容基于ReactOS的开源代码,但我认为假设Windows内核团队以类似的方式实现它是公平的。
  • 步骤 4:进一步调用堆栈 NpCommonFileSystemControl,其中 FSCTL_PIPE_IMPERSONATE 作为参数传递,并在开关大小写指令中使用以确定要执行的操作。
  • 步骤 5NpCommonFileSystemControl 调用 NbAcquireExeclusiveVcb 来锁定对象,并在给定服务器的管道对象和客户端发出的 IRP(I/O 请求对象)的情况下调用 NpImpersonate
  • 步骤 6NpImpersonate 然后反过来调用 SeImpersonateClientEx,并将客户端的安全上下文(已从客户端的 IRP 获取)作为参数。
  • 步骤 7SeImpersonateClientEx 依次调用 PsImpersonateClient,其中包含服务器的线程对象和客户端的安全令牌,后者是从客户端的安全上下文中提取的
  • 步骤 8:然后将服务器的线程上下文更改为客户端的安全上下文。
  • 步骤 9:在客户端的安全上下文中,服务器执行的任何操作和服务器调用的任何功能都是使用客户端的标识进行的,从而模拟客户端
  • 步骤 10:如果服务器在成为客户端时完成了它打算执行的操作,则服务器将调用 RevertToSelf 以切换回其自己的原始线程上下文。

攻击面

客户端模拟

所以最后我们谈论的是攻击面。基于命名管道的最重要的攻击媒介是模拟。
幸运的是,我们已经在上面的部分中介绍并理解了模拟的概念,因此我们可以深入研究。

攻击场景

当您获得允许您指定或控制访问文件的服务,程序或例程时,最好滥用命名管道的模拟(无论它是否允许您读取或写入访问或两者兼而有之)。由于命名管道基本上是FILE_OBJECTs,并且对与常规文件(ReadFileWriteFileCreateFile等)相同的访问功能进行操作,因此您可以指定命名管道而不是常规文件名,并使受害者进程连接到您控制下的命名管道。

先决条件

尝试模拟客户端时,需要检查两个重要方面。
第一种是检查客户端如何实现文件访问,更具体地说,客户端在调用 CreateFile 时是否指定了 SECURITY_SQOS_PRESENT 标志?

对 CreateFile 的易受攻击调用如下所示:

hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

而对 CreateFile 的安全调用如下所示:

// calling with explicit SECURITY_IMPERSONATION_LEVEL
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION , NULL);
// calling without explicit SECURITY_IMPERSONATION_LEVEL
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT, NULL);

默认情况下,不显式指定SECURITY_IMPERSONATION_LEVEL的调用(如上面的后面示例所示)是使用匿名的安全级别模拟进行的。

如果设置SECURITY_SQOS_PRESENT标志时未设置任何其他模拟级别 (IL),或者将 IL 设置为SECURITY_IDENTIFICATION或SECURITY_ANONYMOUS则无法模拟客户端。

要检查的第二个重要方面是文件名,又名。lpFileName 参数,提供给 CreateFile。在调用本地命名管道或调用远程命名管道之间有一个重要的区别。

对本地命名管道的调用由文件位置 定义。
仅当使用高于 SECURITY_IDENTIFICATION 的模拟级别显式设置SECURITY_SQOS_PRESENT标志时,才能模拟对本地管道的调用。因此,易受攻击的调用如下所示:\\.\pipe\<SomeName>

hFile = CreateFile(L"\\.\pipe\fpipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IMPERSONATION, NULL);

需要明确的是。对本地管道的安全调用如下所示:

hFile = CreateFile(L"\\.\pipe\fpipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

即使没有SECURITY_SQOS_PRESENT,此稍后调用也是安全的,因为调用的是本地管道。

另一方面,远程命名管道由以主机名或 IP 开头的 lpFileName 定义,例如:。
现在是重要的一点:\\ServerA.domain.local\pipe\<SomeName>

如果SECURITY_SQOS_PRESENT标志不存在并且远程命名管道称为远程命名管道,则模拟级别由运行名称管道服务器的用户权限定义。

这意味着,当您调用没有SECURITY_SQOS_PRESENT标志的远程命名管道时,运行该管道的攻击者用户必须持有 SeImpersonatePrivilege (SE_IMPERSONATE_NAME) 才能模拟客户端。
如果您的用户不具有此权限,则模拟级别将设置为“安全识别”(允许您识别用户,但不能模拟用户)。
但这也意味着,如果您的用户持有 SeEnableDelegationPrivilege (SE_ENABLE_DELEGATION_NAME),则模拟级别将设置为 SecurityDelete,您甚至可以针对其他网络服务对受害用户进行身份验证。

这里有一个重要的收获是:

可以通过指定 \\127.0.0.1\pipe\<SomeName>对在同一台计算机上运行的命名管道进行远程管道调用>

要最终将各个部分组合在一起:

  • 如果未设置SECURITY_SQOS_PRESENT,则可以在用户至少具有SE_IMPERSONATE_NAME权限时模拟客户端,但对于在同一台计算机上运行的命名管道,您需要通过以下方式调用它们\\127.0.0.1\pipe\...
  • 如果设置了SECURITY_SQOS_PRESENT,则只有在同时设置了高于 SECURITY_IDENTIFICATION 的模拟级别时,才能模拟客户端(无论是在本地还是远程调用命名管道)。

误导性文档

Microsoft 有关模拟级别(授权)的文档声明如下:

当命名管道、RPC 或 DDE 连接是远程连接时,将忽略传递给 CreateFile 以设置模拟级别的标志。在这种情况下,客户端的模拟级别由服务器启用的模拟级别确定,该模拟级别由目录服务中服务器帐户上的标志设置。例如,如果为服务器启用了委派,则客户端的模拟级别也将设置为委派,即使传递给 CreateFile 的标志指定了标识模拟级别也是如此。
来源:Windows 文档:模拟级别(授权)

请注意,这在技术上是正确的,但它有点误导……
准确的版本是:调用远程命名管道时,您只将模拟级别标志(而不是其他任何名称)指定给 CreateFile,那么这些标志将被忽略,但是如果您将模拟标志与SECURITY_SQOS_PRESENT标志一起指定,则将遵循这些标志。

例子:

// In the below call the SECURITY_IDENTIFICATION flag will be respected by the remote server
hFile = CreateFile(L"\\ServerA.domain.local", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, NULL);
/* --> The server will obtain a SECURITY_IDENTIFICATION token */

// In this call the SECURITY_IDENTIFICATION flag will be ignored
hFile = CreateFile(L"\\ServerA.domain.local", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_IDENTIFICATION, NULL);
/* --> The server will obtain a token based on the privileges of the user running the server. 
        A user holding SeImpersonatePrivilege will get an SECURITY_IMPERSONATION token */

// In this call the Impersonation Level will default to SECURITY_ANONYMOUS and will be respected
hFile = CreateFile(L"\\ServerA.domain.local", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT, NULL);
/* --> The server will obtain a SECURITY_ANONYMOUS token. A call to  OpenThreadToken will result in error 1347 (0x543, ERROR_CANT_OPEN_ANONYMOUS)*/

实现

你可以在我的示例代码中找到一个完整的实现。实现的快速运行如下所示:

// Create a server named pipe
serverPipe = CreateNamedPipe(
    pipeName,           // name of our pipe, must be in the form of \\.\pipe\<NAME>
    PIPE_ACCESS_DUPLEX, // The rest of the parameters don't really matter
    PIPE_TYPE_MESSAGE,	// as all you want is impersonate the client...
    1,		// 
    2048,	// 
    2048,	// 
    0,		// 
    NULL	// This should ne NULL so every client can connect
);
// wait for pipe connections
BOOL bPipeConnected = ConnectNamedPipe(serverPipe, NULL);
// Impersonate client
BOOL bImpersonated = ImpersonateNamedPipeClient(serverPipe);
// if successful open Thread token - your current thread token is now the client's token
BOOL bSuccess = OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken);
// now you got the client token saved in hToken and you can safeyl revert back to self
bSuccess = RevertToSelf();
// Now duplicate the client's token to get a Primary token
bSuccess = DuplicateTokenEx(hToken,
    TOKEN_ALL_ACCESS,
    NULL,
    SecurityImpersonation,
    TokenPrimary,
    &hDuppedToken
);
// If that succeeds you got a Primary token as hDuppedToken and you can create a proccess with that token
CreateProcessWithTokenW(hDuppedToken, LOGON_WITH_PROFILE, command, NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);

结果可以在下面看到:

命名管道模拟

当您自己实现此功能时,有一些陷阱:

  • 当您使用CreateProcessWithTokenW创建进程时,您需要在调用CreateProcessWithTokenW之前恢复到自己,否则您将收到错误。
  • 当您想要创建基于窗口的进程(带有弹出窗口的进程,例如calc.exe或cmd.exe)时,您需要授予客户端对窗口和桌面的访问权限。可以在此处找到允许所有用户访问您的窗口和桌面的示例实现。

实例创建争用条件

命名管道实例是在名称管道文件系统 (NPFS) 设备驱动器中的全局“命名空间”中创建并存在的(实际上技术上没有命名空间,但这有助于理解所有命名管道都位于同一屋檐下)。此外,具有相同名称的多个命名管道可以存在于这个屋檐下。

那么,如果应用程序创建了一个已存在的命名管道,会发生什么情况呢?好吧,如果您没有设置正确的标志,则不会发生任何事情,这意味着您不会收到错误,更糟糕的是,由于命名管道实例组织在FIFO(先进先出)堆栈中,因此您将无法获得客户端连接。
此设计使命名管道容易受到实例创建争用条件漏洞的攻击。

攻击场景

利用此类争用条件的攻击情形如下:您已经确定了一个服务、程序或例程,该服务、程序或例程创建了一个命名管道,该管道由在不同安全上下文中运行的客户端应用程序使用(假设它们在 NT 服务用户下运行)。服务器创建一个命名管道,用于与客户端应用程序进行通信。偶尔客户端连接到服务器的命名管道 – 如果服务器应用程序在创建服务器管道后触发客户端连接,这种情况并不少见。您可以确定服务器的启动时间和方式以及它创建的管道的名称。
现在,您正在编写一个程序,该程序在目标服务器的命名管道之前创建命名管道实例的方案中创建具有相同名称的命名管道。如果服务器的命名管道创建不安全,则不会注意到已存在同名的命名管道,并将触发客户端进行连接。由于FIFO堆栈,客户端将连接到您,您可以读取或写入其数据或尝试模拟客户端。

先决条件

要使此攻击起作用,您需要一个目标服务器,该服务器不检查是否已经存在同名的命名管道。通常,服务器没有额外的代码来手动检查是否已经存在同名的管道 – 仔细想想,如果您的管道名称已经存在,您会期望收到错误吗?但这不会发生,因为两个同名的命名管道实例是绝对有效的……无论出于何种原因。
但为了反击这种攻击,Microsoft 添加了FILE_FLAG_FIRST_PIPE_INSTANCE标志,可以在通过 CreateNamedPipe 创建命名管道时指定该标志。设置此标志后,您的创建调用将返回INVALID_HANDLE_VALUE,这将导致在对 ConnectNamedPipe 的后续调用中出现错误。

如果您的目标服务器未指定FILE_FLAG_FIRST_PIPE_INSTANCE标志,则可能容易受到攻击,但是攻击者还需要注意一件事。通过 CreateNamedPipe 创建命名管道时,有一个 nMaxInstances 参数,该参数指定…:

可以为此管道创建的最大实例数。管道的第一个实例可以指定此值;
来源:CreateNamedPipe

因此,如果您将其设置为“1”(如上面的示例代码所示),则会杀死自己的攻击媒介。要利用实例创建争用条件漏洞,请将此漏洞设置为PIPE_UNLIMITED_INSTANCES

实现

利用此漏洞所需要做的就是在正确的时间创建具有正确名称的命名管道实例。
我在这里的示例实现可以用作实现模板。将它放在您喜欢的IDE中,设置在您的管道名称中,确保使用PIPE_UNLIMITED_INSTANCES标志创建命名管道并开火。

实例创建特殊风格

未应答的管道连接

未应答的管道连接是由客户端发出的那些连接尝试(谁会猜到它)不成功,因此未应答,因为客户端请求的管道不存在。
这里的漏洞利用潜力非常清楚和简单:如果客户端想要连接到不存在的管道,我们会创建一个客户端可以连接到的管道,并尝试通过恶意通信操纵客户端或模拟客户端以获取其他权限。

这个漏洞有时也被称为多余的管道连接(但在我看来,这不是最好的术语)。

这里真正的问题是:我们如何找到这样的客户?
我最初的直接答案是:启动Procmon并搜索失败的CreateFile系统调用。但是我测试了这个,结果发现Procmon没有列出这些管道调用……也许这是因为该工具仅通过NTFS驱动程序检查/侦听文件操作,但我还没有对此进行深入研究(也许有一个我不知道的技巧/开关) – 如果我偶然发现答案,我会更新…

另一个选项是 IO Ninja 工具集的管道监视器。该工具需要许可证,但提供免费试用期来使用它。管道监视器提供检查系统上管道活动的功能,并附带一些用于进程、文件名等的基本过滤器。当您要搜索我筛选“*”的所有进程和所有文件名时,请让它运行并使用搜索功能查找“无法打开”:

未应答的命名管道

如果您知道使用开源工具执行此操作的任何其他方法,请告诉我(/ 0xcsandker) 😉

杀死管道服务器

如果找不到未应答的管道连接尝试,但发现了一个有趣的管道客户端,您希望与之通信或模拟,则获取客户端连接的另一个选项是终止其当前管道服务器。
“实例创建争用条件”部分中,我已描述过,您可以在同一“命名空间”中具有多个具有相同名称的命名管道。
如果目标服务器未将 nMaxInstances 参数设置为“1”,则可以创建具有相同名称的第二个命名管道服务器,并将自己放入队列中以为客户端提供服务。只要原始管道服务器正在提供服务,您就不会收到任何客户端调用,因此此攻击的想法是破坏或杀死原始管道服务器以介入您的恶意服务器。

当涉及到杀死或破坏原始管道服务器时,我无法协助任何通用的先决条件或实现,因为这始终取决于谁在运行目标服务器以及您的访问权限和用户权限。
在分析目标服务器以获取 kill 技术时,请尝试跳出框框思考,这不仅仅是向进程发送关闭信号,例如,可能存在导致服务器关闭或重新启动的错误条件(请记住,您在队列中排名第 2 – 重新启动可能足以进入位置)。
另请注意,管道服务器只是在虚拟FILE_OBJECT上运行的实例,因此,一旦所有命名管道服务器的句柄引用计数达到 0,所有命名管道服务器都将终止。例如,句柄由连接到它的客户端打开。因此,服务器也可能通过杀死其所有句柄来杀死(当然,只有当客户端在失去连接后返回给您时,您才能获得一些东西)。

PeekNamedPipe

在某些情况下,您可能对交换的数据感兴趣,而不是对操作或模拟管道客户端感兴趣。
由于所有命名管道实例都位于同一屋檐下,因此。在同一个全局“命名空间”中。在同一虚拟 NPFS 设备驱动器上(如前所述),没有系统屏障阻止您连接到任何任意(系统或非系统)命名管道实例并查看管道中的数据(从技术上讲,“在管道中”是指管道服务器分配的共享内存部分内)。

先决条件

命名管道安全性一节所述,在保护命名管道时,唯一可以转动的齿轮是使用安全描述符作为 CreateNamedPipe 调用的最后一个参数 (lpSecurityAttributes)。这就是阻止您访问任何任意命名管道实例的全部内容。因此,在搜索目标时,您需要检查的只是是否设置并保护了此参数以防止未经授权的访问。
如果您需要有关安全描述符和要查找的内容(DACL 中的 ACL)的一些背景知识,请查看我的帖子:Windows 授权指南

实现

找到合适的目标后,还有一件事需要记住:如果使用 ReadFile 从命名管道读取数据,则从服务器的共享内存中删除数据,而下一个可能合法的客户端(尝试从管道读取)将找不到任何数据,并可能引发错误。
但是,您可以使用 PeekNamedPipe 函数来查看数据,而无需将其从共享内存中删除。

基于我的示例代码的实现片段可能如下所示:

// all the vars you need
const int MESSAGE_SIZE = 512;
BOOL bSuccess;
LPCWSTR pipeName = L"\\\\.\\pipe\\fpipe";
HANDLE hFile = NULL;
LPWSTR pReadBuf[MESSAGE_SIZE] = { 0 };
LPDWORD pdwBytesRead = { 0 };
LPDWORD pTotalBytesAvail = { 0 };
LPDWORD pBytesLeftThisMessage = { 0 };
// connect to named pipe
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS, NULL);
// sneak peek data
bSuccess = PeekNamedPipe(
    hFile,
    pReadBuf,
    MESSAGE_SIZE,
    pdwBytesRead,
    pTotalBytesAvail,
    pBytesLeftThisMessage
);

引用

就是这样,如果你想继续深入研究命名管道,这里有一些很好的参考:

发表评论

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