原文链接:https://github.com/stong/CVE-2020-15368
Exploiting the driver
OK, now how do we actually exploit the driver? Looking around, we see a free arb physical memory r/w primitive. It basically maps whatever physical address you want using MmMapIoSpace
, copies your buffer to it (or vice versa), and unmaps the address.
Small note: when you try to call
MmMapIoSpace
with some stupid arguments like us while kernel debugger is attached, you will bugcheck. You can bypass this by writing a magic byte in WinDbg. Look for comment referencingMiShowBadMapper
in exploit.cpp to see more. I don't really know what that shit is about and I don't care to find out really
How can we leverage this primitive to get code execution in the kernel? The main problem with this primitive is that it operates on physical memory. As a usermode program, we pretty much have no idea what the layout of physical memory looks like---the operating system handles all of that for us. Even if we can get virtual addresses of some kernel data structures or kernel function pointers, we have no idea where they are in physical address space.
One idea is to read CR3, read the page tables, and perform the virtual address translation ourselves. This is a great idea. It does not work. This is because Windows no longer allows you to map page tables with MmMapIoSpace
. So we need to get more clever.
I used the xeroxz's technique from VDM. It's pretty simple, but the technique is quite clever. Although we don't know the layout of physical memory, we can still scan all of the physical memory until we find what we're looking for. One thing we can leverage is that page contents' are always the same both physically and virtually: any offsets relative to page boundaries are always preserved. For example, if I have page 0x7fff000000000XXX
mapped to physical frame 0x0000000123456XXX
, the XXX
of all addresses is same in both physical and virtual address. All of the intra-page structure is preserved; thus we can scan for some interesting page we would like to overwrite.
The easiest thing we can overwrite is probably some easy-to-reach syscall or ioctl handler. On Windows, there is a standard Beep()
function that makes your computer beep. Believe it or not, this is implemented in a driver, Beep.sys which provides the Beep device. (In fact, you can see it in the WinObjEx64 screenshot from earlier.) Anyone can use the Beep device, and it's rarely called. So let's overwrite the Beep ioctl handler.
We can pop Beep.sys into IDA and check out the DeviceIoControl handler.
At page offset 0x270, we have this code with bytes 40 53 48 ...
. None of these bytes are relocated, so scanning for this function is very easy. If there were relocated bytes, we would need to wildcard them out. It's the same idea as signature scanning when you're writing some game hack.
So after we've scanned physical memory to locate this code, we can just overwrite it with our own shellcode. You also have to be careful as there may be multiple copies of this page lying around in physical memory (!) so go find all copies.
At this point, we can quite easily escalate privileges by swapping our process' security token with one of a system process to get nt authority\system
permissions. Unfortunately the asrock driver requires admin permissions to open anyways, so this is not very interesting.
For us, we write a basic shellcode that allocates and copies a stage 2 payload, then spawns a new kernel thread. We can't do everything in our overwritten Beep handler as 1) we're limited to 1 page and 2) we will crash the system when we try to close our handle to the Beep device, as we also trashed the rest of the code in the beep device. As for getting kernel pointers, this is actually easy because NtQuerySystemInformation will give them to us for free if we ask nicely.
__int64 __declspec(dllexport) __fastcall MyIRPHandler(struct _DEVICE_OBJECT* a1, IRP* irp) { MyIrpStruct* user_data = (MyIrpStruct*)irp->AssociatedIrp.SystemBuffer; void* my_rwx = user_data->nt_ExAllocatePoolWithTag(NonPagedPoolExecute, user_data->payload_size, 'lmao'); user_data->nt_memcpy(my_rwx, user_data->payload, user_data->payload_size); HANDLE hThread; user_data->nt_PsCreateSystemThread(&hThread, THREAD_ALL_ACCESS, NULL, NULL, NULL, (PKSTART_ROUTINE)my_rwx, NULL); user_data->nt_IofCompleteRequest(irp, 0); return 0; }
So we quickly patch Beep, call the overwritten ioctl handler, and unpatch Beep. Now we have safely created a kernel thread executing our code without trashing anything else on the system. At this point we can map our own drivers or whatever.