Pwn2Own 2017 Linux 內核提權漏洞分析
0.前言
在2017年PWN2OWN大賽中,長亭安全研究實驗室(Chaitin Security Research Lab)成功演示了Ubuntu 16.10 Desktop的本地提權。本次攻擊主要利用了linux內核IPSEC框架(自linux2.6開始支持)中的一個內存越界漏洞,CVE編號為CVE-2017-7184。
眾所周知,Linux的應用範圍甚廣,我們經常使用的Android、Redhat、CentOS、Ubuntu、Fedora等都使用了Linux操作系統。在PWN2OWN之後,Google、Redhat也針對相應的產品發出了漏洞公告或補丁(見參考資料)。並表示了對長亭安全研究實驗室的致謝,在此也建議還沒有升級伺服器內核的小夥伴們及時更新內核到最新版本:P
不同於通常的情況,為了增加比賽難度,本次PWN2OWN大賽使用的Linux版本開啟了諸多漏洞緩解措施,kASLR、SMEP、SMAP都默認開啟,在這種情況下,漏洞變得極難利用,很多漏洞可能僅僅在這些緩解措施面前就會敗下陣來。
另外值得一提的是,本次利用的漏洞隱蔽性極高,在linux內核中存在的時間也非常長。因為觸發這個漏洞不僅需要排布內核數據結構,而且需要使內核處理攻擊者精心構造的數據包,使用傳統的fuzz方式幾乎是不可能發現此漏洞的。
最終,長亭安全研究實驗室成功利用這個漏洞在PWN2OWN的賽場上彈出了PWN2OWN歷史上的第一個xcalc, ZDI的工作人員們看到了之後也表示驚喜不已。
下面一起來看一下整個漏洞的發現和利用過程。
1.IPSEC協議簡介
IPSEC是一個協議組合,它包含AH、ESP、IKE協議,提供對數據包的認證和加密功能。
為了幫助更好的理解漏洞成因,下面有幾個概念需要簡單介紹一下
(1) SA(Security Associstion)
SA由spi、ip、安全協議標識(AH或ESP)這三個參數唯一確定。SA定義了ipsec雙方的ip地址、ipsec協議、加密演算法、密鑰、模式、抗重放窗口等。
(2) AH(Authentication Header)
AH為ip包提供數據完整性校驗和身份認證功能,提供抗重放能力,驗證演算法由SA指定。
(3) ESP(Encapsulating security payload)
ESP為ip數據包提供完整性檢查、認證和加密。
2.Linux內核的IPSEC實現
在linux內核中的IPSEC實現即是xfrm這個框架,關於xfrm的代碼主要在net/xfrm以及net/ipv4下。
以下是/net/xfrm下的代碼的大概功能
xfrm_state.c 狀態管理xfrm_policy.c xfrm策略管理xfrm_algo.c 演算法管理xfrm_hash.c 哈希計算函數xfrm_input.c 安全路徑(sec_path)處理, 用於處理進入的ipsec包xfrm_user.c netlink介面的SA和SP(安全策略)管理
其中xfrm_user.c中的代碼允許我們向內核發送netlink消息來調用相關handler實現對SA和SP的配置,其中涉及處理函數如下。
xfrm_dispatch[XFRM_NR_MSGTYPES] = {[XFRM_MSG_NEWSA - XFRM_MSG_BASE] = { .doit = xfrm_add_sa },[XFRM_MSG_DELSA - XFRM_MSG_BASE] = { .doit = xfrm_del_sa },[XFRM_MSG_GETSA - XFRM_MSG_BASE] = { .doit = xfrm_get_sa, .dump = xfrm_dump_sa, .done = xfrm_dump_sa_done },[XFRM_MSG_NEWPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_add_policy },[XFRM_MSG_DELPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_get_policy },[XFRM_MSG_GETPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_get_policy, .dump = xfrm_dump_policy, .done = xfrm_dump_policy_done },[XFRM_MSG_ALLOCSPI - XFRM_MSG_BASE] = { .doit = xfrm_alloc_userspi },[XFRM_MSG_ACQUIRE - XFRM_MSG_BASE] = { .doit = xfrm_add_acquire },[XFRM_MSG_EXPIRE - XFRM_MSG_BASE] = { .doit = xfrm_add_sa_expire },[XFRM_MSG_UPDPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_add_policy },[XFRM_MSG_UPDSA - XFRM_MSG_BASE] = { .doit = xfrm_add_sa },[XFRM_MSG_POLEXPIRE - XFRM_MSG_BASE] = { .doit = xfrm_add_pol_expire},[XFRM_MSG_FLUSHSA - XFRM_MSG_BASE] = { .doit = xfrm_flush_sa },[XFRM_MSG_FLUSHPOLICY - XFRM_MSG_BASE] = { .doit = xfrm_flush_policy },[XFRM_MSG_NEWAE - XFRM_MSG_BASE] = { .doit = xfrm_new_ae },[XFRM_MSG_GETAE - XFRM_MSG_BASE] = { .doit = xfrm_get_ae },[XFRM_MSG_MIGRATE - XFRM_MSG_BASE] = { .doit = xfrm_do_migrate },[XFRM_MSG_GETSADINFO - XFRM_MSG_BASE] = { .doit = xfrm_get_sadinfo },[XFRM_MSG_NEWSPDINFO - XFRM_MSG_BASE] = { .doit = xfrm_set_spdinfo, .nla_pol = xfrma_spd_policy, .nla_max = XFRMA_SPD_MAX },[XFRM_MSG_GETSPDINFO - XFRM_MSG_BASE] = { .doit = xfrm_get_spdinfo },};
下面簡單介紹一下其中幾個函數的功能:
xfrm_add_sa
創建一個新的SA,並可以指定相關attr,在內核中,是用一個xfrm_state結構來表示一個SA的。
xfrm_del_sa
刪除一個SA,也即刪除一個指定的xfrm_state。
xfrm_new_ae
根據傳入參數,更新指定xfrm_state結構中的內容。
xfrm_get_ae
根據傳入參數,查詢指定xfrm_state結構中的內容(包括attr)。
3.漏洞成因
當我們發送一個XFRM_MSG_NEWSA類型的消息時,即可調用xfrm_add_sa函數來創建一個新的SA,一個新的xfrm_state也會被創建。在內核中,其實SA就是使用xfrm_state這個結構來表示的。
若在netlink消息裡面使用XFRMA_REPLAY_ESN_VAL這個attr,一個replay_state_esn結構也會被創建。它的結構如下所示,可以看到它包含了一個bitmap,這個bitmap的長度是由bmp_len這個成員變數動態標識的。
struct xfrm_replay_state_esn { unsigned int bmp_len; __u32 oseq; __u32 seq; __u32 oseq_hi; __u32 seq_hi; __u32 replay_window; __u32 bmp[0];};
內核對這個結構的檢查主要有以下幾種情況:
首先,xfrm_add_sa函數在調用verify_newsa_info檢查從用戶態傳入的數據時,會調用verify_replay來檢查傳入的replay_state_esn結構。
static inline int verify_replay(struct xfrm_usersa_info *p, struct nlattr **attrs){ struct nlattr *rt = attrs[XFRMA_REPLAY_ESN_VAL]; struct xfrm_replay_state_esn *rs; if (p->flags & XFRM_STATE_ESN) { if (!rt) return -EINVAL; rs = nla_data(rt); if (rs->bmp_len > XFRMA_REPLAY_ESN_MAX / sizeof(rs->bmp[0]) / 8) return -EINVAL; if (nla_len(rt) < xfrm_replay_state_esn_len(rs) && nla_len(rt) != sizeof(*rs)) return -EINVAL; } if (!rt) return 0; /* As only ESP and AH support ESN feature. */ if ((p->id.proto != IPPROTO_ESP) && (p->id.proto != IPPROTO_AH)) return -EINVAL; if (p->replay_window != 0) return -EINVAL; return 0;}
這個函數要求replay_state_esn結構的bmp_len不可以超過最大限制XFRMA_REPLAY_ESN_MAX。
另外,在這個創建xfrm_state的過程中,如果檢查到成員中有xfrm_replay_state_esn結構,如下函數中的檢查便會被執行。
int xfrm_init_replay(struct xfrm_state *x){ struct xfrm_replay_state_esn *replay_esn = x->replay_esn; if (replay_esn) { if (replay_esn->replay_window > replay_esn->bmp_len * sizeof(__u32) * 8) <-----檢查replay_window return -EINVAL; if (x->props.flags & XFRM_STATE_ESN) { if (replay_esn->replay_window == 0) return -EINVAL; x->repl = &xfrm_replay_esn; } else x->repl = &xfrm_replay_bmp; } else x->repl = &xfrm_replay_legacy; return 0;}
這個函數確保了replay_window不會比bitmap的長度大,否則函數會直接退出。
下面再來看一下xfrm_new_ae這個函數,它首先會解析用戶態傳入的幾個attr,然後根據spi的哈希值以及ip找到指定的xfrm_state,之後xfrm_replay_verify_len中會對傳入的replay_state_esn結構做一個檢查,通過後即會調用xfrm_update_ae_params函數來更新對應的xfrm_state結構。下面我們來看一下xfrm_replay_verify_len這個函數。
static inline int xfrm_replay_verify_len(struct xfrm_replay_state_esn *replay_esn, struct nlattr *rp){ struct xfrm_replay_state_esn *up; int ulen; if (!replay_esn || !rp) return 0; up = nla_data(rp); ulen = xfrm_replay_state_esn_len(up); if (nla_len(rp) < ulen || xfrm_replay_state_esn_len(replay_esn) != ulen) return -EINVAL; return 0;}
我們可以看到這個函數沒有對replay_window做任何的檢查,只需要提供的bmp_len與xfrm_state中原來的bmp_len一致就可以通過檢查。所以此時我們可以控制replay_window超過bmp_len。之後內核在處理相關IPSEC數據包進行重放檢測相關的操作時,對這個bitmap結構的讀寫操作都可能會越界。
4.漏洞利用
(1).許可權不滿足
/* All operations require privileges, even GET */ if (!netlink_net_capable(skb, CAP_NET_ADMIN)) return -EPERM;
在xfrm_user_rcv_msg函數中,我們可以看到,對於相關的操作,其實都是需要CAP_NET_ADMIN許可權的。那是不是我們就無法觸發這個漏洞了呢?
答案是否定的,在這裡我們可以利用好linux的命名空間機制,在ubuntu,Fedora等發行版,User namespace是默認開啟的。非特權用戶可以創建用戶命名空間、網路命名空間。在命名空間內部,我們就可以有相應的capability來觸發漏洞了。
(2).越界寫
當內核在收到ipsec的數據包時,最終會在xfrm_input解包並進行相關的一些操作。在xfrm_input中,找到對應的xfrm_state之後,根據數據包內容進行重放檢測的時候會執行x->repl->advance(x, seq);,即xfrm_replay_advance_esn這個函數。
這個函數會對bitmap進行如下操作1.清除[last seq, current seq)的bit
2.設置bmp[current seq] = 1我們可以指定好spi、seq等參數(內核是根據spi的哈希值以及ip地址來確定SA的),並讓內核來處理我們發出的ESP數據包,多次進行這個操作即可達到對越界任意長度進行寫入任意值。
(3).越界讀
我們的思路是使用越界寫,改大下一個replay_state_esn的結構中的bmp_len。之後我們就可以利用下一個bitmap結構進行越界讀。所以我們需要兩個相鄰的replay_state結構。我們可以使用defragment技巧來達到這個效果。即首先分配足夠多的同樣大小的replay_state結構把堆上原來的坑填滿,之後便可大概率保證連續分配的replay_state結構是相鄰的。
如上所述,使用越界寫的能力將下一個bitmap長度改大,即可使用這個bitmap結構做越界讀了。
圖中所示為被改掉bmp_len的bitmap結構。
(4).繞過kASLR
我們通過xfrm_del_sa介面把沒用的xfrm_state都給刪掉。這樣就可以在堆上留下很多的坑。之後我們可以向內核噴射很多struct file結構體填在這些坑裡。
如下,利用上面已經構造出的越界讀能力,我們可以泄露一些內核里的指針來算出內核的載入地址和bitmap的位置。
5.內核任意地址讀寫及代碼執行
因為已經繞過了內核地址隨機化,這時我們可以進行內核ROP構造了。
1.在這個漏洞的利用當中,我們可以在bitmap中偽造一個file_operations結構。
2.之後通過越界寫可以改寫掉我們剛剛在內核中噴射的struct file結構體的file_operations指針,使其指向合適的ROPgadget。
3.調用llseek函數(實際上已經是rop gadget)來執行我們事先已經準備好的ROP鏈。
4.通過多次改寫file_operations結構中的llseek函數指針來實現多次執行ROPgadget實現提權。
如上所述,因為我們的數據都是偽造在內核裡面,所以這種利用方式其實是可以同時繞過SMEP和SMAP的。
6.許可權提升
下面是長亭安全研究實驗室在pwn2own2017上彈出xcalc的瞬間。
5.後記
非常感謝slipper老師的指導和講解
感謝長亭安全研究實驗室的所有小夥伴:P
6.參考資料
IPSEC協議: IPsec - Wikipedia
linux命名空間機制: Namespaces in operation, part 1: namespaces overview
CVE-2017-7184: CVE-2017-7184: kernel: Local privilege escalation in XFRM framework
@thezdi: https://twitter.com/thezdi/status/842132539330375684
Android漏洞公告:https://source.android.com/security/bulletin/2017-05-01
Redhat:Red Hat Customer Portal
推薦閱讀: