當前位置:
首頁 > 新聞 > Glibc堆漏洞利用基礎-深入理解ptmalloc2 part1

Glibc堆漏洞利用基礎-深入理解ptmalloc2 part1

在本篇文章和本系列的其他文章中,我會把一些內部構建解壓到動態堆數據結構和相關的beast中。這篇文章是專門為沒有堆背景知識的人準備的,不過可能會涉及到一點ELF內部構建和調試的知識。本文還會詳細講解一些實驗,通過完成這些實驗,你可以學到堆的工作原理是什麼。

介紹

堆其實就是執行程序時用來存儲數據的內存區域列表。存儲在堆內存區域中的數據在運行時進行請求調用。它允許像glibc這樣的運行時環境為程序提供動態內存來分配數據。由於內存區域作為一種服務(它的作用就是如此),也就意味著在整個混亂的內存區域中,肯定需要關於內存區域的計算信息。為了實現這一點,堆使用「chunk」這種內部結構來描述或修飾用戶數據區域。根據其屬性對塊進行分類和分組。基本屬性如下:

·是否可用

·塊的大小

·在列表中,塊的前後都有哪些塊等

內存管理中最重要的一點是,它的本質就是圍繞塊搜索函數來定位塊,然後執行釋放或重新分配。

本文我將重點關注的堆分配器是glibc版本的ptmalloc,在glibc版本2.23-2.28中實現。當然,這並不是說只有glibc才能理解;堆分配存在多種方法。關於如何實現各種操作,每一種方法都是獨特的。比如合併空閑的塊,搜索和分類空閑塊,並進行快速分組。當然可能還有更多其他功能—比如提升安全性。所以很多位置因為其複雜性會滋生並惡化為安全問題。但這些問題的根源在於用戶是如何請求數據的,還有管理數據的分配器是如何響應內存區域中的元數據的。

最後再介紹一點,堆通常看起來都非常密集和複雜,並且內部構件非常粗糙,但是大部分構件,如aid memorization(幫助備忘),還有其他計算機技術,都有助於加快鏈表搜索速度。你也可以這樣說,它們只不過是存儲「cheat」元數據的巧妙的方法,這些元數據不需要在每次搜索時都搜索整個堆內存區域。但是這些元數據對我們來說很重要,因為在某些情況下,我們想要改變鏈表的搜索和解釋方式。

Heap speak

你可能已經猜到堆的基本單位是chunk。你可能也想知道這些塊在glibc代碼中是什麼形式,請看下面的代碼:

這裡我會對每一個欄位做一個通俗易懂的解釋(儘可能通俗易懂)。

· INTERNAL_SIZE_T–這是一個大小類型,用於在堆管理中定義「bookeeping」函數的欄位 - 諸如指針(地址)和位欄位之類的東西。這個大小由具體的實現來定義。我們可以猜想glibc想要在不同的硬體和運行時的實現中具有可移植性和靈活性 - 因此映射到INTERNAL_SIZE_T的地址大小可能會有所不同。無論如何,INTERNAL_SIZE_T被定義為size_t - 它可以追溯到C運行時最初解決問題的方式。

· mchunk_prev_size- 是塊格式的第一部分,無論是空閑塊還是已用塊,都會有這個部分。該欄位指示當前塊的前一個塊的大小,並且如果所引用的塊是空閑的,則其最低有效位被設置為0x1。因此,如果你正在查看一個塊,並且其prev_size的最小sig位為0x1,那麼就在此之前仍然是「使用」的塊。

· mchunk_size- 非常標準,實際上只保存當前大小,以位元組為單位lol。

· struct malloc_chunk * fd- 這是struct塊結構中的一個欄位,用於定義另一個塊的地址空間。這是因為它形成了一個鏈表。這裡定義的鏈表是「空閑列表」,它將堆上空閑的所有塊拼接在一起。這裡我們在鏈表中定義「正向指針」。

· struct malloc_chunk * bk– 這個欄位與上面提到的欄位類型相同,只不過這個是「反向指針」。

· struct malloc_chunk * fd_nextsize- 這個欄位來自堆中的另一層空閑鏈表技術。如果指針高於特定大小閾值(我們將在稍後介紹),則將此指針添加到空閑塊中 - 以便堆管理器可以在它們出現時跟蹤大的塊。它有點像在賭場中的高級玩家,當你出場時,他們跟蹤你的行為動作,因為你很大程度上能夠影響當晚的盈利能力。

我們再來看一下這些在執行過程中是怎麼樣的,看看不同類型的塊看起來有什麼區別(空閑塊和已分配的塊)。我們現在通過gdb來運行一個C程序,然後解壓縮堆來顯示它在內部是如何響應的。C程序如下:

#include

#include

#include

char * make_string(size_t length){

char *arr = (char *)malloc(length);

asm("int $3");

return arr;

}

void free_string(char *arr){

free(arr);

asm("int $3");

}

