Android內核提權cve-2014-3153研究筆記

一、簡介

我這裡把我自己的理解總結下,看別人的總是雲山霧繞,不得要領。還是要有自己的思路。當然也希望自己寫的通俗一些,那麼又有一大批人能看懂了就。

文中圖片修改了文尾鏈接處作者的圖片,部分例子採用參考中所得。各位想做下實驗的可以參考我上一篇的編譯過程,也可以看我給出的鏈接。

受影響的Linux內核系統可能被直接DOS,精心設計可以獲取根許可權。這個漏洞利用的核心就是,通過兩個流程bug造成程序棧中變數沒有清理,然後利用棧內存共用修改棧值,最終繞過地址讀寫限制實現提權。

二、觸發機制

科普下鎖的概念:鎖就是由於多線程同時訪問資源會造成資源更改混亂而增加的概念。簡單說,有一個公共資源。一個人在用的時候,其他人就要等著。不能兩個或多個人同時用。

這個漏洞利用Futex(Fast UserspacemuTEX)(是一種鎖機制)的不同喚醒方式,繞過了棧數據清理。從而控制了流程。

如何繞過?

漏洞利用了futex_requeue,futex_lock_pi,futex_wait_requeue_pi三個函數存在的兩個bug位於futex.c

git clone android.googlesource.com -bgoldfish3.4

cd goldfish

git checkout e8c92d268b8b8feb550ca8d24a92c1c98ed65acekernel/futex.c

可以自行下載一下。

2.1.relockBUG

relock漏洞源於futex_lock_pi函數(由futex_lock_pi_atomic實現),futex_lock_pi(&uaddr)調用之後,調用地址uaddr被鎖住,只有利用解鎖futex_unlock_pi後,才能被其他線程利用。

futex_lock_pi_atomic又是由cmpxchg_futex_value_locked(&curval,uaddr, 0,newval)實現並嘗試去鎖住uaddr。它的實現的含義是如果uaddr中存儲的值為0,那麼就說明沒有線程佔用鎖,成功的獲取到了鎖,並將當前線程的id寫進去。(uaddr是用戶空間的一個整形變數,被用於Futex系統架構中的futex互斥量。uaddr的值與其用戶空間的地址都會被Futex用到。)

但是問題來了,既然uaddr是用戶變數,那我們就可以手動設置為0.這時候地址上的鎖其實是釋放了,但上鎖後的堆棧里的內容沒有被清理。而且沒有喚醒阻塞在鎖上的線程,修改pi_state等。

這樣就可以利用通過手動設置uaddr=0的方式使兩個線程同時獲得鎖。這個叫relock。可以叫多重上鎖。

2.2 requeueBUG

futex_wait_requeue_pi的功能是讓調用線程阻塞在uaddr1上,然後等待futex_requeue的喚醒。喚醒過程將所有阻塞在uaddr1上的線程全部移動到uaddr2上去。

syscall(__NR_futex, &uaddr1,FUTEX_WAIT_REQUEUE_PI, 1, 0, &uaddr2, uaddr1); //在uaddr1上等待

syscall(__NR_futex,

&uaddr1, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2,

uaddr1);//嘗試獲取uaddr2上的鎖,然後喚醒uaddr1上等待的線程。如果uaddr2鎖獲取失敗,則將被喚醒線程添加到uaddr2的rt_waiter列表上,進入線程進入內核等待。啥時候進入內核等待,我們下面講。

進入內核等待方式圖

這個時候如果我們再次調用

syscall(__NR_futex, &uaddr1, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr1)

將失敗而直接返回,並不會進入系統調用。

而requeueBUG允許我們在以上兩條語句執行之後,首先設置uaddr2=0,然後執行這樣的語句:

syscall(__NR_futex, &uaddr2, FUTEX_CMP_REQUEUE_PI, 1, 0,&uaddr2, uaddr2);

這個語句中所有地址都變成了uaddr2,也就是說將等待在uaddr2上的線程重排到uaddr2上,這是不合邏輯的,但是Futex沒有檢查這樣的調用,也就是說沒有檢查uaddr1

==uaddr2的情況,從而造成了我們可以二次進入futex_requeue中進行喚醒操作。我們的線程進入內核等待後本來需要用內核喚醒的方式,現在被篡改成了普通的喚醒方式。致使一部分的棧沒有被清空。就是棧上的rt_waiter依然被連在rt_mutex的waiterlist上。

2.3 漏洞觸發

這裡還要了解一下futex_requeue中喚醒futex_wait_requeue_pi線程的兩種方式:

