日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

XP SP3堆研究

發布時間:2023/12/14 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 XP SP3堆研究 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本文原創作者:icq5f7a075d,本文屬i春秋原創獎勵計劃,未經許可禁止轉載! https://bbs.ichunqiu.com/thread-33885-1-1.html

本文如果出現錯誤,歡迎指出,感激不盡!

本文中的所有程序請在虛擬機中運行。 前言 堆溢出漏洞是一種常見的軟件漏洞,但是堆的結構比較復雜,不同版本操作系統的堆結構存在較大差異,網絡上關于堆的介紹也存在名詞不統一的情況,這都極大的增加了新手學習堆溢出漏洞的難度。本文將詳細剖析XP SP3系統的堆結構,讓新手對堆的結構有初步了解。當然,本文只是堆的初步介紹,頁堆、CRT堆等深層次的內容,本文并沒有涉及。不同版本的操作系統堆結構存在較大差異,本文分析的XP SP3系統上的堆結構,請在XP SP3系統上進行本文的實驗。 1.?堆與棧的區別 既然談到了堆,就不得不一起說說棧。堆和棧是兩種數據結構,程序在執行時需要兩種不同數據結構的內存(堆和棧)來協同配合。
??臻g由操作系統自動分配釋放,存放函數的參數值、局部變量的值、函數返回地址等。進程在創建時,操作系統就會為其創建棧。棧在使用的時候不需要額外的申請操作,系統會根據函數中的變量聲明自動在函數棧幀中給其預留空間。棧空間由系統維護,它的分配和回收都由操作系統來完成,最終達到棧平衡。
堆在程序運行時分配,由程序指定分配的大小。堆在使用時需要程序員用專用函數進行申請,如C語言中的 malloc等函數、C++中的 new 函數。堆內存申請有可能成功,也有可能失敗,這與申請內存的大小、機器性能和當前運行環境有關。使用完畢后需要把堆指針傳給堆釋放函數回收這片內存,否則會造成內存泄露。每個進程通常都有很多個堆,程序可以通過自己的需要創建新的堆。每個進程都會有一個默認的進程堆,指向這個堆的指針被存放在進程環境塊PEB中,而這個進程的所有堆,都以鏈表的形式被掛在PEB上。
下圖是某個進程的內存空間,如圖所示,0x12D000的內存空間為棧空間,內存中只有一個??臻g;0x140000的內存空間為進程堆的空間,內存中只有一個進程堆空間,這里大小為0x8000;0x240000、0x250000、0x380000、0x3A0000的內存空間為普通堆的空間,這些是程序根據需要自己創建的新的堆,不同堆的大小可能不同: 2.?堆的管理機制 普通程序員在使用堆時,只需要申請、使用、釋放就可以了。但是在內存中卻需要進行一系列復雜的操作,如何在“雜亂”的內存中“辨別” 出哪些內存是正在被使用的,哪些內存是空閑的,并最終“尋找”到一片“恰當”的空閑內存區域,分配給程序使用?這些復雜的過程都是由堆管理器完成的。 在理解堆管理器如何完成上述復雜的操作前,我們必須先了解如下幾個名詞:堆、堆表、堆段、段表、堆塊、塊首。 2.1.?堆的基本結構 1)堆 在上圖中,可以看到內存中有五段內存塊被標記為堆:0x140000、0x240000、0x250000、0x380000、0x3A0000,它們大小并不相同。每一個這樣的內存塊區域就被稱為一個堆,《0day》書中將其稱為一個堆區。一個堆,最小為0x1000。 2)堆塊和塊首 一個堆的內存按照不同大小組織成塊,以堆塊為單位進行標識,而不是傳統的字節標識。程序申請和使用堆,實際上使用的都是這些堆塊。一個堆塊包括兩個部分:塊首和塊身。塊首是HEAP_ENTRY結構,它是一個堆塊頭部的8個字節,用來標識這個堆塊自身的信息(塊的大小、使用狀態等);塊身是進跟在塊首后面的部分,也是最終分配給用戶使用的數據區。整個堆都被分割成了一個個的堆塊,接下來我們提到的段表和堆表也是堆塊。每個堆塊都是8字節的倍數。 (3)堆段和段表 一個堆會被分成幾個段,這些段又被分成一個個堆塊。堆管理器在創建堆時會創建一個段,這個段是0號段,這個段目前就是整個堆區的大小;如果這個段是可增長的,也就是堆標志中含有HEAP_GROWABLE(2)標志,那么堆管理器會再分配一個段。堆表中Segments[64]數組記錄著每一個堆段。每個堆都至少有一個段,即0號段,最多可以有64個段。段表則是記錄段信息的結構體,是一個HEAP_SEGMENT結構,除了0號段,其他段的段表一般位于段的起始位置,0號段的起始位置記錄著堆表,堆表接下來才是段表。 (4)堆表 堆表,是一個HEAP結構,位于整個堆的起始位置,用于索引堆中所有堆塊和堆段的重要信息,包括堆段的索引、堆塊的位置、堆塊的大小、堆塊的使用狀態等。在 Windows 中,堆表只索引所有空閑的堆塊,正在使用的的堆塊被使用它的程序索引。 下圖,顯示堆的數據布局 如圖所示,堆中的內存區被分割成一列不同大小的堆塊,每個堆塊的起始處一定是一個8byte的HEAP_ENTRY 結構,后面便是提供應用程序使用的區域,也稱為用戶區。堆表所占的空間是一個堆塊,同時也是0號堆段的起始位置。 對于0號段,這個結構位于HEAP結構之后,對于其他段,這個結構就在段的起始處,堆表位于0號段 段中的所有已經提交的空間都屬于一個堆塊,即使是HEAP結構和HEAP_SEGMENT 結構所占的空間也是分別屬于一個單獨的堆塊,這兩個結構的起始處都是一個HEAP_ENTRY結構。 HEAP_ENTRY前兩個字節以分配粒度表示堆塊的大小,分配粒度通常是8,這意味著每個堆塊的最大值是0x10000*8=0x80000=512KB。因為每個堆塊知識有8字節的管理信息,因此應用程序可以使用的最大堆塊便是0x80000-8=0x7FFF8。如果程序想申請大于512KB的堆塊時怎么辦? 當一個應用程序要分配大于512KB的堆塊時,如果堆標志中含有HEAP_GROWABLE(2),那么堆管理器便會直接調用ZwAllocateVirtualMemory()來滿足這次分配,并把分得的地址記錄在HEAP結構的VirualAllocdBlocks所指向的鏈表中,這意味著,堆管理器批發過來的大內存塊,有兩種形式,一種形式是段,另一種形式是直接的虛擬內存分配,將后一種形式稱為大虛擬內存塊。因為堆管理器是以鏈表方式來管理大虛擬內存塊的,因此數量是沒有限制的。每個大虛擬內存塊的起始處是一個HEAP_VIRTuAL_ENTRY結構(32字節)。 2.2.?堆管理器 堆表索引堆中所有空閑的堆塊,那么堆表是如何索引的?堆塊又是如何組織的?這就不得不說堆管理器(又叫堆分配器)。堆管理器主要分為前端堆管理器和后端堆管理器,有些文章里也叫前端堆管理器和核心堆管理器。前端堆管理器是一個高性能的子系統,而核心堆層管理器則是一個強大的通用堆的實現,這兩個組件在結構上是分開的。 2.2.1.?前端管理器 在處理內存分配和釋放的時候,前端堆管理器是優先做處理的。前端堆管理器有三種模式:none、LAL(Look-aside Lists,預讀列表\旁氏列表\快表)和LFH(Low-Fragmentation Heap,低碎片堆 )。none模式實際上就是不使用前端堆管理器意思,當一個堆不可擴展時,前端堆管理器為none模式。當堆為可擴展堆時,前端堆管理器開啟,XP SP3下默認使用LAL,Windows Vista系統默認使用LFH。進程堆默認開啟前端分配器。 LAL可以處理小于1024字節的分配請求。它用一系列的單鏈表的數據結構去存放大小在0到1024之間的空塊。LFH可以處理大小在0-16k的請求。 (1)LAL Look-aside Lists,在《軟件調試》一書中被稱為旁視列表,在《0day》中被稱為快表。 LAL是一張表,包含128個項(是一個有128個元素的數組),每一項對應一個單項鏈表,每個單向鏈表中都包含一組固定大小的空閑堆塊,這個固定大小的值等于數組下標*8。因為塊首就要占有8字節,所以索引為0的項和索引為1的項永遠不會被使用。如果應用程序請求24字節的空間,前端分配器將查找大小為32字節的空閑堆塊。 HEAP結構中 FrontEndHeap字段記錄這前端管理器信息,當啟動LAL時,這個值一般指向堆基址偏移+0x688的地方,這里就是LAL。 在XP系統中分析LAL,HEAP結構中 FrontEndHeap的指針一般指向堆基址偏移+0x688的地方,在這里可以找到這個LAL。LAL是一個數組,每個元素對應一個單項鏈表索引。每個元素的數據結構大小為0x30,包括了性能相關的變量,包括當前長度,最大長度,以及更重要的一個指向與索引對應的單鏈表堆塊的指針。如果當前沒有空閑塊,則該指針為NULL。同樣,在單鏈表末尾是指向的是NULL。 LAL也被稱為“快表”,是因為這類單向鏈表中從來不會發生堆塊合并(其中的空閑塊標記會被標為BUSY,阻止后端堆管理器對它進行分配或合并的操作)。 LAL總是被初始化為空,而且每條鏈表最多只有 4 個結點,故很快就會被填滿。 當分配內存的時候, 列表頭部的結點被彈出。 當一個塊被釋放, 把它從單鏈表的頭部壓入, 并且把指針更新。 如果用戶的請求是小于(1024-8)字節,那么它便可以用 LAL 前端來分配。下圖顯示了 LAL的結構,如圖前端有一個大小為 1016 的列表,它對應著的序列號是 127,用戶可以申請 1008 字節。
(2)LFH Low-Fragmentation Heap,低碎片堆。堆的內存空間被反復分配和釋放,堆上可用空間可能被分割得支離破碎,當試圖從這個堆上分配空間時,即使可用空間加起來的總額大于請求的空間,但是因為沒有一塊連續的空間可用滿足要求,那么分配請求仍會失敗。堆碎片化和磁盤碎片化的形成機理是一樣的,但多個磁盤碎片相加仍可以滿足磁盤分配請求,但是堆碎片是無法通過累加來滿足內存分配要求,因為堆函數返回的必須是地址聯系的一段空間。于是便引入了LFH來降低碎片。 LFH將堆上的可用空間劃分成128個桶位(Bucket),編號為1~128,每個桶位的空間大小依次遞增,1號桶為8個字節,128號桶為16384字節(16KB)。當需要從LFH上分配空間時,堆管理器會根據堆函數參數中所請求的字節將滿足要求的最小可用桶分配出去。例如,如果程序請求分配7個字節,而且1號桶空閑,那么便將1號桶分配給它(分配8字節),如果1號桶已經分配出去了(busy),那么便嘗試分配2號桶。 LFH不同編號區域的桶使用不同的粒度,桶的容量越大,粒度也越大: LFH在下面三種情況下不會開啟,1,固定大小的堆,2,Heap_no_serialize,3,debug 狀態。XP SP3默認不使用LAF,只有在應用程序調用了HeapSetInformation以后LFH才被打開。從Windows Vista開始,LFH默認啟用。 [C++]?純文本查看?復制代碼?
123456ULONG HeapFragValue=2;#HEAP_LFHBOOL bSuccess=HeapSetInformation(GetProcessHeap(),HeapCompatibilityInformation,&HeapFragValue,sizeof(HeapFragValue));
調用HeapQueryInformation()可以查詢一個堆是否啟用LFH支持。 2.2.2.?后端堆管理器 (1)FreeLists 后端管理器組織堆塊的方式是使用FreeLists,又被稱為空閑列表或空表,空閑列表的堆塊塊首中包含一對重要的指針,這對指針用于將空閑堆塊組織成雙向鏈表。 和Lookaside類似,空表也有 128 項,在堆區一開始的堆表區中有一個 128 項的指針數組,被稱做空表索引(Freelist array),也被稱作空表位圖(Freelist Bitmap)。每一項對應一個雙項鏈表,除第一項,每個雙向向鏈表中都包含一組固定大小的空閑堆塊,這個固定大小的值等于數組下標*8。空表索引的第一項(FreeList[0])所標識的空表相對比較特殊。這條雙向鏈表鏈入了所有大于等于 1024 字節的堆塊(小于 512KB)。這些堆塊按照各自的大小在零號空表中升序地依次排列下去。 空表索引里的每一項包括兩個指針,用于標識一條空表。 如圖,空表索引的第二項(FreeList[1])標識了堆中所有大小為 8 字節的空閑堆塊,之后每個索引項指示的空閑堆塊遞增 8 字節,例如,FreeList[2]標識大小為16字節的空閑堆塊,FreeList[3]標識大小為 24 字節的空閑堆塊,FreeList[127]標識大小為1016 字節的空閑堆塊。因為堆塊塊首(HEAP_ENTR)就占據8字節,所以FreeList[1]鏈表中的堆塊實際上不可用。 對于一個給定的小于1016的分配請求,前端堆管理器首先會處理其請求。假設LAL或LFH不存在或者不處理這個請求, 系統將會直接在FreeList[n]的列表中找給定大小的列表。注意在這種情況下, 空表索引是沒有被使用到的。如果FreeList[n] 中也找不到合適的塊, 那么系統將會使用空表索引來處理。 它通過搜索整個空表索引,然后找到一個置位,通過這個置位, 可以在這個列表中找到下一個最大的空閑塊。如果系統跑完這個索引還沒有找到合適的塊,它將試著從FreeList[0]中找到一塊出來。 舉個例子, 如果一個用戶在堆中請求32字節的空間, 在LAL[5]中沒有相應的塊, 并且FreeList[5]也是空的, 那么, 空表索引就被用作在預處理列表中來查找大于40字節的塊(從FreeList[6]位圖搜索)。 (2)堆緩存?Heap Cache 正如我們討論的,所有等于或大于1024的空閑塊,都被存放在FreeList[0]中。這是一個從小到大排序的雙向鏈表。因此,如果FreeList[0]中有越來越多的塊,當每次搜索這個列表的時候,堆管理器將需要遍歷多外節點。 堆緩存可以減少對FreeList[0]多次訪問的開銷。它通過在FreeList[0]的塊中創建一個額外的索引來實現。值得注意的是,堆管理器并沒有真正移動任何空的塊到堆緩存。這些空的塊依舊保存在FreeList[0],但堆緩存保存著FreeList[0]內的一些節點的指針,把它們當作快捷方式來加快遍歷。 只有在FreeList[0]中至少同時存在32個塊或者共有256個塊必須已經被分配的時候堆緩存才會被激活。 堆緩存是一個簡單的數組,數組中的每個元素大小都是int ptr_t字節,并且包含指向NULL指針或指向FreeList[0]中的塊的指針。默認的,這個數組包含896個元素,指向的塊在1024到8192之間。這是一個可配置的大小,我們將稱它為最大緩存索引(maximum cache index) 每個元素包含一個單獨的指向FreeList[0]中第一個塊的指針,它的大小由這個元素決定。如果FreeList[0]中沒有大小與它匹配的元素,這個指針將指向NULL。 堆緩存中最后一個元素是唯一的:它不是指向特殊大小為8192的塊,而是代表所有大于或等于最大緩存索引的塊。所以,它會指向FreeList[0]中第一個大小大于最大緩存索引的塊。 堆緩存中大部分的元素是空的,所以有一個額外的索引用來加快搜索。這個索引的工作原理跟加速空閑列表的索引是一樣的。 (3)虛擬分配表 每個堆都有一個虛擬分配的閥值VirtualMemoryThreshold。這個值默認是0xfe00, 與大小508k或更高的內存塊相對應。已分配的塊保存在堆基址的一個雙向鏈表中。當需要釋放它們的時候,由后端管理器直接將它們釋放給內核 (VirtualAllocdBlocks在偏移 +0x50 和 +0x54處)。 2.2.3.?分配與釋放 堆塊在分配和釋放時,根據操作內存大小的不同,Windows 采取的策略也會有所不同。在開啟LAL的情況下,可以把內存塊按照大小分為三類: 小塊:SIZE<1KB 大塊:1KB≤SIZE 巨塊:SIZE≥512KB

