- 寫在前面
本文為本科期間想不開報名的科技創(chuàng)新論文,由于重視程度不夠以及時間分配不合理,最終趕在deadline前完成。希望這篇能夠給在這方面存在疑惑的人提供一些參考和幫助,但同時由于本人研究深度不夠等因素,導(dǎo)致本文中存在或多或少與實(shí)際情況不符的結(jié)論,希望大家提高甄別能力。
摘要
隨著時代的進(jìn)步和科技的發(fā)展,在計算機(jī)的操作性能都急速發(fā)展的今天,人們對個人計算機(jī)的需求逐漸從可操作性向安全性進(jìn)行轉(zhuǎn)變。尤其是近年來個人計算機(jī)內(nèi)存漏洞的頻頻披露,嚴(yán)重危害到用戶的個人信息安全的事件頻發(fā)更是加速了思想轉(zhuǎn)變的進(jìn)程。Windows操作系統(tǒng)作為個人電腦的主流操作系統(tǒng),肩負(fù)著對用戶個人信息負(fù)責(zé)的重任,Windows操作系統(tǒng)的安全性直接關(guān)乎著絕大多數(shù)人的個人信息安全。
Windows操作系統(tǒng)內(nèi)存管理中的棧由于操作單一,已經(jīng)被研究得很透徹,也被防御地很透徹,很難再掀起波瀾。而堆則擁有著相對更為復(fù)雜的管理機(jī)制以及操作方式,擁有著無數(shù)耐人尋味的排列組合。
本文將通過對Windows操作系統(tǒng)堆的歷史沿革、底層算法、實(shí)現(xiàn)原理進(jìn)行探尋,總結(jié)歸納出各階段Windows操作系統(tǒng)的堆管理機(jī)制具體實(shí)現(xiàn)及典型堆漏洞的產(chǎn)生原理及利用方法。旨在通過歸納和總結(jié)加深對Windows操作系統(tǒng)底層的了解,以及為對Windows操作系統(tǒng)堆有強(qiáng)烈興趣的安全愛好者們提供一定的幫助。
關(guān)鍵詞:Windows、操作系統(tǒng);安全性;堆管理機(jī)制;堆漏洞
ABSTRACT
With the advancement of the times and the development of science and technology, as the operational performance of computers has rapidly developed, the demand for personal computers has gradually changed from operability to security. In particular, in recent years, the frequent disclosure of personal computer memory vulnerabilities has seriously jeopardized the user's personal information security incidents, which has accelerated the process of ideological transformation. As the mainstream operating system of personal computers, the Windows operating system shoulders the responsibility of being responsible for the personal information of users. The security of the Windows operating system is directly related to the personal information security of most people.
The stack in the memory management of the Windows operating system has been thoroughly researched due to its single operation, and it has been thoroughly defended. It is difficult to make waves again. The heap has a relatively more complex management mechanism and operation mode, and has a myriad and intriguing arrangement.
This paper will explore the history of the Windows operating system heap, the underlying algorithm, and the implementation principle, and summarize the implementation of the heap management mechanism and the generation and utilization of the typical heap vulnerability in each stage of the Windows operating system. It aims to deepen the understanding of the underlying Windows operating system by summarizing and summarizing, and to provide some help to security enthusiasts who have a strong interest in the Windows operating system heap.
Keywords:Windows operating system;Safety;Heap management mechani-sm;Heap vulnerability
一、研究背景
近年來,作為操作系統(tǒng)主流的Windows系統(tǒng)漏洞層出不窮,嚴(yán)重威脅到了計算機(jī)使用者的信息安全。其中,緩沖區(qū)溢出漏洞作為老牌漏洞發(fā)揮著不可忽視的作用。本著對漏洞成因的好奇,筆者開始了對Windows堆緩沖區(qū)的探索。Windows內(nèi)存中,堆是最為神秘、迷人甚至有些耐人尋味的地方,同時堆也是Windows內(nèi)存中較為混亂的區(qū)域。由于微軟對Windows操作系統(tǒng)中的堆管理細(xì)節(jié)并未完全公開,所以一切探索都只能靠OllyDbg、WinDbg等調(diào)試工具,及各個前輩們探索的資料的指引才能緩緩前行。
Windows操作系統(tǒng)經(jīng)過了很多年的發(fā)展,其中堆管理機(jī)制也發(fā)生了巨大的變化,目前的堆管理機(jī)制考慮到了Windows操作系統(tǒng)內(nèi)存有效利用、分配決策效率、安全性、健壯性等各種因素,在帶來各種性能上優(yōu)化的同時,這也使得Windows操作系統(tǒng)的堆管理機(jī)制變得異常復(fù)雜。本文選取了比較有代表性的Win32平臺的堆管理機(jī)制,研究Windows操作系統(tǒng)堆管理機(jī)制的發(fā)展。經(jīng)過研究,我們可以將Windows下堆管理機(jī)制的發(fā)展分為三個階段,
Windows 2000 – Windows XP SP1:這時的堆管理系統(tǒng)比較原始,其完全不考慮堆內(nèi)存的安全性等問題,將全部精力放在任務(wù)分配和提高性能的方面。此時的堆的安全問題比較嚴(yán)重,比較容易被攻擊。
Windows XP SP2 – Windows 2003:在吸取了上一階段的經(jīng)驗(yàn)后,在這一階段,Windows將堆管理分為了前端堆管理器和后端堆管理器。同時也加入了許多安全保護(hù)措施,比如,堆塊的首部格式被改變并且加入了安全驗(yàn)證機(jī)制,即Cookie機(jī)制,當(dāng)雙向鏈表節(jié)點(diǎn)在觸發(fā)刪除操作時,系統(tǒng)會對堆塊的指針進(jìn)行驗(yàn)證。這些安全保護(hù)措施使得針對堆的攻擊變得非常困難,但是攻擊者仍能通過一些高級的攻擊手段在軟件開發(fā)人員編碼不規(guī)范的情況下對堆溢出實(shí)施成功利用。
Windows Vista – Windows 7:在經(jīng)歷了長時間的發(fā)展后,改革了前端堆管理機(jī)制,引入了新的堆管理機(jī)制以及堆塊結(jié)構(gòu)。使得在該階段中,不論在分配效率還是在安全防護(hù)上,都有了里程碑式的飛躍。
下面本文將就這三個階段堆的環(huán)境準(zhǔn)備、重要結(jié)構(gòu)、分配機(jī)制、保護(hù)機(jī)制以及常見漏洞的成因和利用方法做出詳細(xì)講解說明。
二、Windows 2000 – Windows XP SP1
2.1 環(huán)境準(zhǔn)備
32位Windows 2000 SP4虛擬機(jī)、OllyDbg、WinDbg。
2.2 重要結(jié)構(gòu)
在該階段,整個堆空間主要由4個結(jié)構(gòu)來維護(hù),分別是段表(segment list)、虛表(Virtual Allocation list)、空表(freelist)和快表(lookaside)。其中,與空表伴生的還有兩個數(shù)據(jù)結(jié)構(gòu),分別是空表位圖(Freelist Bitmap)和堆緩存(Heap Cache),這兩個數(shù)據(jù)結(jié)構(gòu)的引入減少了在分配時對空表的遍歷次數(shù),加快了分配速度。
2.2.1 堆塊基本結(jié)構(gòu)
該階段中占用狀態(tài)的堆塊結(jié)構(gòu)如圖1所示。

該階段中空閑狀態(tài)的堆塊結(jié)構(gòu)如圖2所示。

2.2.2 空表
在堆的分配過程中,我們主要關(guān)心管理空閑堆塊的空表與快表的分配規(guī)則??毡砉灿?28個雙向鏈表,每一條雙向鏈表為一條空表,除第0號、1號空表外,從第2號空表到127號空表分別維護(hù)著從16字節(jié)(含堆頭)開始到1016字節(jié)(含堆頭)每8字節(jié)遞增的空表,即(空表號*8字節(jié))大小。由于空閑狀態(tài)的堆頭信息占8字節(jié),因此1號空表始終不會有堆塊鏈入。進(jìn)入空表的堆塊遵從先進(jìn)后出(FILO)的規(guī)律。而0號空表則維護(hù)著按大小升序排列的,所有大于1016字節(jié)的小塊和大塊(<512KB)??毡斫Y(jié)構(gòu)如圖3所示。

