Thursday, February 14, 2013

Re-visiting the Exynos Memory Mapping Bug

On December 15, 2012, a member of the XDA Developer Forums going by the handle "alephzain" published a vulnerability affecting all Android devices using the Samsung Exynos chipset and running Android 4.0 (Ice Cream Sandwich) or greater. Affected devices include the extremely popular international variant of the Galaxy S3 (the North American version is not affected because it uses a Qualcomm chipset instead), and Exynos variants of the Galaxy S2, Galaxy Note, Galaxy Note 2, and Galaxy Tab.


The vulnerability allows any unprivileged user to read and write to arbitrary physical memory on an affected device by mmap()-ing a file descriptor to the world-writable device file at /dev/exynos-mem. Alephzain's exploit utilizes this capability by mapping the kernel address space, modifying a format string used by the kptr_restrict security feature in order to disable it, and finally modifying the .text segment of the kernel itself in order to trigger a privilege escalation payload and gain root privileges. The original exploit is available on XDA. Shortly after the publication of this vulnerability, Samsung released updates for several of its devices, including the Galaxy S3.

On February 2, 2013, alephzain published an APK version of his exploit, which he called Framaroot. Examining this APK revealed that alephzain had added exploits for additional vulnerabilities besides the original Exynos flaw.

Analyzing Framaroot

First, I decompiled the Framaroot APK to Java using dex2jar and jad (other toolchains exist, but this works fine for me). Taking a look at com.alephzain.framaroot.FramaActivity revealed the following logic:

    public native String[] Check();
    public native long Launch(String s);

    protected void onCreate(Bundle bundle)
    {
        ...
        String as[] = Check();
        if(as.length == 0)
        {
            /* Device not affected, exit */
           ...

        } else
        {
            ...
            /* Device affected, launch new thread to exploit */
            LaunchThread launchthread = 
                new LaunchThread(adapterview.getItemAtPosition(i).toString());
            launchthread.start();
            ...
        }
    }

The above code first invokes the Check() native method to probe for the existence of a supported vulnerability, and if successful, launches a new thread that invokes the native Launch() method. These methods are both implemented in a bundled dynamic library, lib/armeabi/libframalib.so.

Reverse engineering the Check() method in the bundled library revealed that alephzain had defined a structure containing information for each supported target. This structure looks something like the following:

struct target {
    char *tag;
    char *device_name;
    int fd;
    int flags;
    unsigned long offset;
    unsigned long size;
    unsigned long start_offset;
    unsigned long device_len;
    int (*)(void) func1;
    int (*)(void) func2;
};

Multiple Exploit Targets

A global array of targets contains four of these structures, with tags "Sam", "Gimli", "Merry", and "Frodo" (I guess alephzain is a Lord of the Rings fan). The device_name field contains an encoded representation of a vulnerable device file (for example, /dev/exynos-mem). The Check() function iterates through each target, decodes its device file name using a simple XOR cipher, and checks for the existence of a world-writable device file of that name as follows:

    struct *target;
    struct stat st;
    char file[32];
    char *key;

    /* Simplified... */
    key = get_key();

    for (i = 0; i < 4; i++) {

        memset(file, 0, 32);

        target = targets[i];

        idx = 0;

        for (j = 0; j < target->device_len; j++) {
            file[j] = (key[idx] + 1) ^ *(target->device_name + j);
            idx = (idx + 1) % 4;
        }

        if ((stat(file, &st) != -1) && (st.st_mode & (S_IROTH|S_IWOTH))) {

            /* Target found */
            ...
        }
        ...
    }

Rather than going through the trouble of reversing the above XOR cipher, it's also possible to install the Framaroot APK, launch the application, attach GDB to the process, place a breakpoint right before the call to stat(), and inspect the r0 register. Doing so yields the names of the device files exploited by this library: /dev/exynos-mem, /dev/s5p-smem, and /dev/DspBridge. While the first of these vulnerable devices was disclosed with the original Exynos exploit, the latter two were previously unpublished.

Same Bug, Different Devices

Inspecting affected kernel code reveals that /dev/s5p-smem is another world-writable device file on Exynos Android phones running ICS or greater. The code is implemented in arch/arm/mach-exynos/secmem-allocdev.c in Exynos kernel trees. This device file functions identically to /dev/exynos-mem in that it allows unprivileged users to map arbitrary physical memory. Fortunately, Samsung's latest update to fix the issues with /dev/exynos-mem modified the file permissions on the s5p-smem device file so that it is no longer world-accessible, mitigating the vulnerability.

