1. 程式人生 > 其它 >深入分析win32堆結構與管理策略

深入分析win32堆結構與管理策略

1. 深入分析win32堆結構與管理策略

1.1. 前言

1.1.1. 堆與棧的區別

1、棧(stack)由作業系統自動分配釋放,用於存放函式的引數值、區域性變數等,在程式編譯後就已經規定好如何使用,使用多少記憶體空間。棧總是成“線性”變化。棧向低地址空間增長。

2、堆(heap)由開發人員分配和釋放,若開發人員不釋放,程式結束時由OS回收,分配方式類似於連結串列。堆向高地址增長。

下圖是經典的32位系統記憶體佈局,暫時我們只需要記住棧和堆的增長方向即可,後面實驗部分會用到。


1.2. 理論篇

現代作業系統的堆資料結構一般包括堆塊堆表兩類。

堆表:堆表一般位於堆區的起始位置,用於索引堆區中所有堆塊的重要資訊,包括堆塊的位置、堆塊的大小、空閒還是佔用等。堆表的資料結構決定了整個堆區的組織方式,是快速檢索空閒塊、保證堆分配效率的關鍵。堆表在設計時可能會考慮採用平衡二叉樹等高階資料結構,用於優化查詢效率。現代作業系統的堆表往往不止一種資料結構。

堆塊:出於效能的考慮,堆區的記憶體按不同大小組織成塊,以堆塊為單位進行標識,而不是傳統的按位元組標識。一個堆塊包括兩個部分:塊首塊身。塊首位於一個堆塊頭部的幾個位元組,用來標識這個堆塊自身的資訊,例如,本塊的大小、本塊空閒還是佔用等資訊;塊身是緊跟在塊首後面的部分,也是最終分配給使用者使用的資料區。

在windows中佔用態的堆塊被應用程式自身所索引,堆表只索引空閒態的堆塊。其中,堆表有兩個重要的資料結構:空閒雙向連結串列(freelist)快速單身連結串列(lookaside)

堆的記憶體組織如下圖:

1.2.1. 堆塊

根據堆塊是否被佔用分為佔用態堆塊和空閒態堆塊。

佔用態堆塊的資料結構如下:

空閒態堆塊的資料結構如下:

對比上面兩圖可知,空閒態堆塊和佔用態堆塊的塊首結構基本一致。相對於佔用態的堆塊來說,空閒態堆塊的塊首後8個位元組存放了兩個指標地址,分別指向前驅堆塊和後向堆塊。

1.2.2. 堆表

1.2.2.1. 空閒雙向連結串列(空表)

堆區一開始的堆表區中有一個128項的指標陣列,被稱做空表索引(Freelist array)。該陣列的每一項包括兩個指標,用雙向連結串列組織一條空表,如下圖。

空表索引的第二項(free[1])連結了堆中所有大小為8位元組的空閒堆塊,之後每個索引項鍊接的空閒堆塊大小遞增8位元組,例如,free[2]連結大小為16位元組的空閒堆塊,free[3]連結大小為24位元組的空閒堆塊,free[127]標識大小為1016位元組的空閒堆塊。因此有:

空閒堆塊的大小=索引項(ID)×8(位元組)

空表索引的第一項(free[0])所標識的空表相對比較特殊。這條雙向連結串列連結了所有大於等於1024位元組的堆塊(小於512KB)。這些堆塊按照各自的大小在零號空表中升序地依次排列下去.把空閒堆塊按照大小的不同鏈入不同的空表,可以方便堆管理系統高效檢索。

1.2.2.2. 快速單向表(快表)

快表是Windows用來加速堆塊分配而採用的一種堆表。這裡之所以把它叫做“快表”是因為這類單向連結串列中從來不會發生堆塊合併,快表也有128條,組織結構與空表類似,只是其中的堆塊按照單鏈表組織。快表總是被初始化為空,而且每條快表最多隻有4個結點,故很快就會被填滿。

快表結構:

1.2.3. 堆塊分配

