作者:啟明星辰ADLab
公眾號:https://mp.weixin.qq.com/s/fb5MM7q9k3U1Ni5JoIvxaQ

1. 漏洞背景

Linux內核中的POSIX 消息隊列實現中存在一個UAF漏洞CVE-2017-11176。攻擊者可以利用該漏洞導致拒絕服務或執行任意代碼。本文將從漏洞成因、補丁分析以及漏洞復現等多個角度對該漏洞進行詳細分析。

2. 漏洞分析

Posix消息隊列允許異步事件通知,當往一個空隊列放置一個消息時,Posix消息隊列允許產生一個信號或啟動一個線程。這種異步事件通知調用mq_notify函數實現,mq_notify為指定隊列建立或刪除異步通知。由于mq_notify函數在進入retry流程時沒有將sock指針設置為NULL,可能導致UAF漏洞。

從補丁代碼可知,將sock設置為NULL即可。

img

接下來看看漏洞起因,這里以4.1.0版本源碼為例。

img

在mq_notify函數中, u_notification是從用戶層傳進來的,1193行判斷u_notification是否為空,如果非空,通過copy_from_user將u_notification中 的數據拷貝到notification中,這里將數據從用戶層拷貝到了內核層。如果拷貝失敗,直接退出。

img

接下來,nc和sock分別置空。行1203,如果u_notification不為空,首先依次判斷notification.sigev_notify必須為SIGEV_NONE或SIGEV_SIGNAL或SIGEV_THREAD。如果notification.sigev_notify為SIGEV_SIGNAL,就判斷該信號是否合法。

img

行1212,如果notification.sigev_notify為SIGEV_THREAD,進入關鍵代碼塊。行1216,通過alloc_skb創建一個notify_skb,用于接收數據。行1221,通過copy_from_user將notification.sigev_value.sival_ptr指向的數據拷貝到nc->data中。這里必須成功,不然直接退出;行1229,調用skb_put設置消息數據頭部。行1231到行1248是retry循環體。行1232,調用fdget函數獲取文件描述符。行1237,調用netlink_getsockbyfilp函數通過文件描述符獲取netlink_sock,具體看一下netlink_getsockbyfilp函數。

img

調用file_inode通過filp找到對應的inode節點,然后通過SOCK_I函數處理inode節點。

img

這里通過宏container_of在socket_alloc結構體中找出socket成員。這里解釋一下,SOCKET_I返回值是socket結構體。其實sock結構體中第一個成員sock_common也是socket類型,是一個迷你版socket。

img

下面看一下sock_common結構體。

img

行1609,獲取到sock后,然后判斷sock->sk_family是否等于AF_NETLINK。行1613,接著調用sock_hold增加引用計數。sock_hold函數如下:

img

這里atomic_inc進行sk_refcnt加1。netlink_getsockbyfilp函數返回sock,這時sock的引用計數為1。接下來,行1246,調用netlink_attachskb。這是個關鍵函數,該函數功能是將skb綁定到netlink socket上,具體關鍵代碼如下:

img

行1683,調用sock_put減少引用計數一次,最后return 1,函數返回,直接goto到retry標簽地方。

img

這里行1237和行1246,這兩處調用正好進行了引用計數抵消。行1247的if語句中并沒有將sock置空,再看行1233,如果f.file為空,那就直接goto到out標簽。out標簽代碼如下:

img

行1306,判斷sock是否為空,如果不為空,調用netlink_detachskb函數。

img

釋放skb,并減少sk引用計數,進行釋放。 那么就有問題了,如果我們創建A線程保持netlink_attachskb返回1,并重復retry邏輯,這個時候sock的引用計數是保持平衡的,一加一減,但是sock并不是為空。同時再創建B線程去關閉netlink socket對應的文件描述符。由于B線程關閉了netlink socket的文件描述符,那A線程在retry邏輯中,行1232,調用fdget時會失敗,然后直接goto到out標簽,進行釋放,進行了二次釋放,導致漏洞。這個漏洞是屬于條件競爭型的二次釋放漏洞,只在一個線程中,是無法觸發漏洞。

這個漏洞原理比較簡單,但是如何觸發這個漏洞還是比較復雜。首先,如何讓netlink_attachskb返回1,從而順利進入retry邏輯。再次回看netlink_attachskb的實現。

img

行1657,通過nlk_sk函數通過sk獲取netlink_sock。這里的nlk_sk如下。

img

通過調用宏container_of獲取netlink_sock。netlink_sock結構體如下:

img

netlink_sock結構體第一個成員是sock類型,而sock結構體的第一個成員是socket。行1660,第一個if判斷必須得進入。

img

!netlink_skb_is_mmaped(skb)肯定返回true,關鍵是sk->sk_rmem_alloc>sk->sk_rcvbuf || test_bit(NETLINK_CONGESTED, &nlk->state)結果必須是true。

