原文來自安全客,作者:[email protected] Vulpecker Team
原文連結:https://www.anquanke.com/post/id/129468
最近在整理自己以前寫的一些Android核心漏洞利用的代碼,發現了一些新的思路。
CVE-2017-10661的利用是去年CORE TEAM在hitcon上分享過的:https://hitcon.org/2017/CMT/slide-files/d1_s3_r0.pdf。他們給出的利用是在有CAP_SYS_TIME這個capable權限下的利用方式,而普通使用者沒這個權限。最近整理到這裡的時候,想了想如何利用這個漏洞從0權限到root呢?沒想到竟然還能有一些收獲,分享一哈:
- CVE-2017-10661簡單分析
- CAP_SYS_TIME下的利用
- pipe的TOCTTOU
- 思考下連結清單操作與UAF
- 0權限下的利用
CVE-2017-10661簡單分析
關于CVE-2017-10661的分析和SYS_TIME下的利用,CORE TEAM的ppt中已經有比較清晰的解釋。我這裡再簡單的用文字描述一遍吧。
這個漏洞存在于Linux核心代碼 fs/timerfd.c的timerfd_setup_cancel函數中:
static void timerfd_setup_cancel(struct timerfd_ctx *ctx, int flags)
{
if ((ctx->clockid == CLOCK_REALTIME ||
ctx->clockid == CLOCK_REALTIME_ALARM) &&
(flags & TFD_TIMER_ABSTIME) && (flags & TFD_TIMER_CANCEL_ON_SET)) {
if (!ctx->might_cancel) { //[1][2]
ctx->might_cancel = true; //[3][4]
spin_lock(&cancel_lock);
list_add_rcu(&ctx->clist, &cancel_list); //[5][6]
spin_unlock(&cancel_lock);
}
} else if (ctx->might_cancel) {
timerfd_remove_cancel(ctx);
}
}
這裡會有一個race condition:假設兩個線程同時對同一個ctx執行timerfd_setup_cancel操作,可能會出現這樣的情況(垂直方向為時間線):
Thread1 Thread2
[1]檢查ctx->might_cancel,值為false
. [2]檢查ctx->might_cancel,值為false
[3]将ctx->might_cancel指派為true
. [4]将ctx->might_cancel指派為true
[5]将ctx加入到cancel_list中
. [6]将ctx再次加入到cancel_list中
是以,這裡其實是因為ctx->might_cancel是臨界資源,而這個函數對它的讀寫并沒有加鎖,雖然在
if(!ctx->might_cancel)
和
ctx->might_cancel
的時間間隔很小,但是還是可以産生資源沖突的情況,也就導緻了後面的問題:會對同一個節點執行兩次
list_add_rcu
操作,這是一個非常嚴重的問題。
首先
cancel_list
是一個帶頭結點的循環雙連結清單。
list_add_rcu
是一個頭插法加入節點的操作,是以第一次調用後,連結清單結構如圖:

