本文寫給對堆溢出無的放矢的童鞋,分為如下幾部分:
一.經典的unlink利用方法簡介
二.在當今glibc的保護下如何繞過進行unlink利用
建議閱讀本文之前先對glibc的malloc.c有所了解
首先簡要介紹一下堆chunk的結構
我們可以在malloc.c中找到關于堆chunk結構的代碼
#!c
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
這指明了一個heap chunk是如下的結構
#!c
+-----------+---------+------+------+-------------+
| | | | | |
| | | | | |
| prev_size |size&Flag| fd | bk | |
| | | | | |
| | | | | |
+-----------+---------+------+------+-------------+
如果本chunk前面的chunk是空閑的,那么第一部分prev_size會記錄前面一個chunk的大小,第二部分是本chunk的size,因為它的大小需要8字節對齊,所以size的低三位一定會空閑出來,這時候這三個位置就用作三個Flag(最低位:指示前一個chunk是否正在使用;倒數第二位:指示這個chunk是否是通過mmap方式產生的;倒數第三位:這個chunk是否屬于一個線程的arena)。之后的FD和BK部分在此chunk是空閑狀態時會發揮作用。FD指向下一個空閑的chunk,BK指向前一個空閑的chunk,由此串聯成為一個空閑chunk的雙向鏈表。如果不是空閑的。那么從fd開始,就是用戶數據了。(詳細信息請參考glibc的malloc.c部分,在此不再多做解釋。)
首先,為了方便,我直接引用一位外國博主的漏洞示例程序,以便繼續解釋
#!c
/*
Heap overflow vulnerable program.
*/
#include <stdlib.h>
#include <string.h>
int main( int argc, char * argv[] )
{
char * first, * second;
/*[1]*/ first = malloc( 666 );
/*[2]*/ second = malloc( 12 );
if(argc!=1)
/*[3]*/ strcpy( first, argv[1] );
/*[4]*/ free( first );
/*[5]*/ free( second );
/*[6]*/ return( 0 );
}
這個程序在[3]處有很明顯的堆溢出漏洞,argv[1]中的內容若過長則會越界覆蓋到second部分。
簡單給出此程序的堆結構
#!c
+---------------------+ <--first chunk ptr
| prev_size |
+---------------------+
| size=0x201 |
+---------------------+ <--first
| |
| allocated |
| chunk |
+---------------------+ <--second chunk ptr
| prev_size |
+---------------------+
| size=0x11 |
+---------------------+ <--second
| Allocated |
| chunk |
+---------------------+ <-- top
| prev_size |
+---------------------+
| size=0x205d1 |
+---------------------+
| |
| |
| |
| TOP |
| |
| CHUNK |
| |
+---------------------+
此處不贅余介紹exploit具體代碼,只介紹利用方法.
只要我們通過溢出構造,使得second chunk
#!c
prev_size=任意值
size=-4(因為最低位的flag沒有設置,所以prev_size是什么值是無所謂了)
[email protected]([email protected]?定技術”)
bk=shellcode地址
在我們的payload將指定位置的數值改好后。下面介紹在[4][5]行代碼執行時發生的詳細情況。
第四行執行free(first)發生如下操作
1).檢查是否可以向后合并
首先需要檢查previous chunk是否是空閑的(通過當前chunk size部分中的flag最低位去判斷),當然在這個例子中,前一個chunk是正在使用的,不滿足向后合并的條件。
2).檢查是否可以向前合并
在這里需要檢查next chunk是否是空閑的(通過下下個chunk的flag的最低位去判斷),在找下下個chunk(這里的下、包括下下都是相對于chunk first而言的)的過程中,首先當前chunk+當前size可以引導到下個chunk,然后從下個chunk的開頭加上下個chunk的size就可以引導到下下個chunk。但是我們已經把下個chunk的size覆蓋為了-4,那么它會認為下個chunk從prev_size開始就是下下個chunk了,既然已經找到了下下個chunk,那就就要去看看size的最低位以確定下個chunk是否在使用,當然這個size是-4,所以它指示下個chunk是空閑的。
在這個時候,就要發生向前合并了。即first chunk會和 first chunk的下個chunk(即second chunk)發生合并。在此時會觸發unlink(second)宏,想將second從它所在的bin list中解引用。
具體如下
#!c
BK=second->bk(在例子中bk實際上是shellcode的地址)
FD=second->fd ([email protected] - 12)
FD->bk=BK
/*shellcode的地址被寫進了FD+12的位置,[email protected],[email protected]*/
BK->fd=FD
執行unlink宏之后,再調用free其實就是調用shellcode,這時就可以執行任意命令了。
但是,在現如今,glibc已經不這么簡單了,為了使堆溢出不那么容易就被利用,它加入了許多新的保護措施,如何繞過也就是要在第二部分中討論的內容。
以glibc中的代碼作為示例,首先拿出最新版本的unlink宏。
#!c
1413 /* Take a chunk off a bin list */
1414 #define unlink(AV, P, BK, FD) {
1415 FD = P->fd;
1416 BK = P->bk;
1417 if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
1418 malloc_printerr (check_action, "corrupted double-linked list", P, AV);
1419 else {
1420 FD->bk = BK;
1421 BK->fd = FD;
1422 if (!in_smallbin_range (P->size)
1423 && __builtin_expect (P->fd_nextsize != NULL, 0)) {
1424 if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
1425 || __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
1426 malloc_printerr (check_action,
1427 "corrupted double-linked list (not small)",
1428 P, AV);
1429 if (FD->fd_nextsize == NULL) {
1430 if (P->fd_nextsize == P)
1431 FD->fd_nextsize = FD->bk_nextsize = FD;
1432 else {
1433 FD->fd_nextsize = P->fd_nextsize;
1434 FD->bk_nextsize = P->bk_nextsize;
1435 P->fd_nextsize->bk_nextsize = FD;
1436 P->bk_nextsize->fd_nextsize = FD;
1437 }
1438 } else {
1439 P->fd_nextsize->bk_nextsize = P->bk_nextsize;
1440 P->bk_nextsize->fd_nextsize = P->fd_nextsize;
1441 }
1442 }
1443 }
1444 }
1445
1446 /*
我們可以看到我們最大的阻礙是下面的這部分代碼
#!c
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P);
這段代碼被添加到了unlink宏中,所以現在再調用unlink宏的時候,chunk指針P->fd->bk(即代碼中的大寫FD->bk)應該還是p指針自己。對于BK->fd != p這部分也是同樣的道理。
在第一部分的利用方法中,我們修改了
#!c
p->[email protected]
p->bk=shellcode_adress
我們在此記FD=p->fd , BK=p->bk,再去看FD->bk已經是[email protected]了,這部分是不能滿足要求的。再看BK->fd已經是shellcode+16了,所以如上文的利用方法已經不能成功了。之所以還加以介紹,是因為這會使我們理解第二部分變得又快又好。
如果繞過還是要根據這段保護代碼來談。我們勢必需要構造合適的條件的來過掉這行代碼,那么就要找一個指向p的的已知的地址,然后根據這個地址去設置偽造的fd和bk指針就能改掉原p指針。
以64bit為例,假設找到了一個已知地址的ptr是指向p(p指向堆上的某個地方)的,通過堆溢出,我們可以做如下的修改。
#!c
p->fd=ptr-0x18
p->bk=ptr-0x10
布置好如此結構后,再觸發unlink宏,會發生如下情況。
#!c
1.FD=p->fd(實際是ptr-0x18)
2.BK=p->bk(實際是ptr-0x10)
3.檢查是否滿足上文所示的限制,由于FD->bk和BK->fd均為*ptr(即p),由此可以過掉這個限制
4.FD->bk=BK
5.BK->fd=FD(p=ptr-0x18)
這時候再對p進行寫入,可以覆蓋掉p原來的值,例如我們用合適的payload將[email protected]寫入。p就變成了[email protected],那么再改一次p,把[email protected]改為shellcode的地址或者說system的地址都可以。之后再調用free功能,就可以任意命令執行。
為了方便,在這邊拿出一個最近的wargame出現的一個邏輯非常簡單的程序作為漏洞示例程序,可以在此下載
首先簡單介紹這個Binary的功能以及基本情況
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH shellman
1.顯示已經建立的堆塊中存儲的內容
2.建立一個新的堆塊,大小和內容又用戶決定
3.對一個已經分配的堆塊做編輯,這個地方沒有限制大小,若太長可造成堆溢出
4.釋放一個已經分配的堆塊
.bss:00000000006016C0 ; __int64 usingFLAG[]
.bss:00000000006016C0 usingFLAG dq ? ; DATA XREF: main+38o
.bss:00000000006016C0 ; .text:0000000000400A90o ...
.bss:00000000006016C8 ; __int64 LEN[]
.bss:00000000006016C8 LEN dq ? ; DATA XREF: new+B5w
.bss:00000000006016C8 ; delete+79w
.bss:00000000006016D0 ; __int64 content[]
.bss:00000000006016D0 content dq ? ; DATA XREF: new+BCw
程序有一個全局數組會存儲好每一個經過malloc分配的堆塊返回的指針。以及在全局數組中存儲長度以及本塊是否正在使用的標志。
按照前文所介紹的,我們希望使用Unlink的方法去利用這個堆溢出漏洞。首先,我們要找一個指向堆上某處的指針。因為存儲malloc返回指針的全局數組的存在,這讓我們的利用變得異常的簡單。因為bss段的地址也是固定的,我們可以知道,從而設置滿足需要的bk和fd指針,下面介紹具體步驟。
1.我們可以首先分配兩個長度合適的堆塊。(如下圖所示)
chunk0 malloc返回的ptr chunk1 malloc返回的ptr
| | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+
| | | | | | | | | | |
| | | | | | prev | size&| | | |
| prev_size |size&Flag| | | | size | flag | | | |
| | | | | | | | | | |
| | | | | | | | | | |
+-----------+---------+---+---+-------------+------+------+----+----+------+
這時候這兩塊的fd和bk區域其實都是空的,因為他們都是正在使用的
2.對第一塊進行編輯,編輯的過程中設置好第零塊的bk和fd指針并溢出第一塊,改好第一塊的chunk頭的控制信息(如下圖所示)
chunk0 malloc返回的ptr chunk1 malloc返回的pt
| | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
| | |fake|fake|fake|fake| D | fake | fake | | | |
| | |prev|size| FD | BK | A | prev | size&| | | |
| prev_size |size&Flag|size| | | | T | size | flag | | | |
| | | | | | | A | | | | | |
| | | | | | | | | | | | |
+-----------+---------+----+----+----+----+----+------+------+----+----+------+
|--------new_size--------|
我們為了欺騙glibc,讓它以為堆塊零malloc返回的指針(我們后文中簡記為p)出就是chunk0指針,所以我們偽造了prev_size和size的部分,然后溢出堆塊1,改掉第1個堆塊的prev_size,數值應該是上圖所示new_size的大小;另外第1塊的size部分還要把prev_inuse的flag給去掉。如此就做好了unlink觸發之前的準備工作
3.刪掉chunk1,觸發unlink(p),將p給改寫。
在刪除堆塊1時,glib會檢查一下自己的size部分的prev_inuse FLAG,發現到到比較早的一個chunk是空閑的(實際是我們偽造的),glibc希望將即將出現的兩個空閑塊合并。glibc會先將chunk0從它的Binlist中解引用,所以觸發unlink(p)。
1).FD=p->fd(實際是0x6016D0-0x18,因為全局數組里面指向p的那個指針就是0x6016D0)
2).BK=p->bk(實際是6016D0-0x10)
3).檢查是否滿足上文所示的限制,由于FD->bk和BK->fd均為*6016D0(即p),由此可以過掉這個限制
4).FD->bk=BK
5).BK->fd=FD(p=0x6016D0-0x18)
4.對p再次寫入,[email protected]
[email protected],[email protected]實地址,進而算出libc的基址來過掉ASLR。
6.根據已經算出的libc基址再次算出system函數的真實[email protected]? (如果沒有libc,可以考慮簡歷多個chunk,[email protected]??面的函數,這樣在list時,我們可以得到兩個libc函數的真實地址,根據其偏移,便可以找出服務器上的libc,若保護再夠復雜無法改got,我們還可以構造ropchain,同樣利用這樣的方式,把ropchain丟進全局數組中)
7.因為free已經變成了system,只要再建立一個內容為/bin/sh的塊,再刪掉,就可以得到shell,由此全部利用完成。