最近在研究一些堆上面的漏洞。然后某一天騎自行車在路上跑的時候我突然悟出了Double Free的真諦。 2333好像太中二了一點,我是根據堆溢出的利用方法啟發,再結合linux中libc的源碼。研究出了double free的利用方法。雖然有關double free的利用技巧已經不是一個秘密。不過好像很少有相關的中文的介紹,所以以一個初學者的角度來講解一下double free漏洞的利用方法。 如果有錯誤,希望大家批評指正。Orz
Double Free其實就是同一個指針free兩次。雖然一般把它叫做double free。其實只要是free一個指向堆內存的指針都有可能產生可以利用的漏洞
double free的原理其實和堆溢出的原理差不多,都是通過unlink這個雙向鏈表刪除的宏來利用的。只是double free需要由自己來偽造整個chunk并且欺騙操作系統。
這里是glibc中有關內存管理的源代碼
這里先簡單的介紹一下glibc中內存管理的一些內容。幫助理解漏洞產生的原理。因為沒有進行非常深入的研究,歡迎大神們指正補充。為了照顧新手,我寫的比較詳細,大神們請跳過
首先是申請的內存塊在內存中的結構。所有malloc等等函數申請的內存都會被系統申請在一個叫做堆的地方,其實也就是一塊比較大的內存。當程序需要內存時,系統就會從堆里面找一塊還沒有被使用的內存出來告訴程序,這一塊內存給你用。如果使用了超出你申請范圍的內存被系統發現了,會強制結束程序。
一般的情況,如果程序連續申請內存的話,操作系統會按次序的在堆里碼放內存,一塊緊挨著一塊。中間沒有空隙。打個比方吧,就像一座旅館一樣,所有需要的入住的旅客會老板從0號房開始依次安排下去。
如果申請了一大堆內存塊,再將其中的某幾塊給釋放掉的。就像旅店里面有幾間房子的人離開了一樣,這個時候老板需要知道哪幾間房子是空的,以便讓新來的人住進去。
那么操作系統是怎么知道那些內存被釋放了呢?先看看內存中chunk的結構吧
復制自glibc中源碼,并翻譯了注釋
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* 前一個chunk的大小 (如果前一個已經被free)*/
INTERNAL_SIZE_T size; /* 字節表示的chunk大小,包括chunk頭 */
struct malloc_chunk* fd; /* 雙向鏈表 -- 只有在被free后才存在 */
struct malloc_chunk* bk;
};
這里的的結構是chunk頭部分的內容。在內存塊free之前,最后兩個指針是不存在的,只有前2項的內容。在第二項之后就是可供程序使用的內存了,也就是malloc返回的那個指針指向的地址。而在內存被釋放之后,系統在內存塊中添加最后的這兩個指針。這兩個指針的作用是構成雙向鏈表,它們分別指向了前一個和后一個已經被釋放的空閑內存。(順帶一提,這是個環形的雙向鏈表,收尾是相接的)
當程序申請一塊內存的時候,系統會遍歷這個由空閑內存構成的雙向鏈表。如果有合適(>=)的空閑內存,就會將它(或者一部分,具體沒有研究)分配給程序。
所有空閑的chunk之間的聯系就像這樣
手繪輕噴。
然后是prev_size和size的作用,prev_size是前一個chunk的大小,值得注意的是如果前一個chunk在使用中,這里會是0,唯有前一個chunk已經被釋放的情況下這里才會有數值。所以prev_size應該叫前一個空閑堆快的大小。然后是size,這個就是當前chunk的大小,包括給程序使用的和chunk頭的大小加在一起。因為所有的chunk的大小都是4字節對齊的,所以size最低3位一定是0。被操作系統拿來當做flag標志位。(最低位:指示前一個chunk是否正在使用;倒數第二位:指示這個chunk是否是通過mmap方式產生的;倒數第三位:這個chunk是否屬于一個線程的arena)這里只需要關心最低位的涵義,它指示前一個chunk是否是空閑的。這個flag位加上prev_size一起作為系統判斷一塊內存是否正在使用,從哪里開始的依據。
要注意的是,只有大小合適的內存才會用這種方法分配,太小的內存會用fastbins的方法管理,有興趣的可以了解一下(~~其實是我不會~~)。這里給出使用fastbins的閾值。32位操作系統上是0x40,64位操作系統上是0x80。小于這個數值的內存會用fastbins的方法管理。如果chunk的大小大于512個字節之后,系統除了兩個指針雙向鏈表指針之外還會再添加2個指針指向下一塊較大的內存。然后是chunk頭中幾個數據的大小,INTERNAL_SIZE_T其實就是unsigned long型的數據,而另外2個是指針不用多說。所以chunk頭的大小也和操作系統的位數有關。(~~某人被坑過~~)
然后再看看在free的時候到底發生了什么
先來看看free的源代碼吧。在剛才我提供的源代碼的3829行開始到4100行結束共200行左右的代碼就是free函數的源代碼→_→還不只。當然其實只有3978-4040中的代碼是實際要看的,其他的都是fastbins和mmap等內存的free,不用關心。
首先是一大堆的檢查,這個咱不管。然后就是各種操作,這里選取最關心的,和堆溢出有關的部分來說(有興趣的可以慢慢看)。free時的主要操作是這樣的,先看看這個被free的內存塊的前后2個內存塊是否是空閑的。通過當前chunk的flag和下下個chunk的flag來查看上一個和下一個chunk是否是空閑的。如果空閑,會先把他們從空閑鏈表中刪除。從鏈表中刪除的工作是通過一個unlink的宏來完成的。關于這個unkink的宏待會再來說明。
主要的行為操作分別是在4011行和4037行,先看4011行。
clear_inuse_bit_at_offset(nextchunk, 0);
因為是宏,所以先將他宏展開
(((mchunkptr) (((char *) (nextchunk)) + (0)))->size &= ~(0x1))
這一行的作用就是將當前被free的內存塊的下一個內存塊的flag的第一位清空為0,指示當前內存塊已經被free。
然后就是4037行的代碼
set_foot(p, size);
宏定義的非常徹底,所以也要宏展開再看
(((mchunkptr) ((char *) (p) + (size)))->prev_size = (size))
把下一個內存塊的prev_size更改為當前內存塊,或者是已經合并了的更大內存塊的大小。然后就是一些各種各樣的別的操作,就不詳細解釋了。
最后來仔細看看unlink的宏代碼 直接把unlink宏內容貼出來
#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;
// }
// }
}
}
當然很多的代碼其實是在當內存塊的大小過大的時候才會執行的代碼(就是被我注釋掉的那一部分),在內存塊不大的情況下不需要關心,最主要的代碼就是下面4行
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
這里在宏中傳入參數FD,BK,P分別是指向后一個,前一個,還有當前的chunk(當然,是從chunk頭而不是data段開始的)。很經典的鏈表節點刪除,當然萬一被溢出覆蓋的的話就糟糕了。不過也是有防止溢出的檢測代碼存在的,就是這個if判斷
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
當前內存塊的上一塊內存中指向下一塊內存指針和當前內存塊的下一塊內存塊的指向上一塊內存塊的指針如果不是指向當前內存塊的話,程序就會崩潰退出。233333直接看代碼比解釋簡單。
要利用Double Free的漏洞。我們就要讓系統進行unlink的操作,達到篡改指針的目的。但是一般的情況下,我們兩次釋放同一塊內存會被操作系統給檢測出來,怎么欺騙過操作系統才是最重要的。
我們結合實際的情況來講解會比較好。這里我自己寫了個demo程序,代碼發比較長,所以我放在gitcafe上。
因為是自己寫自己玩的demo程序,所以這程序是堆漏洞大禮包。用Double Free,heap corruption,use after free這3種方法都用各拿了一次shell 這里我們用Double Free,其他漏洞一律不使用。
這個程序在free的時候很明顯的沒有檢驗指針的有效性,而且沒有在free之后將野指針清零。而且可以任意的指定每一個chunk的大小。所以可以很容易的構造double free。我們首先構造一個野指針。
>malloc(504)
>malloc(512)
然后釋放這2塊內存。這樣子我們就可以在距離第一個指針偏移量為0x200的地方有了一個野指針。
我們留下了一個野指針p指向偏移為0x200的地方。然后我們需要做的就是偽造chunk。再free野指針p。首先是申請一塊更大的內存,大小應該等于我們剛才申請的內存的總和。
>malloc(768)
最好和剛才2塊內存大小總和一樣,如果不一樣大也也可以,就是待會偽造第二快內存塊的大小的時候,要讓偽造的大小等于我們申請的chunk的大小,否則會無法繞過檢查。會被系統檢查出double free。
然后這是我在第二次申請的內存中填入的內容。
>0x0 + 0x1f9 + 0x0804bfc0 - 0xc + 0x0804bfc0 - 0x8 + 'a'*(0x200-24) + 0x000001f8 + 0x108
現在的chunk就是這個樣子了
可以看到現在我們在內存中偽造了出了2個chunk。它們的結構就像圖中我們看到的樣子。首先是第一個chunk的chunk頭部分。我們分別填上了0x0和0x1f9代表了前一個chunk正在使用,當前chunk的大小是1f8。然后就是偽造的雙向鏈表指針了。為了繞過unlink中的檢查,這里需要稍微構造一下這個雙向鏈表的指針了。payload中的0x0804bfc0位置其實就是存放在.data段中的指針ptr。這樣子就可以繞過保護了。
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
結合而源代碼,可以看到現在FD->bk的值正好也是指向我們偽造的chunk的頭部分。然后我們在野指針p的前面又偽造了一個chunk頭。prve_size部分填0x1f8正好是前一個偽造chunk的大小。然后size部分填的是0x108。這樣的話兩個chunk正將我們申請的空間填滿。然后第二個偽造chunk的size中最低位的flag置為0.這樣free指針p的時候,就會將前一個偽造的chunk給unlink。
現在,只要在free一次指針p。就可以觸發漏洞了。這時候,我們的操作系統不會報錯,而且我們本來正常的指針ptr已經變成了ptr-0xc。這要如果我們如果調用Edit函數來修改這個chunk的話,就可以干各種各樣的事情了。
我吧完整利用的poc也放在了gitcafe上,如果需要的話可以看看,最終通過ret2kibc的方法拿到的shell.所以只要吧幾個函數的地址稍微修改一下,可以在隨意的一臺機器上使用。PS:沒有開PIE保護的。