而對我們的victim ctx再次調用list_add_rcu會變成什麼樣子呢?
static inline void list_add_rcu(struct list_head *new, struct list_head *head) {
__list_add_rcu(new, head, head->next);
}
static inline void __list_add_rcu(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
new->next = next;
new->prev = prev;
rcu_assign_pointer(list_next_rcu(prev), new); //可以看做 prev->next = new;
next->prev = new;
}
要注意的是,第二次操作,我們的new == head->next,于是操作相當于:
victim->next = victim;
victim->prev = victim;
那麼連結清單這時候就變成了這樣:
可以看到victim的next指針和prev指針都指向了自己。這時候就會發生一系列問題,第一我們再也沒辦法通過連結清單來通路到victim ctx後面的節點了(這點和漏洞利用關系不大),第二我們也沒辦法将victim這個節點從連結清單上删除,盡管我們可以在kfree ctx之前對其執行
list_del_rcu
操作:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
static inline void list_del_rcu(struct list_head *entry)
{
__list_del_entry(entry);
//上一句可描述為:
//entry->next->prev = entry->prev;
//entry->prev->next = entry->next;
entry->prev = LIST_POISON2;
}
于是
list_del_rcu
執行之後,連結清單又變成了這樣子:
是以盡管之後會執行kfree将victim ctx給free掉,但是我們的
cancel_list
連結清單還儲存着這段free掉的ctx的指針:
head->next
以及
ctx->prev
。是以如果後續有對
cancel_list
連結清單的一些操作,就會産生USE-AFTER-FREE的問題。
這也就是這個漏洞的成因了。
CAP_SYS_TIME下的利用
CORE TEAM的ppt裡給出了這種利用方式。他們從victim ctx釋放後并沒有真正從cancel_list拿下來,仍然可以通過周遊cancel_list通路到victim ctx這一點做文章。
對cancel_list的周遊在函數
timerfd_clock_was_set
:
void timerfd_clock_was_set(void)
{
ktime_t moffs = ktime_get_monotonic_offset();
struct timerfd_ctx *ctx;
unsigned long flags;
rcu_read_lock();
list_for_each_entry_rcu(ctx, &cancel_list, clist) {
if (!ctx->might_cancel)
continue;
spin_lock_irqsave(&ctx->wqh.lock, flags);
if (ctx->moffs.tv64 != moffs.tv64) {
ctx->moffs.tv64 = KTIME_MAX;
ctx->ticks++;
wake_up_locked(&ctx->wqh); //會走到 __wake_up_common函數
}
spin_unlock_irqrestore(&ctx->wqh.lock, flags);
}
rcu_read_unlock();
}
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) && //curr->func
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
}
思路就是
- 等victim ctx被free之後,進行堆噴将victim ctx覆寫成自己精心構造的資料(這裡可以用keyctl或者是sendmmsg實作)。
- 然後調用
函數,這時會周遊cancel_list,由于head->next就是我們的victim ctx,是以victim ctx會被這次操作引用到。資料構造得OK的話,會調用timerfd_clock_was_set
,而ctx就是我們的victim ctxwake_up_locked(&ctx->wqh)
- 這以後ctx->wqh是自己定義的資料,是以
的curr,curr->func也是我們可以決定的。\_\_wake\_up\_common
- 是以執行到curr->func的時候,我們就控制了PC寄存器,而X0等于我們的curr
- 劫持了pc,之後找rop/jop就能輕松實作提權操作,這裡不再多說。
為什麼說這是CAP_SYS_TIME權限下的利用方法呢?因為
timerfd_clock_was_set
函數的調用鍊是這樣:
timerfd_clock_was_set <-- clock_was_set <-- do_settimeofday <-- do_sys_settimeofday <--SYS_setttimeofday
使用者态需要調用settimeofday這個系統調用來觸發。而在
do_sys_settimeofday
函數裡有對CAP_SYS_TIME的檢查:
int do_sys_settimeofday(const struct timespec *tv, const struct timezone *tz)
{
...
error = security_settime(tv, tz); //權限檢查
if (error)
return error;
...
if (tv)
return do_settimeofday(tv);
return 0;
}
static inline int security_settime(const struct timespec *ts,
const struct timezone *tz)
{
return cap_settime(ts, tz);
}
int cap_settime(const struct timespec *ts, const struct timezone *tz)
{
if (!capable(CAP_SYS_TIME)) //檢查CAP_SYS_TIME
return -EPERM;
return 0;
}
是以我們如果想以這種方式來利用這個漏洞,就需要程序本身有CAP_SYS_TIME的權限,這也就限制了這種方法的适用範圍。于是我們想要從0權限來利用這個漏洞,就得另辟蹊徑。
pipe的TOCTTOU
在介紹0權限的利用方法思路之前,我覺得得先介紹下pipe的TOCTTOU機制,因為這個是接下來利用思路的一個基礎。關于這部分的内容,也可以參考shendi大牛的slide
TOCTTOU : time of check to time of use .寫程式的時候通常都會在使用前,對要使用的資料進行一個檢查。而這個檢查的時間點,和使用的時間點之間,其實是有空隙的。如果能在這個時間空隙裡,做到對已經check的資料的更改,那麼就可能在use的時刻,使用到非法的資料。
pipe的readv / writev就是這樣一個典型。以readv為例,readv會在
do_readv_writev
的
rw_copy_check_uvector
函數裡對使用者态傳進來的所有iovector進行合法性檢查:
struct iovec {
void *iov_base;
size_t iov_len;
};
ssize_t rw_copy_check_uvector(int type, const struct iovec __user * uvector,
unsigned long nr_segs, unsigned long fast_segs,
struct iovec *fast_pointer,
struct iovec **ret_pointer)
{
unsigned long seg;
ssize_t ret;
struct iovec *iov = fast_pointer;
...
if (nr_segs > fast_segs) {
iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL); //[1]
...
}
if (copy_from_user(iov, uvector, nr_segs*sizeof(*uvector))) {
...
}
...
for (seg = 0; seg < nr_segs; seg++) {
void __user *buf = iov[seg].iov_base;
ssize_t len = (ssize_t)iov[seg].iov_len;
...
if (type >= 0
&& unlikely(!access_ok(vrfy_dir(type), buf, len))) { //[2]
ret = -EFAULT;
goto out;
}
...
}
}
可以看到這個檢查函數做了兩件事:
[1]如果iovector的個數比較多(大于8),就會kmalloc一段記憶體,然後将使用者态傳來的iovector拷貝進去。當然如果比較小,就直接把使用者态傳來的iovector放到棧上。
[2]對iovector進行合法性檢查,確定所有的iovecor的iov_base都是使用者态位址。
這裡也就是pipe的time of check。
在檢查通過之後,會去執行pipe_read函數,相信分析過CVE-2015-1805的朋友們都知道,pipe_read函數裡對iovector的iov_base隻會做是不是可寫位址的檢查,而不會做是不是使用者态位址的檢查,然後有資料就寫入。pipe_read函數往iovector的iov_base裡寫入資料的時刻(__copy_to_user),就是pipe的time of use。
那麼這個check 和 use的間隙是多長呢?這取決于我們什麼時候往pipe的buffer裡寫入資料。因為pipe_read預設是阻塞的,如果pipe的buffer裡沒有資料,pipe_read就會一直被阻塞,直到我們調用writev往pipe的buffer寫資料。
是以,pipe的time of check to time of use這個間隔,可以由我們自己控制。
如果在這個時間間隔有辦法對iovector進行更改,那麼就可能往非法位址寫入資料:
那麼,怎麼才能在這個時間間隔,對iovector進行更改呢?
這當然要通過漏洞來實作:
1,堆溢出漏洞。前面分析知道,如果有8個以上的的iovctor,就會調用kmalloc來存儲這些iovector。如果能有一個核心堆溢出漏洞,那麼隻要把堆布局好,就能讓溢出的資料,該卸掉iovector的iov_base.
2,UAF漏洞。要知道,我們kmalloc的iovector也是有占位功能的,如果使用iovector進行堆噴,将free過的victim進行占位。然後觸發UAF,如果這個use的操作,能對占位的iovector進行更改,那麼也就實作了目的。
知道了pipe的TOCTTOU的基礎,我們可以來重新思考下CVE-2017-10661。
思考下連結清單操作與UAF
連結清單其實是個變化過程比較多的資料結構,對某節點的删除或者添加都會影響相鄰的節點。那如果一個節點出現了問題,對它的相鄰節點進行一系列操作會産生什麼樣的變化呢?在基于CVE-2017-10661将連結清單破壞之後,我在這裡将給出兩種情景。首先貼一張已經釋放了victim ctx之後,cancel_list的狀态圖吧:
victim ctx已經被free,但是head->next和ctx_A->prev仍然保留着這段記憶體的指針。那麼:
情景一:添加一個新的節點ctx_B
同樣還是頭插法,于是下面這幾段代碼會執行:
ctx_B->next = head->next;
ctx_B->prev = head;
head->next->prev = ctx_B; //這裡等價于 victim_mem->data2 = ctx_B
head->next = ctx_B;
可以看到,這個添加操作(list_add_rcu)會對已經free了的記憶體進行操作,會将victim_mem->data2指派為ctx_B。語言總是沒有圖檔來的直覺,添加操作執行後連結清單的狀态如圖:
結合我們之前讨論的pipe TOCTTOU,如果victim_mem剛好是由我們的pipe的iovector所占位,那麼這裡對data2的更改,可能就會對某個iov_base進行更改:iov_base = ctx_B。那麼這樣就允許我們對ctx_B->list進行任意寫入。
情景二:删除節點ctx_A
删除操作會影響前後兩個節點,我們假設ctx_A的next節點是ctx_C,那麼就有:
ctx_A->prev->next = ctx_A->next;//等價于 victim_mem->data1 = ctx_C
ctx_A->next->prev = ctx_A->prev;//等價于 ctx_C->prev = victim_mem
ctx_A->prev = LIST_POISION2;
與情景1類似,這個删除操作(list_del_rcu),也會已經free了的記憶體進行操作,将victim_mem->data1指派為ctx_C:
同樣的,如果victim_mem剛好是由我們的pipe的iovector占位,對data1的更改,也可能改掉
iov_base:iov_base = ctx_C
。這樣也就能對ctx_C->list進行任意寫入。
為什麼要給出兩種情景呢?因為我們需要考慮一個究竟是data1對應iov_base,還是data2對應iov_base。iovector的結構是這樣:
struct iovec {
void *iov_base;
size_t iov_len;
};
64位下,struct iovec是16位元組大小,跟上面list結構的大小一樣。于是data1和data2中必有一個是iov_base,一個是iov_len。而我們需要改的是iov_base。是以上述兩種情景,根據具體情況就能找到一種适用的。
問題又來了,比如說情景二,能夠對ctx_C->list進行任意寫入又能做什麼呢?
能夠對雙連結清單某節點的next,prev指針進行完全控制,是一件很恐怖的事情。因為在删除這個節點的時候,會導緻一個很嚴重的問題。具體怎麼回事我們看代碼:
static inline void list_del_rcu(struct list_head *entry)
{
__list_del_entry(entry);
//上一句可描述為:
//entry->next->prev = entry->prev;
//entry->prev->next = entry->next;
entry->prev = LIST_POISON2;
}
假設我們将prev指針改為target_address,next指針改為target_value。那麼上述代碼就等價于:
*(uint64_t)(target_value + 8) = target_address;
*(uint64_t)(target_address) = target_value;
于是這導緻了一個任意位址寫入任意内容的問題。當然,寫入的内容沒那麼任意,它的值必須也要是一個可寫的位址。
0權限下的利用
有了上述的讨論之後,我們利用的思路逐漸明朗。
我們的ctx是0xF8的大小,處于0x100的slab塊裡面,是以位址總是0位址對其。那麼如果要做iovector進行占位,得到的位址也總是0位址對其,是以裡面元素的iov_base也會是0位址對其。在我測試的機器(nexus6p)上,next指針偏移是0xE0,prev指針是0xE8。是以我們需要選擇情景二:删除victim的next節點。那麼我們的步驟應該是:‘
在創造victim ctx之前,将ctx_C加入cancel_list,然後将ctx_A加入cancel_list
赢得競争,導緻victim ctx被list_add_rcu兩次
對victim ctx執行list_del_rcu操作,并将victim_ctx釋放,此時cacncel_list是這樣:
用iovector進行堆噴,使得其将victim mem占位:
這時pipe_read被阻塞,執行删除ctx_A的操作,會導緻iov_base的更改,改成指向我們的ctx_C:
然後我們執行pipe_write,這時會導緻ctx_C的next指針和prev指針被我們改寫。next指針改寫為target_value,prev指針改寫為target_addr:
最後我們對ctx_C執行删除節點的操作,就能實作任意位址寫任意内容了,當然寫的内容不能那麼任意。 在這之後,再進行提權是一件很容易的事情。這裡簡單描述兩種做法:
1,target_addr設定為&ptmx_cdev->ops,target_value設定為0x30000000。這樣我們在使用者态0x30000000布置好函數指針, 後續操作就很容易了。修改task_prctl相關的也是一樣的道理。
2,增加/修改位址轉換表中的記憶體描述符。這個雖然說原理比較複雜,介紹起來可能比本文之前說的所有的内容還要長,但是實作起來卻是很友善。像nexus6p這樣的機器,kernel的第一級位址轉換表的位址固定為0xFFFFFFC00007d000,在中添加一條合适的記憶體描述符,就能實作在使用者态讀取/修改kernel的text段的内容,實作kernel patch。提權也就很輕松了,而且好處是不需要找各種各樣的位址,自己讀取kernel的内容,自己能計算出來,可以做成通用的root。不過這種方法在三星這種有RKP保護的機器上不适用,或者說得繞過才行。
然後,這個漏洞,其實還是可以轉化為任意位址寫任意内容,這次的寫的内容可以任意,但是做法就不一樣了。需要把iov_len做得長一點,把對ctx_C的寫入轉化為一個堆溢出的漏洞。然後達成目标。
江湖規矩放圖:
最後,對于文中出現的問題,還請各路大牛加以斧正,歡迎技術交流:[email protected]
參考文檔 1, https://hitcon.org/2017/CMT/slide-files/d1_s3_r0.pdf
2, https://android.googlesource.com/kernel/msm/+/0fecf48887cf173503612936bad2c85b436a5296%5E%21/#F0
3, https://android.googlesource.com/kernel/msm/+/e7a3029ebf4175889e8bdb278fd9cf02a211118c/fs/read_write.c
4, https://github.com/retme7/My-Slides/blob/master/The-Art-of-Exploiting-Unconventional-Use-after-free-Bugs-in-Android-Kernel.pdf