1.futex_proxy_trylock_atomic調用嘗試獲取uaddr2上的鎖,如果成功,則喚醒等待線程,函數返回,否則繼續執行。注意,這一步沒有進入內核互斥量中,如果成功,將不進入內核互斥量,而是直接返回到用戶空間,從而減小內核互斥量的開銷;

2.rt_mutex_start_proxy_lock嘗試獲取uaddr2鎖,如果成功,則喚醒等待線程,如果失敗,則將線程阻塞到uaddr2的內核互斥量上,將rt_waiter加入rt_mutex的waiterlist。

我們來總結下正常程序的執行狀態。

futex_wait_requeue_pi(uaddr1,uaddr2)等待被喚醒,正常情況下我們喚醒的方式要麼在內核喚醒,要麼普通的喚醒。這個要看uaddr2的鎖狀態。

漏洞觸發圖

但是我們這裡利用uaddr2加鎖使線程進入內核等待狀態,然後relockBUG uaddr2=0,最後

requeueBUGfutex_wait_requeue_pi(uaddr2,uaddr2),使阻塞在內核等待的線程用普通方式喚醒。構造了程序的異常執行流。

如何使一個線程按我們的方式執行如下圖:

異常流程構造圖

1.我們使用主線程1futex_lock_pi鎖住uaddr2。

2、3.創建線程2,等待被喚醒futex_requeue(uaddr1,uaddr2),uaddr2被鎖,所以進入內核等待,futex_wait_requeue_pi中的rt_waiter加入到rt_waiter的waiterlist上。

4.利用relockBUG,將uaddr2賦值為0,釋放uaddr2上的鎖.

5.利用requeue漏洞,調用futex_requeue(uaddr2,uaddr2),uaddr2沒鎖,觸發的普通喚醒模式。

導致rt_waiter沒有被清理。

而至於這個棧上的沒有owner的rt_waiter被鏈接在rt_mutex上,如果線程2結束,內核清理環境的時候,會去嘗試喚醒這個rt_waiter,結果就是造成內核崩潰。

二、提權過程

上一節我們講到了rt_waiter沒有了owner,但是有什麼用呢?

這裡我們會用一種機制來更改這個沒有owner的rt_waiter的數據

2.1 棧內存共用問題

#include <stdio.h> void A(int val) { int local; local =val; printf("A locacladdr =0x%x
",&local); } void B(int val2){ int local; printf("B locacladdr =0x%x
",&local); printf("B local =%d
",local); } int main(){ A(6); B(2); return 0;}這裡用GCC編譯,不進行優化 gcc -m32 foo.c -o foo -g ./foo A locacladdr = 0xffd119b8 B locacladdr = 0xffd119b8 B local = 6

圖棧

我們可以看到,這裡A,B函數的局部變數的地址是一樣的。有堆棧概念的人都知道,我們的每調用一個函數就會產生一個新的堆棧。但是上一個調用函數的棧中的數據如果沒銷毀,下一個函數構造的棧中就能利用。我們就可非法篡改數據。如上圖的實例。

哈,有啥用,我們可以直接調用A函數修改B函數中的數據。

2.2 修改內核中的數據

我們可以調用另一個結構相似的函數修改我們的rt_waiter結構數據。我們選取__sys_sendmmsg函數。

有其他選擇么?有。有一個分析棧空間的腳本,checkstack.pl的腳本,斷到futex_wait_requeue_pi上可以看到很多函數。這裡選擇可以完成rt_waiter所在棧深度修改的一個。

這裡我們要修改的是鏈表。

rt_waiter結構

