Linux Kernel pktcdvd Memory Disclosure
Saturday, October 23, 2010
A vulnerability in the pktcdvd driver in the Linux kernel allows for the disclosure of 4 bytes of kernel memory. In this post, I'll describe the tad bit of magic that's necessary to exploit the vulnerability on both 32-bit and 64-bit hosts to disclosure an arbitrary amount of kernel memory.
The Vulnerability
The vulnerability was first introduced in 2.6.10 (way back in 2004) and was recently discovered in late September 2010 by Dan Rosenberg. As described by Dan:
The PKT_CTRL_CMD_STATUS device ioctl retrieves a pointer to a
pktcdvd_device from the global pkt_devs array. The index into this
array is provided directly by the user and is a signed integer, so the
comparison to ensure that it falls within the bounds of this array will
fail when provided with a negative index.
This can be used to read arbitrary kernel memory or cause a crash due to
an invalid pointer dereference. This can be exploited by users with
permission to open /dev/pktcdvd/control (on many distributions, this is
readable by group "cdrom").
The vulnerable code is present in drivers/block/pktcdvd.c:
static struct pktcdvd_device *pkt_find_dev_from_minor(int dev_minor)
{
if (dev_minor >= MAX_WRITERS)
return NULL;
return pkt_devs[dev_minor];
}
The vulnerable path is reachable via a PKT_CTRL_CMD_STATUS ioctl on the /dev/pktcdvd/control device. By specifying a negative dev_minor value, an attacker can bypass the MAX_WRITERS check and cause the kernel to reference an address far outside in the intended range of pkt_devs. The kernel later uses the specified pktcdvd_device structure to copy a bit of data back to user space. If an attacker tricks the kernel into referencing a fake pktcdvd_device structure under his control via a negative dev_minor index into pkt_devs, he may be able to disclosure sensitive kernel memory back to userspace.
CVE-2010-3437 has been assigned to this vulnerability.
The Initial Exploit
On a 32-bit host, the exploit for this vulnerability is fairly straightforward. Our goal is to specify a large negative device index during our ioctl in order to trick the kernel into dereferencing memory under our control in userspace. For example, if we know the address of the pkt_devs symbol and specify a large negative index (eg. -30000000), we can calculate the address in userspace that the kernel will access (pkt_devs + (-30000000 * sizeof(void *))). The kernel will follow the value at that address to find the address of the pktcdvd_device structure. We can provide a fake pktcdvd_device structure at that address to cause the kernel to reference any memory address and copy 4 bytes worth of data at that address back to userspace. Since we can leak 4 bytes of arbitrary memory for each ioctl, we can repeatedly perform the same operation with a different target address to dump as much kernel memory as we desire.
This exploit approach is illustrated in the following diagram:
How the initial exploitation approach operates on a 32-bit host.
The full version of this initial exploit approach is available here.
The Revised Exploit
This previous approach operates perfectly fine on a 32-bit host, since the TASK_SIZE boundary between kernel space and user space memory can be hurdled. That is, we can specify a negative device index that is large enough to cause the kernel to reference memory that under our control in userspace (pkt_devs + (device_index * sizeof(void *)) < TASK_SIZE).
However, on 64-bit hosts, the expanded address space has an unfortunate side effect on the efficacy of our exploit. No matter how large we make the negative device index, there's no way to reach an address that is under our control in userspace, as illustrated in the following diagram:
How the initial exploitation approach fails on a 64-bit host.
To work around this problem, we need to revise our exploitation approach. While causing the kernel to reference a memory address under our control in userspace is certainly preferable, it is not a strict requirement. In theory, we can cause the kernel to reference an address within kernel memory since we can't reach userspace. However, there are a few requirements that must be fulfilled to result in successful exploitation:
- The address in kernel memory must be predictable.
- The address in kernel memory must be less than the address of pkt_devs since we're using a negative index.
- The value contained at that address must be predictable.
- The value contained at that address must represent an address in userspace (< TASK_SIZE).
- The value contained at that address must represent an address that can be mapped (>= mmap_min_addr).
My initial thoughts were to look for constants at predictable addresses in kernel memory. For example, the pkt_misc structure (of type miscdevice) has a member named "minor" that has the constant value MISC_DYNAMIC_MINOR (0xff). With access to the kernel symbols for pkt_devs and pkt_misc, we could calculate the proper negative device index to specify to cause the kernel to reference the address of the pkt_misc.minor and hop down to the address 0x000000ff in userspace. However, this fails to satisfy the 5th condition, since many modern distributions specify a non-zero mmap_min_addr, disallowing the mapping of the page containing the 0x000000ff address.
Then Captain Obvious paid me a visit. What kernel address can we reference that has a predictable value and is greater than or equal to mmap_min_addr? Well, why not mmap_min_addr itself?!? We know its address in kernel memory from /proc/kallsyms, we can infer its value by simply trying to mmap a couple common mmap_min_addr values (4096, 65536), and it obviously satisfies the greater than or equal to mmap_min_addr requirement.
Success! The revised approach is illustrated as follows and operates effectively on both 32-bit and 64-bit hosts:
How the revised exploitation approach succeeds on both 32-bit and 64-bit
hosts.
The full version of this revised exploit approach is available here.
All in all, a great find by Dan, a fun puzzle to get exploitation functional on 64-bit, and a nice long-standing vuln providing arbitrary kernel memory disclosure on several popular Linux distributions. Patch up, hide kernel symbols to hamper reliable exploitation, use UDEREF, etc.