Sagi Kedmi

Sagi Kedmi

May 5, 16

CVE-2016-2437: Untrusted App to Kernel Heap Overflow

"From Zero to One"

#android #kernel #vuln

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.

android bot small

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-keys

The 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:

25481685623108 (mod 232)2548 * 1685623 \equiv 108\space\text{(mod}\space 2^{32}\text{)}

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:

  1. Set count so that is passes the validate_reg() validation function.
  2. Inject data to the iomem portion pointer p for 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:

count=remaining=25484=637\text{count}=\text{remaining}=\frac{2548}{4}=637

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.


© 2023 Sagi Kedmi