Just like babyqemu and q-escape I did not play this ctf and probably wouldn’t have been able to complete this challenge in time even if I did. Me and a couple teammates did these as exercise and I wanted to document the methods and analysis.
As with the previous QEMU escape challenges the QEMU system is configured with a vulnerable PCI device. At initialization the device creates a new SCSI Bus and registers the Bus’s callbacks. By interacting with the vulnerable PCI device via MMIO we are given the ability to attach other external SCSI devices to our custom Bus. Whenever SCSI request is queued on this Bus and a condition is reached for the request to be left uncompleted, the next time we create a request the Bus callbacks will free the pointer to the old request without clearing it and thus creating a UAF. If we race the system for this buffer and we manage to get control over it we can then overwrite any of the callbacks.
From the command line argument of the start up script we see the vulnerable PCI Device -device ctf-scsi. The qemu-system binary is compiled with debugging symbols so let’s head over to the Local Types Subview in IDA and find the device’s state structure.
Thanks to the appropriate naming convention by the author we can search for ctf_ in the Functions subview in IDA and see all of the associated functions with our device and the Bus it creates. Let’s start by device creation ctf_class_init.
The PCIDeviceClass properties are just helpful to us to identify the device. The CTFState initialization along with the MMIO and Bus creation happen in ctf_realize.
We see the hardcoded password we will need to authenticate to the device with, the mmio region creation of 0x1000 bytes and the Bus creation with scsi_bus_new and its callbacks in ctf_scsi_info global. Let’s start with the mmio ops.
Simply allows us to read some of the state of the device.
Case 4 allows us to authenticate with the device, and with combination with case 20 in ctf_mmio_read we can do a byte-by-byte brute-force past the pw buffer and leak the cur_req (heap leak) and dma_read (elf leak) pointers.
After we have authenticated we need to set the io status using case 0. Once we have done that we can use ctf_process_req which allows us to communicate with other peripheral SCSI devices via our Bus.
case 12, 16 and 20 are self-explanatory. I assume ctf_process_reply and ctf_add_cmd_data are to get the reply and write to the SCSI target, we will use them later.
When a SCSI request is completed the ctf_request_complete callback is executed and when the request is cancelled ctf_request_cancelled is executed, we can see a potential UAF there.
To confirm we have a dangling pointer we can trace the cur_req from ctf_mmio_write -> ctf_process_req -> scsi_req_cancel -> scsi_req_dequeue -> scsi_req_unref -> g_free.
To reach the scsi_req_cancel I used the following CDB.
It was a bit of a struggle to find the exact CDB and it’s parameters for my request to not reach a completed status because once the request has been completed by one reason or another (invalid opcodes/parameters or command for the type of device) the Bus will execute the ctf_request_complete callback. Which we need to avoid if we want to keep the cur_req pointer from being NULL-ed. A combination of static and dynamic analysis of scsi_req_new and scsi_target_send_command helped me with that.
To find out the target SCSI Device on the system we can use the following commands.
Once we have a dangling pointer, we need to leak it using the authentication feature. This needs to be performed quickly because QEMU can allocate this buffer for something else before we get ahold of it. To control a sized allocation I used ctf_add_cmd_data, the function also writes into the allocated buffer at the same time, so we need to have all the leaks ready and know how to create a fake SCSIRequest structure so we can control it’s ops (callbacks). In my case I used SCSIRequest->ops->get_buf and invoked it from ctf_transfer_data. See the full exploit with comments for details about each step.