原文來自安全客,作者:huahuaisadog@360 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實現)。
-
然后調用
timerfd_clock_was_set函數,這時會遍歷cancel_list,由于head->next就是我們的victim ctx,所以victim ctx會被這次操作引用到。數據構造得OK的話,會調用wake_up_locked(&ctx->wqh),而ctx就是我們的victim ctx -
這以后ctx->wqh是自己定義的數據,所以
\_\_wake\_up\_common的curr,curr->func也是我們可以決定的。 -
所以執行到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的寫入轉化為一個堆溢出的漏洞。然后達成目標。
江湖規矩放圖:

最后,對于文中出現的問題,還請各路大牛加以斧正,歡迎技術交流:huahuaisadog@gmail.com
參考文檔 1, https://hitcon.org/2017/CMT/slide-files/d1_s3_r0.pdf
2, https://android.googlesource.com/kernel/msm/+/0fecf48887cf173503612936bad2c85b436a5296%5E%21/#F0
本文經安全客授權發布,轉載請聯系安全客平臺。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/596/
暫無評論