type = struct rt_mutex_waiter{ struct plist_nodelist_entry; struct plist_nodepi_list_entry; struct task_struct*task; struct rt_mutex *lock} plist結構 struct plist_node{ int prio;struct list_head prio_list;//有個next和prev的指針struct list_head node_lsit;//有個next和prev的指針} sendmmsg的函數聲明及主要結構如下:int sendmmsg(int sockfd, struct mmsghdr *msgvec, unsigned intvlen,unsigned int flags); struct mmsghdr {struct msghdr msg_hdr;unsigned int msg_len;}; struct msghdr { void *msg_name; socklen_t msg_namelen;struct iovec *msg_iov;size_t msg_iovlen; void *msg_control; size_t msg_controllen; int msg_flags;}; struct iovec { void *iov_base;__kernel_size_t iov_len;};

其中位置重疊部分如圖

看下我們鎖機制中鏈表的形式是這樣的

我們利用內核鎖的喚醒在內核中插入鏈表,這個插入的位置可以根據prio參數來選擇,因為程序會按順序排,我們只要適當的修改prio參數即可。雖然可以更改內核中的值了,但這個地址內核地址不可控。

怎麼利用?

這裡分兩步:

(一)、內核任意地址寫入值(寫入的值不可控)

我們在用戶態地址上利用mmap構建一個rt_waiter的結構fake_node。如圖

在內核中把rt_waiter指向用戶態的fake_node.這時候我們我們在用戶態的fake_node就可以隨意指定內核地址.

假設我們要修改內核地址A的值,我們就把fake_node的node.prev指向(A-offset),這裡offset=sizeof(prio)+sizeof(list_head);我們把A-offset當成了一個plist結構。其實沒人知道是不是。這個時候再利用漏洞在A節點和fake_node之間插入一個內核節點,那麼A節點的node.prev就指向了新節點的地址,雖然這個地址我們暫時不可控,但我們實現了任意內核地址A寫入數據。

(二).實現線程任意地址可讀寫

我們這裡需要找到特定線程的thread_info,方法很簡單線程任意棧地址與上0xffffe000。這個位置是固定的。thread_info的地址,再定義正確的thread_info的結構,就可以得到addr_limit的地址了。addr_limit是限制我們訪問空間地址位置的,限制在哪,我們就只能讀小於它的地址,只要我們把它改成0xffffffff。我們就可以實現,任意地址的讀寫。

目測不容易。我們現在只能實現任意地址寫,但地址上寫了啥,還不知道。

沒關係,我們這裡創建兩個線程A,B.線程B循環創建。

A實現循環讀取addr_limit的值,顯然開始的時候讀不到,就一直讀著。線程B利用任意地址寫值得方式把自己的不可控的rt_wait地址寫到A的addr_limit中,由於內核中不同線程棧位置不同。我們的線程B不斷的創建,總有機會得到一個比A線程高的地址。只要我們把這個高地址寫到A線程的addr_limit中,那麼線程A的addr_limit位置就能任意改寫了。(不同線程使用不同位置的棧)

簡單說就是先利用內核漏洞把addr_limit值的改到比本線程高的值,用戶態可以改寫了,然後直接在用戶態addr_limit=0xffffffff.這下任意內核地址就都可以讀寫了。

2.3、內核提權

thread_info包含了線程的主要信息,當然也就包括了線程的task_struct。而task_struct結構體包含了該線程的所有信息。這其中就包括許可權方面的重要證書信息cred,該結構體是線程許可權的管理者,標識了當前線程的許可權。我們只要如下更改:

credbuf.uid = 0;

credbuf.gid = 0;

credbuf.suid = 0;

credbuf.sgid = 0;

credbuf.euid = 0;

credbuf.egid = 0;

credbuf.fsuid = 0;

credbuf.fsgid = 0;

credbuf.cap_inheritable.cap[0] = 0xffffffff;

credbuf.cap_inheritable.cap[1] = 0xffffffff;

credbuf.cap_permitted.cap[0] = 0xffffffff;

credbuf.cap_permitted.cap[1] = 0xffffffff;

credbuf.cap_effective.cap[0] = 0xffffffff;

credbuf.cap_effective.cap[1] = 0xffffffff;

credbuf.cap_bset.cap[0] = 0xffffffff;

credbuf.cap_bset.cap[1] = 0xffffffff;

securitybuf.osid = 1;

securitybuf.sid = 1;

taskbuf.pid = 1;

三、總結

本文利用自己的思路將CVE-2014-3153漏洞利用過程整理了下,整個過程所獲很多,把自己很多連不起來的知識融匯了一下。下面總結下:

1.分析得出兩個BUG,利用bug實現了,實現了內核棧上殘留有效數據。

2.利用棧內存共用問題,實現了內核棧數據的更改。(修改的值不可控)

3.通過多線程配合實現了內核數據任意讀寫從而提權。

參考:

1.blog.topsec.com.cn/ad_l

2.《漏洞戰爭》CVE-2014-3153Android內核Futex提取漏洞

本文由看雪論壇 inquisiter 原創 轉載請註明來自看雪社區


推薦閱讀:

操作系統為什麼都有無數的漏洞?是故意留下的還是技術上無法達到完美?
Metasploit 學習筆記 (三)
熱門MySQL開源管理工具phpMyAdmin &lt; 4.7.7 CSRF漏洞
無Sockets的遠程溢出漏洞利用方法
利用驗證碼繞過的小技巧

TAG:Android | 漏洞 | 移动设备提权 |