CVE-2020-9964 - An iOS infoleak
iOS 14 is now available to the public, and with it comes the iOS 14.0 security content update. One of the vulnerabilities you'll see listed is CVE-2020-9964, a vulnerability in IOSurfaceAccelerator, and my first infoleak :)
Both myself (@Muirey03) and Mohamed Ghannam (@_simo36) are credited with the discovery of this vulnerability. I would not be at all surprised if I found out there were more people who knew about this.
Apple describes the impact of this bug as "A local user may be able to read kernel memory" and refer to it as a "memory initialisation issue" in the description, so what is the bug?
IOSurfaceAcceleratorClient is the user client for the AppleM2ScalerCSCDriver IOService, and is one of the few user clients that can be opened from within the App Sandbox. We are interested in one particular
external method on this user client, method 9, IOSurfaceAcceleratorClient::user_get_histogram. IOSurfaceAcceleratorClient uses the legacy IOUserClient::getTargetAndMethodForIndex for its external methods, and this is what the IOExternalMethod descriptor looks like for method 9:
From this we can see that user_get_histogram takes only 8 bytes of input data, and returns nothing as output data, so let's have a look at the implementation. This is my annotated pseudocode:
From this we can see that the 8 bytes of structure input are intended to be a userspace pointer, which AppleM2ScalerCSCDriver::get_histogram can write to/read from. In fact, get_histogram calls through to get_histogram_gated which looks like this:
We see that client->histogramBuffer gets written back to userspace, so the question now is, what is client->histogramBuffer? Where is it initialised and where is it populated?
The answer to this question ends up being IOSurfaceAcceleratorClient::initClient, which looks like this:
This is immediately suspicious. histogramBuffer is allocated, but not populated, and IOMalloc does not zero the memory, leaving histogramBuffer entirely uninitialised. It was at this point that I tried calling the method for myself, and to nobody's surprise, found myself looking at a lot of 0xdeadbeef, a tell-tale sign of uninitialised memory.
Fantastic, we're leaking uninitialised memory back to userspace, but what can we do with this? Infoleaks like this one are relatively harmless by themselves, but sometimes essential when exploiting other memory corruption issues. One common requirement for exploitation is finding mach port addresses, so that was my goal for this exploit, but it's worth mentioning that this same vulnerability could be used to defeat kASLR as well.
The target allocation I chose for this exploit was mach message out-of-line port arrays. When sending mach messages, you have the option to mark the message as "complex". This tells the kernel that following the header is not raw data, but a "body" followed by descriptors to be sent along with the message. One of these descriptors is mach_msg_ool_ports_descriptor_t, an array of out-of-line port rights to be inserted into the receiving task.
The kernel handles these OOL ports by creating a buffer containing pointers to every port in the array when the message is sent, and freeing the buffer when the message is received (the code for this is in ipc_kmsg_copyin_ool_ports_descriptor if you're interested, it isn't very complicated, just too long to paste here). This is perfect for us! We can use this to trigger a kernel allocation of any size, containing the exact data we want to read (mach port pointers) and that we can free at any point, entirely deterministically.
High-level exploit flow
My plan for the exploit therefore looks like this:
- Send a few messages with OOL ports arrays the same size as client->histogramSize
- Free these arrays by receiving the messages
- Open a IOSurfaceAcceleratorClient connection, allocating the histogramBuffer which should now be overlapping with one of these free'd port arrays
- Call external method 9, reading the port pointers back to userspace
On my device, client->histogramSize is 0x300, meaning my port arrays need to be 96 ports in length. I chose to send 0x80 messages, but this was a totally arbitrary number that I pulled from thin air, don't look too much into it.
The final exploit looks like this:
I have found this exploit to have a near 100% success rate, with any failures being trivial to detect, allowing the exploit to keep being reran until it is successful.
I have been informed that the exploitability of this vulnerability was affected by iOS 14's heap separation. I don't know enough about the changes made in iOS 14 to confirm this, but it will definitely need to be something to be considered when looking at future uninitialised memory leaks.
A final goodbye
Thank you for getting to this point, I hope you learned something or at least found it mildly interesting. This is my first real writeup so I'd be very grateful for any feedback on anything discussed. If you have any questions, feel free to ask me on twitter @Muirey03.