2.2.3 空表位圖
空表位圖大小為128bit,每一bit都對應(yīng)著相應(yīng)一條空表。若該對應(yīng)的空表中沒有鏈入任何空閑堆塊,則對應(yīng)的空表位圖中的bit就為0,反之為1。在從對應(yīng)大小空表分配內(nèi)存失敗后,系統(tǒng)將嘗試從空表位圖中查找滿足分配大小且存在空閑堆塊的最近的空表,從而加速了對空表的遍歷。
2.2.4 堆緩存
堆緩存是一個包含有896個指針的數(shù)組,數(shù)組中的指針為NULL指向0號空表中1024-8192字節(jié)的空閑堆塊。數(shù)組中的每個元素都對應(yīng)著0號空表中大小為(1K+8字節(jié)*其索引號)的空閑堆塊,若0號空表中存在與其大小匹配的空閑堆塊,則堆緩存數(shù)組中對應(yīng)的元素為指向該空閑堆塊的指針,若無,則對應(yīng)元素為NULL。堆緩存數(shù)組中的最后一個元素較為特殊,該元素并不會僅指向大小為8192字節(jié)的空閑堆塊,而是指向0號空表中第一個大于等于8192字節(jié)的空閑堆塊。為加快對堆緩存的遍歷,又引入了堆緩存位圖對堆緩存中的非空指針進(jìn)行了標(biāo)記,其作用機(jī)理與上文中的空表位圖相同,在此不做過多贅述。在利用空表位圖從非0號空表中分配內(nèi)存失敗后,系統(tǒng)將嘗試通過堆緩存位圖索引到堆緩存數(shù)組查找滿足分配大小的0號空表中的空閑堆塊。
2.2.5 快表
快表是與Linux系統(tǒng)中Fastbin相似的存在,是為加速系統(tǒng)對小塊的分配而存在的一個數(shù)據(jù)結(jié)構(gòu)??毂砉灿?28條單向鏈表,每一條單鏈表為一條快表,除第0號、1號快表外,從第2號快表到127號快表分別維護(hù)著從16字節(jié)(含堆頭)開始到1016字節(jié)(含堆頭)每8字節(jié)遞增的快表,即(快表號*8字節(jié))大小。由于空閑狀態(tài)的堆頭信息占8字節(jié),因此0號和1號快表始終不會有堆塊鏈入。每條快表最多有4個結(jié)點(diǎn),進(jìn)入快表的堆塊遵從先進(jìn)后出(FILO)的規(guī)律。為提升小堆塊的分配速度,在快表中的空閑堆塊不會進(jìn)行合并操作??毂硭饕齾^(qū)結(jié)構(gòu)如圖4所示。

2.3 堆塊操作
在內(nèi)存中,堆塊按大小分為3種,分別為小塊(<1KB)、大塊(<512KB)和巨塊(≥512KB),堆塊間主要存在3中操作方式,分別是堆塊的分配、堆塊的釋放、堆塊的合并。
2.3.1 堆塊分配
堆塊在進(jìn)行分配時,主要會從上文提到的快表和空表中進(jìn)行分配。
從快表進(jìn)行堆塊分配時,首先會通過用戶申請堆塊大小索引到維護(hù)對應(yīng)大小的快表,將最后鏈入表中的空閑堆塊從表中卸下,分配給用戶使用,并將快表頭指向后項(xiàng)空閑堆塊。
從空表進(jìn)行堆塊分配時,首先會找到維護(hù)對應(yīng)大小的空表,將最后鏈入表中的空閑堆塊從表中卸下,分配給用戶使用,并將空表頭的后項(xiàng)指針指向被卸下的堆塊的后項(xiàng)堆塊。若對應(yīng)大小的空表內(nèi)分配失敗,則會尋找次優(yōu)項(xiàng),在下一個空表中進(jìn)行分配,直到尋找到能夠滿足內(nèi)存分配的最小內(nèi)存的空閑堆塊。當(dāng)在空表中尋找次優(yōu)項(xiàng)成功時,會進(jìn)行切割分配,即從找到的較大堆塊中切割下申請大小的堆塊分配給程序使用,并將切割剩余的部分按大小加上堆頭鏈入對應(yīng)的空表。若將所有除0號空表外的所有空表都遍歷完仍然沒有分配成功,則判斷0號空表中的最后一個堆塊大小是否大于所需分配內(nèi)存大小,若大于則從0號空表中正向查找滿足分配大小的最小堆塊進(jìn)行分配。
在用戶申請分配某一大小的內(nèi)存空間時,系統(tǒng)會首先判斷申請的堆塊是否屬于巨塊范疇,若是巨塊,則采用虛分配,在漏洞利用中遇到較少,本文不予討論。若申請大塊,則首先考慮堆緩存進(jìn)行分配,若分配不成功,則從0號空表中尋找最合適的空閑塊進(jìn)行分配。若申請小塊,則首先查看對應(yīng)大小的快表中有沒有空閑的堆塊,若無則查看對應(yīng)大小的空表中有沒有空閑的堆塊,若無則通過空表位圖查找更大的空表中有沒有空閑的堆塊進(jìn)行切割分配,若無則采用堆緩存進(jìn)行分配,若分配失敗,則從0號空表中尋找最適合的空閑快進(jìn)行分配,若依然失敗,則會先進(jìn)行內(nèi)存緊縮后再嘗試分配。堆塊分配流程如圖5所示。

2.3.2 堆塊釋放
堆塊釋放,即將堆塊從占用狀態(tài)更改為空閑狀態(tài)。在準(zhǔn)備釋放某一大小的內(nèi)存空間時,首先會判斷釋放釋放的堆塊是否屬于巨塊范疇,若是巨塊,則直接將該空間釋放,不會進(jìn)入任何堆表。若是大塊,則嘗試將其釋放入堆緩存,若堆緩存已滿,則鏈入0號空表。若是小塊,則首先嘗試鏈入對應(yīng)大小的快表,若鏈入快表,為了加快堆塊的分配,系統(tǒng)不會更改其占用狀態(tài)。若對應(yīng)大小的快表中已經(jīng)鏈滿了4個空閑堆塊,則將該堆塊鏈入對應(yīng)大小的空表中。
2.3.3 堆塊合并
在進(jìn)行堆塊釋放時,若釋放堆塊直接進(jìn)入空表(鏈接在快表中的空閑堆塊不會進(jìn)行合并操作),并且與該堆塊物理地址相鄰的堆塊同為空閑態(tài),則會進(jìn)行堆塊的合并。在進(jìn)行堆塊合并時,會將堆塊從空表中卸下,將兩個相鄰的內(nèi)存空間整合后更新新空閑堆塊的堆頭信息,并根據(jù)新空閑堆塊的大小鏈入相應(yīng)大小的空表中。除了堆塊的釋放會觸發(fā)堆塊合并外,在申請堆塊時,若未成功從快表、堆緩存及空表中分配空間,則會觸發(fā)內(nèi)存緊縮。內(nèi)存緊縮會將堆空間中的所有空閑堆塊,無論地址是否連續(xù),都整合成一個大的空閑堆塊再進(jìn)行堆塊分配。
2.4 保護(hù)機(jī)制
微軟對于Windows系統(tǒng)的內(nèi)存保護(hù)機(jī)制是從Windows XP SP2版本才開始有明顯建樹的,在Windows 2000 – Windows XP SP1版本這一階段,微軟僅考慮了操作系統(tǒng)的性能和功能完整性,并沒有過多考慮安全性因素,也正是由于這點(diǎn),導(dǎo)致在該階段系統(tǒng)中存在的漏洞極易被利用。
2.5 漏洞利用
如上文所說,該階段為Windows系統(tǒng)原生階段,只考慮了系統(tǒng)的性能和功能完整性,并沒有過多的考慮安全性因素。因此在該階段的堆漏洞的利用方法是最多樣、最自由也是最穩(wěn)定的,如DWORD SHOOT、Heap Spray等。接下來將詳細(xì)介紹在該階段操作系統(tǒng)中比較經(jīng)典和常見的漏洞的產(chǎn)生原因以及利用方式。
2.5.1 DWORD SHOOT
2.5.1.1 漏洞成因
該漏洞產(chǎn)生的主要原因是空表在將堆塊進(jìn)行Unlink操作時,未對堆塊前項(xiàng)指針和后項(xiàng)指針的合法性進(jìn)行安全檢測,在對其賦值時產(chǎn)生的漏洞,Unlink算法偽代碼如圖6所示。

