1. 程式人生 > >堆入門的必備基礎知識

堆入門的必備基礎知識

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 堆的使用場景

  1. new 一個新物件
  2. 傳參為陣列時

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 ];
*/

參考文章

Libc堆管理機制及漏洞利用技術 (一)

Heap overflow using unlink

ctf-wiki

淺析Linux堆溢位之fastbin

 

大家有任何問題可以提問,更多文章可到i春秋論壇閱讀喲~