分配釋放
小塊首先使用LAL分配;若LAL分配失敗,進行普通FreeLists分配;若普通FreeLists分配失敗,使用堆緩存(heap cache)分配;若堆緩存分配失敗,嘗試零號空表分配(FreeLists[0]);若零號空表分配失敗,進行內存緊縮后再嘗試分配;若仍無法分配,返回 NULL優先鏈入LAL;如果LAL滿,則將其鏈入相應的FreeLists
大塊首先使用堆緩存進行分配;若堆緩存分配失敗,使用FreeLists[0]中的大塊進行分配優先將其放入堆緩存;若堆緩存滿,將鏈入 freelists[0]
巨塊一般說來,巨塊申請非常罕見,要用到虛分配方法(實際上并不是從堆區分配的);直接釋放,沒有堆表操作
在分配過程中堆管理器首先查看前端分配器是否存在滿足條件的堆塊。如果存在將返回給調用者。否則堆管理器繼續查看后端分配器,如果找到剛好合適的堆塊,將此堆塊標記為占用狀態從空閑鏈表移除并返還給調用者,如果沒有找到,堆管理器將會將更大的堆塊分割為兩個更小的堆塊。將其中一塊標記為占用狀態并從空閑鏈表移除。另一塊則添加到新的空閑鏈表中。最初的大堆塊將從空閑鏈表中移除。 分配過程中首先檢查前端分配器能否處理該空閑塊。如果前端分配器沒有處理,則交由后端分配器。堆管理器判斷該空閑塊的左右是否存在空閑堆塊,如存在會將這些空閑堆塊合并成更大的堆塊,合并步驟如下: a:將相鄰的空閑塊從空閑鏈表移除。 b:將新的大堆快添加到空閑列表。 c:將新的大堆快設置為空閑。 3:如果不能進行合并操作,該空閑塊將被移入空閑列表。 雖然某些堆塊沒有被應用程序使用,但是在后端分配器看來這些堆塊仍然是占用狀態。這是因為所有在前端分配器中的堆塊,在后端分配器的眼里均為占用狀態。 3.?堆的數據結構 3.1.?HEAP結構 堆表結構640字節,+0x58記錄段表索引,+0x178記錄空閑鏈表索引,+0x580記錄前端管理器列表指針: 3.2.?HEAP_SEGMENT結構 段表結構40字節長,堆結構后便是第一個用戶的堆塊,FirstEntry字段用來直接指向這個堆塊。HEAP_ENTRY結構來描述每個堆塊。
3.3.?HEAP_ENTRY結構 塊首結構8字節: 調用HeapAlloc函數將返回HEAP_ENTRY之后的地址。此地址減去8Byte便可以得到_HEAP_ENTRY結構。 Flags字段的值: 4.?堆的調試 調試堆的時候不能直接使用調試器加載實驗程序,而要利用附加程序的方式進行調試,這是因為調試態堆管理策略和常態堆管理策略有很大差異,集中體現在: (1)調試堆不使用前端管理器; (2)所有堆塊都被加上了多余的 16 字節尾部用來防止溢出(防止程序溢出而不是堆溢出攻擊),這包括 8 個字節的 0xAB 和 8 個字節的 0x00; (3)塊首的標志位不同。 為了避免程序檢測出調試器而使用調試堆管理策略,我們可以在創建堆之后加入一個人工斷點:_asm int 3,然后讓程序單獨執行。當程序把堆初始化完后,斷點會中斷程序,這時再用調試器 attach 進程,就能看到真實的堆了。 我們需要將調試器設置為及時調試器,程序遇到int3中斷時會自動附加調試器。選用Windbg調試器,Windbg調試器的符號表可以很清晰的展現出堆的結構。 Windgb設置為JIT調試器的方式很簡單,只需要在命令行中執行“windbg.exe -I”命令: 設置成功后,注冊表HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\AeDebug下存在Debugger項,數值為windbg.exe信息 4.1.?FreeList調試 調試代碼: [C++]?純文本查看?復制代碼?
010203040506070809101112131415161718192021#include <windows.h>int main(){HLOCAL h1,h2,h3,h4,h5,h6;HANDLE hp;hp = HeapCreate(0,0x500,0x10000);__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 coalesesHeapFree(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]return 0;}
調試環境:XP SP3,VC6.0,Release版本 4.1.1.?使用Windbg識別堆表 運行程序,程序執行到INT3中斷,自動打開Windbg進行附加,Windbg啟動后顯示如下信息: 程序因為0x80000003異常中斷,目前執行到0x40101d的位置,寄存器eax=0x3a0000。 使用ub命令回溯匯編代碼,結合源碼得知,int3指令前執行的是HeapCreate()函數,此時eax的值就是HeapCreate()創建的堆基址。 我們也可以使用PEB遍歷堆,驗證0x3a0000內存區是不是堆區。 ①??!peb??命令獲取peb位置 ②解析peb,在peb+0x18 ProcessHeap字段記錄著進程堆的位置0x140000,+0x088 NumberOfHeaps字段用來介紹堆的總數5,+090 ProcessHeaps字段0x7c99cfc0是一個數組,用來記錄每個堆的句柄。 ③根據ProcessHeaps字段,查看有哪些堆,可以看到一共有5個堆,和前文OD中情況一致: 還有一種更簡單的方法,直接打印出所有堆,使用擴展命令:!heap -h打印出當前程序的所有堆,關于!heap命令的使用,可以使用!heap -?進行查看。 4.1.2.?分析堆表
我們使用HeapCreate (0,0x500,0x10000)創建了一個大小為0x500,但是系統卻為我們分配了0x1000的內存空間,這是因為堆段的大小最小為0x1000, 圖中+0x584的地方指向前端管理器,但是這里這個指針為 NULL,說明沒有啟用前端管理器。這是因為只有堆是可擴展的時候前端管理器才會啟用,要想啟用前端管理器,在創建堆的時候就不能使用 HeapCreate (0,0x500,0x10000)來創建堆了,而要使用 HeapCreate(0,0,0)創建一個可擴展的堆。 當一個堆剛剛被初始化時,它的堆塊狀況是非常簡單的。 (1)HEAP結構+0178的位置為FreeLists索引區,總共128對指針用來索引128條空閑雙鏈表。目前除了Freelist[0]之外,所有索引指向自身,也就是說這些空閑鏈表都為空。 (2)Freelist[0]位于堆偏移 0x0688 處(啟用前端堆管理器后這個位置將是旁氏列表/低碎片堆),這里算上堆基址就是 0x003a0688。Freelist[0]前向指針和后向指針都指向0x003a0688,說明Freelist[0]指向的是目前堆中的唯一一個塊“尾塊”。 在觀察0x3a0688這個堆塊之前,首先我們來看一下一個正在使用的堆塊的結構,圖中的Block head塊首結構實際上就是我們前文提到的HEAP_ENTRY: 空閑態堆塊和占用態堆塊的塊首結構基本一致,只是將塊首后數據區的前 8 個字節用于存放空表指針了。這 8 個字節在變回占用態時將重新分回塊身用于存放數據。 現在我們來看看0x3a0688 這個尾塊的狀態: 實際上這個堆塊開始于 0x3a0680,一般引用堆塊的指針都會躍過8字節的塊首,直接指向數據區。 Windbg顯示的數據是小端模式,尾塊目前的大小為 0x0130,計算單位是 8 個字節,也就是 0x980 字節。尾塊的前向指針和后向指針都指向0x3a0178,這與我們前面的分析一致。 4.1.3.?堆的分配 現在我們繼續調試,在Windbg中使用p命令單步執行,執行6此堆申請的操作。 第一次申請3字節的內存,系統將0x3a0680~0x3a0690的空間分配出去,FreeList[0]指向變為0x3a0698(尾塊3a0690)。雖然申請了3個字節,但實際分配了0x16字節的內存空間,解析尾塊的塊首結構,前一個堆塊的確是了0x16個字節。0x3a0680的標志位也被修改為了0x01,busy狀態。 接下來繼續執行5次內存申請的操作,分別申請5、6、8、19、24字節的內存,共041字節,追蹤內存分配的情況: 由此總結出下表:
堆句柄請求字節實際分配字節基址
H13160x30680
H25160x30690
H36160x306a0
H48160x306b0
H519320x306c0
H624320x306e0