由于可以達(dá)到對任意地址寫4字節(jié)數(shù)據(jù)的效果,因此被命名為DWORD SHOOT。也是為了防止攻擊者對該漏洞的利用,Windows在下一階段的版本中更新了Safe Unlink機(jī)制,對將要Unlink堆塊的前項(xiàng)指針及后項(xiàng)指針的合法性進(jìn)行安全檢測。
2.5.1.2 利用方式
在堆溢出的基礎(chǔ)上,修改相鄰堆塊堆頭中的前項(xiàng)指針和后項(xiàng)指針,之后在對被修改后的堆塊進(jìn)行Unlink操作時,由于不會檢測前項(xiàng)指針及后項(xiàng)指針的合法性,按照Unlink算法的邏輯會將Flink的數(shù)據(jù)寫到Blink指向地址字節(jié)的位置,即可實(shí)現(xiàn)任意地址寫4字節(jié)可控數(shù)據(jù)的操作。在得到任意地址寫4字節(jié)的機(jī)會后,可以有各式各樣的利用方式,比如將敏感函數(shù)的地址寫到另一個函數(shù)的跳轉(zhuǎn)地址或虛表上,再引導(dǎo)程序流去觸發(fā)該跳轉(zhuǎn)達(dá)到利用目的,當(dāng)然該敏感函數(shù)的地址也可以為提前布置好的shellcode起始地址。接下來舉一個較為常見的利用DWORD SHOOT漏洞的方法。
該方法通過篡改P.E.B中的函數(shù)指針為shellcode起始地址實(shí)現(xiàn)惡意代碼的執(zhí)行。P.E.B結(jié)構(gòu)中存放的RtlEnterCriticalSection()和RtlLeaveCriticalSection()函數(shù)指針是一個比較理想的攻擊地址。在程序正常退出時會調(diào)用ExitProcess(),為了同步線程該函數(shù)又會調(diào)用RtlEnterCriticalSection()及RtlLeaceCriticalSection()進(jìn)行處理。除此之外,在此階段的Windows系統(tǒng)中P.E.B結(jié)構(gòu)擁有著固定的地址為0x7FFDF000,向下偏移0x20位RtlEnterCriticalSection()的函數(shù)指針,即地址為0x7FFDF020,緊接著0x7FFDF024的地址存放著RtlLeaveCriticalSection()的函數(shù)指針。由于以上原因,導(dǎo)致在該階段的操作系統(tǒng)中,P.E.B結(jié)構(gòu)成了DWORD SHOOT等任意地址寫漏洞利用方法的絕佳狙擊點(diǎn)。為防止攻擊者有機(jī)可乘,Windows在下一階段的版本中更新了P.E.B Random機(jī)制,將P.E.B結(jié)構(gòu)的地址進(jìn)行了隨機(jī)化。
了解該漏洞利用方式的原理后,我們將堆塊的后項(xiàng)指針篡改為0x7FFDF020,將前項(xiàng)指針篡改為提前布置好的shellcode的起始地址,在將該堆塊從空表中申請回來時觸發(fā)Unlink操作就完成了漏洞的利用,導(dǎo)致shellcode中代碼的執(zhí)行。
除了狙擊P.E.B結(jié)構(gòu)外,該漏洞還常常攻擊Windows異常處理機(jī)制中的S.E.H結(jié)構(gòu)、V.E.H結(jié)構(gòu)、U.E.F結(jié)構(gòu)等,由于上述結(jié)構(gòu)在內(nèi)存中都有固定地址,利用方法與剛剛提到的P.E.B結(jié)構(gòu)相同,因此不再一一贅述。
2.5.2 Heap Spray
2.5.2.1 漏洞成因
Heap Spray,又稱堆噴,與典型能夠?qū)嵤┚珳?zhǔn)攻擊的堆漏洞不同,堆噴是一種比較暴力且相對不穩(wěn)定的攻擊手法,并且該手法常常被用來針對瀏覽器。其產(chǎn)生的原因主要是應(yīng)用程序在堆分配空間時沒有過多的約束,使得攻擊者能夠多次申請堆塊占據(jù)大部分內(nèi)存,再通過地毯式的覆蓋,最終劫持程序控制流導(dǎo)致惡意代碼被執(zhí)行。
在棧溢出的利用方式中,劫持程序控制流后往往會將EIP修改為shellcode布置的地址,而為了提高shellcode成功執(zhí)行的幾率,往往會在前方加一小段不影響shellcode執(zhí)行的滑梯指令(slide code),常用的滑梯指令有nop指令(0x90)及or al指令(0x0c0c)。而隨著操作系統(tǒng)安全性的提升,尤其是地址隨機(jī)化的誕生,使得普通的溢出漏洞難以再掀起波瀾。于是研究者們發(fā)明了堆噴這一種攻擊手法作為輔助攻擊的方式。
2.5.2.2 利用方式
該攻擊手法的前提條件為已經(jīng)可以修改EIP寄存器的值為0x0c0c0c0c。每次申請1M的內(nèi)存空間,利用多個0x0c指令與shellcode相結(jié)合用來填充該空間,一般來說shellcode只占幾十字節(jié),相對的滑梯指令占了接近1M,導(dǎo)致滑梯指令的大小遠(yuǎn)遠(yuǎn)大于shellcode大小。通過多次申請1M的空間來將進(jìn)程空間中的0x0c0c0c0c地址覆蓋。因?yàn)橛羞h(yuǎn)大于shellcode的滑梯指令的存在,該地址上的值有99%以上的幾率被覆蓋為0x0c0c0c0c,從而執(zhí)行到shellcode。由于堆分配是從低地址向高地址分配,因此一般申請200M(0x0c800000)的堆塊就能夠覆蓋到0x0c0c0c0c的地址。
該利用方式中之所以不采用0x90作為滑梯指令,主要是因?yàn)閮?nèi)存空間中存放了許多對象的虛函數(shù)指針,當(dāng)將這些虛函數(shù)指針覆蓋到0x90909090后,在調(diào)用該函數(shù)就會導(dǎo)致程序崩潰,該階段操作系統(tǒng)分配給用戶使用的內(nèi)存為前2G,即0x00000000 - 0x7FFFFFFF,其中進(jìn)程僅能訪問0x00010000 – 0x7FFEFFFF,從0x80000000 – 0xffffffff的后2G內(nèi)存被設(shè)計來只有內(nèi)核能夠訪問。而覆蓋為0x0c0c0c0c時,0x0c0c0c0c地址有很大幾率已經(jīng)被我們用滑梯指令所覆蓋,從而直接執(zhí)行shellcode。因此,若虛函數(shù)指針被覆蓋為0x90909090為內(nèi)核空間,不能被進(jìn)程所訪問,采用0x0c作為滑梯指令一舉兩得。
該利用方式由于會很暴力地申請多次內(nèi)存,并將構(gòu)造好的大量滑梯指令及小部分的shellcode像井噴一樣“噴”滿內(nèi)存各處,因此又被很形象地命名為“堆噴”。
三、 Windows XP SP2 – Windows 2003
3.1 環(huán)境準(zhǔn)備
32位Windows XP SP3虛擬機(jī)、OllyDbg、WinDbg。
3.2 重要結(jié)構(gòu)
在該階段,堆塊的數(shù)據(jù)結(jié)構(gòu)基本繼承于Windows 2000 – Windows XP SP1階段的數(shù)據(jù)結(jié)構(gòu)。但由于增加了一些保護(hù)機(jī)制,導(dǎo)致了堆塊的堆頭的基本結(jié)構(gòu)與原始結(jié)構(gòu)有所差別。
該階段中占用狀態(tài)的堆塊結(jié)構(gòu)如圖7所示。

該階段下空閑狀態(tài)的堆塊結(jié)構(gòu)如圖8所示。