堆中的操作可以分為堆塊分配堆塊釋放堆塊合併(三種。其中,"分配"和"釋放"是在程式提交申請執行的,而堆塊合併則是由堆管理系統自動完成的。

注意:堆管理系統所返回的指標一般指向塊身的起始位置,在程式中是感覺不到塊首的存在的。然而,連續地進行記憶體申請時,可能會發現返回的記憶體之間存在“空隙”,那就是塊首!

堆塊分配可以分為三類:快表分配普通空表分配零號空表(free[0])分配。

  • 快表分配:找到大小匹配的空閒堆塊、將其狀態修改為佔用態、把它從堆表中“卸下”、最後返回一個指向堆塊塊身的指標給程式使用;
  • 普通空表分配: 首先尋找最優的空閒塊分配,若失敗,則尋找次優的空閒塊分配,即最小的能夠滿足要求的空閒塊;
  • 零號空表(free[0]):先從 free[0]反向查詢最後一個塊(即表中最大塊),看能否滿足要求,如果能滿足要求,再正向搜尋最小能夠滿足要求的空閒堆塊進行分配

1.2.4. 堆塊釋放

釋放堆塊的操作包括將堆塊狀態改為空閒,鏈入相應的堆表。所有的釋放塊都鏈入堆表的末尾,分配的時候也先從堆表末尾拿。

1.2.5. 堆塊合併

當堆管理系統發現兩個空閒堆塊彼此相鄰的時候,就會進行堆塊合併操作.

堆塊合併將兩個塊從空閒連結串列中“卸下”、合併堆塊、調整合並後大塊的塊首資訊(如大小等)、將新塊重新鏈入空閒連結串列。


1.3. 實驗篇

1.3.1. 實驗環境

作業系統 編譯環境 build 版本
win xp sp3 vc6.0 release

1.3.2. 實驗程式碼

#include <windows.h>
#include<stdio.h>
int main()
{
	HLOCAL h1, h2, h3, h4, h5, h6;
	HANDLE hp;


	//LoadLibrary("ntdll.dll");
	HANDLE hHeap = GetProcessHeap();

	hp = HeapCreate(0, 0x1000, 0x10000);
	//printf("%d", hp);
	__asm int 3
	h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 3);
	h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 5);
	h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 6);
	h4 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
	h5 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 19);
	h6 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24);
	//free block and prevent coaleses
	HeapFree(hp, 0, h1); //free to freelist[2]
	HeapFree(hp, 0, h3); //free to freelist[2]
	HeapFree(hp, 0, h5); //free to freelist[4]
	HeapFree(hp, 0, h4); //coalese h3,h4,h5,link the large block to
	//freelist[8]
	printf("%s","xxx");
	return 0;
}

注意,程式裡包含一句int 3的中斷指令,這裡用來中斷程式的執行的,因為我們不能直接把程式載入到偵錯程式裡面進行除錯,否則堆管理函式會檢測到有偵錯程式存在會啟用除錯態堆管理策略,與常態的堆管理策略會存在些許差異:

  • 除錯堆不使用快表,只用空表分配
  • 所有堆塊都被加上了多餘的16位元組尾用來防止溢位(防止程式溢位而非堆溢位攻擊)
  • 包括8個位元組的0xAB和8個位元組的0x00
  • 塊首的標識位不同

1.3.3. 除錯

編譯成功後,我們直接雙擊執行,如下

此時,我們附加我們的及時偵錯程式。根據原始碼可知,中斷是發生在HeapCreate函式執行完成後的,HeapCreate執行後會返回堆地址,結果儲存在eax中,我們在偵錯程式發現eax值是:0x003A0000

也就是說HeapCreate建立的堆區起始位置在003A0000.即堆表從此位置開始,堆表中依次為段表索引(Segment List)虛表索引(Virtual Allocation list)空表使用標識(freelist usage bitmap)空表索引區

此處我們只關心堆偏移0x178 處的空表索引區,這個偏移是堆表起始的位置(根據上次我們介紹的堆表結構,堆表包含128的8個位元組的flinkblink地址。所以堆表的結束位置在:128*8=1024=0x400,加上偏移,0x178+0x400=0x578)

加上堆基址0x003A0000+0x178=0x0x003A0178,我們來到這個地址。

如圖,這個地址便是free[0],佔8個位元組,flinkblink指向的地址都是0x003a0688。後面的依次是free[1]、free[2],依次類推,我們發現free[1]、free[2]...free[127]都指向自身,根據連結串列的特點可知,它們都是空連結串列。

所以當一個堆剛剛被初始化時,只包含一個空閒態的大塊,這個塊也叫為"尾塊" free[0]指向這個"尾塊"

我們轉到"尾塊"的位置去看看(因為這裡只有一個堆塊,即free[0]指向的地址,free[0]=0x003a0688

上面理論篇,我們講過,空閒態的堆塊有8個位元組的flink與blink,分別指向前驅節點與後繼節點,此處的值均為0x003a0178,這個地址是堆表free[0]的地址,可知,實驗與理論相符。

實際上。在上面我們有提到,堆管理系統返回的堆地址是指向塊身的。在其前面還有8個位元組的塊首,所以這個堆塊起始於0x003a0680, 根據上面談到的塊首的結構。前2個位元組為塊大小,此處值是130, 堆的計算單位是8位元組,也就是980位元組。

注意:堆大小包含塊首在內。

1.3.3.1. 堆塊分配

在繼續之前, 我們需要先了解堆塊的分配細節,

  • 堆塊的大小包括了塊首在內,即如果請求32位元組,實際會分配的堆塊為40位元組:8位元組塊首+32位元組塊身;

  • 堆塊的單位是8位元組,不足8位元組的部分按8位元組分配;

  • 初始狀態下,快表和空表都為空,不存在精確分配。請求將使用“次優塊”進行分配(這個“次優塊”就是位於偏移 0x0688 處的尾塊。)。

  • 由於次優分配的發生,分配函式會陸續從尾塊中切走一些小塊,並修改尾塊塊首中的size 資訊,最後把 freelist[0]指向新的尾塊位置。

記憶體請求分配情況

堆控制代碼 請求位元組數 實際分配(堆單位) 實際分配(位元組)
h1 3 2 16
h2 5 2 16
h3 6 2 16
h4 8 2 16
h5 19 4 32
h6 24 4 32

在偵錯程式中,我們單步走過第一個HeapAlloc,然後觀察記憶體空間。

tips: 對於我們主動設定的int 3指令,如果偵錯程式忽略異常後仍無法步過的話,可以在下一行程式碼處右鍵,此處設為新的eip。

按上面的分析,執行完h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 3);後,會從0x3a0680地址開始切出一塊大小為2個單位(16位元組)的空間分配給h1, 新的尾塊起始地址則為0x003a0690,flink與blink地址位於0x003a06980x003a069c,其值0x003a0178指向freelist[0], freelist[0]則指向新的起始地址0x003a0698,(003a0690+8位元組的塊首,我們上面有提到過指向塊身。)