The second new device is more of the same: yet another device file that allows unprivileged users to map arbitrary physical memory. This bug appears in kernels for devices using the TI OMAP3 chipset, which includes a number of popular older devices, such as the Motorola Droid, Droid 2, and Droid X. For reference, the affected code is implemented in drivers/dsp/bridge/rmgr/drv_interface.c and even includes a helpful comment:

/* This function maps kernel space memory to user space memory. */
static int bridge_mmap(struct file *filp, struct vm_area_struct *vma)
{

    u32 status;

    vma->vm_flags |= VM_RESERVED | VM_IO;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);

    status = remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
                     vma->vm_end - vma->vm_start, vma->vm_page_prot);
    if (status != 0)
        status = -EAGAIN;

    return status;
}

As in the other examples, this code maps an arbitrary range of physical frames specified by the offset argument to mmap() into the user's address space.

Unlike the Exynos vulnerabilities, which were quickly patched by Samsung, most devices affected by this OMAP vulnerability no longer receive carrier updates, so it is likely that this bug will remain exploitable indefinitely on all affected devices.

Samsung's Incomplete Fix

So far, we've discovered three variants of the same bug, all of which are exploited in Framaroot. So what's Framaroot's fourth target?

Looking at the patch Samsung released to address the original Exynos flaw reveals the answer. In drivers/char/exynos_mem.c, we see the addition of the following lines:

int exynos_mem_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct exynos_mem *mem = (struct exynos_mem *)filp->private_data;
    bool cacheable = mem->cacheable;
    dma_addr_t start = 0;
    u32 pfn = 0;
    u32 size = vma->vm_end - vma->vm_start;

    if (vma->vm_pgoff) {
        start = vma->vm_pgoff << PAGE_SHIFT;
        pfn = vma->vm_pgoff;
    } else {
        start = mem->phybase << PAGE_SHIFT;
        pfn = mem->phybase;
    }

    if (!cma_is_registered_region(start, size)) {
        pr_err("[%s] handling non-cma region (%#x@%#x)is prohibited\n",
                        __func__, size, start);
        return -EINVAL;
    }
    ...
}

Presumably, this new function ensures that the requested mmap() region is safe to map to userland. The cma_is_registered_region() function is implemented in mm/cma.c:

bool cma_is_registered_region(phys_addr_t start, size_t size)
{
    struct cma_region *reg;

    cma_foreach_region(reg) {
        if ((start >= reg->start) &&
            ((start + size) <= (reg->start + reg->size)))
            return true;
    }
    return false;
}

This function iterates over registered CMA regions, and attempts to check that the requested mapping falls within one of the allowed regions.

However, this code contains a fairly obvious integer overflow in the check: what happens if (start + size) overflows its 32-bit representation, wrapping around to a smaller number? As a result, it's possible to provide pgoff and size values to mmap() that circumvent this check and map arbitrary kernel memory once again.

Quickly disassembling the Launch() function in alephzain's native library shows that Framaroot employs this tactic to sidestep Samsung's fix. After running the device name decoding loop again, Launch() opens the affected device file and invokes mmap() with arguments provided by the target structure:

    target = targets[idx];

    target->fd = = open(file, O_RDWR);
    if ( target->fd == -1 ) {
        return -1;
    }

    addr = mmap(0, 
                target->size,
                PROT_READ|PROT_WRITE|PROT_EXEC,
                MAP_SHARED,
                target->fd,
                target->offset);

Examining Framaroot's fourth target reveals a size value of 0x50000000 and an offset value of 0xfffff000. These values will pass the above check due to the integer overflow, and map a large chunk of physical memory outside the allowed CMA region, allowing overwriting kernel memory.

At the time of this blog post, this fourth vulnerability remains unpatched on the latest Samsung Galaxy S3 build, and presumably affects the rest of the Exynos models as well.

5 comments:

  1. thank you for the very detailed breakdown! very nice work.

    ReplyDelete
  2. thanks so much for writing this up, keep up the work!

    ReplyDelete
  3. can you help me? I disassembled libframalib.so, but i found uninitialized register usage at start of Check() and Launch(...)

    00001844 :
    1844: b5f0 push {r4, r5, r6, r7, lr}
    1846: 465f mov r7, fp
    1848: 4656 mov r6, sl
    184a: 464d mov r5, r9
    184c: 4644 mov r4, r8

    same prologue found at begin of Check() function

    it is objdump bug, or r8 has some default value?

    ReplyDelete
  4. This is old news I'm sure, but for the benefit of those trawling the internets (as I was), this bug has been fixed. The most recent kernel now has:

    bool cma_is_registered_region(phys_addr_t start, size_t size)
    {
    struct cma_region *reg;

    if (start + size <= start)
    return false;

    cma_foreach_region(reg) {
    ...

    ReplyDelete
  5. Giving some idea on how it is trying to root

    ReplyDelete