在某些情况下,具有高完整性或系统完整性的进程会向权限进程/线程/令牌请求句柄,然后生成低完整性进程。如果这些句柄足够强大,且类型正确,并且由子进程继承,我们可以从另一个进程复制它们,然后滥用它们来升级权限或绕过UAC。在这篇文章中,我们将介绍如何寻找和滥用这种漏洞。

 介绍

本质上,这个想法是看看我们是否可以自动找到拥有高完整性(也就是提升)或SYSTEM进程的权限句柄的非权限进程,然后检查我们是否可以作为一个非权限用户附加到这些进程上,并复制这些句柄,以便以后滥用它们。我们的工具会受到哪些限制?

1.它必须作为中等完整性进程运行;

2. 进程令牌中没有 SeDebugPrivilege(中等完整性的进程默认没有这个权限);

3. 没有 UAC 绕过,因为它也必须适用于非管理用户;

这个过程有点复杂,我们将经历的步骤或多或少如下:

1.枚举所有进程持有的所有句柄;

2.过滤掉我们不感兴趣的句柄,现在我们只关注进程、线程和令牌的句柄,因为它们更容易被武器化;

3.过滤掉引用低完整性进程/线程/令牌的句柄;

4.过滤掉完整性大于中等的进程持有的句柄,除非获得SeDebugPrivilege,否则我们不能附加到它们上,这违背了本文的目的;

5.复制其余的句柄并将它们导入我们的进程,并试图滥用它们来升级权限或者至少绕过UAC;

当然,我们不太可能在一台全新的Windows设备上满足这些条件,所以为了避免这个问题,我将使用一个我专门为此目的编写的易受攻击的应用程序。

句柄处理

正如我在这个Twitter线程中简要讨论的那样,Windows是一个基于对象的操作系统,这意味着每个实体(进程、线程、互斥锁等)在内核中都以数据结构的形式有一个“对象”表示。例如,对于进程,该数据结构的类型是_EPROCESS。作为存在于内核空间的数据,普通的用户模式代码无法直接与这些数据结构交互,因此操作系统公开了一个间接机制,该机制依赖于特殊的HANDLE类型变量以及用于服务的 SC_HANDLE 等派生类型。句柄只不过是内核空间表中的索引,对每个进程来说都是私有的。表中的每一项都包含了它所指向的对象的地址以及该句柄对该对象的访问级别。这个表由每个进程的_EPROCESS结构的ObjectTable成员(它的类型是_HANDLE_TABLE*,因此它指向一个_HANDLE_TABLE)指向。

为了更容易理解,让我们看一个例子。要获得进程的句柄,我们可以使用OpenProcess Win32 API,定义如下:

它需要3个参数:

dwDesiredAccess是一个DWORD,它指定了我们希望对我们试图打开的进程拥有的访问级别;

bInheritHandle是一个布尔值,如果设置为TRUE,将使句柄可继承,这意味着调用进程在子进程生成时将返回的句柄复制给子进程(以防我们的程序调用CreateProcess之类的函数);

dwProcessId是一个DWORD,用于指定我们想打开哪个进程(通过提供它的PID);

在下一行中,我将尝试打开系统进程(它始终具有 PID 4)的句柄,向内核指定我希望句柄拥有尽可能少的特权,只需要查询有关信息的子集进程(PROCESS_QUERY_LIMITED_INFORMATION),并且我希望该程序的子进程继承返回的句柄(TRUE)。

OpenProcess返回的System进程的句柄(如果它没有因为某种原因失败)被放入hProcess变量中以供以后使用。

在后台,内核执行一些安全检查,如果这些检查通过,则获取所提供的PID,解析相关_EPROCESS结构的地址,并将其复制到句柄表中的一个新条目中。之后,它将访问掩码(即提供的访问级别)复制到相同的条目中,并将条目值返回给调用代码。

当你调用其他函数(如OpenThread和OpenToken)时,也会发生类似的事情。

 查看句柄