3.3 堆塊操作
在該階段,堆的分配被劃分為前端堆管理器(Front-End Manager)和后端堆管理器(Back-End Manager),其中前端堆管理器主要由上文中提到的快表有關(guān)的分配機(jī)制構(gòu)成,后端堆管理器則是由空表有關(guān)的分配機(jī)制構(gòu)成。除前、后端堆管理器以外的堆塊分配、釋放、合并等操作基本繼承于Windows 2000 – Windows XP SP1階段的堆塊操作。
3.4 保護(hù)機(jī)制
從該階段開始,微軟漸漸開始重視Windows操作系統(tǒng)的安全性,逐步在內(nèi)存中加入了許多安全保護(hù)機(jī)制,如GS、Safe S.E.H、DEP、ASLR及部分堆保護(hù)機(jī)制等。本部分將就該階段中Windows系統(tǒng)中新增加的堆保護(hù)機(jī)制做出部分說明。
3.4.1 Heap Cookie
Heap Cookie從Windows XP SP2版本開始使用,為上文提到的改變了Windows堆塊結(jié)構(gòu)的保護(hù)機(jī)制,該機(jī)制將堆頭信息中原1字節(jié)的段索引(Segment Index)的位置新替換成了security cookie用來校驗(yàn)是否發(fā)生了堆溢出,相應(yīng)的原1字節(jié)的標(biāo)簽索引(Tag Index)的位置替換為段索引位置,取消掉了標(biāo)簽索引。
該機(jī)制是在堆塊分配時在堆頭中隨機(jī)生成1字節(jié)的cookie用于保護(hù)其之后的標(biāo)志位(Flags)、未使用大小(Unused bytes)、段索引及前項(xiàng)堆塊指針(Flink)、后項(xiàng)堆塊指針(Blink)等敏感數(shù)據(jù)不被堆溢出所篡改。并在堆塊被釋放時檢查堆頭中的cookie是否被篡改,若被篡改則調(diào)用RtlpHeapReportCorruption()結(jié)束進(jìn)程。值得一提的是,此函數(shù)在HeapEnableTerminateOnCorrupton字段被設(shè)置后才會起到結(jié)束進(jìn)程的效果,而在該階段的版本中該字段默認(rèn)不啟用,因此該函數(shù)并沒有起到結(jié)束進(jìn)程的作用。
3.4.2 Safe Unlink
Safe Unlink保護(hù)機(jī)制在前一階段版本中的Unlink算法前加上了安全檢查機(jī)制。該機(jī)制在堆塊從堆表中進(jìn)行拆卸的操作時,對堆頭前項(xiàng)指針和后項(xiàng)指針的合法性進(jìn)行了檢查,解決了之前版本中可通過篡改堆頭的前項(xiàng)指針和后項(xiàng)指針輕易執(zhí)行惡意代碼的安全隱患。Safe Unlink算法偽代碼如圖9所示。

3.4.3 PEB Random
P.E.B結(jié)構(gòu)(Process Envirorment Block Structure)中包含了進(jìn)程的信息。該機(jī)制將在老版本W(wǎng)indows中固定為0x7FFDF000的P.E.B結(jié)構(gòu)地址進(jìn)行了隨機(jī)化,解決了之前版本中能輕易對固定地址的P.E.B結(jié)構(gòu)中函數(shù)指針進(jìn)行非法操作,從而執(zhí)行惡意代碼的安全隱患。
3.5 漏洞利用
如上文所說,該階段的版本只是在前一階段的基礎(chǔ)上加入了一些保護(hù)機(jī)制,盡管這些保護(hù)機(jī)制的引入杜絕了前一版本中的一些常見漏洞,如空表堆塊拆卸時的DWORD SHOOT,和一些通用的攻擊手法,如狙擊P.E.B結(jié)構(gòu)中的函數(shù)指針。在常規(guī)空表利用條件日漸苛刻的環(huán)境下,安全研究人員將目光轉(zhuǎn)向了快表、0號空表、堆緩存及空表位圖的利用。
3.5.1 Bypass Safe Unlink
3.5.1.1 漏洞成因
雖然在加入了Safe Unlink條件后,極大的限制了DWORD SHOOT攻擊的使用場景,但隨著研究人員對Safe Unlink檢測機(jī)制的研究,仍然構(gòu)造出了一種十分苛刻的場景達(dá)到去繞過Safe Unlink檢測機(jī)制,觸發(fā)漏洞最終導(dǎo)致任意地址寫。
3.5.1.2 利用方式
按照上文Safe Unlink保護(hù)機(jī)制所述,在unlink一個堆塊時,會檢查該堆塊后項(xiàng)堆塊的Flink字段和該堆塊前項(xiàng)堆塊的Blink字段是否都指向該堆塊,根據(jù)堆塊指針和前項(xiàng)后項(xiàng)指針的偏移為0和4字節(jié),可以將判斷條件簡化為如圖10所示偽代碼。

當(dāng)需要unlink的堆塊為該空表上的唯一一個堆塊,此時會存在一個特殊情況:堆塊的Flink字段等于Blink字段等于空表頭結(jié)點(diǎn),空表頭結(jié)點(diǎn)的Flink字段也等于Blink字段等于堆塊地址,如圖11所示。

若能夠通過堆溢出漏洞將該堆塊的Flink字段修改為Freelist[x-1].Blink,將Blink字段修改為Freelist[x].Blink,此時仍然可以通過Unlink之前的安全檢測,如圖12所示。

并且此時繞過安全檢測后執(zhí)行Unlink操作的結(jié)果如圖13所示。

在下次申請該大小的堆塊時,按照算法會將Freelist[x].Blink指向的堆塊分配給用戶使用,而在之前構(gòu)造好的條件下會將Freelist[x-1].Blink及下方的空間當(dāng)成堆塊分配給用戶,并且該堆塊的用戶區(qū)指針為Freelist[x].Blink。此時我們第一次對指針進(jìn)行寫時,會從Freelist[x-1].Blink往下寫,很容易將Freelist[x].Blink覆蓋為任意地址,第二次寫時即可往任意地址寫任意數(shù)據(jù)。
3.5.2 LookAside Attack
3.5.2.1 漏洞成因
該漏洞的產(chǎn)生是由于快表在分配堆塊時,未檢測其Flink字段指向地址的合法性,會造成在按照快表分配算法執(zhí)行時,會將非法地址作為堆頭分配給用戶,最終導(dǎo)致任意地址寫任意長度數(shù)據(jù)的漏洞。
3.5.2.2 利用方式
在堆溢出的基礎(chǔ)上,使與可溢出堆塊相鄰的下一個堆塊鏈入空表,再利用堆溢出將鏈入空表堆塊的前項(xiàng)指針修改為函數(shù)跳轉(zhuǎn)地址或虛表地址。構(gòu)造好堆塊后,在接下來快表第一次分配相應(yīng)大小的堆塊時會將被篡改堆頭的堆塊分配給用戶使用,并將非法Flink地址作為堆頭鏈入空表頭結(jié)點(diǎn),在快表第二次分配相應(yīng)大小的堆塊時,即可將指定地址及其后方空間作為堆塊申請給用戶使用,再對堆塊進(jìn)行賦值即可造成任意地址寫任意數(shù)據(jù)的操作。該偽造的地址一般可以為敏感函數(shù)、虛表地址等以及上文所提到的該版本中的堆攻擊重災(zāi)區(qū):P.E.B結(jié)構(gòu)及異常處理機(jī)制中的各種結(jié)構(gòu)。整個過程如圖14所示。

3.5.3 Bitmap XOR Attack
3.5.3.1 漏洞成因
該漏洞產(chǎn)生的原因?yàn)樵诟驴毡砦粓D狀態(tài)時,以當(dāng)前堆塊的Size字段作為索引,且在之前未有適當(dāng)?shù)陌踩珯z測機(jī)制,可能會導(dǎo)致空表位圖狀態(tài)與實(shí)際空表狀態(tài)不同步的效果,最終通過利用漏洞會達(dá)到任意地址寫任意數(shù)據(jù)的效果??毡砦粓D更新算法的偽代碼如圖15所示。

3.5.3.2 利用方式
如上文空表位圖更新算法所示,在每次空表中的堆塊進(jìn)行Unlink操作后會判斷相應(yīng)的空表位圖是否需要更新,若Unlink的堆塊為該空表中的最后一個堆塊,則會對堆塊當(dāng)前Size字段對應(yīng)的空表位圖做異或操作。在基于堆溢出的場景中,該算法中存在多處漏洞。
首先構(gòu)造只存在一個堆塊的空表且與該堆塊相鄰前一堆塊存在堆溢出的場景。如圖16所示。

若此時上方堆塊只存在單字節(jié)溢出漏洞,即僅能覆蓋到空表中堆塊的Size字段。在對空表中堆塊進(jìn)行Unlink操作前,先將其Size字段篡改為8*n,如圖17所示。

按照空表位圖更新算法,該堆塊會正常進(jìn)行Unlink操作,并且會執(zhí)行更新空表位圖的代碼。但是由于Size字段已被覆蓋,導(dǎo)致在索引空表位圖時不再是Bitmap[x]而是Bitmap[n],然后對索引到的空表位圖做異或操作,即Bitmap[x]不改變,Bitmap[n]進(jìn)行反轉(zhuǎn)。如圖18所示。

