CVE-2020-27786
POC and Analysis of CVE-2020-27786 , a race condition leading to UAF in the sound driver MIDI . Tested on kernel 5.6.0
CVE-2020-27786
demo

starting up
looking up the advisory in github , we can see the bug is in the MIDI driver which is a sound driver . It also says that it the bug results in UAF write primitive .
analysis
looking up the patch commit specified in the advisory , we get more insight on the bug . Essencially , what the author says is , a race condition exists when reading/writing against other IOCTL functionnalities , for example the resize IOCTL option , which can result in these read/write operating on already freed (and maybe reallocated) memory
code analysis
first let’s get a little bit familiar with the code
- device registration :
static int snd_rawmidi_dev_register(struct snd_device *device)
{
...
err = snd_register_device(SNDRV_DEVICE_TYPE_RAWMIDI,
rmidi->card, rmidi->device,
&snd_rawmidi_f_ops, rmidi, &rmidi->dev);
if (err < 0) {
rmidi_err(rmidi, "unable to register\n");
goto error;
}
if (rmidi->ops && rmidi->ops->dev_register) {
err = rmidi->ops->dev_register(rmidi);
if (err < 0)
goto error_unregister;
}
...
sprintf(name, "midi%d", rmidi->device);
entry = snd_info_create_card_entry(rmidi->card, name, rmidi->card->proc_root);
if (entry) {
entry->private_data = rmidi;
entry->c.text.read = snd_rawmidi_proc_info_read;
if (snd_info_register(entry) < 0) {
snd_info_free_entry(entry);
entry = NULL;
}
}
rmidi->proc_entry = entry;
return 0;
}
we can see it registers a char device (available in /dev) with some snd_rawmidi_f_ops struct
static const struct file_operations snd_rawmidi_f_ops = {
.owner = THIS_MODULE,
.read = snd_rawmidi_read,
.write = snd_rawmidi_write,
.open = snd_rawmidi_open,
.release = snd_rawmidi_release,
.llseek = no_llseek,
.poll = snd_rawmidi_poll,
.unlocked_ioctl = snd_rawmidi_ioctl,
.compat_ioctl = snd_rawmidi_ioctl_compat,
};
your typical menu , we’ll use open , write and ioctl for the exploit
- write function : it all comes down to this function
snd_rawmidi_kernel_write1
{
...
spin_lock_irqsave(&runtime->lock, flags);
while (count > 0 && runtime->avail > 0) {
...
else if (userbuf) {
spin_unlock_irqrestore(&runtime->lock, flags);
if (copy_from_user(runtime->buffer + appl_ptr,
userbuf + result, count1)) {
...
}
spin_lock_irqsave(&runtime->lock, flags);
}
result += count1;
count -= count1;
}
...
}
as you can see (and this part of the code is also addressed in the patch) , the function takes a spinlock , then releases the spinlock just before the copy_from_user which effectivily copies data from userland to the kernel buffer . But why even release the lock ? are the devs really that stupid ? the question really bugged me , especially seeing that the patch didnt just place the spin_unlock_irqrestore after the copy_from_user , but introduced a different reference counting methodology which is way more complicated . So i had to go for a sidequest (which i will document down below) .
-
read function : same as write , with even the same bug , but for some reason , i couldn’t trigger it from userland easily like
writeso i ended up not using it in the exploit (took me a lot of time to move on xd) -
IOCTL : one of the ioctl commands is
SNDRV_RAWMIDI_IOCTL_PARAMSwhich allows us to resize the buffer we write into
static int resize_runtime_buffer(struct snd_rawmidi_runtime *runtime,
struct snd_rawmidi_params *params,
bool is_input)
{
char *newbuf, *oldbuf;
if (params->buffer_size < 32 || params->buffer_size > 1024L * 1024L)
return -EINVAL;
if (params->avail_min < 1 || params->avail_min > params->buffer_size)
return -EINVAL;
if (params->buffer_size != runtime->buffer_size) {
newbuf = kvzalloc(params->buffer_size, GFP_KERNEL);
if (!newbuf)
return -ENOMEM;
spin_lock_irq(&runtime->lock);
oldbuf = runtime->buffer;
runtime->buffer = newbuf;
runtime->buffer_size = params->buffer_size;
__reset_runtime_ptrs(runtime, is_input);
spin_unlock_irq(&runtime->lock);
kvfree(oldbuf);
}
runtime->avail_min = params->avail_min;
return 0;
}
- allocates a new buffer (from normal kmalloc slubs) with user controlled size
- replaces the old buffer with the new buffer
- frees the old buffer
- notice that the function also takes the lock then releases it , but that doesnt even matter
sidequest
trying to understand the spinlock mechanism , i stumbled upon the chapter of some book , it explains concurrency very well btw . reading through it , i stumbled upon this
Imagine for a moment that your driver acquires a spinlockand goes about its business within its critical section. Somewhere in the middle, your driver loses the processor. Perhaps it has called a function (copy_from_user, say) that puts the process to
sleep. Or, perhaps, kernel preemption kicks in, and a higher-priority process pushes
your code aside. Your code is now holding a lockthat it will not release any time in
the foreseeable future. If some other thread tries to obtain the same lock, it will, in
the best case, wait (spinning in the processor) for a very long time. In the worst case,
the system could deadlock entirely.
Most readers would agree that this scenario is best avoided. Therefore, the core rule
that applies to spinlocks is that any code must, while holding a spinlock, be atomic.
It cannot sleep; in fact, it cannot relinquish the processor for any reason except to
service interrupts (and sometimes not even then).
that’s it for the sidequest .
putting it all together
- we can resize our to-write buffer in the kernel
- the lock is released write before
copy_from_user, so in the scenario of an interrupt or context switch in the middle of it , some race condition with another thread might happen (a thread which can free our current buffer for example , which creates a write UAF situation )
extending the race condition
one known technique for extending such tight racing windows , is using the userfaultd syscall , which creates a kind of hooking system from the kernel to userland . For example , we can make it such that right when a kernel thread accesses some userland memory , the kernel immediately transfers control to some userland function .
In our case, we use it in the following way :
- place this ‘hook’ on some userland memory address , lets call it X .
- perform
write(fd,X,0x67) - when the
copy_from_usertries accessing our userland address X , it triggers this hook and returns execution to userland - we prepare this hook (in step 1) to perform the
IOCTL resize, so it frees our current buffer that thecopy_from_useris operating on - return execution to kernel thread
copy_from_usercopies data into freed memory
primitives
- we have arbitrary write UAF on
kmallocslabs , some useful structs that are allocated on general use slabs aretty deckstruct,seq_operations,shm_file_data,msg_msg… We’ll usetty deckstructfor function pointer overwrite later - we need some leaks to bypass KASLR , it would’ve been easy if the
readfunction worked … Now we need to some other way for leaks .
2 ways came to my mind :
- use this UAF on a pipe_buffer to partially overwrite its
page *pointer and read the data of an arbitray page . Note that in later versions , pipe_buffers are allocated fromkmalloc-cg slabs, but not in kernel 5.6 . This technique is very unreliable as thebuddy allocatoris very upredictable - overwrite the
sizemember of msg_msg struct , with this if we change the size (it should be less than 0xfd0) , we can get OOB read , i used this method
exploit
- UAF on
msgmsg.sizeto OOB read already sprayedtty deckstructobjects , slab used for this is kmalloc-1k - use another fd , UAF directly on
tty deckstructto corruptconst struct tty_operations *ops, this struct will be corrupted as we will place our ropchain here on the object (Note that SMEP and SMAP and enabled) - use IOCTL fct ptr as it is the less noisy one (we can read , write or close too)
- place a
leave ; retgadget on the function pointer as rbp points to our corrupted object when calling the fct pointer - pivot to our rop chain
- profit
into the next one.