May 5, 16
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-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:
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
count
so that is passes thevalidate_reg()
validation function. - Inject data to the
iomem
portion pointerp
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:
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.
↩