此時x號空表中沒有堆塊,表頭的前項(xiàng)指針和后項(xiàng)指針都指向自身,并且對應(yīng)的空表位圖置位為1,即堆管理器認(rèn)為x號空表中仍有空閑堆塊。在下一次申請8x大小的堆塊時,則會將Freelist[x].Blink指向的地址作為堆塊分配給用戶使用,即將8x大小的空表表頭當(dāng)做堆塊,在用戶進(jìn)行編輯后的第二次申請編輯時即可造成任意地址寫任意數(shù)據(jù)。
以上討論了堆溢出僅能覆蓋堆塊Size字段時的場景,在當(dāng)堆溢出能夠覆蓋到堆塊的前項(xiàng)和后項(xiàng)指針字段時,該攻擊手法的應(yīng)用場景更加廣泛。
首先,將前項(xiàng)指針和后項(xiàng)指針覆蓋為相同值,此時按照空表位圖更新算法,在Safe Unlink的安全檢測機(jī)制處會被檢查出來,且不會對該堆塊執(zhí)行Unlink操作,而是調(diào)用了RtlpHeapReportCorruption()。如上文所提到的,在現(xiàn)階段的版本中該函數(shù)不會導(dǎo)致進(jìn)程結(jié)束。因此,prev和next并未被新賦值,仍然為覆蓋后相等的狀態(tài),因此會被判斷為需要更新空表位圖,并且此時的Size字段也是在堆溢出的覆蓋范圍內(nèi),之后的操作與第一種場景中相同,此處不再贅述。
在構(gòu)造的第二種場景中,由于跳過了Unlink的賦值,prev和next始終相等,一定會更新空表位圖。因此不需要滿足被溢出堆塊為其空表中的唯一一個堆塊的條件,所以應(yīng)用場景更加廣泛
3.5.4 Freelist[0] Linking Attack
3.5.4.1 漏洞成因
在引入Safe Unlink機(jī)制使得Unlink操作變得困難后,研究人員們將目光投向了Unlink的逆過程Link。很快他們就發(fā)現(xiàn)了Link操作尚未添加保護(hù)機(jī)制檢測堆塊前項(xiàng)指針和后向指針的合法性,并在對指針進(jìn)行賦值操作時能產(chǎn)生和DWORD SHOOT效果相似的漏洞。但是相較于DWORD SHOOT存在一定的局限性,該漏洞最終只能達(dá)到任意地址寫4字節(jié)堆塊地址的效果。Link算法偽代碼如圖19所示,其中ChunkA為將要鏈入0號空表的堆塊,ChunkB為本就在0號空表中的堆塊。

3.5.4.2 利用方式
首先構(gòu)造一個在Freelist[0]中并且與該堆塊相鄰的前一堆塊存在至少0x10字節(jié)溢出的場景,并且該堆塊的大小應(yīng)該大于其后項(xiàng)堆塊大小的兩倍,本場景中為0x550>0x220*2。如圖20所示。

在從0號空表中申請堆塊前將空表中堆塊的堆頭溢出,覆蓋其前項(xiàng)后項(xiàng)指針,在該攻擊方式中Size字段甚至可以不變。如圖21所示。

此時,申請一個大于其后項(xiàng)堆塊小于自身,且與自身的差大于申請堆塊大小的堆塊,本場景中為0x250(0x550>0x250>0x220且0x550-0x250>0x250)。按照0號空表的分配算法,首先會對0x550堆塊進(jìn)行Unlink操作,由于前項(xiàng)后項(xiàng)指針被篡改后不通過Safe Unlink的檢查機(jī)制,因此不會執(zhí)行Unlink而直接將該堆塊切分為0x250堆塊和0x300堆塊。其中的0x250堆塊分配給用戶使用,0x300堆塊被插入到0號空表中的合適位置。根據(jù)算法,該合適位置將會是0x250堆塊之后。如圖22所示。

該堆塊在合適位置進(jìn)行Link操作前,需滿足相對Fake_Flink和Fake_Blink的Size字段大于插入堆大小的條件,本場景中為[Fake_Flink-8]>0x300和[Fake_Blink-0xc]>0x300。除此之外,還需滿足并且Fake_Flink及Fake_Blink+4的地址可寫的條件。
在滿足上述各項(xiàng)條件后,0x300堆塊插入0號空表合適位置時,按照Link算法將會觸發(fā)漏洞的效果如圖23所示。

由于Fake_Flink及Fake_Blink都是通過堆溢出可控的字段,因此觸發(fā)該漏洞達(dá)到了任意地址寫堆塊地址的效果。雖然該攻擊手段利用條件十分苛刻且單靠其很難劫持控制流,但可同時配合其他漏洞達(dá)成更具有攻擊性的目的。
3.5.5 Freelist[0] Searching Attack
3.5.5.1 漏洞成因
該漏洞的產(chǎn)生是由于0號空表在進(jìn)行遍歷搜索合適堆塊時,未對鏈表中堆塊前項(xiàng)指針的合法性進(jìn)行校驗(yàn),導(dǎo)致在遍歷時跳出0號空表,最終通過利用漏洞達(dá)到任意地址寫任意數(shù)據(jù)的效果。
3.5.5.2 利用方式
首先構(gòu)造一個在Freelist[0]中并且與該堆塊相鄰的前一堆塊存在至少0xc字節(jié)溢出的場景,并且該堆塊不能為0號空表中的最大塊。如圖24所示。

在遍歷0號空表中前將空表中堆塊的堆頭溢出,覆蓋其前項(xiàng)指針,在該攻擊方式中Size字段甚至可以不變。如圖25所示。

此時,申請一個大于該堆塊且小于0號空表中最大堆塊大小的堆塊。按照0號空表的搜索算法,在遍歷過被溢出堆塊后,會將偽造的Fake_Flink作為下一個堆塊的入口地址,比較其Size字段是否滿足申請空間的大小。如圖26所示。

為了使堆管理器將該Fake_Flink地址作為堆塊入口分配給用戶使用,需要滿足[Fake_Flink-8]的Size字段大于申請大小,并且為了不產(chǎn)生堆切割及其后續(xù)繁瑣操作,應(yīng)該控制該Size字段在申請堆塊大小+8字節(jié)之內(nèi),即RequstSize≤Size≤RequestSize+8。在Size字段條件滿足后,該偽造堆塊會進(jìn)行Unlink操作,雖然會毫無懸念地被Safe Unlink機(jī)制檢測出來,但仍然會被分配給用戶使用。由于會進(jìn)行Safe Unlink檢測,因此該堆塊的Flink及Bilnk,即Fake_Flink和Fake_Flink+4應(yīng)該是可讀的。
由于Fake_Flink為堆溢出所偽造,因此只需要攻擊者構(gòu)造滿足上述條件的Fake_Flink,即可達(dá)到任意地址寫任意數(shù)據(jù)的效果。
四、 Windows Vista – Windows 7
4.1 環(huán)境準(zhǔn)備
32位Windows 7 SP1虛擬機(jī)、OllyDbg、WinDbg。
4.2 重要結(jié)構(gòu)
從Windows Vista版本開始,Windows系統(tǒng)舍棄了前版本中的以快表為核心的前端堆管理器,而引入了一套稱為低碎片堆(Low Fragmentation Heap)的全新的數(shù)據(jù)結(jié)構(gòu)和算法作為前端堆管理器,后端堆管理器為了適配新的前端堆管理器的在管理機(jī)制也與前版本的后端管理器有部分差異。
從該版本開始,由前端堆管理器分配給用戶的堆塊的結(jié)構(gòu)改變成了UserBlocks,與之前版本中的Lookaside相類似。有后端堆管理器分配給用戶的堆塊結(jié)構(gòu)改變成了ListHints,與之前版本中的Freelist相類似。除此之外,管理堆的HeapBase中有個FreeLists成員容易與之前版本中的Freelist相混淆,該成員鏈接了該HeapBase所管理的所有空閑堆的指針。值得一提的是,在下文中將用到新的單位block,1block=8byte。
4.2.1 UserBlocks
該結(jié)構(gòu)體位于HeapBase(_HEAP)->FrontEndHeap(_LFH_HEAP)->LocalData(_HEAP_LOCAL_DATA)->SegmentInfo(_HEAP_LOCAL_SEGMENT_INFO)->ActiveSubsegment/Hint(_HEAP_SUBSEGMENT)結(jié)構(gòu)體中。
由于前端堆的管理結(jié)構(gòu)較為復(fù)雜,本文挑選其中重要結(jié)構(gòu)體中的重要成員進(jìn)行闡述。
4.2.1.1 _LFH_HEAP
FrontEndHeap的數(shù)據(jù)結(jié)構(gòu)_LFH_HEAP如圖27所示。

_LFH_HEAP結(jié)構(gòu)體中我們主要關(guān)心LocalData字段,該字段是一個指針,保存了每個維護(hù)UserBlocks的SubSegment的信息。
4.2.1.2 _HEAP_LOCAL_DATA
LocalData的數(shù)據(jù)結(jié)構(gòu)_HEAP_LOCAL_DATA如圖28所示。

_HEAP_LOCAL_DATA結(jié)構(gòu)體中共包含了大小為128的SegmentInfo數(shù)組,該數(shù)組中的每個元素都按照_RtlpBucketBlockSizes數(shù)組中所對應(yīng)的大小(不包括堆頭大小)維護(hù)著所有小于16KB的UserBlocks。該數(shù)組如圖29所示。

