日誌 2019.7.13 pwn 堆溢位基礎知識
十六進位制兩位表示一個位元組
堆溢位
先上堆圖:
堆的資料結構
一般情況下,物理相鄰的兩個空閒 chunk 會被合併為一個 chunk
struct malloc_chunk { INTERNAL_SIZE_T prev_size; /*上一個chunk空閒時記錄上一個chunk的大小,上一個chunk被使用時作為上個chunk的資料部分 */ INTERNAL_SIZE_T size; /* 本chunk的大小,低三位是標誌位*/ struct malloc_chunk* fd; /* 本chunk空閒時指向下一空閒的chunk,被使用時作為資料部分的指標 */ struct malloc_chunk* bk; /* 本chunk空閒時指向上一空閒的chunk,被使用時作為資料部分 */ /* 僅在大chunk中使用,指向的都是大的chunk ,用於對大的chunk按照大小排序,以減少遍歷chunk的開銷*/ struct malloc_chunk* fd_nextsize; /* 指向上一個與本chunk不同大小的chunk */ struct malloc_chunk* bk_nextsize; /* 指向下一個與本chunk不同大小的chunk */ }; size的低三位標誌位從高到低是: A:NON_MAIN_ARENA,是否不屬於主執行緒,1 表示不屬於,0 表示屬於。 M:IS_MAPPED,是否由 mmap 分配 P:PREV_INUSE,是否被分配
bin
會根據空閒的 chunk 的大小以及使用狀態將 chunk 初步分為 4 類:fast bins,small bins,large bins,unsorted bin
其中 unsorted bin, small bins,large bins 依次被組織到一個數組中,陣列下標表示的是chunk上的fd或者bk指標,並不代表第幾個bin。每一個bin中都是一個連結串列,包含著多個chunk
1 unsorted bin
沒有排序的bin
來源:
1)當一個較大的 chunk 被分割成兩半後,如果剩下的部分大於 MINSIZE,就會被放到 unsorted bin 中。
2)釋放一個不屬於 fast bin 的 chunk,並且該 chunk 不和 top chunk 緊鄰時,該 chunk 會被首先放到 unsorted bin 中。
2 small bins
陣列下標 2 - 63 是small bin,每一個bin下的連結串列儲存著相同大小的chunk,每兩個相鄰的bin代表的大小為 2個機器字長
small bins 中每個 chunk 的大小與其所在的 bin 的 index 的關係為:chunk_size = 2 * SIZE_SZ *index,具體如下
| 下標 | SIZE_SZ=4(32 位)| SIZE_SZ=8(64 位)|
|--|--|--|--|
| 2 | 16 | 32 |
|3 | 24 | 48|
|4 |32 | 64|
|5 |40 |80|
|x | 2*4*x | 2*8*x|
|63 | 504
分配採用 FIFO 的規則
3 large bin
small bin之後是large bin,一共有63個bin,每個bin代表一定範圍內的chunk,相同大小的按照最近使用排列
以 32 位平臺的 large bin 為例,第一個 large bin 的起始 chunk 大小為 512 位元組,位於第一組,所以該 bin 可以儲存的 chunk 的大小範圍為 [512,512+64)。
組 | 數量 | 公差 |
---|---|---|
1 | 32 | 64B |
2 | 16 | 512B |
3 | 8 | 4096B |
4 | 4 | 32768B |
5 | 2 | 262144B |
6 | 1 | 不限制 |
4 fast bin
當釋放的空間大小小於MAX_FAST_SIZE時,為了節省下一次的遍歷開銷,會將這部分的空間存放到fastbin中,這樣這些經常用到的小塊就不用花時間在合併和分割上。(fastbin 範圍的 chunk 的 inuse 始終被置為 1。因此它們不會和其它被釋放的 chunk 合併。)
分配採用LIFO後進先出的規則
fastbin 最多可以支援的 bin 的個數為 10 個
/* The maximum fastbin request size we support */
#define MAX_FAST_SIZE (80 * SIZE_SZ / 4)
預設情況下(32 位系統為例), fastbin 中預設支援最大的 chunk 的資料空間大小為 64 位元組
top chunk
地址最高的chunk
當所有的bin都無法滿足時,若 top chunk 大於申請的空間,則分割,分割後剩餘的部分作為新的 top chunk。
初始情況下,我們可以將 unsorted chunk 作為 top chunk。
名詞:
chunk:塊
arena:作業系統分配給程式的一塊較大的(大於所申請的)記憶體區域
last remainder:在使用者使用 malloc 請求分配記憶體時,ptmalloc2 找到的 chunk 可能並不和申請的記憶體大小一致,這時候就將分割之後的剩餘部分稱之為 last remainder chunk ,unsort bin 也會存這一塊。top chunk 分割剩下的部分不會作為 last remainer.
堆操作:
- malloc(size_t n)
返回對應大小位元組的記憶體塊的指標
當 n=0 時,返回當前系統允許的堆的最小記憶體塊
當 n 為負數時,由於在大多數系統上,size_t 是無符號數(這一點非常重要),所以程式就會申請很大的記憶體空間,但通常來說都會失敗,因為系統沒有那麼多的記憶體可以分配
使用brk申請的堆空間會挨著資料段,而使用mmap的堆空間在較高的地址
這些記憶體在釋放之後一般不會返回系統而是在程式中等待申請
malloc()會使用兩種系統呼叫:
1)brk()或者sbrk()函式
作業系統提供了 brk 函式,glibc 庫提供了 sbrk 函式,sbrk會呼叫brk
初始時,堆的起始地址 start_brk 以及堆的當前末尾 brk 指向同一地址
brk(address) 直接將program break修改到address,返回值 0成功 -1 失敗
sbrk(n) 修改program break向高地址增長n個位元組,返回開闢之前的program break地址
sbrk(0)返回當前program break的地址
但是program break的增長不是無限制的,天花板是Memory Mapping Segment
2)mmap()
當申請的空間大於128k時,malloc()呼叫mmap()
- free(void* p)
會釋放由 p 所指向的記憶體塊
當 p 為空指標時,函式不執行任何操作
當 p 已經被釋放之後,再次釋放會出現亂七八糟的效果,這其實就是double free
除了被禁用 (mallopt) 的情況下,當釋放很大的記憶體空間時,程式會將這些記憶體空間還給系統,以便於減小程式所使用的記憶體空間
申請空間
malloc --> __libc_malloc --> _int_malloc
__libc_malloc(size)
使用者申請的位元組一旦進入申請記憶體函式中就變成了 無符號整數。
尋找鉤子hook ----》 尋找arena ----》 呼叫_int_malloc分配記憶體 -+--》成功,返回記憶體
↑ |
| ↓
+-----分配失敗,再尋找一個arena
_int_malloc()
--------------------------------------------------------------------------------
將size轉化為對應的chunk大小 ----》 fastbin ----》 遍歷(後進先出),檢查大小是否符合 ----》 符合則計算索引 ----》 chunk轉換為記憶體返回
根據大小選擇bin ----》 smallbin ----》獲取索引、指標 ----》 檢查該bin是否為空 ----》 不為空 ----》將連結串列中最後一個chunk分配(先進先出)
| +----》 初始化
+---》 該bin為空
----》 不在fastbin和smallbin中 ----》 malloc_consolidate():處理fastbin ----》 可以合併的合併,然後放 unsorted bin ----》大迴圈
----------------------------------------------------------------------------------
大迴圈 ----》 遍歷unsorted bin ----》 FIFO尋找大小剛好合適的bin ----》若有,bin轉為記憶體後返回
迴圈10000次 ----》若沒有,則將當前的unsorted bin按照大小放至對應的small或large中
----》 遍歷large bin ----》對應的 bin 中從小(連結串列尾部)到大(頭部)進行掃描 ----》 找到第一個合適的返回
----》 若大小合適的bin都不存在,則在map中找更大的bin遍歷 ----》 找到,返回記憶體
----》 找不到,使用top chunk ----》 滿足,分割後返回
----》 不滿足,使用 sysmalloc 來申請記憶體
------------------------------------------------------------------------------------
//從 fastbin 的頭結點開始取 chunk(LIFO)
sysmalloc()
用於當前堆記憶體不足時,需要向系統申請更多的記憶體
函式內呼叫了brk()和mmap()
釋放空間
free --> __libc_free --> _int_free
_int_free()
檢查 ----》是否fastbin ----》是fastbin,放至fastbin連結串列表頭
+---》是否mmap分配 ----》 是,munmap_chunk()
+---》 否,合併chunk ----》 向低地址合併 ----》想高地址合併 ----》 下一個是否是top chunk ----》 是,合併到top chunk
+---》 否,合併加入unsorted bin
tcache
ubuntu 17.10中的新資料結構,提升了堆的效能,犧牲了一定的安全效能
堆溢位原理
條件:
- 可以寫入堆
- 大小無限制
利用策略:
- 覆蓋下一chunk的內容
- 利用堆機制實現任意地址寫
要點
由低地址向高地址方向增長
堆管理器會對使用者所申請的位元組數進行調整,這也導致可利用的位元組數都不小於使用者申請的位元組數
puts()內部會呼叫 malloc 分配堆記憶體
rwxp中的p許可權是指隱私資料
一個程式一旦編譯好後,text segment ,data segment 和 bss segment 是確定下來的
堆分配的最小單位是機器字長的2倍,32 位系統最小是 8 個位元組,64 位系統最小是 16 個位元組,比如 64 位系統執行malloc(0)會返回使用者區域為 16 位元組的塊。
prev_size欄位為8位元組
strlen 是我們很熟悉的計算 ascii 字串長度的函式,這個函式在計算字串長度時是不把結束符 '\x00' 計算在內的,但是 strcpy 在複製字串時會拷貝結束符 '\x00'
qword是四位元組
申請一個超大的塊,來使用 mmap 擴充套件記憶體。因為 mmap 分配的記憶體與 libc 之前存在固定的偏移因此可以推算出 libc 的基地址
堆分配函式
- malloc(size):見上文“堆操作”
- calloc(size):calloc 在分配後會自動進行清空,這對於某些資訊洩露漏洞的利用來說是致命的
calloc(0x20);
//等同於
ptr=malloc(0x20);
memset(ptr,0,0x20);
- realloc(ptr,size)
當 realloc(ptr,size) 的 size 不等於 ptr 的 size 時
1)如果申請 size > 原來 size
----如果 chunk 與 top chunk 相鄰,直接擴充套件這個 chunk 到新 size 大小
----如果 chunk 與 top chunk 不相鄰,相當於 free(ptr),malloc(new_size)
2)如果申請 size < 原來 size
----如果相差不足以容得下一個最小 chunk(64 位下 32 個位元組,32 位下 16 個位元組),則保持不變
----如果相差可以容得下一個最小 chunk,則切割原 chunk 為兩部分,free 掉後一部分
3)當 realloc(ptr,size) 的 size 等於 0 時,相當於 free(ptr)
當 realloc(ptr,size) 的 size 等於 ptr 的 size,不進行任何操作
危險函式標誌
輸入
gets,直接讀取一行,忽略 '\x00'
scanf
vscanf
輸出
sprintf
字串
strcpy,字串複製,遇到 '\x00' 停止
strcat,字串拼接,遇到 '\x00' 停止
bcopy
除錯方法
cat /proc/pid/maps 可以檢視該程序記憶體對映
檢視堆:
vmmap:檢視堆地址
x/10gx addr:64位地址寬度以16進位制列印10個地址內容
find xxxx:用來尋找你的輸入內容
具體漏洞型別
- off_by_one 單位元組溢位
1)溢位可控制任意位元組:修改size來洩露或者覆蓋其他資料
2)溢位NULL:在 size 為 0x100 的時候,溢位 NULL 位元組可以使得 prev_in_use 位被清,這樣前塊會被認為是 free 塊。
(1) 這時可以選擇使用 unlink 方法(見 unlink 部分)進行處理。
(2) 另外,這時 prev_size 域就會啟用,就可以偽造 prev_size(間接控制本chunk的size) ,從而造成塊之間發生重疊。此方法的關鍵在於 unlink 的時候沒有檢查按照 prev_size 找到的塊的後一塊(理論上是當前正在 unlink 的塊)與當前正在 unlink 的塊大小是否相等