堆入門的必備基礎知識
i春秋作家:W1ngs
原文來自:堆入門的必備基礎知識
前言
堆的利用相對於棧溢位和格式化字串會複雜很多,這裡對堆的一些基本知識點和實現原理進行了一些小小的總結,寫的如有不當懇請大佬們斧正。
堆的實現原理
對堆操作的是由堆管理器來實現的,而不是作業系統核心。因為程式每次申請或者釋放堆時都需要進行系統呼叫,系統呼叫的開銷巨大,當頻繁進行堆操作時,就會嚴重影響程式的效能
例如 glibc 中使用了 ptmalloc2 作為堆管理器:
目前 Linux 標準發行版中使用的堆分配器是 glibc 中的堆分配器:ptmalloc2。ptmalloc2 主要是通過 malloc/free 函式來分配和釋放記憶體塊。
程式向系統申請堆空間的時候相當於一種 "批發" 和 "零售" 的關係:
堆管理器就像一箇中間商,將向核心申請到空間根據分配演算法來把空間真正的分配給程式。
這裡為了理解簡單畫了一張圖,如果有錯誤的話敬請指正。
0x00 建立、釋放堆的函式
malloc、realloc、calloc
malloc 函式:
#include <stdlib.h>
void *malloc(size_t size);
- 在使用malloc的時候要進行強制型別轉換為指標型別
malloc函式申請地址成功後返回一個指標,指向大小為至少size位元組的記憶體塊
- 當size = 0 時,返回當前系統允許的堆的最小記憶體塊
即malloc(0) 在32位系統下會分配 8 個位元組的空間,在 64 位系統下會分配 16 位元組的空間
malloc會使用 mmap 來建立獨立的匿名對映段,malloc 的背後是用 brk 函式來實現記憶體地址申請的。
檢視方法:cat /proc/PID/maps
使用 malloc() 申請的記憶體,釋放後,仍然歸還回原處,再次申請同樣大小的記憶體區時,還是從第 1 次那裡獲得
每次申請會獲取比申請到更大的值,這樣的話,就避免了多次核心態與使用者態的切換,提高了程式的效率
分配器視堆為一組不同大小的塊(chunk)的集合。每個塊就是一個連續的虛擬記憶體片。
free 函式:
#include <stdlib.h>
void free(void *ptr);
free 函式會釋放由 p 所指向的記憶體塊,這個記憶體塊可以是通過malloc或者readlloc函式分配的塊。
- 當 p 為空指標時,函式不執行任何操作,當釋放過 p 記憶體塊再次釋放後,會產生錯誤(double free)。
當一個堆塊釋放了(通過呼叫free函式),它會檢查之前的堆塊是否被釋放了。如果之前的堆塊沒有在使用,那麼就會和當前的堆塊合併。
unlink 的原始碼:
/* Take a chunk off a bin list */
void unlink(malloc_chunk *P, malloc_chunk *BK, malloc_chunk *FD)
{
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
chunk 合併的過程與雙向連結串列刪除節點的過程相同:
其實這塊論壇裡有一篇關於 unlink 函式的利用這一塊講的很清楚了,可以參考他的文章:
https://bbs.ichunqiu.com/thread-46614-1-1.html
0x01 記憶體分配有關的函式
brk
sbrk
對於每個堆,變數brk指向堆的頂部,不過有下面的兩個前提
- 不開啟 ASLR 保護時,brk 會指向 data/bss 段的結尾。
- 開啟 ASLR 保護時,brk 也會指向同一位置,只是這個位置是在 data/bss 段結尾後的隨機偏移處。
brk和sbrk主要的工作是實現虛擬記憶體到記憶體的對映
- 當 sbrk() 中的引數為 0 時,我們可以找到 program break 的位置。 也就是 sbrk(0) 時,指標指向的就是program break,也就是堆頂
- brk 函式和 sbrk 函式通常都配合使用,如下示例:
示例:
1.sbrk(0) -- > 初始化堆,將 start_brk 以及堆的當前末尾 brk 指向同一地址
在執行下面的兩條語句之後,使用 cat /proc/PID/maps
會發現沒有堆空間
tmp_brk = curr_brk = sbrk(0);
printf("Program Break Location1:%p\n", curr_brk);
2.brk(curr_brk+4096) -- > 重新定義堆頂的指標
brk(curr_brk+4096);
curr_brk = sbrk(0);
printf("Program break Location2:%p\n", curr_brk);
3.此時再呼叫 sbrk(0),堆頂指標就會變化
brk(tmp_brk);
curr_brk = sbrk(0);
printf("Program Break Location3:%p\n", curr_brk);
0x02 mmap、munmap函式
mmap函式要求核心建立一個新的虛擬記憶體區域
map函式原型:
void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset)
prot引數指定虛擬記憶體區域的訪問許可權,有以下幾個
PROT_EXEC //這個區域由可以被CPU執行的指令組成
PROT_READ //可讀
PROT_WRITE //可寫
PROT_NONE //不可訪問
flags引數描述被對映物件型別的位組成,有以下幾個
MAP_ANON或者MAP_ANONYMOUS //表示被對映的物件是一個匿名物件,相應的虛擬頁面是請求二進位制零的
MAP_PRIVATE //物件屬性為私有、寫時複製的
MAP_SHARED //表示共享物件
- 對於大於 128 KB 的堆申請請求來說,根據分配演算法會使用 mmap 函式為她分配一塊匿名空間,在這個匿名空間裡為使用者分配空間。
eg.申請132KB的虛擬記憶體區域
addr = mmap(NULL, (size_t)132*1024, PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
mmap 函式與 brk 函式的區別
對於小於 128 KB 的請求來說,會在現有的空間中按照堆分配演算法(brk、sbrk)為它分配一個堆空間,大於 128 kB 時,就使用 mmap 函式分配一個匿名空間給使用者使用。
簡單來說就是兩個區別:
1.一個是在現有的堆空間中分配,一個是在設定了 MAP_ANONYMOUS 屬性的匿名空間中分配。
2.一個是用於申請小空間時使用,一個是在用於申請大空間時使用
mmap 函式的另一種用法
在棧溢位的利用時,若 system 和 execve 函式都被禁用的時候,我們可以使用 mmap 或者 mprotect 函式將 bss 段的記憶體許可權設定為可執行,這樣我們再把 shellcode 寫入到裡面,接著將 eip 執行他,就可以達到直接執行 shellcode 的效果。
例如,jarvisoj 的 level5:
WriteUp的連結如下,裡面講到了詳細的用法和引數設定。
https://blog.csdn.net/zszcr/article/details/79703642
munmap函式原型:
int munmap(void *start,size_t length)
eg.刪除已經建立的虛擬記憶體區域
ret = munmap(addr, (size_t)132*1024);
分配的過程:
0x03 堆的使用場景
- new 一個新物件
- 傳參為陣列時
0x03 Bin
fast bins
small bins
large bins
unsorted bin
fast bins
用於一些較小的 chunk 釋放之後發現存在與之相鄰的空閒的 chunk 並將它們進行合併
typedef struct malloc_chunk *mfastbinptr;
/*
This is in malloc_state.
/* Fastbins */
mfastbinptr fastbinsY[ NFASTBINS ];
*/
參考文章