正如我们前面介绍的,句柄本质上是表的索引。每个条目都包含句柄所引用对象的地址以及句柄的访问级别。我们可以使用 Process Explorer 或 Process Hacker 等工具查看这些信息:

从这个 Process Explorer 屏幕截图中,我们可以获得一些信息:

红框:句柄所指的对象类型;

蓝色框:句柄值(表项的实际索引);

黄色框:句柄所指对象的地址;

绿色框:访问掩码及其解码值(访问掩码是在 Windows.h 头文件中定义的宏),这告诉我们在对象上授予句柄持有者哪些特权;

有很多方法可以获得这些信息,不一定需要使用在内核模式下运行的代码。在这些方法中,最实用和最有用的是依赖原生 API NtQuerySystemInformation,当调用它时传递 SystemHandleInformation (0x10) 值作为其第一个参数,返回一个指向 SYSTEM_HANDLE 变量数组的指针,其中每个变量都引用一个由系统上的进程打开的句柄。

让我们来看看用c++实现它的一种可能的方法。

在这段代码中,我们使用以下变量:

  • queryInfoStatus 将保存 NtQuerySystemInformation 的返回值;

  • tempHandleInfo 将保存有关系统 NtQuerySystemInformation 为我们获取的所有句柄的数据;

  • handleInfoSize 是对所说数据量的“猜测”。不要担心,因为每次 NtQuerySystemInformation 将返回 STATUS_INFO_LENGTH_MISMATCH 时这个变量都会加倍,这是一个告诉我们分配的空间不够的值;

  • handleInfo 是指向内存位置的指针 NtQuerySystemInformation 将填充我们需要的数据;

不要对这里的 while 循环感到困惑,正如我们所说,我们只是反复调用函数,直到分配的内存空间足够大,可以容纳所有的数据。在使用Windows本机API时,这种类型的操作非常普遍。

NtQuerySystemInformation 获取的数据可以通过简单的迭代来解析,如下所示:

从代码中可以看出,变量句柄是 SYSTEM_HANDLE 类型的结构(自动从代码中删除)有许多成员提供有关它所引用的句柄的有用信息。最有趣的成员是:

ProcessId:持有句柄的进程;

Handle:持有句柄本身的进程内部的句柄值;

Object:句柄指向的对象在内核空间中的地址;

ObjectTypeNumber:一个未记录的 BYTE 变量,用于标识句柄所指对象的类型。为了解释它,需要进行一些逆向工程和挖掘,只要说进程由值 0x07 标识,线程由 0x08 标识,令牌由 0x05 标识就足够了;

GrantedAccess 句柄授予的对内核对象的访问级别,对于进程,你可以找到诸如 PROCESS_ALL_ACCESS、PROCESS_CREATE_PROCESS 等值。

让我们运行上述代码并查看其输出结果:

我们可以从对象类型的 0x7 值推断出,在这段摘录中,我们看到 PID 为 4 的进程(即任何 Windows 机器上的系统进程)当前已打开 3 个句柄。所有这些句柄都引用进程类型的内核对象,每个都有自己的内核空间地址,但只有第一个是特权句柄,正如你可以从其值推断出的那样, 0x1fffff,这是 PROCESS_ALL_ACCESS 翻译的内容。不幸的是,在我的研究中,我发现没有直接的方法可以直接提取 SYSTEM_HANDLE 结构的 ObjectAddress 成员所指向的进程的 PID。稍后我们将看到一个巧妙的技巧来规避这个问题,但现在让我们使用 Process Explorer 检查它正在使用哪个进程。



正如你所看到的,值为0x828的句柄的类型是process,它引用进程services.exe。对象地址和被授予的访问也都签出了,如果你查看图像的右侧,你将看到解码的访问掩码显示PROCESS_ALL_ACCESS,正如预期的那样。