例如SegmentInfo[0]維護(hù)著所有用戶區(qū)為0字節(jié)的堆塊,由于不存在,所以SegmentInfo[0]不維護(hù)堆塊,SegmentInfo[8]則維護(hù)著所有用戶區(qū)為0x40字節(jié)的堆塊。
4.2.1.3 _HEAP_LOCAL_SEGMENT_INFO
SegmentInfo的數(shù)據(jù)結(jié)構(gòu)_HEAP_LOCAL_SEGMENT_INFO如圖30所示。

Hint和ActiveSubsegment都是直接管理UserBlocks的字段,初始化為NULL,其中Hint字段僅在free了前端管理器所分配的堆塊后才會被賦值,ActiveSubsegment字段在第一次請求分配時就會被賦值,兩個字段相輔相成,為方便表述,以下以Hint字段為例進(jìn)行進(jìn)一步的說明。
LocalData字段指向管理該SegmentInfo的LocalData地址。
BucketIndex字段表示該SegmentInfo所維護(hù)的堆塊用戶區(qū)的blcok尺寸。
4.2.1.4 _HEAP_SUBSEGMENT
Hint的數(shù)據(jù)結(jié)構(gòu)_HEAP_SUBSEGMENT如圖31所示。

LocalInfo字段指向管理該Hint/ActiveSubsegment的SegmentInfo地址。
UserBlocks字段為用戶堆塊開始的頭部,緊接著UserBlocks之后就是相連的大小固定的用戶區(qū)。下面以SegmentInfo[5]->Hint.UserBlcks所維護(hù)的大小為0x30(用戶區(qū)為0x28)的堆塊為例,其在內(nèi)存空間上如圖32所示。

值得一提的是,空閑狀態(tài)堆塊用戶區(qū)的前2字節(jié)會存放下一個空閑堆的偏移,以方便在申請堆塊時及時更新下文中提到的AggregateExchg中的FreeEntryOffset字段,如圖33所示。

BlockSize字段表示該結(jié)構(gòu)體所維護(hù)堆塊(包括堆頭)的block尺寸。
BlockCount字段表示該結(jié)構(gòu)體所維護(hù)的所有堆塊的數(shù)量。
SizeIndex字段表示該結(jié)構(gòu)體所維護(hù)堆塊用戶區(qū)的block尺寸,與之前提到的BucketIndex相同,即存在以下等式:BucketIndex=SizeIndex=BlockSize-1。
AggregateExchg字段指向_INTERLOCK_SEQ結(jié)構(gòu)體,該用于在分配和釋放堆塊時索引相應(yīng)堆塊。
4.2.1.5 _INTERLOCK_SEQ
AggregateExchg的數(shù)據(jù)結(jié)構(gòu)_INTERLOCK_SEQ如圖34所示。

Depth字段記錄了在UserBlocks中的空閑堆塊個數(shù)。
FreeEntryOffset字段表示從UserBlocks頭部索引到下一個將要分配的堆塊的block尺寸,即下一個分配堆塊的地址為UserBlocks+8*FreeEntryOffset。
4.2.2 FreeLists
該結(jié)構(gòu)體位于HeapBase(_HEAP)結(jié)構(gòu)體中。
4.2.2.1 _LIST_ENTRY
FreeLists的_LIST_ENTRY結(jié)構(gòu)如圖35所示。

該結(jié)構(gòu)相對簡單,F(xiàn)link和Blink即后項(xiàng)指針和前項(xiàng)指針。FreeLists為雙向鏈表,將該HeapBase所管理的所有后端分配的空閑堆塊按照大小由小到大的順序鏈在一起。
4.2.3 ListHints
該結(jié)構(gòu)體位于HeapBase(_HEAP)->BlocksIndex(_HEAP_LIST_LOOKUP)結(jié)構(gòu)體中。
4.2.3.1 _HEAP_LIST_LOOKUP
管理ListHints的_HEAP_LIST_LOOKUP結(jié)構(gòu)體數(shù)據(jù)結(jié)構(gòu)如圖36所示。