尾塊起始處,如下圖,如我們所預期的一樣

另外,尾塊的大小為12e, 等於原來的130減去分配出去的2個單位,還剩下0x13-2=0x12e個單位(堆的單位,不是位元組),如上圖,也可以驗證。
h1所指向的堆塊起始位置則是0x003a0680,如上圖可知,大小為2個單位

堆表freelist[0]處,如下圖,如我們所預期的一樣

接著,會執行h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 5); 按分配原則,會從尾塊中再切一塊大小為2個單位(16位元組)的空間給h2,然後freelist[0]指向新的尾塊起始地址,新的尾塊仍指向freelist[0],剩下的尾塊大小為12e-2=12c個單位。
剩下的依次類推,

當執行完h6 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24);後,堆分配情況如下圖所示,

剩下的堆大小為 130-2-2-2-2-4-4=120單位,尾塊扔指向freelist[0](0x003a0178),我們去看下freelist[0]的值,此時應該指向,尾塊的塊首0x003a0708,如下圖

到此,堆的分配則執行完了。根據上面的理論可知,堆表中仍只有一個尾塊,不存在其它的堆塊。

1.3.3.2. 堆塊釋放

根據堆塊的大小,h1,h3為16位元組,則會鏈入freelist[2], h5為32位元組,會鏈入到freelist[4]。

當執行HeapFree(hp, 0, h1)後,根據h1的大小為16位元組,可知它會被鏈入到freelist[2]中,我們先看下freelist[2]的地址,如下圖,freelist[2]的地址是0x003a0188,根據連結串列規則freelist[2]會指向h1的地址,h1則會指向freelist[2],

執行後,原來h1所指向的堆塊變為空閒態並指向freelist[2]。如下圖,flink與blink都指向freelist[2],因為此時只有連結串列中就一個節點,

freelist[2]則指向原來的h1地址,如下圖:

接著會釋放h3,執行HeapFree(hp, 0, h3),執行完後,h3所指向的堆塊會被鏈入到freelist[2],並插入到整個連結串列的末尾。如下圖所示,原來h3所在的堆塊的blink(地址0x003a06ac)指向前一個堆塊,即原來的h1,h3的flink則指向freelist[2],因為它是最後一個元素。原來的h1的blink指向freelist[2],flink指向h3.

freelist[2]如下圖所示

形成的連結串列大概如下,

freelist[2] <---> h1 <---> h3

注h3的flink與freelist[2]的blink未給出。

再下一步,執行HeapFree(hp, 0, h5);,釋放h5所在的堆塊,並鏈入freelist[4]

1.3.3.3. 堆塊合併

因為h1,h3,h5記憶體地址不相鄰,所以並不會發生堆塊合併,當釋放h4後,堆管理系統會發生現h3,h4,h5相鄰,則會進行堆塊合併。
首先這 3 個空閒塊都將從空表中摘下,然後重新計算合併後新堆塊的大小,最後按照合併後的大小把新塊鏈入空表。

h3、h4 的大小都是2個堆單位(8位元組),h5是4個堆單位,合併後的新塊為8個堆單位,將被鏈入 freelist[8]。

合併之前freelist[8]如下圖,

合併之前h3,h4,h5如下圖,

合併之後freelist[8]如下圖,flink與blink都指向合併之後的地址

合併之後的堆塊如下圖,大小8個堆塊,flink與blink都指向freelist[8]

另外,我們看合併之後的freelist[4]已經指向自己,為空連結串列(圖中框選中的位置),freelis[2]中,只剩下原來的h1所在的一個堆塊了。

1.4. 總結

  • 堆的資料結構: 堆塊堆表
  • 堆塊:包含塊首塊身
  • 堆表空閒雙向連結串列(freelist)快速單向連結串列(lookaside)
  • 佔用態的堆塊:8位元組的塊首+塊身
  • 空閒態的堆塊:16位元組的塊首(多了flink與blink)+塊身。空閒態的堆塊變為佔用態時,flink與blink所在的空間將變為data區。

在win中,佔用態的堆塊被使用它的程式所索引,而堆表只索引所有空閒的堆塊。


  • 參考:《0day,軟體安全漏洞分析技術》
    </stdio.h></windows.h>