這里通過設置sk->sk_rmem_alloc的大小繞過check更為方便,代碼如下。

img

假如if判斷不通過,接著調用netlink_skb_set_owner_r函數,如下所示。

img

行878,調用宏atomic_add,該宏執行原子加操作。這行代碼的含義是:在sk->sk_rmem_alloc的基礎上加上skb->truesize。等同于sk->sk_rmem_alloc += skb->truesize。既然該函數里這行代碼可以直接增加sk->sk_rmem_alloc的大小,那么可不可以多次調用netlink_skb_set_owner_r函數增加sk->rmem_alloc的值?理論上是完全可以的,看看如何從用戶層到達這個函數。

img

通過understand工具可以快速找到netlink_skb_set_owner_r的調用鏈:

netlink_sendmsg->netlink_unicast->netlink_attachskb->netlink_skb_set_owner_r

如何順利的通過函數調用路徑?這里需要分析如何從netlink_sendmsg到達netlink_skb_set_owner_r。

netlink_sendmsg函數實現如下。

img

行2285,首先判斷msg->msg_flag不能為MSG_OOB,繼續往下看。

img

行2292,判斷msg->msg_namelen的長度,這里必須不為空,當然也不會為空。進入if后,判斷addr->nl_family是否等于AF_NETLINK。行2299,判斷dst_group或dst_portid不為空,dst_group表示多播模式,dst_portid來自于addr->nl_pid,因此保證dst_portid不為空比較容易。接下來:

img

行2320,判斷了msg->msg_iter.iov->iov_base不能為空。并且len不可以大于sk->sk_sndbuf-32。

img

其實整個函數中,用戶層可控的只有這么多。直接看netlink_unicast的調用。

img

netlink_unicast函數實現如下:

img

整個函數中,用戶能控制的不多。行1783,設置了timeo,這里要保證nonblock為msg->msg_flags&MSG_DONTWAIT,這樣線程才不會被block。行1790,判斷sk是否為內核版的sk,在用戶層創建socket時應使用NETLINK_USERSOCK。行1793,判斷是否有sk_filter,這里保證不進入該if語句,不要設置過濾器。行1800,直接調用netlink_attachskb,成功到達netlink_skb_set_owner_r函數。這算是通過調用netlink_sendmsg來增加sk->sk_rmem_alloc的過程。其實我們不光可以增加sk->sk_rmem_alloc,還可以減小sk->sk_rcvbuf。

那么如何減小sk->sk_rcvbuf?在setsockopt函數中,找到sock_setsockopt函數中對sk->sk_rcvbuf的操作。

img

行773,sk->sk_rcvbuf取val*2和SOCK_MIN_RCVBUF之間的最大值。行755,val取val和sysctl_rmem_max之間的最小值。行749,這個case為SO_RCVBUF。繼續往上看。

img

行693,要保證optlen不小于sizeof(int)。行696,將optval賦值到val中,這里optval是用戶可控的。行703,switch分發optname,所以要保證optname為SO_RCVBUF。這樣就可以保證順利到達修改sk->rcvbuf的代碼處。

到這里,我們通過兩種方式進行繞過netlink_attachskb函數中的第一個check。

1) 通過netlink_sendmsg增加sk->sk_rmem_alloc的值。

2) 通過sock_setsockopt盡可能地減小sk->rcvbuf的值。

進入if語句后,看如下代碼:

img

這段代碼會讓當前線程進入等待狀態,直接block。如果不想進入等待狀態,只有設置sock_flag為SOCK_DEAD。但是如果把sock_flag設置成SOCK_DEAD,那后面也沒有必要進行,因此這里是必然要進入等待狀態的。一種巧妙的方法是直接調用wake_up_interruptible強行喚醒線程。那如何調用wake_up_interruptible呢?函數調用鏈非常簡短:netlink_setsockopt->wake_up_interruptible。

在Netlink_setsockopt函數中:

img

行2182,調用wake_up_interruptible喚醒線程。行2178,case為NETLINK_NO_ENOBUFS。

img

行2131,判斷level必須為SOL_NETLINK,行2134,判斷optname不能為NETLINK_RX_RING和NETLINK_TX_RING,同時保證optlen大于等于sizeof(int)。行2139,switch分發optname,這里要保證optname為NETLINK_NO_ENOBUFS。到這里,基本上就可以保證netlink_attachskb返回1。

保證進入retry循環后,這個時候sock已經不為空。接下來要使retry循環中出錯,直接跳轉到out,代碼如下:

img

行1232,通過fdget獲取notification.sigev_signo的fd。Notification.sigev_signo是用戶態傳進來的,因此完全可以在用戶層直接close這個socket。在用戶層close這個socket后,行1233,進入if邏輯,然后跳到out標簽。

img

這個時候sock是非空的,if判斷為真,進入netlink_destachskb,接著就是free崩潰。

3. 漏洞復現

