CVE-2020-27786 exploitation: userfaultfd + patching file struct /etc/passwd

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 https://www.cvedetails.com/cve/CVE-2020-27786/ 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


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.



Mitigation

To better understand the vulnerability, take a look at the patch https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c1f6e3c818dd734c30f6a7eeebf232ba2cf3181d where it is added a refcount on runtime->buffer.


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.


 

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).



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:


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.



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


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: 

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/