这是非常有趣的,因为它本质上允许我们查看任何进程的句柄表,而不管它的安全上下文和PP(L)级别。

 从目标进程的对象地址获取目标进程的PID

如上所述,我没有找到一种方法来取回给定进程的 SYSTEM_HANDLE 进程的 PID,但我确实找到了一个有趣的解决方法。让我们先来看看一些假设:

1.SYSTEM_HANDLE结构包含Object成员,该成员保存内核对象地址,该地址在内核空间中;

2.在Windows上,所有进程都有自己的地址空间,但是地址空间的内核空间部分(64位进程的最大128TB)对所有进程是相同的。内核空间中的地址在所有进程中保存相同的数据;

3.提到进程的句柄时,SYSTEM_HANDLE的Object成员指向进程本身的_EPROCESS结构;

4.每个进程只有一个 _EPROCESS 结构;

5.我们可以通过调用 OpenProcess 并将 PROCESS_QUERY_LIMITED_INFORMATION 指定为所需的访问值来获取任何进程的句柄,而不管其安全上下文如何;

从这些假设中,我们可以推断出以下信息:

1.如果句柄在同一个对象上打开,则两个不同 SYSTEM_HANDLE 结构的 Object 成员将相同,而与持有该句柄的进程无关,例如,由两个不同进程在同一文件上打开的两个句柄将具有相同的 Object 值:

1.1由两个不同进程打开的同一进程的两个句柄将具有匹配的 Object 值;

1.2线程、令牌等也是如此;

2.当调用 NtQuerySystemInformation 时,我们可以枚举我们自己的进程持有的句柄;

如果我们通过 OpenProcess 获得一个进程的句柄,我们就知道该进程的 PID,并且通过 NtQuerySystemInformation,它的 _EPROCESS 的内核空间地址

你能看到我们要去哪里吗?如果我们设法打开一个对所有进程具有访问 PROCESS_QUERY_LIMITED_INFORMATION 的句柄,然后通过 NtQuerySystemInformation 检索所有系统句柄,我们就可以过滤掉所有不属于我们进程的句柄,并从那些属于我们进程的句柄中提取对象值并在它与生成的 PID 之间进行匹配。当然,线程也可以这样做,只使用 OpenThread 和 THREAD_QUERY_INFORMATION_LIMITED。

为了有效地打开系统上的所有进程和线程,我们可以依赖 TlHelp32.h 库的例程,它会允许我们拍摄系统上所有进程和线程的快照,并遍历该快照以获取拍摄快照时运行的进程和线程的 PID 和 TID(线程 ID)。

下面的代码块显示了我们如何获取所述快照并遍历它以获取所有进程的 PID。

首先定义一个std::map,这是c++中的一个类似字典的类,它允许我们跟踪指向PID的句柄,我们将其称为 mHandleId。

完成后,我们使用 CreateToolhelp32Snapshot 获取有关进程的系统状态快照,并指定我们只需要进程(通过 TH32CS_SNAPPROCESS 参数)。这个快照被分配给快照变量,它的类型是 wil::unique_handle,它是 WIL 库的一个 C++ 类,它使我们摆脱了在使用句柄后必须正确清理句柄的负担。完成后,我们定义并初始化一个名为 processEntry 的 PROCESSENTRY32W 变量,一旦我们开始遍历快照,它将保存我们正在检查的进程的信息。

通过调用 Process32FirstW 并用快照中第一个进程的数据填充 processEntry。对于每个进程,我们尝试在其 PID 上使用 PROCESS_QUERY_LIMITED_INFORMATION 调用 OpenProcess,如果成功,我们将句柄 - PID 对存储在 mHandleId 映射中。

在每个 while 循环中,我们执行 Process32NextW 并用新进程填充 processEntry 变量,直到它返回 false 并且我们退出循环。现在,我们的句柄和它们指向的进程的 PID 之间有一个 1 对 1 的映射。现在进入第二阶段!