下面針對該版本下此結(jié)構(gòu)體中的重要成員進(jìn)行講解。
若該結(jié)構(gòu)體需要擴(kuò)展,則ExtendedLookup為指向下一個_HEAP_LIST_LOOKUP的指針,若不需要擴(kuò)展,則為NULL。
ArraySize為在該結(jié)構(gòu)中ListHints可以尋址到的最大的block尺寸,在該階段的版本中ArraySize的值為0x80或0x800。例如,HeapBase->BlocksIndex中的ArraySize為0x80,若有擴(kuò)展,則擴(kuò)展后結(jié)構(gòu)中的ArraySize為0x800,即HeapBase->BlocksIndex->ExtendedLookup.ArraySize=0x800。
ItemCount的值表示該_HEAP_LIST_LOOKUP結(jié)構(gòu)中free狀態(tài)堆塊的個數(shù)。
OutOfRangeItems的值表示該_HEAP_LIST_LOOKUP結(jié)構(gòu)中超過ArraySize大小的堆塊,即接下來即將提到的ListHints[ArraySize-BaseIndex-1]鏈表中的堆塊個數(shù)。例如,該_HEAP_LIST_LOOKUP結(jié)構(gòu)有擴(kuò)展,則OutOfRangeItems為0。
BaseIndex的值表示該_HEAP_LIST_LOOKUP結(jié)構(gòu)的起始block尺寸。例如,從_HEAP結(jié)構(gòu)中的BlocksIndex索引到的_HEAP_LIST_LOOKUP結(jié)構(gòu)中的該字段為0,而從_HEAP_LIST_LOOKUP結(jié)構(gòu)中的ExtendedLookup索引到的_HEAP_LIST_LOOKUP結(jié)構(gòu)中的改字段為0x80。
ListHead與HeapBase->FreeLists指向同一個地方,鏈接了該HeapBase所管理的所有空閑堆的指針。
ListsInUseUlong為一個數(shù)組,相當(dāng)于一個ListHints 的BitMap。
ListHints也是一個_LIST_ENTRY結(jié)構(gòu)體數(shù)組,_LIST_ENTRY結(jié)構(gòu)體僅占8個字節(jié),其中有2個大小為4字節(jié)的Flink和Blink字段。ListHints數(shù)組的索引號代表著所管理堆塊的block尺寸,每個Flink指向_HEAP->FreeLists鏈上的第一個對應(yīng)大小堆塊。此處的Blink較為特殊,不會指向堆塊的地址,而是在該大小堆塊開啟了LFH分配機(jī)制后會指向索引號對應(yīng)的Buckets(_HEAP_BUCKET)+1地址;在未開啟LFH分配機(jī)制時,Blink的前2字節(jié)表示所有占用狀態(tài)該大小堆塊總數(shù)的2倍,后2字節(jié)表示申請該大小堆塊的總次數(shù)。另外,如前文中所提到的,ListHints[ArraySize-BaseIndex-1]的Flink指針會指向FreeLists鏈上第一個block尺寸大于ArraySize-1的空閑堆塊,類似于前版本中的Freelist[0]。
總的來說,ListHints的Flink起著FreeLists鏈表堆緩存的作用,ListHints的Blink則起著連接后端堆管理器和前端堆管理器的作用,因?yàn)樗鼧?biāo)志著對應(yīng)大小的堆塊是否已啟用LFH進(jìn)行分配。
4.3 堆塊操作
4.3.1 堆塊分配
堆塊在被申請時,主要會從上文提到的前端堆管理器和后端堆管理器中進(jìn)行分配。
從前端堆管理器進(jìn)行堆塊分配時,會通過用戶申請堆塊大小索引到維護(hù)對應(yīng)大小堆塊的SegmentInfo數(shù)組,并獲得SegmentInfo->Hint->AggregateExchg->OffsetAndDepth字段,在Depth非0的情況下,將UserBlocks+8*FreeEntryOffset地址的堆塊分配給用戶使用,然后將FreeEntryOffset字段更新為位于該堆塊用戶區(qū)前2字節(jié)的Offset,便于在下一次分配時進(jìn)行尋址,并將Depth字段-1。
從后端堆管理器進(jìn)行堆塊分配時,會通過用戶申請堆塊大小索引到維護(hù)對應(yīng)大小堆塊的ListHints數(shù)組,并通過Flink指針找到在FreeLists鏈表中大小相對應(yīng)的堆塊,并進(jìn)行Unlink操作將其從鏈表上卸下返回給用戶使用。若未找到對應(yīng)大小堆塊則會向后遍歷FreeLists鏈表,直到找到第一個最小滿足申請大小的堆塊進(jìn)行切割分配。若遍歷完整個鏈表仍然沒有成功分配,則會擴(kuò)展堆。該版本的后端堆管理器分配機(jī)制與前版本中有許多相似之處。
在用戶申請分配某一大小的內(nèi)存空間時,首先會判斷申請大小,若大于0xFE00blocks,即504KB,則調(diào)用VirtualAlloc()進(jìn)行分配,若大于0x800blocks,即16KB,則直接以后端堆管理器進(jìn)行分配。若小于16KB,則先以后端堆管理器對這次分配操作進(jìn)行響應(yīng),在BlocksIndex及ExtendedLookup結(jié)構(gòu)中尋找相應(yīng)大小的ListHints,在找到相對應(yīng)大小的ListHints數(shù)組時會判斷其Blink是否為奇數(shù),即Buckets+1,若是則會將該分配操作交給前端堆管理器進(jìn)行響應(yīng)。若不為奇數(shù),則判斷Blink的低2字節(jié)是否大于0x20或高2字節(jié)是否大于0x1000,即判斷在占用狀態(tài)的該大小堆塊的總數(shù)是否大于0x10或是否進(jìn)行了0x1000次該大小堆塊的申請。若判斷為真,則會設(shè)置HeapBase->CompatibilityFlags,在下次再分配同樣大小堆塊時將Blink賦值為Buckets+1,并啟用前端堆管理器響應(yīng)堆塊分配;若判斷為假,則仍然采用后端堆管理器響應(yīng)堆塊分配,并將Blink的值加0x10002。
4.3.2 堆塊釋放
在進(jìn)行堆塊釋放操作時,系統(tǒng)遵循“從哪來,回哪去”的規(guī)則。在接收到堆塊釋放的請求時,系統(tǒng)會先判斷堆的大小,所有大于504KB的堆塊都直接調(diào)用VirtualFree()進(jìn)行釋放,小于504KB大于16KB的堆塊都將鏈入FreeLists鏈表中。小于16KB的堆塊,系統(tǒng)會通過堆頭信息判斷該堆塊是從前端堆管理區(qū)進(jìn)行分配還是后端堆管理區(qū)進(jìn)行分配,若從前端分配,則將其釋放回前端堆中,并將AggregateExchg結(jié)構(gòu)中的FreeEntryOffset寫入堆塊用戶區(qū)的前2字節(jié),并用該堆塊對于UserBlocks的偏移更新FreeEntryOffset字段,再將Depth字段+1。若從后端分配,則將其鏈入FreeLists鏈表中,并更新對應(yīng)大小的ListHints中的Flink指針,再判斷該對應(yīng)大小是否已開啟LFH分配策略,若未開啟,則將Blink-0x0002。
4.3.3 堆塊合并
該階段的堆塊合并操作與前版本中幾乎相同。在釋放前端堆塊時不會觸發(fā)合并操作,在釋放后端堆塊時,若與該堆塊毗鄰的堆塊為空閑堆塊,則會進(jìn)行堆塊合并操作,合并后的堆塊會重新鏈入FreeLists的合適位置,并更新相應(yīng)大小的ListHints的對應(yīng)數(shù)值。
4.4 保護(hù)機(jī)制
該階段Windows系統(tǒng)在保護(hù)堆方面除繼承前版本的保護(hù)機(jī)制外上又引入了一些額外的保護(hù)機(jī)制,如堆基址隨機(jī)化、堆頭編碼、Safe Link等。這些保護(hù)機(jī)制的引入使得Windows操作系統(tǒng)的堆漏洞更難以被攻擊者所利用。
4.4.1 堆基址隨機(jī)化
該機(jī)制會在創(chuàng)建堆時,將HeapBase的地址隨機(jī)對齊到64KB地址,即將HeapBase隨機(jī)對齊到低4字節(jié)為0的地址。該堆基址隨機(jī)化與?;冯S機(jī)化有異曲同工之處,目的是讓每次產(chǎn)生堆的地址都不相同,使得在漏洞利用時需要首先泄露隨機(jī)化的堆基址,增大了漏洞利用的難度。
4.4.2 堆頭編碼
如上文所述,在前一階段的Windows版本中,引入了Heap Cookie這一重要保護(hù)機(jī)制。但經(jīng)過實(shí)踐的檢驗(yàn),這1字節(jié)的Heap Cookie并不能十分有效地阻止攻擊者對堆頭敏感數(shù)據(jù)的篡改:攻擊者可通過多次爆破來碰撞僅僅1字節(jié)的Heap Cookie。
為了更好的保護(hù)堆頭敏感信息不被攻擊者惡意篡改,Windows在該階段引入了堆頭編碼的保護(hù)機(jī)制。在介紹該機(jī)制前首先簡要介紹一下堆頭的_HEAP_ENTRY結(jié)構(gòu)體,如圖37所示。

堆頭編碼機(jī)制會首先確定該堆區(qū)是否已開啟堆頭編碼機(jī)制,若已開啟則將堆頭中代表Size以及Flags的前三個字節(jié)逐字節(jié)異或后賦值給SmallTagIndex,之后再與隨機(jī)生成的每個HeapBase都不同的HeapBase->Encoding進(jìn)行異或運(yùn)算得到編碼過后的堆頭。堆頭編碼算法偽代碼如圖38所示。

在解碼時,會首先判斷該堆頭是否已經(jīng)編碼,若已編碼則將堆頭與HeapBase->Encoding進(jìn)行異或運(yùn)算解碼得到真實(shí)的堆頭,從而獲得Size以及Flags字段中的數(shù)據(jù)。堆頭解碼算法偽代碼如圖39所示。

在開啟了該機(jī)制后,攻擊者將很難在不泄露任何信息的條件下修改堆頭中的Size及Flags等敏感信息。該機(jī)制較前版本中的Heap Cookie機(jī)制更有效地保護(hù)了堆頭信息。
4.4.3 Safe Link
如上文所述,前一階段的Windows版本中,引入了Safe Unlink保護(hù)機(jī)制后,攻擊者又在Link操作時發(fā)現(xiàn)了可利用的漏洞。為了完善操作鏈表時的保護(hù)機(jī)制,Windows在該階段引入了Safe Link的保護(hù)機(jī)制。該保護(hù)機(jī)制會判斷在鏈入堆塊前判斷鏈表上將要斷鏈的地方的Blink和Flink是否合法,若合法則進(jìn)行Link操作,若不合法則調(diào)用RtlpLogHeapFailure()結(jié)束進(jìn)程。Safe Link的算法偽代碼如圖40所示。

