關於heap exploit常見體位在這裡就不多做說明了,這個網站把常見的招數都介紹了一遍,有興趣可以到這邊了解,現在想來談談這些攻擊在實戰中的細節、以及一些變化。
這個題目存在著off-by-one NULL byte漏洞,令人可以迅速聯想到做shrinking chunk產生overlap,但有兩個問題馬上浮現出來
1. How to defeat ASLR (leak libc) ?
2. How to do arbitrary write ?
第一個問題比較簡單,由於free small chunk會記錄著fd、bk來維持small bin,而small bin的起頭在main_arena中(存在libc .data段),所以只要能leak這兩個pointer就能得知libc位址,一開始直覺的想到做兩個overlap chunk,free一個在printf另一個,但因為必須藉由printf(“%s”)做leak,所以兩塊overlap chunk必須起始位址一樣,否則當你printf另一個chunk時就會遇到含有fd、bk的chunk的prev_size、size位(有NULL byte)。
第二個問題在得到overlap chunk後馬上能想到fast bin attack,但最麻煩的就在要繞過煩人的size檢查 -malloc(): memory corruption (fast)
,再加上程式有開RELRO,造成能寫入的位址非常侷限,一開始我想到三個地方
__free_hook一定是第一人選,因為他幫我們準備好了參數(一個heap上的位址,可以拿來放/bin/sh),不像malloc參數是一個int,但不幸的是實際看了一下在__free_hook記憶體位址前面全部都是0,根本沒辦法拿來偽造size。
而往tls_dtor_list寫是我一開始決定的方式,因為在他記憶體位址前確實有些數字可以拿來偽造size,悲劇的是弄一弄發現他前方那塊竟然是Read-Only,害我浪費了不少時間......但我之後沒放棄,還是往這個方向繼續查資料,最後讓我找到了一篇害死我又幫助我很多的文章,在這裡面有個關鍵處:
But in late 2014, google’s project zero team found out a way to successfully bypass “corrupted double linked list” condition by unlinking a large chunk!!
他提到large bin unlink時的檢查是assert,並非__builtin_expect,而assert對於glibc來說只是debug用,並不會compile到release版本中,而我點進去那篇google’s project zero也只有挑實作的地方看,並沒有很仔細的看過,之後弄了將近兩天弄不出來,終於回去仔細地審視這篇文章,才看到了一個重點:
... Aside: there’s some evidence that Ubuntu glibc builds might compile these asserts in, even for release builds. Fedora certainly does not.
......fuck my life,看來unlink large bin這條路是不可......等一等,此時我又發現了另一篇文章 -Revisiting Defcon CTF Shitsco Use-After-Free Vulnerability — Remote Code Execution (你知道的,當你弄了兩天卻跟你說你這方法不行,你通常沒辦法很快接受事實),文章內提到了一個有趣的東西:
Disabling glibc protection by overwriting check_action
簡單來說libc內有個check_action的static variable (也就是固定offset),把它寫成0出錯時就不會abort了,對我來說是長姿勢了,第一次知道這個東西。雖說後來並不是用這個方法,但事後去研究一下我認為這的確可行,但應該僅限large bin,我們來看看unlink的code
#define unlink(AV, P, BK, FD) {
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}
如果要從FD、BK下手應該是不能,可以看到進入第一個if判斷式之後,即使malloc_printerr沒有call abort,他還是不會跑到else去執行unlink,但後面的large bin就不同了,檢查的地方只用if包起來而沒有後續的else,也就是如果malloc_printerr沒有中斷程式,它還是會跑回來繼續執行底下的unlink。
有個小細節是check_action沒有被export所以會找不到symbol,如果要找它的offset可以去 mallopt()看,這個function有用到check_action。
最後我的第一個解法是用__malloc_hook,這也滿出乎我意料的,由於malloc的參數是int,所以要不是餵one gadget rce的位址、不然就是要做ROP了,不過這個程式的操作基本上沒用到stack,所以要leak stack就麻煩了點(libc的environ),而one gadget rce由於要滿足一些條件滿吃運氣的,一開始我透過了最正常的方式讓程式調用malloc (也就是使用add功能)發現不行,而之後再嘗試一次,但這次呼叫malloc的方法是透過delete功能讓程式abort (double free corruption等等),而malloc_printerr裡面會呼叫malloc,此時就成功了。