int main(int argc, char **argv){

int _len = 128;

int index = 0;

char *array,*array_1,*array_2,*array_3,*array_4;

//char *array_;

//char *array__;

//find a way to show the chosen candiate for each round

int inner_array = 0;

int _char = 1;

for(index = 0;index

array = make_string(_len);

memset(array,0xAA,_len);

printf("[*] array @[%p]
",array);

/* 4 more allocations*/

array_1 = make_string(_len 80*1);

printf("[*] array @[%p]
",array_1);

memset(array_1,0xBB,_len 80*1);

array_2 = make_string(_len 80*2);

printf("[*] array @[%p]
",array_2);

memset(array_2,0xCC,_len 80*2);

array_3 = make_string(_len 80*3);

printf("[*] array @[%p]
",array_3);

memset(array_3,0xDD,_len 80*3);

array_4 = make_string(_len 80*4);

printf("[*] array @[%p]
",array_4);

memset(array_4,0xEE,_len 80*4);

/*free each array and clear it */

memset(array,0xFF,_len);

free_string(array);

memset(array_1,0xFF,_len 10);

free_string(array_1);

memset(array_2,0xFF,_len 20);

free_string(array_2);

memset(array_3,0xFF,_len 30);

free_string(array_3);

memset(array_4,0xFF,_len 40);

free_string(array_4);

_char ;

printf("

");

}

//printf("[*] done");

return 0;

}

我知道這段代碼可能有點長,你可能不想一行一行的看,所以你可以完全忽略其他的mallocs和空閑塊。這裡我添加它們是為了舉例說明,然後逆向一些更有意思的數據。

在上面的代碼中,我添加了一個簡單的wrapper函數,而且在最後一個return之前下了一個斷點。這樣我就可以隔離我們正在研究的內存區域上的空閑塊和malloc調用效果。

現在讓我們看看堆分配內存時會發生什麼。我們需要先找到一個指向堆的指針。這很簡單,因為malloc會在返回主函數後將其保存在rax中,我在前幾個gdb命令中顯示:

所以當我設置好hook-stop時,它會輸出$rax-0x10附近的所有內容,這是保存塊頭信息的地址。我這樣做是因為當我們下這個斷點時,malloc剛好返回並將寄存器設置為其返回值 - 這就是分配的內存區域的地址。我們可以直接看到這些宏指令如何在glibc / malloc/malloc.c中對堆元數據數據進行操作:

你可以看到它只是簡單地增加或減少2個地址以獲取mem(用戶數據啟動的原始內存指針)或兩個地址之前的塊信息。還有許多其他操作可以提取和設置其他元數據。

好的,這就是基本格式,接下來我們來看看它是如何運作的。

不斷增長的堆

在下了第一個斷點後你應該看到gdb顯示分配的第一個堆塊,下圖是一個帶有注釋版本的堆分配圖,顯示了堆的格式:

當在你的屏幕中出現這段內容時,嘗試執行「c」gdb命令跳到下一個斷點。你會看到更多已分配塊的示例,然後在你的屏幕上會顯示如下:

這就表明我們不能像以前在hook-stop中使用$ rax中的值那樣。可能你會想,這是因為$ rax不再保留內存指針,它現在轉到了一個空閑調用,因此它保留了一些其他值。無論如何,我們可以使用傳遞給free_string函數的地址來轉儲塊,因為在這裡我們可以非常方便地看到它顯示出來。這是塊被釋放後的樣子:

除了空閑塊之外,上面的圖片中顯示的是第一個空閑塊fd(正向空閑鏈表)和bk(反向空閑鏈表)指針。這裡我們可以看到,如果我們使用gdb的內存檢查器函數來跟蹤它們,它們最終會在0x602a00位置結束,這是頂部塊的地址; 指向當前分配的堆地址頂部的指針。

好的,這就是當塊被分配和釋放時的樣子,我們能夠查看塊是如何合併成更大的空閑塊呢?當然可以,這是下一篇文章的內容。

空閑塊合併

在分配了塊之後,我們的程序將按照它們分配的順序釋放每個塊。這意味著我們可以認為彼此相鄰的被分配的兩個塊,也是相繼被釋放並且相鄰的-所以,我們就有了兩個空閑塊合併成了一個大的塊。

看起來是下圖這樣的:

上圖的左邊我們能看到的是地址0x602580和0x6024a0處的兩個塊(名為塊1和塊2)。在右邊0x6024a0地址處我們可以看到一個新的塊,但是我們可以看到合併後的大小欄位是0x211(如圖所示 也就是0xe1 0x130)。

這差不多就是塊合併的整個行為。這也是我這篇文章想要講解的東西。在這個系列文章中,我還會講到fast-bins,大塊管理,也可能講一些堆重定向技巧。

敬請期待。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

通過Post Types進行WordPress提權
Golang語言TLS雙向身份驗證拒絕服務漏洞分析

TAG:嘶吼RoarTalk |