對于UAF類型的漏洞,通用方法就是使用堆噴射占位。本次漏洞中被多次釋放的對象是netlink_sock對象。netlink_sock對象大小為0x3f0字節,即是1008byte。

img

根據內核對象內存分配規則, netlink_sock對象應該從kmalloc-1024這個緩存中進行分配。

slab分配器在分配對象時,遵守后進先出的規則。

下面是slab分配器釋放對象的過程。

img

要釋放的對象objp放在了ac->entry[]的末端。下面是slab分配器分配對象的過程:

img

分配對象直接從ac->entry[]末端彈出一個對象。

所以一個剛剛被釋放的對象是排在鏈表末段,如果此時恰好在同一緩存中進行對象分配,那剛剛釋放的對象就會被重新分配出去,這就出現兩個指針指向同一塊內存地址。要想保證申請的內存正好落在漏洞對象的內存位置中,需要把握住幾點:

  1. 堆噴對象使用的內核緩存應該和漏洞對象內存在同一個緩存中。即大小必須落在同一個kmalloc-X中。

  2. ac本身是array_chche結構體,該結構體是本地高速緩存,每個CPU對應一個,所以還要保證堆噴申請的對象和漏洞對象在同一個CPU本地高速緩存中。

  3. 如果堆噴申請的對象只是短暫駐留,當該函數返回時將申請的對象進行了釋放,導致無法正確占位。所以要能保證申請的對象不被釋放,至少保證在使用漏洞對象時不被釋放,這里要采用駐留式內存占位,可以采取讓某些系統調用過程阻塞。

  4. slab緩存碎片化問題,這里要占位的對象大小為1008,對象尺寸比較大,占據四分之一頁,比較整齊,應該沒有碎片化問題。

那么如何判斷堆噴是否成功呢?

通用情況下,在進行堆噴時候,構造堆噴對象時,有必要在對應漏洞對象的一些特殊成員域的內存偏移處設置magic value,然后可以采用系統調用去獲取漏洞對象中相關數據進行判斷。netlink_sock結構體幾個關鍵的成員如下。

img

采用getsockname系統調用獲取數據,getsockname會調用netlink_getname。具體看一下netlink_getname函數:

img

代碼1576行,將netlink_sock對象中的portid復制給nladdr->nl_pid。代碼1577行,如果nlk->group為0,將nladdr->nl_groups賦值為NULL,這里避免解引用nlk->groups指針,直接可以在構造堆噴對象時將groups域填零。而nladdr是從addr轉換過來的,addr就是從用戶層傳入的緩沖區。

堆噴成功如下:

img

通常情況是覆蓋結構體中的函數指針或者包含函數指針的結構體成員,這視情況而定。這里選擇覆蓋wait等待隊列。netlink_sock結構體如下:

img

wait_queue_haed_t結構體如下:

img

task_list成員是一個雙向循環鏈表頭,task_list中鏈接的每一個成員都是需要處理的等待例程元素。那該如何使用這個成員?看如下代碼。

img

這是netlink_setsockopt函數中的代碼片段,前面恢復線程復活分析過,這里將會調用netlink_sock對象中的等待例程,直接使用參數nlk->wait。繼續深入分析:

img

調用__wake_up_common函數:

img

代碼70行,宏list_for_each_entry_safe遍歷q->task_list中的成員,返回到curr。代碼68行,curr為wait_queue_t指針,說明q->task_list鏈表中存的是wait_queue_t類型的元素,wait_queue_t結構體如下:

img

wait_queue_t結構體中有一個函數指針func。再看wake_up_common函數中,代碼73行,直接執行curr>func函數,可以通過構造wait_queue的func參數控制RIP。再回過頭看list_for_each_entry_safe宏:

img

pos是wait_queue元素,代碼62行,對pos->member.next進行了解引用,這里的pos->member就是wait_queue中的task_list。__wait_queue中的task_list也是一個鏈表頭,需要指向一個list_head,所以還必須要構造一個假的list_head以便于該宏進行解引用。測試如下:

img

接下來就是通過ROP鏈繞過SMEP執行提權代碼。成功提權后如下所示:

img

4. 參考鏈接

[1] https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html

[2] https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part2.html

[3] https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.html

[4] https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part4.html


啟明星辰積極防御實驗室(ADLab)

ADLab成立于1999年,是中國安全行業最早成立的攻防技術研究實驗室之一,微軟MAPP計劃核心成員。截止目前,ADLab通過CVE發布Windows、Linux、Unix等操作系統安全或軟件漏洞近400個,持續保持國際網絡安全領域一流水準。實驗室研究方向涵蓋操作系統與應用系統安全研究、移動智能終端安全研究、物聯網智能設備安全研究、Web安全研究、工控系統安全研究、云安全研究。研究成果應用于產品核心技術研究、國家重點科技項目攻關、專業安全服務等。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/785/