bsauce / blog-comment

0 stars 0 forks source link

Linux内核中利用`msg_msg`结构实现任意地址读写 — bsauce #20

Open bsauce opened 3 years ago

bsauce commented 3 years ago

https://bsauce.github.io/2021/09/05/msg_msg/

Linux内核中利用msg_msg结构实现任意地址读写文章首发于安全客:Linux内核中利用msg_msg结构实现任意地址读写题目及exp下载 —— https://github.com/bsauce/CTF/tree/master/corCTF%202021介绍:本文示例是来自corCTF 2021中 的两个内核题,由 BitsByWill 和 D3v17 所出。针对UAF漏洞,漏洞对象从kmalloc-64到kmalloc-4096,都能利用 msg_msg 结构实现任意写。本驱动是基于NetFilter所写,有两个模式,简单模式(对应题目Fire_of_Salvation)和复杂模式(对应题目Wall_of_Perdition),所用的内核bzImage相同。二者的区别是,简单模式下,rule_t 规则结构包含长度 0x800 的字符串成员 rule_t->desc,漏洞对象位于kmalloc-4k,复杂模式下rule_t 规则 也即漏洞对象位于kmalloc-64。总结:如果UAF的漏洞对象是kmalloc-4096,就很容易构造重叠的漏洞对象和msg_msg结构消息块(都位于kmalloc-4096),篡改msg_msg->m_ts和msg_msg->next实现任意地址读写。如果UAF的漏洞对象小于kmalloc-4096,例如kmalloc-64,则可以先构造重叠的漏洞对象和msg_msg结构消息块(都位于kmalloc-64),篡改msg_msg->m_ts和msg_msg->next实现越界读和任意地址读;然后篡改msg_msg->next实现任意地址释放,再构造重叠的消息块(位于kmalloc-4096的msg_msgseg消息和msg_msg消息),利用userfault用户页错误处理控制消息写入的时机,篡改msg_msg->next指针指向cred地址,实现任意地址写。注意,调用msgrcv()读取内核数据时,如果带上MSG_COPY标志,就能避免内核unlink消息,以避免第一次泄露地址时未正确伪造msg_msg->m_list.next和msg_msg->m_list.prev导致unlink时崩溃。缓解机制:如果开启 CONFIG_SLAB_FREELIST_HARDENED 或者 在5.11以后的内核版本(开始禁止非特权用户使用userfault),本文的利用技巧就不适用了。前者导致堆喷不确定,后者不能精确控制篡改的时机。1. 漏洞分析代码分析:共5个函数功能,用户通过传入 user_rule_t 结构来创建路由规则并存入 rule_t 结构中,多条进出处理规则分别存入 firewall_rules_in 和 firewall_rules_out 全局数组中(每个数组最多存0x80条规则)。 firewall_add_rule()——添加一条规则。rule_t 规则结构如下。 typedef struct{ char iface[16]; // 设备名 char name[16]; // 规则名 uint32_t ip; uint32_t netmask; uint16_t proto; // 只能是 TCP 或 UDP uint16_t port; uint8_t action; // 只能是 DROP 或 ACCEPT uint8_t is_duplicated; #ifdef EASY_MODE char desc[DESC_MAX]; #endif} rule_t; firewall_delete_rule()——释放规则,并将全局数组上对应的指针清0。 firewall_show_rule()——未实现。 firewall_edit_rule()——编辑规则。 firewall_dup_rule()——复制规则,将firewall_rules_in 指针复制到firewall_rules_out 数组,或者相反。每条规则只能复制一次,通过rule_t->is_duplicated来记录是否被复制过。漏洞就在这里,可以先复制规则,再释放规则,导致UAF或double-free,只能写不能读,而且只能UAF写 0x28 - 0x30 字节。 process_rule()处理规则:(本函数与漏洞利用无关)nf_register_net_hook()——NetFilter hooks注册钩子函数。nf_hook_ops 是注册的钩子函数的核心结构。本驱动的钩子点是NF_INET_PRE_ROUTING 和 NF_INET_POST_ROUTING,应该是分别在在路由前和路由后执行钩子函数 firewall_inbound_hook() 和 firewall_outbound_hook() 函数。钩子函数 firewall_inbound_hook() 和 firewall_outbound_hook() 函数在收到进出的 sk_buff 数据后,分别按照进出规则调用 process_rule() 函数来处理数据。 首先设备名skb->dev->name 和 rule_t->ifaces 要匹配; 如果是进数据,则源ip所属的子网要匹配;如果是出数据,则目的ip所属的子网要匹配; 如果是TCP数据包,rule_t->port 要和目标端口匹配,rule_t->action 要为NF_DROP 或 NF_ACCEPT 接收状态,打印信息。 如果是UDP数据包,rule_t->port 要和目标端口匹配,rule_t->action 要为NF_DROP 或 NF_ACCEPT 接收状态,打印信息。 漏洞:只能UAF写 0x28 - 0x30 字节,不能UAF读,因为没有实现firewall_show_rule()功能。保护机制:SMAP/SMEP/KPTI, FG-KASLR, SLAB_RANDOM, SLAB_HARDENED, STATIC_USERMODE_HELPER。使用SLAB分配器。可以从给出的配置文件中看出,允许userfaultfd 调用、hardened_usercopy、CHECKPOINT_RESTORE。利用局限: 由于使用了SLAB分配器,所以chunk上没有 freelist 指针(即便有freelist指针,也不在前0x30用户可控的区域,可能内核把freelist指针后移了); FG-KASLR机制会阻碍你覆盖内核结构上的函数指针,例如sk_buff结构中的destructor arg回调函数指针,多数不在.text前面的gadget受到影响;ROP还能用,不过必须先任意读ksymtab泄露所在函数的地址; 设置CONFIG_STATIC_USERMODEHELPER,使得覆盖modprobe_path或core_pattern的方法不再适用;physmap喷射可用,但是不稳定;综上,绕过SMAP最直接的方法是构造任意读,来读取task双链表,找到当前的task并覆盖cred。2. 内核IPC——msgsnd()与msgrcv()源码分析介绍:内核提供了两个syscall来进行IPC通信, msgsnd() 和 msgrcv(),内核消息包含两个部分,消息头 msg_msg 结构和紧跟的消息数据。长度从kmalloc-64 到 kmalloc-4096。消息头 msg_msg 结构如下所示。struct msg_msg { struct list_head m_list; long m_type; size_t m_ts; / message text size / struct msg_msgseg next; void security; // security指针总为0,因为未开启SELinux / the actual message follows immediately /};2.1 msgsnd() 数据发送总体流程:当调用 msgsnd() 来发送消息时,调用 msgsnd() -> ksys_msgsnd() -> do_msgsnd() -> load_msg() -> alloc_msg() 来分配消息头和消息数据,然后调用 load_msg() -> copy_from_user() 来将用户数据拷贝进内核。示例:例如,如果想要发送一个包含 0x1fc8 个 A的消息,用户态首先调用msgget() 创建消息队列,然后调用 msgsnd()发送数据:[...]struct msgbuf{ long mtype; char mtext[0x1fc8];} msg;msg.mtype = 1;memset(msg.mtext, 'A', sizeof(msg.mtext));qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT));msgsnd(qid, &msg, sizeof(msg.mtext), 0);[...]创建消息: do_msgsnd() -> load_msg() -> alloc_msg() 。总结,如果消息长度超过0xfd0,则分段存储,采用单链表连接,第1个称为消息头,用 msg_msg 结构存储;第2、3个称为segment,用 msg_msgseg 结构存储。消息的最大长度由 /proc/sys/kernel/msgmax 确定, 默认大小为 8192 字节,所以最多链接3个成员。static struct msg_msg alloc_msg(size_t len){ struct msg_msg msg; struct msg_msgseg *pseg; size_t alen; alen = min(len, DATALEN_MSG); // [1] len 是用户提供的数据size,本例中为0x1fc8。 DATALEN_MSG = ((size_t)PAGE_SIZE - sizeof(struct msg_msg)) = 0x1000-0x30 = 0xfd0。 本例中 alen = 0xfd0 msg = kmalloc(sizeof(msg) + alen, GFP_KERNEL_ACCOUNT); // [2] 这里分配 0x1000 堆块,对应 kmalloc-4096 if (msg == NULL) return NULL; msg->next = NULL; msg->security = NULL; len -= alen; // [3] 待分配的size,继续分配,用单链表存起来。 len = 0x1fc8-0xfd0 = 0xff8 pseg = &msg->next; while (len > 0) { struct msg_msgseg seg; cond_resched(); alen = min(len, DATALEN_SEG); // [4] DATALEN_SEG = ((size_t)PAGE_SIZE - sizeof(struct msg_msgseg)) = 0x1000-0x8 = 0xff8。 alen = 0xff8 seg = kmalloc(sizeof(seg) + alen, GFP_KERNEL_ACCOUNT); // [5] 还是分配 0x1000,位于kmalloc-4096 if (seg == NULL) goto out_err; pseg = seg; // [6] 单链表串起来 seg->next = NULL; pseg = &seg->next; len -= alen; } return msg;out_err: free_msg(msg); return NULL;}拷贝消息: do_msgsnd() -> load_msg() -> copy_from_user() 。将消息从用户空间拷贝到内核空间。struct msg_msg load_msg(const void user src, size_t len){ struct msg_msg msg; struct msg_msgseg *seg; int err = -EFAULT; size_t alen; msg = alloc_msg(len); // [1] if (msg == NULL) return ERR_PTR(-ENOMEM); alen = min(len, DATALEN_MSG); if (copy_from_user(msg + 1, src, alen)) // [2] 从用户态拷贝数据,0xfd0字节 goto out_err; for (seg = msg->next; seg != NULL; seg = seg->next) { len -= alen; src = (char user )src + alen; alen = min(len, DATALEN_SEG); if (copy_from_user(seg + 1, src, alen)) // [3] 剩下的拷贝到其他segment,0xff8字节 goto out_err; } err = security_msg_msg_alloc(msg); if (err) goto out_err; return msg;out_err: free_msg(msg); return ERR_PTR(err);}内核消息结构:2.2 msgsrv() 数据接收总体流程: msgrcv() -> ksys_msgrcv() -> do_msgrcv() -> find_msg() & do_msg_fill() & free_msg()。 调用 find_msg() 来定位正确的消息,将消息从队列中unlink,再调用 do_msg_fill() -> store_msg() 来将内核数据拷贝到用户空间,最后调用 free_msg() 释放消息。long ksys_msgrcv(int msqid, struct msgbuf __user msgp, size_t msgsz, long msgtyp, int msgflg){ return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);}static long do_msgrcv(int msqid, void user buf, size_t bufsz, long msgtyp, int msgflg, long (msg_handler)(void user , struct msg_msg , size_t)){ // 注意:msg_handler 参数实际指向 do_msg_fill() 函数 int mode; struct msg_queue msq; struct ipc_namespace ns; struct msg_msg msg, copy = NULL; DEFINE_WAKE_Q(wake_q); ... ... if (msgflg & MSG_COPY) { if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT)) return -EINVAL; copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax)); // [4] if (IS_ERR(copy)) return PTR_ERR(copy); } mode = convert_mode(&msgtyp, msgflg); rcu_read_lock(); msq = msq_obtain_object_check(ns, msqid); ... ... for (;;) { struct msg_receiver msr_d; msg = ERR_PTR(-EACCES); if (ipcperms(ns, &msq->q_perm, S_IRUGO)) goto out_unlock1; ipc_lock_object(&msq->q_perm); / raced with RMID? / if (!ipc_valid_object(&msq->q_perm)) { msg = ERR_PTR(-EIDRM); goto out_unlock0; } msg = find_msg(msq, &msgtyp, mode); // [1] 调用 find_msg() 来定位正确的消息。之后检查并unlink消息。 if (!IS_ERR(msg)) { / Found a suitable message. Unlink it from the queue. / if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) { msg = ERR_PTR(-E2BIG); goto out_unlock0; } / If we are copying, then do not unlink message and do not update queue parameters. / if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); // [5] 若设置了MSG_COPY,则跳出循环,避免unlink goto out_unlock0; } list_del(&msg->m_list); ... ... }out_unlock0: ipc_unlock_object(&msq->q_perm); wake_up_q(&wake_q);out_unlock1: rcu_read_unlock(); if (IS_ERR(msg)) { free_copy(copy); return PTR_ERR(msg); } bufsz = msg_handler(buf, msg, bufsz); // [2] 调用 do_msg_fill() 把消息从内核拷贝到用户。具体代码如下所示 free_msg(msg); // [3] 拷贝完成后,释放消息。 return bufsz;}消息拷贝: do_msg_fill() -> store_msg() 。和创建消息的过程一样,先拷贝消息头(msg_msg结构对应的数据),再拷贝segment(msg_msgseg结构对应的数据)。static long do_msg_fill(void user dest, struct msg_msg msg, size_t bufsz){ struct msgbuf user msgp = dest; size_t msgsz; if (put_user(msg->m_type, &msgp->mtype)) return -EFAULT; msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz; // [1] 检查请求的数据长度是否大于 msg->m_ts ,超过则只能获取 msg->m_ts 长度的数据(为了避免越界读)。本例中,msgsz 为0x1fc8字节, if (store_msg(msgp->mtext, msg, msgsz)) // [2] 最后调用 store_msg()将 msgsz也即0x1fc8字节拷贝到用户空间,代码如下所示 return -EFAULT; return msgsz;}int store_msg(void __user dest, struct msg_msg msg, size_t len){ size_t alen; struct msg_msgseg seg; alen = min(len, DATALEN_MSG); // [1] 和创建消息的过程一样,alen=0xfd0 if (copy_to_user(dest, msg + 1, alen)) // [2] 先拷贝消息头 return -1; for (seg = msg->next; seg != NULL; seg = seg->next) { // [3] 遍历其他segment len -= alen; dest = (char __user )dest + alen; alen = min(len, DATALEN_SEG); // [4] 本例中为0xff8 if (copy_to_user(dest, seg + 1, alen)) // [5] 再拷贝segment return -1; } return 0;}消息释放:store_msg() 。先释放消息头,再释放segment。void free_msg(struct msg_msg msg){ struct msg_msgseg seg; security_msg_msg_free(msg); seg = msg->next; kfree(msg); // [1] 释放 msg_msg while (seg != NULL) { // [2] 释放 msg_msgseg struct msg_msgseg tmp = seg->next; cond_resched(); kfree(seg); // [3] seg = tmp; }}MSG_COPY:见 do_msgrcv() 中 [4]处,如果用flag MSG_COPY来调用 msgrcv() (内核编译时需配置CONFIG_CHECKPOINT_RESTORE选项,默认已配置),就会调用 prepare_copy() 分配临时消息,并调用 copy_msg() 将请求的数据拷贝到该临时消息(见 do_msgrcv() 中 [5]处)。在将消息拷贝到用户空间之后,原始消息会被保留,不会从队列中unlink,然后调用free_msg()删除该临时消息,这对于利用很重要。为什么?因为本漏洞在第一次UAF的时候,没有泄露正确地址,所以会破坏msg_msg->m_list双链表指针,unlink会触发崩溃。本题的UAF会破坏前16字节,如果某漏洞可以跳过前16字节,是否不需要注意这一点?void *memdump = malloc(0x1fc8);msgrcv(qid, memdump, 0x1fc8, 1, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);3. Fire of Salvation 简单模式利用特点:大小为kmalloc-4096的UAF。任意读:hardened_usercopy 机制不允许修改size越界读写。可利用UAF篡改msg_msg->m_ts和msg_msg->next(指向的下一个segment前8字节必须为null,避免遍历消息时出现访存崩溃)。任意写:创建一个需要多次分配堆块的消息(>0xfd0),在拷贝消息头(msg_msg结构)的时候利用userfault进行挂起,然后利用UAF篡改msg_msg->next指向目标地址,目标地址的前8字节必须为NULL(避免崩溃),解除挂起后就能实现任意写。任意写的原理如下图所示:3.1 步骤1——泄露内核基址泄露内核基址:由于开启了FG-KASLR,只能喷射大量shm_file_data对象(kmalloc-32)来泄露地址,因为FG-KASLR是在boot时对函数和某些节进行二次随机化,而shm_file_data->ns这种指向全局结构的指针不会被二次随机化。我们可以传入消息来分配1个kmalloc-4096的消息头和1个kmalloc-32的segment,然后利用UAF改大msg_msg->m_ts,调用msgrcv()读内存,这样就能越界读取多个kmalloc-32结构,泄露地址。注意,需使用MSG_COPY flag避免unlink时崩溃。原理如下图所示:3.2 步骤2——泄露cred地址泄露cred地址:再次利用任意读,从init_task开始找到当前进程的task_struct(也可以调用 prctl SET_NAME来设置comm成员,以此标志来暴搜,详见 Google CTF Quals 2021 Fullchain writeup)。本题提供了vmlinux符号信息,task_struct->tasks偏移是0x398,该位置的前8字节为null,可以当作1个segment;real_cred和cred指针在偏移0x538和0x540处,前面8字节也是null。利用UAF改大msg_msg->m_ts,将msg_msg->next改为&task_struct+0x298-8,调用msgrcv()读内存。3.3 步骤3——篡改cred & real_cred指针篡改cred & real_cred指针:根据pid找到当前进程后,利用UAF篡改msg_msg->next指向&real_cred-0x8,调用msgsnd()写内存,即可将real_cred和cred指针替换为init_cred即可提权。4. Wall of Perdition 复杂模式利用特点:大小为kmalloc-64的UAF。现有的任意写、任意释放技术: Four Bytes of Power: Exploiting CVE-2021-26708 in the Linux kernel 中介绍了如何伪造msg_msg->m_ts来实现任意写,也通过msg_msg->security指针实现了任意释放,但是本题关闭了SELinux,则msg_msg->security指针总是指向NULL,本题不适用。4.1 步骤1——越界读泄露内核基址、msg_msg->m_list.next / prev创建2个消息队列:[...]void send_msg(int qid, int size, int c){ struct msgbuf { long mtype; char mtext[size - 0x30]; } msg; msg.mtype = 1; memset(msg.mtext, c, sizeof(msg.mtext)); if (msgsnd(qid, &msg, sizeof(msg.mtext), 0) == -1) { perror(

GKForFun commented 2 years ago

想问一下师傅这道题debugging的时候,add-symbol-file 后下函数断点没有按照module base+offset的格式下,而是单纯的offset,有可能是什么问题呢?[抱拳]

bsauce commented 2 years ago

想问一下师傅这道题debugging的时候,add-symbol-file 后下函数断点没有按照module base+offset的格式下,而是单纯的offset,有可能是什么问题呢?[抱拳]

关KASLR了吗?

GKForFun commented 2 years ago

@bsauce

想问一下师傅这道题debugging的时候,add-symbol-file 后下函数断点没有按照module base+offset的格式下,而是单纯的offset,有可能是什么问题呢?[抱拳]

关KASLR了吗?

加了 nokalsr,还是有一些问题,师傅方便加个微信吗?GhostK1911 [抱拳]