链表游戏:CVE-2017-10661之完全利用
原文來自安全客,作者: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_TIMEreturn -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_Bhead->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_Cctx_A->next->prev = ctx_A->prev;//等價于 ctx_C->prev = victim_memctx_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
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
總結
以上是生活随笔為你收集整理的链表游戏:CVE-2017-10661之完全利用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解密PreAngel区块链布局:平台协议
- 下一篇: 以太坊智能合约安全入门了解一下(上)