4.4.4 HeapEnableTerminateOnCorrupton
如上文所述,在前一階段版本加入的安全機(jī)制中,檢測不通過時會調(diào)用RtlpHeapReportCorruption()。但是由于HeapEnableTerminateOnCorrupton字段默認(rèn)不啟用,導(dǎo)致在檢測不通過后繼續(xù)進(jìn)程,因此導(dǎo)致了上文所述的多種利用手法的存在。在本階段的版本中,默認(rèn)啟用了HeapEnableTerminateOnCorruption字段,使得在安全機(jī)制檢測不通過時直接結(jié)束進(jìn)程,杜絕了上一階段版本中的多種攻擊手法。
4.5 漏洞利用
該階段的版本中,Windows的堆管理機(jī)制有了較大的修改,新加入了多種數(shù)據(jù)結(jié)構(gòu)以及一些關(guān)鍵性的保護(hù)機(jī)制,是Windows操作系統(tǒng)安全性發(fā)展的一個里程碑,同時也使得堆漏洞的利用難度提升到了一個新高度。
4.5.1 突破堆頭編碼
在該階段加入的眾多安全機(jī)制中,堆頭編碼機(jī)制有著關(guān)鍵性的地位。在之前介紹的多種漏洞利用方式中,幾乎都是以相鄰前一堆塊溢出作為前提。在前一階段版本中,可通過多次碰撞僅1字節(jié)的Heap Cookie,從而繞過安全機(jī)制覆蓋到堆頭的敏感信息。而在本階段版本中該機(jī)制的引入,導(dǎo)致堆頭信息皆被編碼,阻斷了對堆頭敏感信息的篡改,以及對后方前項(xiàng)、后項(xiàng)指針的覆蓋,幾乎阻絕了上文中介紹的所有攻擊方式。
但通過分析堆頭編碼的算法,如圖38所示,可以發(fā)現(xiàn)堆頭的敏感信息是通過異或進(jìn)行編碼,并且異或運(yùn)算可逆。如果我們擁有一次泄露的機(jī)會,可將已知狀態(tài)堆塊編碼后的堆頭泄露出來,并且由于我們已知堆塊狀態(tài),即前3字節(jié),通過逐字節(jié)異或可計算出第4字節(jié)的SmallTagIndex字段,再用前4字節(jié)與泄露出的編碼后堆頭相異或即可得到HeapBase->Coding的值。
雖說對堆頭編碼的突破嚴(yán)格上來說并不算是漏洞的利用,但是通過突破堆頭編碼所得到的HeapBase->Encoding字段可在利用其他漏洞時對構(gòu)造堆頭進(jìn)行編碼,從而繞過堆頭編碼的檢測。可以說突破堆頭編碼使得上文中提到的多種攻擊方式有了一線生機(jī)。
4.5.2 LFH FreeEntryOffset OverFlow
4.5.2.1 漏洞成因
如上文介紹的,在該階段版本中新引入的前端堆管理器LFH中,由其管理的每個空閑堆塊用戶區(qū)前2字節(jié)都存儲著可以用于更新FreeEntryOffset字段的Offset值。而FreeEntryOffset字段在前端堆管理器分配堆時起著極為重要的尋址作用。
在突破堆頭編碼后,由前端堆管理器管理的空閑堆塊用戶區(qū)上前2字節(jié)的Offset顯得脆弱不堪,十分容易被覆蓋,導(dǎo)致前端堆管理器分配時被劫持,極易形成漏洞。
4.5.2.2 利用方式
在介紹該漏洞利用方式之前,首先將_INTERLOCK_SEQ結(jié)構(gòu)體中關(guān)鍵字段在堆塊分配和釋放時的具體操作進(jìn)行詳細(xì)介紹。接下來以BlockSize為6,即0x30字節(jié)的UserBlock為例進(jìn)行講解。
首先在該大小UserBlock剛被初始化時,會將FreeEntryOffset字段初始化為0x2,原因是在第一個堆塊前會有0x10字節(jié)大小的_HEAP_USERDATA_HEADER結(jié)構(gòu);Depth字段會通過當(dāng)前可用內(nèi)存量(UserDataAllocSize)運(yùn)算出該大小堆塊的總個數(shù),即Depth=(UserDataAllocSize-sizeof(_HEAP_USERDATA_HEADER)/BlockSize,在本例中假設(shè)為0x2A。初始化的堆塊如圖41所示。

此時申請第一個0x28字節(jié)大小堆塊時(含堆頭共0x30字節(jié)),會通過前端堆管理器將FreeEntryOffset字段所指的堆塊分配發(fā)給用戶使用,同時將FreeEntryOffset更新為用戶區(qū)前2字節(jié)存放的用于尋址下一堆塊的Offset,并將Depth的值-1。分配后堆塊結(jié)構(gòu)如圖42所示。

同理。在第三次申請完0x28字節(jié)大小的堆塊后堆塊結(jié)構(gòu)如圖43所示。

/v:imagedata></v:shape>
此時若將第二次申請的堆塊釋放掉,則會將FreeEntryOffset當(dāng)前的值存放到該釋放堆塊的前兩字節(jié)作為Offset,并通過該堆塊與_HEAP_USERDATA_HEADER的相對block偏移更新FreeEntryOffset字段,再將Depth的值+1。釋放第二個堆塊后的堆塊結(jié)構(gòu)如圖44所示。

通過示例不難發(fā)現(xiàn),空閑堆塊前2字節(jié)Offset值在前端堆管理器分配和釋放堆塊算法中的重要性,若能夠通過堆溢出將其覆蓋為含有虛表函數(shù)指針對象的堆塊偏移,就能夠通過申請堆塊拿到該對象的使用權(quán),并修改虛表指針劫持程序控制流。
承接上例所述,構(gòu)造漏洞利用場景如下:第一個堆塊作為可由用戶控制的存在堆溢出的用戶堆塊,第三個堆塊作為含有虛表函數(shù)指針對象的占用堆塊。如圖45所示。

此時,對用戶堆塊進(jìn)行編輯,導(dǎo)致第二個空閑堆塊前2字節(jié)存放的Offset的值被用戶對快溢出所覆蓋,并修改值為0xE,即第三個對象堆塊的偏移。如圖46所示。

緊接著,申請大小為0x28的堆塊,前端堆管理器會按照算法將FreeEntryOffset更新為偽造的Offset即0xE。如圖47所示。

再次申請大小為0x28字節(jié)的堆塊時,前端堆管理器會按照算法將對象堆塊分配給用戶使用,并將FreeEntryOffset更新為Vtable_ptr的前2字節(jié)。如圖48所示。

最終,會有2指針指向第3個堆塊,一個用戶指針,一個對象指針,通過編輯用戶指針可覆蓋虛表函數(shù)指針為任意地址,最后調(diào)用對象指針執(zhí)行篡改后的虛表指針達(dá)到漏洞利用。
五、 總結(jié)與展望
本文從堆管理視圖出發(fā),將Windows7操作系統(tǒng)及之前的系統(tǒng)版本分為三個階段,分階段按照重要結(jié)構(gòu)、堆塊操作、保護(hù)機(jī)制及漏洞利用五個部分進(jìn)行了詳細(xì)的講解,并在一些較難理解的關(guān)鍵部分佐以圖片進(jìn)行輔助講解。
在第一部分的環(huán)境準(zhǔn)備中,主要對在研究當(dāng)前階段版本中堆管理所采用的環(huán)境進(jìn)行了說明;在第二部分的重要結(jié)構(gòu)中,主要對當(dāng)前階段版本中堆管理所涉及到的重要數(shù)據(jù)結(jié)構(gòu)進(jìn)行了詳細(xì)講解;在第三部分的堆塊操作中,主要對當(dāng)前階段版本中堆管理所涉及到的分配、釋放及合并等操作的算法進(jìn)行了詳細(xì)講解;在第四部分的保護(hù)機(jī)制中,主要對當(dāng)前階段版本中堆管理所涉及到的系統(tǒng)新增加的安全保護(hù)機(jī)制進(jìn)行了詳細(xì)講解;在第五部分漏洞利用中,主要對當(dāng)前階段版本中堆管理所涉及到的典型堆漏洞的產(chǎn)生原理以及利用方式進(jìn)行了詳細(xì)講解。
總的來看,本文對Windows下典型堆漏洞產(chǎn)生原理及利用方法的研究不夠深入徹底,仍存在部分盲區(qū)。如本文在操作系統(tǒng)的階段劃分中不夠全面,未覆蓋到Windows最新的Windows 8 – Windows 10這一階段;以及在漏洞利用部分的講解中,僅僅挑選了較為常見、應(yīng)用較廣泛的漏洞,對漏洞種類研究的不夠全面;而且對典型堆漏洞的闡述僅僅停留在了理論層次,缺少本地Demo復(fù)現(xiàn)以及實(shí)際漏洞分析進(jìn)行實(shí)踐佐證,導(dǎo)致對漏洞的存在和利用缺乏說服力。
由于本人的學(xué)識有限,在文中難免存在錯誤,望海涵并及時指正。雖說論文已經(jīng)結(jié)束,但學(xué)習(xí)卻永無止境,今后應(yīng)該針對上方的總結(jié)對癥下藥,完成好對Windows下典型堆漏洞產(chǎn)生原理及利用方法的進(jìn)一步研究。
參考文獻(xiàn)
[1] John Mcdonald,Chris Valasek. Practical Windows XP/2003 Heap Exploitation[EB/OL].https://www.blackhat.com/presentations/bh-usa-09/MCDONALD/BHUSA09-McDonald-WindowsHeap-PAPER.pdf,2009.
[2] Moore,Brett. Exploiting Freelist[0] on XP Service Pack 2[EB/OL].http://www.insomniasec.com/publications/Exploiting_Freelist%5B0%5D_On_XPSP2.zip,2005-12.
[3] Chris Valasek. Understanding the Low Fragmentation Heap[EB/OL].http://illmatics.com/Understanding_the_LFH_Slides.pdf,2010-07.
[4] Ben Hawkes. Attacking the Vista Heap[EB/OL].https://www.lateralsecurity.com/downloads/hawkes_ruxcon-nov-2008.pdf,2008-11.
[5] coneco. 讀后感之“Understanding the LFH”[EB/OL].https://bbs.pediy.com/thread-248443.htm,2018-12-16.
[6] Magictong. Heap Spray原理淺析[EB/OL].https://blog.csdn.net/magictong/article/details/7391397,2012-03.
[7] 王清,張東輝.0day安全:軟件漏洞分析技術(shù)(第2版)[M].電子工業(yè)出版社:北京,2011-06:144.