javierprtd Blog

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

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

5.png

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

6.png

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

7.png

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

8.png

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

asciicast

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/