现在是获取所有系统句柄并过滤掉不属于我们进程的句柄的时候了,我们已经了解了如何检索所有句柄,现在只需检查每个 SYSTEM_HANDLE 并将其 ProcessId 成员与我们的进程的 PID 进行比较,可通过恰当命名的 GetCurrentProcessId 函数获得。然后,我们以与处理句柄-PID 对类似的方式存储属于我们进程的那些 SYSTEM_HANDLE 的 Object 和 Handle 成员的值,使用我们称为 mAddressHandle 的映射。

你可能想知道为什么使用 switch 语句而不是简单的 if。一些代码已被删除,因为这些是我们高级持久性 Tortellini 专门为寻找我们在文章开头提到的漏洞而编写的工具的摘录。

现在我们已经填充了两个映射,当我们只知道它的 _EPROCESS 地址时,获取一个进程的 PID 是一件轻而易举的事。

我们首先将对象的地址保存在地址变量中,然后使用 find 方法在 mAddressHandle 映射中查找该地址,该方法将返回< uint64_t,HANDLE >。这对包含地址和它对应的句柄。我们通过保存对成员的值来获取句柄second并将其保存在foundHandle变量中。之后,只需要做我们刚才所做的事情,但是使用mHandleId映射和handlePid变量将保存进程的 PID,其地址是我们开始的那个进程。

现在我们有了一种可靠的方法来匹配地址和 PID,我们需要专门寻找那些完整性小于高进程持有有趣的句柄的情况,这些句柄与完整性等于或大于高的进程保持一致。但是从安全的角度来看,是什么让句柄“有趣”呢?我们将关注的句柄是具有以下访问掩码的句柄:

PROCESS_ALL_ACCESS

PROCESS_CREATE_PROCESS

PROCESS_CREATE_THREAD

PROCESS_DUP_HANDLE

PROCESS_VM_WRITE

如果你在非特权进程中找到具有至少一个此访问掩码的特权进程的句柄,那非常幸运。让我们看看我们如何做到这一点。

在这段代码中,我们首先定义一个名为 vSysHandle 的 std::vector,它将保存有趣的 SYSTEM_HANDLE。之后,我们开始对 NtQuerySystemInformation 返回的数据进行常规迭代,只是这次我们跳过了当前进程持有的句柄。然后,我们通过我编写的名为 GetTargetIntegrityLevel 的帮助函数检查持有我们当前正在分析的句柄的进程的完整性级别。这个函数基本上返回一个 DWORD,告诉我们与它作为参数接收的 PID 相关联的令牌的完整性级别,并且改编自许多在线可用的 PoC 和 MSDN 函数。

一旦我们检索到进程的完整性级别,我们要确保它小于高完整性,因为我们感兴趣的是持有感兴趣的句柄的中完整性或低完整性进程,我们还要确保我们正在处理的SYSTEM_HANDLE类型是进程(0x7)。检查后,我们转到检查句柄授予的访问权限。如果句柄不是PROCESS_ALL_ACCESS或不包含任何指定的标志,则跳过它。否则,我们更进一步,检索句柄所指进程的 PID,并获取其完整性级别。如果它是高完整性或更高的(例如SYSTEM),我们将SYSTEM_HANDLE保存在我们的vsyhandle中供以后使用。

首先,我们打开持有权限句柄的进程,然后复制该句柄。

这是相当简单的,首先,你使用PROCESS_DUP_HANDLE访问权限打开进程,这是复制句柄所需的最小权限,然后在该进程上调用DuplicateHandle,告诉函数你希望复制保存在syhandle中的句柄,并将其保存到clonedHandle变量中的当前进程中。

通过这种方式,我们的进程现在处于权限句柄的控制之下,我们可以使用它来生成一个新进程,把它的父进程伪装成该句柄所指向的权限进程,从而使新进程继承它的安全上下文,并获得命令shell等。

让我们看看它的实际应用:

参考及来源:https://aptw.tf/2022/02/10/leaked-handle-hunting.html