在某些情况下,具有高完整性或系统完整性的进程请求特权进程/线程/令牌的句柄,然后产生低完整性性进程.如果这些句柄足够强大,类型正确,并且由子进程继承,我们可以从另一个进程复制它们并滥用它们来提升权限或绕过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)。为了更容易理解,我们来看一个例子。要获得一个进程的句柄,我们可以使用OpenProcessWin32API,定义如下:它有3个参数:dwDesiredAccess是一个DWORD,它指定了我们希望对我们试图打开的进程具有的访问级别;bInheritHandle是一个布尔值,如果设置为TRUE使句柄可继承,这意味着调用进程会在子进程生成时将返回的句柄复制给子进程(以防我们的程序调用CreateProcess等函数);dwProcessId是一个DWORD,用于指定我们要打开哪个进程(通过给它的PID);在下一行,我试图打开系统进程的句柄(它始终具有PID4),向内核指定我希望该句柄拥有尽可能少的特权,只需查询子进程的信息(PROCESS_QUERY_LIMITED_INFORMATION),并且我希望该程序的子进程继承返回的句柄(TRUE)。OpenProcess返回的系统进程句柄(如果它没有因为某种原因失败)被放入hProcess变量中供以后使用。在幕后,内核执行一些安全检查,如果这些检查通过,则获取提供的PID,解析相关_EPROCESS结构的地址,并将其复制到句柄表中的新条目中。之后,它将访问掩码(即提供的访问级别)复制到同一条目中,并将条目值返回给调用代码。当您调用其他函数(如OpenThread和OpenToken)时,也会发生类似的事情。查看句柄如前所述,句柄本质上是表的索引。每个条目包含句柄引用的对象的地址和句柄的访问级别。我们可以使用ProcessExplorer或ProcessHacker之类的工具来查看这些信息:从这张ProcessExplorer截图中,我们可以得到一些信息:红框:句柄所指的对象类型;蓝框:句柄值(表项的实际索引);黄框:句柄指向的对象的地址;绿框:访问掩码及其解码值(访问掩码是Windows.h头文件中定义的一个宏),告诉我们授予句柄持有者对对象的哪些权限;有很多方法可以获取这些信息,不一定使用在内核模式下运行的代码。在这些方法中,最实用和有用的是依赖本机APINtQuerySystemInformation,调用时将SystemHandleInformation(0x10)值作为其第一个参数传递,返回指向SYSTEM_HANDLE变量数组的指针,其中每个变量引用一个A由系统上的进程打开的句柄。让我们看一下在C++中执行此操作的一种可能方法。在这段代码中,我们使用了以下变量:queryInfoStatus将保存NtQuerySystemInformation的返回值;tempHandleInfo将保存有关系统NtQuerySystemInformation为我们获取的所有句柄的数据;handleInfoSize是对所述数据量的“猜测”。不用担心,因为每次NtQuerySystemInformation返回STATUS_INFO_LENGTH_MISMATCH时,这个变量都会加倍,这个值告诉我们分配的空间不够;handleInfo是指向内存位置的指针,NtQuerySystemInformation将填充我们需要的数据;不要被这里的while循环搞糊涂了,正如我们所说,我们只是一遍又一遍地调用函数,直到分配的内存空间大到足以容纳所有数据。在使用Windows本机API时,这种类型的操作非常常见。NtQuerySystemInformation获取的数据可以通过简单的迭代解析如下:从代码中可以看出,变量handle是一个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。稍后我们将看到一个巧妙的技巧来规避此问题,但现在让我们使用ProcessExplorer来检查它正在使用哪个进程。可以看到,值为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.我们可以通过将PROCESS_QUERY_LIMITED_INFORMATION指定为所需访问值调用OpenProcess来获取任何进程的句柄,而不管其安全上下文如何;从这些假设中,我们可以推导出以下信息:1.如果句柄是在同一个对象上打开的,那么无论持有句柄的进程是什么,两个不同SYSTEM_HANDLE结构的Object成员都是相同的,例如打开了两个句柄两个不同进程对同一个文件将具有相同的对象值:1.1两个不同进程打开的同一个进程的两个句柄将具有匹配的对象值;1.2线程、令牌等同理;2、调用NtQuerySystemInformation时,我们可以枚举自己进程持有的句柄;如果我们通过OpenProcess得到一个进程的句柄,我们就知道那个进程的PID,并且通过NtQuerySystemInformation,它的内核空间地址_EPROCESS你能看到我们要去哪里吗?如果我们设法为所有进程打开一个可以访问PROCESS_QUERY_LIMITED_INFORMATION的句柄,那么通过NtQuerySystemInformation检索所有系统句柄允许我们过滤掉所有不属于我们进程的句柄,并从属于我们进程的句柄中提取对象值并匹配在它和生成的PID之间。当然线程也可以这样做,只需使用OpenThread和THREAD_QUERY_INFORMATION_LIMITED。为了有效地打开系统上的所有进程和线程,我们可以依赖TlHelp32的例程。线程的PID和TID(线程ID)。下面的代码块显示了我们如何获取所述快照并对其进行迭代以获取所有进程的PID。首先定义一个std::map,它是C++中类似于字典的类,它允许我们跟踪PID的句柄,我们将其称为mHandleId。完成后,我们使用CreateToolhelp32Snapshot拍摄有关该进程的系统状态快照,指定我们只需要该进程(通过TH32CS_SNAPPROCESS参数)。此快照被分配给类型为wil::unique_handle的snapshot变量,它是WIL库的一个C++类,它使我们免于在使用句柄后必须正确清理句柄的负担。完成后,我们定义并初始化一个名为processEntry的PROCESSENTRY32W变量,一旦我们开始遍历快照,该变量将保存有关我们正在检查的进程的信息。通过调用Process32FirstW并使用快照中第一个进程的数据填充processEntry。对于每个进程,我们尝试使用PROCESS_QUERY_LIMITED_INFORMATION对其PID调用OpenProcess,如果成功,我们将handle-PID对存储在mHandleId映射中。在每个while循环中,我们执行Process32NextW并用新进程填充processEntry变量,直到它返回false并退出循环。我们现在在我们的句柄和它们指向的进程的PID之间有一个一对一的映射。现在进入第二阶段!现在是获取所有系统句柄并过滤掉不属于我们进程的句柄的时候了,我们已经看到了如何检索所有句柄,现在我们只需要检查每个SYSTEM_HANDLE并将其ProcessId成员与我们的进程进行比较用于比较的PID,可通过恰当命名的GetCurrentProcessId函数获得。然后,我们以类似的方式存储属于我们进程的那些SYSTEM_HANDLE的Object和Handle成员的值,以处理句柄-PID对,使用我们称为mAddressHandle的映射。您可能想知道为什么使用switch语句而不是简单的if。一些代码已被删除,因为这些是我们的AdvancedPersistenceTortellini工具的摘录,专门用于查找我们在文章开头提到的漏洞。现在我们已经填充了两个映射,当我们只知道它的_EPROCESS地址时,获取进程的PID是一件轻而易举的事。我们首先将对象的地址保存在地址变量中,然后使用find方法在mAddressHandle映射中查找它,该方法返回一个
