Introduction
In this blog post I will show how I wrote an exploit for CVE-2020-27786 to achieve local privilege escalation in Linux. MIDI is a sound device. Looking at cvedetails it says: A flaw was found in the Linux kernel’s implementation of MIDI, where an attacker with a local account and the permissions to issue ioctl commands to midi devices could trigger a use-after-free issue. We are going to realize this cve in kernel 4.9.220.
The vulnerability
The fops of the midi device can be found at https://elixir.bootlin.com/linux/v4.9.220/source/sound/core/rawmidi.c#L1484
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,
};
In the fop write (fop read is similar), at last, it calls to snd_rawmidi_kernel_write1() where there is a race condition frame between spin_unlock_irqrestore and spin_lock_irqsave where copy_from_user can give a value to the object runtime->buffer, but this only happen in a really small window.
1281 else if (userbuf) {
1282 spin_unlock_irqrestore(&runtime->lock, flags);
1283 if (copy_from_user(runtime->buffer + appl_ptr,
1284 userbuf + result, count1)) {
1285 spin_lock_irqsave(&runtime->lock, flags);
1286 result = result > 0 ? result : -EFAULT;
1287 goto __end;
1288 }
1289 spin_lock_irqsave(&runtime->lock, flags);
1290 }
Mitigation
To better understand the vulnerability, take a look at the patch where it is added a refcount on runtime->buffer.
diff --git a/sound/core/rawmidi.c b/sound/core/rawmidi.c
index 20dd08e1f6756..2a688b711a9ac 100644
--- a/sound/core/rawmidi.c
+++ b/sound/core/rawmidi.c
@@ -120,6 +120,17 @@ static void snd_rawmidi_input_event_work(struct work_struct *work)
runtime->event(runtime->substream);
}
+/* buffer refcount management: call with runtime->lock held */
+static inline void snd_rawmidi_buffer_ref(struct snd_rawmidi_runtime *runtime)
+{
+ runtime->buffer_ref++;
+}
+
+static inline void snd_rawmidi_buffer_unref(struct snd_rawmidi_runtime *runtime)
+{
+ runtime->buffer_ref--;
+}
Observe how the patch checks if the refcount is in use to ensure that it works correctly. If the refcount is already in use creating one (runtime->buffer), it returns an error. When the object is going to be used, it is increased, and when it is no longer needed, the refcount is decreased, controlling the object runtime->buffer in this manner.
static int snd_rawmidi_runtime_create(struct snd_rawmidi_substream *substream)
{
struct snd_rawmidi_runtime *runtime;
@@ -669,6 +680,11 @@ static int resize_runtime_buffer(struct snd_rawmidi_runtime *runtime,
if (!newbuf)
return -ENOMEM;
spin_lock_irq(&runtime->lock);
+ if (runtime->buffer_ref) {
+ spin_unlock_irq(&runtime->lock);
+ kvfree(newbuf);
+ return -EBUSY;
+ }
oldbuf = runtime->buffer;
runtime->buffer = newbuf;
runtime->buffer_size = params->buffer_size;
@@ -1019,8 +1035,10 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
long result = 0, count1;
struct snd_rawmidi_runtime *runtime = substream->runtime;
unsigned long appl_ptr;
+ int err = 0;
spin_lock_irqsave(&runtime->lock, flags);
+ snd_rawmidi_buffer_ref(runtime);
while (count > 0 && runtime->avail) {
count1 = runtime->buffer_size - runtime->appl_ptr;
if (count1 > count)
@@ -1039,16 +1057,19 @@ static long snd_rawmidi_kernel_read1(struct snd_rawmidi_substream *substream,
if (userbuf) {
spin_unlock_irqrestore(&runtime->lock, flags);
if (copy_to_user(userbuf + result,
- runtime->buffer + appl_ptr, count1)) {
- return result > 0 ? result : -EFAULT;
- }
+ runtime->buffer + appl_ptr, count1))
+ err = -EFAULT;
spin_lock_irqsave(&runtime->lock, flags);
+ if (err)
+ goto out;
}
result += count1;
count -= count1;
}
+ out:
+ snd_rawmidi_buffer_unref(runtime);
spin_unlock_irqrestore(&runtime->lock, flags);
- return result;
+ return result > 0 ? result : err;
}
The vulnerability reliable way
Let’s analyze the entire scenario. Continuing to examine the fops. open: The flow of this file operation (fop), when the driver is opened, we observe that it invokes a series of function calls from snd_rawmidi_open() to rawmidi_open_priv() -> open_substream() -> snd_rawmidi_runtime_create(). In this final process, it initializes the runtime structure using kzalloc and allocates memory for runtime->buffer using kmalloc. By default, the size of the buffer is set to PAGE_SIZE (4096).
111 static int snd_rawmidi_runtime_create(struct snd_rawmidi_substream *substream)
112 {
113 struct snd_rawmidi_runtime *runtime;
114
115 if ((runtime = kzalloc(sizeof(*runtime), GFP_KERNEL)) == NULL)
116 return -ENOMEM;
117 runtime->substream = substream;
118 spin_lock_init(&runtime->lock);
119 init_waitqueue_head(&runtime->sleep);
120 INIT_WORK(&runtime->event_work, snd_rawmidi_input_event_work);
121 runtime->event = NULL;
122 runtime->buffer_size = PAGE_SIZE;
123 runtime->avail_min = 1;
124 if (substream->stream == SNDRV_RAWMIDI_STREAM_INPUT)
125 runtime->avail = 0;
126 else
127 runtime->avail = runtime->buffer_size;
128 if ((runtime->buffer = kmalloc(runtime->buffer_size, GFP_KERNEL)) == NULL) {
129 kfree(runtime);
130 return -ENOMEM;
131 }
132 runtime->appl_ptr = runtime->hw_ptr = 0;
133 substream->runtime = runtime;
134 return 0;
135 }
ioctl: Examining this file operation (fop) snd_rawmidi_ioctl(), we notice that it invokes the function snd_rawmidi_output_params() if the command matches SNDRV_RAWMIDI_IOCTL_PARAMS and the parameter is SNDRV_RAWMIDI_STREAM_OUTPUT. In this function, there is an interesting section that we can explore:
637 int snd_rawmidi_output_params(struct snd_rawmidi_substream *substream,
638 struct snd_rawmidi_params * params)
639 {
640 char *newbuf, *oldbuf;
641 struct snd_rawmidi_runtime *runtime = substream->runtime;
642
643 if (substream->append && substream->use_count > 1)
644 return -EBUSY;
645 snd_rawmidi_drain_output(substream);
646 if (params->buffer_size < 32 || params->buffer_size > 1024L * 1024L) {
647 return -EINVAL;
648 }
649 if (params->avail_min < 1 || params->avail_min > params->buffer_size) {
650 return -EINVAL;
651 }
652 if (params->buffer_size != runtime->buffer_size) {
653 newbuf = kmalloc(params->buffer_size, GFP_KERNEL);
654 if (!newbuf)
655 return -ENOMEM;
656 spin_lock_irq(&runtime->lock);
657 oldbuf = runtime->buffer;
658 runtime->buffer = newbuf;
659 runtime->buffer_size = params->buffer_size;
660 runtime->avail = runtime->buffer_size;
661 runtime->appl_ptr = runtime->hw_ptr = 0;
662 spin_unlock_irq(&runtime->lock);
663 kfree(oldbuf);
664 }
665 runtime->avail_min = params->avail_min;
666 substream->active_sensing = !params->no_active_sensing;
667 return 0;
668 }
The argument struct snd_rawmidi_params is passed from ioctl command. On line 657 runtime->buffer is assigned to oldbuf and on line 663 it is freed. Next, let’s examine the following file operation (fop).
write: Writing to the MIDI device, it will invoke snd_rawmidi_write(). Upon examining the flow, observe that it calls snd_rawmidi_kernel_write1() where we find the crucial part that allows us to exploit it. In snd_rawmidi_kernel_write1(). Let’s see how we can benefit from it.
1281 else if (userbuf) {
1282 spin_unlock_irqrestore(&runtime->lock, flags);
1283 if (copy_from_user(runtime->buffer + appl_ptr,
1284 userbuf + result, count1)) {
1285 spin_lock_irqsave(&runtime->lock, flags);
1286 result = result > 0 ? result : -EFAULT;
1287 goto __end;
1288 }
1289 spin_lock_irqsave(&runtime->lock, flags);
1290 }
On line 1283 copy_from_user with runtime->buffer as first argument is used, this means that the user buffer will be copied to it, accessing in the buffer user to a page registered in userfaultfd, we can block the function copy_from_user until the userfaultfd handler finishes. In kernel 4.9.220 and many more versions, userfaultfd is avalaible by exploiting hang execution and winning race conditions deterministically, making it easier to escalate privileges :)
The exploitation
Creating an object (with ioctl command) and then start writing in the MIDI to block copy_from_user using userfaultfd (runtime->buffer will be blocked), and later, freeing the previously created object (the same, with ioctl command, runtime->buffer is freed), we will have a use-after-free (UAF). This is because when copy_from_user continues, it will have a reference to the object that has been freed before (ruintime->buffer), patching what we want.
Here are the steps to exploit the vulnerability:
- Open the midi driver (it will create a 4096 bytes object).
- Send an ioctl command SNDRV_RAWMIDI_IOCTL_PARAMS with param SNDRV_RAWMIDI_STREAM_OUTPUT (0) and size object 232 (it will be in kmalloc-256), this * will create a new object as size 256 and it will free the first object (4096) even though it is not important.
- Write in the midi driver and block at copy_from_user by userfaultfd on runtime->buffer.
- Send another ioctl command SNDRV_RAWMIDI_IOCTL_PARAMS with param SNDRV_RAWMIDI_STREAM_OUTPUT (0) and size object 234 (it will be in kmalloc-256) * that will create a new object of size 256 and will free the previous object (runtime->buffer UAF).
- Spray some file structs of /etc/passwd which will reclaim the place of runtime->buffer (kmalloc-256).
- Release the userfaultfd patching flags and mode of the file struct /etc/passwd.
- Add user pwned as root by writing to /etc/passwd.
Why this works
In Kernel 4.9.220 the cache created for file is: https://elixir.bootlin.com/linux/v4.9.220/source/fs/file_table.c#L314
314 filp_cachep = kmem_cache_create("filp", sizeof(struct file), 0,
315 SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
316 percpu_counter_init(&nr_files, 0, GFP_KERNEL);
This means that it is not carrying the flag SLAB_ACCOUNT, which would mean a cache isolate, and the cache can be merged with another cache with the compatibility flag GFP_KERNEL aliasing.
How the exploitation and userfaultfd works
Userfaultfd is a mechanism for handling page faults in user space. This means that when we access a registered userfaultfd user page, we can block the data copy until we complete a desired task. The documentation is here. The offset of fields flags and mode in file struct are at 64 and 68. Lets take a look at the required steps:
Steps:
- Open midi.
- Mapp two pages starting at 0x10000 (PAGE_SIZE = 4096).
- Register userfaultfd page at 0x11000 (PAGE_SIZE = 4096).
- Send a ioctl command which will create a object of size 256 (runtime->buffer).
- Write to midi so that copy_from_user starts triggering the userfaultfd.
- Send another ioctl command which will free the before object created (runtime->buffer) generating UAF.
- Userfaultfd copies the whole new page handled to the page registered.
- Spray some file struct /etc/passwd and once the file is mapped on UAF (runtime->buffer), then we release the userfaultfd handler (previous point, copying the new page handled), so that the function copy_from_user now can finish by patching flags and mode.
Finally, as the last step, add an user with root privileges in /etc/passwd and set a password for it, the “su” command will not work otherwise.
Demo
Full exploit
https://gist.github.com/soez/14bedacaadf2f3db15226a98dcfca5bf
References
https://man7.org/linux/man-pages/man2/userfaultfd.2.html
https://duasynt.com/blog/linux-kernel-heap-feng-shui-2022
https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html
https://www.willsroot.io/2022/01/cve-2022-0185.html
https://syst3mfailure.io/wall-of-perdition/