雖然申請的內存總數是0x41字節,但實際分配的情況存在“找零錢”現象,一共分配了0x80字節,使得尾塊的大小由 0x130 被削減為 0x120。堆塊的大小實際包括了塊首在內,即如果請求 32 字節,實際會分配的堆塊為 40 字節:8 字節塊首+32 字節塊身。堆塊的單位是 8 字節,不足 8 字節的部分按 8 字節分配。但是堆的指針卻直接指向數據區,而不是塊首,中間差了8字節,表中的基址一列都經過了減8處理,后文如無說明,默認做減8處理。 初始狀態下,快表和空表都為空,不存在精確分配。請求將使用“次優塊”進行分配。 這個“次優塊”就是位于偏移 0x0688 處的尾塊。 由于次優分配的發生,分配函數會陸續從尾塊中切走一些小塊,并修改尾塊塊首中的 size 信息,最后把 freelist[0]指向新的尾塊位置。 4.1.4.?堆塊的釋放 前文的操作,我們創建了6個堆塊,他們在內存中是連續存放的,接下來我們釋放堆,直觀了解堆塊釋放的過程。 首先執行HeapFree(hp,0,h1),釋放第一個堆塊,執行之后的效果如下圖所示,h1(堆塊0x3a0680)被釋放后,HEAP_ENTRY的標志位由忙碌態修改為0x00,前向指針和后向指針均指向0x3a0188,0x3a0188實際上就是FreeList[2],查看FreeList[2],其前向指針和后向指針的確都指向0x3a0688。 我們繼續單步,釋放h3和h5,這三次釋放的堆塊在內存中不連續,所以不會發生堆塊合并的現象,此時查看內存,h1、h3、h5均是空閑塊,h1、h3都被鏈入了FreeList[2],其后向鏈表關系:FreeList[2]→h1→h3→FreeList[2],h5被鏈入了FreeList[4]: 可以看到空表在鏈入新的堆塊時,將其鏈入鏈表尾部。 4.1.5.?堆塊的合并 繼續單步,釋放h4。h3、h4、h5這3個空閑塊彼此相鄰,這時會發送堆塊合并操作。 堆塊合并是主動的,我們可以看到h3、h4 的大小都是 16 字節,h5 是32 字節,合并后的新塊為64字節(8個堆單位),將被鏈入 freelist[8]。但是h4在釋放時,數據沒有反生變化,而是h3塊首的堆塊大小標志修改為8,h6塊首前一個堆塊大小標志修改為8,同時h3前/后向指向 freelist[8]。也就是說,在發生堆合并時,h4并不會被鏈入空表,事實上h4并沒有執行釋放的操作,h4直接被并入了空閑堆塊h3中,可以說h4在一無所知的情況下直接被抹殺了。 空表索引區的 freelist[2],原來標識的空表中有兩個空閑塊 h1和 h3,而現在只剩下h1,因為h3在合并時被摘下了; freelist[4]原來有一個空閑塊h5,現在被改為指向自身,因為h5在合并時被摘下了。 4.2.?LAL調試 前文我們介紹了Freelist中堆塊的申請與釋放過程,現在我們再來看看LAL中堆塊的申請與釋放過程。使用下列代碼進行分析: [C++]?純文本查看?復制代碼?
01020304050607080910111213141516171819#include <stdio.h>#include <windows.h>void main(){HLOCAL h1,h2,h3,h4,h5;HANDLE hp;hp = HeapCreate(0,0,0);__asm int 3h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);HeapFree(hp,0,h1);HeapFree(hp,0,h2);HeapFree(hp,0,h3);HeapFree(hp,0,h4);h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,16);HeapFree(hp,0,h5);}
運行程序并成功附加后,我們首先識別堆表,程序附加后eax=0x3a0000,我們創建的堆就在這里,堆區大小是0x3000,而使用HeapCreate(0,0x500,0x10000)創建的不可擴展堆大小只有0x1000。 解析0x3a0000的堆表結構,現在FrontEndHeap的值不再是空,而是指向了0x3a0688,在前文中FrontEndHeap的值為空,0x3a0688是“尾塊”,現在這個位置被LAL列表霸占了;FreeLists現在依然指向“尾塊”,但尾塊是0x3a1e90 接下來,我們看一下0x3a0688處的LAL表到底長啥樣,可以看到堆剛初始化后快表是空的,每個元素大小是0x30: (這個圖和《0day》書中的不一樣,數組的劃分有區別) 首先我們執行4次內存申請的操作,這個時候是從FreeLists中分配內存: 再一次驗證了我們前文的分析,我們可以總結出下表:
堆句柄請求字節實際分配字節基址
H18160x31e88
H28160x31e98
H316240x31ea8
H424320x31ec0
4.2.1.?堆塊釋放 繼續單步,釋放h1,查看內存,FreeLists列表沒有發生變化,說明h1釋放后沒有并入空閑鏈表,LAL列表發生變化,h1并入了LAL,準確的說是鏈入了Lookaside[2] 此時查看h1,發現其狀態位仍然是0x01(Busy),指針為NULL,說明他是數組里唯一的元素。h1在釋放的過程中,堆塊h1里的數據沒有發生任何變化,內存里唯一變化的Lookaside[2]里多了指向h1的指針。 繼續單步,釋放h2和h3,LAL內堆塊都是忙碌態,我們不需要擔心堆塊合并的問題,這個時候查看內存的,h2被鏈入了Lookaside[2],現在鏈表關系Lookaside[2]→h2→h1,這里的鏈入方式和FreeLists的鏈入方式不同: 繼續單步,釋放h3和h4,可以看到他們分別被鏈入了Lookaside[3]和Lookaside[4]: 4.2.2.?堆的分配 本程序開始的地方已經進行了堆塊的分配,但那里進行的是FreeList的堆塊分配,現在才是快表的堆塊分配。 繼續單步,執行HeapAlloc(hp,HEAP_ZERO_MEMORY,16)申請一個堆塊,分配到的的堆塊為0x3a1eb0-0x8 分配的h5實際上,就是剛剛被鏈入Lookaside的h4,h4從Lookaside卸下變為h5。 再次釋放h5,h5又被鏈入Lookaside: 4.2.3.?LAL和FreeLists的堆塊使用機制比較 [C++]?純文本查看?復制代碼?
0102030405060708091011121314151617181920212223#include <stdio.h>#include <windows.h>void main(){HLOCAL? h1,h2,h3,h4,h5,h6,h7;HANDLE hp;hp = HeapCreate(0,0,0);__asm int 3h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);HeapFree(hp,0,h1);HeapFree(hp,0,h3);HeapFree(hp,0,h5);HeapFree(hp,0,h2);HeapFree(hp,0,h4);HeapFree(hp,0,h6);h7 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);HeapFree(hp,0,h7);}
程序初始狀態,LooKaside為空,程序從FreeLists中申請堆塊,在Windbg中單步調試,申請6次堆塊,查看內存,內存分配成功,此時Lookaside為空。 繼續單步,這次釋放這6個堆塊,釋放順序h1、h3、h5、h2、h4、h6,查看內存,可以看到Lookaside[2]→h2→h5→h3→h1,FreeLists[2]→h4→FreeLists[2],FreeLists[0]→h6→FreeLists[0],h6發生了堆塊合并: 可以看到,LAL鏈入新的堆塊時,鏈入列表的頭部,而在分配堆塊,也是優先將列表頭部的結點彈出。而FreeList則是優先鏈入/彈出鏈表尾部的結點。
本章最后說一點,本章沒有介紹LFH,因為在XP SP3上使用LFH的情況很少,從Windows Vista開始,LFH才會默認啟用。在之后的Win 7堆研究文章里,我們再詳細研究LFH。 5.?堆的安全研究
5.1.?堆Cookie _HEAP_ENTRY結構中的SmallTagIndex 字段記錄堆的Cookie信息,大小只有1字節。當一個塊通過RtlHeapFree()被釋放的時候,這個值被檢查,但當這個塊被分配的時候并不會被檢查。堆溢出發生時,cookie數據會被破壞。但是,XP SP3中只是引入了cookie了,并沒有對cookie的值進行檢測,在XP之后的系統中這個機制才開始使用。因此在XP SP3中我們仍然可以進行堆溢出利用。 5.2.?堆溢出利用 XP上的堆漏洞利用主要堆溢出利用,但是隨著之后版本的操作系統開始應用堆Cookie機制,堆溢出利用越來越來少,越來越多的堆利用使用堆風水和堆構造的技術。本章只是簡單分析兩個堆溢出利用,和大家分享一下堆溢出的知識,在之后的文章中,筆者會再分析其他類型的漏漏洞利用。 來看兩個攻擊實例: 5.2.1.?覆蓋CommitRoutine 調試代碼: [C++]?純文本查看?復制代碼?
010203040506070809101112131415161718192021#include <stdio.h>#include <windows.h>int main(){HLOCAL h1,h2,h3,h4;HANDLE hp;//alloc a heaphp = HeapCreate(0,0x1000,0);h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);HeapFree(hp,0,h1);HeapFree(hp,0,h2);memcpy(h1,"AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD\x78\x05\x3a\x00",36);//0x3a0578,0x3a0000是新建堆的基址,此時覆蓋h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);//當h2從lookaside表中移掉//這時候,再分配時,將會從0x3a0578的地址開始分配h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);//然后,我們覆蓋CommitRoutine的值memcpy((char *)h4+4,"AAAA",4);//之后如果我們繼續申請大內存,會觸發CommitRoutine這個函數指針,而由于這個指針我們可控,所以可以導致執行任意代碼。HeapDestroy(hp);return 0;}
開始調試: 初始狀態 釋放h1、h2之后,Lookaside[4]→h2→h1; 執行memcpy(h1,"AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD\x78\x05\x3a\x00",36); h2被覆蓋為0x3a0578(0x3a0578+4= CommitRoutine),Lookaside[4]→h2→0x3a0578→....這個時候實際上塊首也被破壞了 然后申請堆塊h3,h4,將h2分配給h3,將0x3a0578分配給h4,這個時候就可以實現0x3a0578地址處的任意讀寫了。如圖,執行memcpy((char *)h4+4,"AAAA",4)修改CommitRoutine的值 之后如果我們繼續申請大內存,會觸發CommitRoutine這個函數指針,而由于這個指針我們可控,所以可以導致執行任意代碼。 5.2.2.?覆蓋虛函數表 調試代碼: [C++]?純文本查看?復制代碼?
0102030405060708091011121314151617181920212223242526272829303132333435363738#include <stdio.h>#include <windows.h>class test {//定義一個類結構public:test(){memcpy(m_test,"1111111111222222",16);};virtual void testfunc(){//等下我們要覆蓋的虛函數printf("aaaa\n");}char m_test[16];};int main(){HLOCAL hp;HLOCAL h1,h2,h3;hp = HeapCreate(0,0x1000,0);//新創建一個堆塊__asm int 3;//申請一樣大小的三塊,申請24.h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);//將第一塊內充滿shellcode,memcpy(h1,"AAAABBBBCCCCDDDDEEEEFFFFGGGG",24);test *testt = new test();test *tp;memcpy(h3,testt,24);//將創建的類的結構拷貝到第三個堆塊中去//釋放后將它們都會自動添加到lookaside鏈表中去。H3->h2->h1HeapFree(hp,0,h1);HeapFree(hp,0,h2);HeapFree(hp,0,h3);//添加完后,其虛函數的地址被修改為h1的地址//下面調用其虛函數。tp = (test *)h3;tp->testfunc();//此時執行的是0000AAAABBBB這些填充的shellcodedelete testt;HeapDestroy(hp);return 0;}
本段堆溢出,覆蓋了虛函數表指針,需要了解C++類的內存結構,我們在日后的文章里再分析C++類的內存結構(有興趣可以看《0day》相關章節)。 程序首先開辟了一個堆區,并申請了3個堆塊h1(0x3a1e90),h2(0x3a1eb0),h3(0x3a1ed0),之后向h1寫入0x24字節的數據,將創建的類的結構拷貝到h3中去: 其中0x3a1ed0起始處的4個字節存儲的是虛函數表的指針(虛函數表存儲的是虛函數的指針):0x4060b0。 之后釋放這三個堆塊,Lookaside[4]->h3->h2->h1 可以看到這個時候0x3a1ed0起始處的4個字節存儲虛函數的指針變為h1:0x3a1eb0。 接下來調用類的虛函數testfunc(),實際調用的是0x3a1e90(0x3a1eb0指向0x3a1e90),實現了任意代碼的執行。
6.?小結 本文分析了XP SP3的堆,對堆管理器的使用、堆漏洞的利用都進行了介紹。XP系統已經有了十幾年的歷史,本文敘述的許多知識不免過時,但也正是因為XP有著這么多年的歷史,各路大神對其的研究也已經很詳細,我們這些新人就能沿著前人走過的路,迅速掌握堆的知識,掌握堆分析研究的技巧。當然,本文只是拋磚引玉,更復雜堆的就在不遠處等著我們呢。

參考資料: 《BHUSA09-Practical Windows Heap Exploitation》 《軟件調試》 《0day安全》

總結

以上是生活随笔為你收集整理的XP SP3堆研究的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。