May 5, 2016
CVE-2016-2437: Untrusted App to Kernel Heap Overflow
The nvhost GPU driver for the Tegra kernel contains a heap overflow in the
NVHOST_IOCTL_CTRLL_MODULE_REGRDWR ioctl command. The bug results from an
integer overflow that makes the kernel allocate a small heap buffer, and
eventually overruns it with an attacker controllable payload. The current
SELinux sepolicy allows any untrusted_app to trigger it.
The vulnerability was verified, using an app with JNI, on the latest Nexus 9
Android images (LTE and non-LTE):
google/volantis/flounder:6.0.1/MOB30D/2704746:user/release-keys
google/volantisg/flounder_lte:6.0.1/MOB30D/2704746:user/release-keysThe vulnerability report and proof of concept can be found on github.
This is a duplicate discovery. The awesome researchers of
C0RETEAM (Chiachih Wu,
Xuxian Jiang, Yuan-Tsung Lo and Lubo Zhangand) disclosed it a couple of weeks before I did :-).
The vulnerability was rated critical by Google.
Vulnerable Code
The following code path, taken from [1], is the code that the kernel executes
when the NVHOST_IOCTL_CTRL_MODULE_REGRDWR command is issued with the ioctl
syscall on the /dev/nvhost-ctrl character device. args is a pointer to a
userspace defined buffer.
static int nvhost_ioctl_ctrl_module_regrdwr(struct nvhost_ctrl_userctx *ctx,
struct nvhost_ctrl_module_regrdwr_args *args)
{
u32 num_offsets = args->num_offsets;
u32 __user *offsets = (u32 *)(uintptr_t)args->offsets;
[...]
u32 *vals;
u32 *p1;
int remaining;
int err;
struct platform_device *ndev;
[...]
if (num_offsets == 0 || args->block_size & 3) return -EINVAL;
ndev = nvhost_device_list_match_by_id(args->id);
[...]
remaining = args->block_size >> 2;
vals = kmalloc(num_offsets * args->block_size, GFP_KERNEL); [...]
p1 = vals;
if (args->write) {
[...]
} else {
while (num_offsets--) {
u32 offs;
if (get_user(offs, offsets)) {
[...]
}
offsets++;
err = nvhost_read_module_regs(ndev, offs, remaining, p1); [...]
p1 += remaining;
}
[...]
}
return 0;
}In line 19 there is an integer overflow. Both num_offsets and
args->block_size are controllable from userspace, and apart from line 14,
they are not verified for correctness.
For example, a malicious app may use num_offsets=1685623 and args->blocksize=2548,
which makes the kernel allocate a heap buffer of size 108 at val, since both
variables are 32 bit unsigned integers and that:
The actual buffer overrun happens when nvhost_read_module_regs() is invoked
(line 32 above). Using our previous example, where num_offsets=1685623 and
args->block_size=2548, nvhost_read_module_regs() is fed with remaining=637,
p1=val (a heap allocated buffer of size 108) and offs (an offset, also
controllable from userspace).
The code path below, taken from [2], shows that a while loop is used to
copy contents from an iomem memory portion p using readl().
int nvhost_read_module_regs(struct platform_device *ndev,
u32 offset, int count, u32 *values)
{
void __iomem *p = get_aperture(ndev);
int err;
[...]
/* verify offset */
err = validate_reg(ndev, offset, count);
[...]
err = nvhost_module_busy(ndev);
[...]
p += offset;
while (count--) { *(values++) = readl(p); p += 4; }
[...]
}Therefore, a heap buffer overflow would occur in line 14 if we could:
- Set
countso that is passes thevalidate_reg()validation function. - Inject data to the
iomemportion pointerpfor Heap Feng-Shui.
1. Passing validate_reg()
The code path below, taken from [3], contains validate_reg(). Lines
10 and 11 show the restrictions on offset and count. But, does this
really restrict us? Nope :)
static int validate_reg(struct platform_device *ndev, u32 offset, int count)
{
int err = 0;
struct resource *r;
struct nvhost_device_data *pdata = platform_get_drvdata(ndev);
[...]
r = platform_get_resource(pdata->master ? pdata->master : ndev,
IORESOURCE_MEM, 0);
[...]
if (offset + 4 * count > resource_size(r) || (offset + 4 * count < offset)) err = -EPERM;
return err;
}A simple printk() shows that resource_size(r)=262144.
Recall that due to the integer overflow values points to a heap buffer
of length 108, so count simply needs to be larger than that.
In our previous example, args->block_size=2548, and since
remaining=args->block_size>>2 we get:
Which passes the validation conditions (with a sufficiently small offset).
2. Injecting data to the iomem memory portion
Luckily, when args->write is set to 1 (line 23 below), the same ioctl command,
NVOST_IOCTL_CTRL_MODULE_REGRDWR, allows an attacker to make the kernel copy
data from userspace (using args->values) to the same iomem memory portion
that is used to overrun the buffer (line 35 below).
static int nvhost_ioctl_ctrl_module_regrdwr(struct nvhost_ctrl_userctx *ctx,
struct nvhost_ctrl_module_regrdwr_args *args)
{
u32 num_offsets = args->num_offsets;
u32 __user *offsets = (u32 *)(uintptr_t)args->offsets;
u32 __user *values = (u32 *)(uintptr_t)args->values;
u32 *vals;
u32 *p1;
int remaining;
int err;
struct platform_device *ndev;
[...]
if (num_offsets == 0 || args->block_size & 3)
return -EINVAL;
ndev = nvhost_device_list_match_by_id(args->id);
[...]
remaining = args->block_size >> 2;
vals = kmalloc(num_offsets * args->block_size, GFP_KERNEL);
[...]
p1 = vals;
if (args->write) { if (copy_from_user((char *)vals, (char *)values,
num_offsets * args->block_size)) {
kfree(vals);
return -EFAULT;
}
while (num_offsets--) {
u32 offs;
if (get_user(offs, offsets)) {
[...]
}
offsets++;
err = nvhost_write_module_regs(ndev, offs, remaining, p1);
[...]
p1 += remaining;
}
[...]
} else {
[...]
}
return 0;
}And the actual copy to iomem happens in line 14 below.
int nvhost_write_module_regs(struct platform_device *ndev,
u32 offset, int count, const u32 *values)
{
int err;
void __iomem *p = get_aperture(ndev);
[...]
/* verify offset */
err = validate_reg(ndev, offset, count);
[...]
err = nvhost_module_busy(ndev);
[...]
p += offset;
while (count--) { writel(*(values++), p); p += 4; }
[...]
return 0;
}Proof of Concept & Exploitation
Both the vulnerability report that was sent to Google and the proof of concept can be found at github. The crash dump is embedded within the report.
Not everyday does one discover a kernel memory corruption vuln that is triggerable
from the untrusted_app context. This motivated me to research kernel heap exploitation
techniques. But once I learned that other researchers have already found the
vulnerability I gave up my exploitation efforts.
Edit: Peter Pi, of Trend Micro Zero, fully exploited this exact vulnerability in HiTB Singapore 2016.
-
Tegra’s Kernel Tree, IOCTL handling. https://android.googlesource.com/kernel/tegra/+/android-tegra-flounder-3.10-marshmallow/drivers/video/tegra/host/host1x/host1x.c#315.
↩ -
Tegra’s Kernel Tree, Read from device iomem. https://android.googlesource.com/kernel/tegra/+/android-tegra-flounder-3.10-marshmallow/drivers/video/tegra/host/bus_client.c#110.
↩ -
Tegra’s Kernel Tree, Validate offset. https://android.googlesource.com/kernel/tegra/+/android-tegra-flounder-3.10-marshmallow/drivers/video/tegra/host/bus_client.c#59.
↩
