驗證Windows內(nèi)核漏洞的框架
Pwn2Own大賽之后,HP的0day防御計劃(Zero Day Initiative, ZDI) 并不要求研究者給我們提供相關(guān)攻擊利用。ZDI的分析師對每個提交的案例進行評估,可能也會挑選一些進行全面的攻擊利用。
在內(nèi)核級的漏洞(操作系統(tǒng)本身或者硬件驅(qū)動)中,有一項就是我們常說的’任意寫(write-what-where)’[1]。 為便于分析, 很有必要寫個基本的框架,對給定的’任意寫(write-what-where)’漏洞進行封裝,并驗證其對操作系統(tǒng)的利用。
要把任意的寫操作轉(zhuǎn)換為攻擊利用,需要三個基本的步驟, 我們將來逐一介紹。
禁用Windows訪問權(quán)限檢查的負載
Windows訪問控制系統(tǒng)的核心是nt!SeAccessCheck函數(shù)。它決定著我們是否有權(quán)訪問操作系統(tǒng)中的對象(文件,進程等等)。
我們將采用的手法,于1992年由Greg Hoglund[2]首次提及, 2006年由John Heasman改進使用[3]。 這里將采用后者作為我們的起點。

關(guān)鍵點在于高亮標注的內(nèi)容。 如果是出于對用戶態(tài)進程的檢查,那么操作系統(tǒng)就會檢查確認是否具有正確的權(quán)限。 但如果是內(nèi)核態(tài)的,就會永遠成功。 那么我們的目標,就是對Windows內(nèi)核進行動態(tài)補丁,使其根據(jù)設(shè)置的AccessMode認為該訪問檢查調(diào)用是針對內(nèi)核的。
在Windows XP上,這是非常簡單的。 如果我們在IDA里檢查內(nèi)核(這里,我們處理的是ntkrnlpa.exe), 在SeAccessCheck實現(xiàn)中發(fā)現(xiàn)如下的代碼:
PAGE:005107BC xor ebx, ebx
PAGE:005107BE cmp [ebp+AccessMode], bl
PAGE:005107C1 jnz short loc_5107EC
由于在wdm.h中KernelMode被定義為0,我們只需將比較語句之后的條件跳轉(zhuǎn)Nop掉,這樣所有的訪問權(quán)限檢查就能順利通過了。
在XP之后的版本中,情況變得有點復(fù)雜了。Nt!SeAccessCheck函數(shù)調(diào)用了nt!SeAccessCheckWithHint, 后者正是我們要處理的。在后面我們收集用于攻擊的信息時,就會看到這如何使得問題復(fù)雜化了。在Windows8.1中,會看到?jīng)]有跳轉(zhuǎn)到內(nèi)核函數(shù),而是這樣的分支:
.text:00494613 loc_494613:
.text:00494613 cmp [ebp+AccessMode], al
.text:00494616 jz loc_494B28
僅需將條件跳轉(zhuǎn)替換為無條件跳轉(zhuǎn),即可使得對SeAccessCheck的調(diào)用好像來自內(nèi)核一樣。
現(xiàn)在目標清楚了,但還有一個小問題要解決。 我們要改寫的內(nèi)存地址在只讀頁中。 我們可以更改頁的屬性設(shè)置,但有個更簡單的解決方法。在x86和x64處理器上,在控制寄存器0 (Control Register 0)上有個標志決定著管態(tài)下的代碼(即Ring 0代碼,我們的攻擊利用代碼)是否在乎內(nèi)存的只讀狀態(tài)。 引用Barnaby Jack[4]的話:
禁用CR0中的寫保護位。
執(zhí)行代碼和內(nèi)存改寫。
再恢復(fù)寫保護位。
到這里,我們實際的攻擊利用核心代碼如下所示:

我們對寫保護位的處理還有一個問題。我們要將攻擊利用和處理器進行關(guān)聯(lián),確保我們始終處于設(shè)定的處理器的核心上。 我們對寫保護位的處理似乎是非必須的, 但在更復(fù)雜的,要求我們禁用SMEP(稍后介紹)的攻擊利用中,這著實是個事。 無論哪種方式,都可以的,我們要做的,就是一個簡單的調(diào)用:
1SetProcessAffinityMask(GetCurrentProcess(), (DWORD_PTR)1);
此時,你會注意到在攻擊代碼中,我們實際上并不知道具體的補丁信息。該攻擊代碼僅是獲取我們提供的信息,然后應(yīng)用它。我們將在實際的漏洞研究中提供這些信息。#p#
攻擊:將控制轉(zhuǎn)給攻擊代碼
我們有了讓代碼在Ring 0運行的條件了。 現(xiàn)在需要兩件事來使其運行:關(guān)于操作系統(tǒng)配置的信息,以及當處理器運行在管態(tài)時如何將控制權(quán)轉(zhuǎn)移給我們的代碼。
由于最初是想處理’任意寫(write-what-where)’問題, 這里我們將改寫HalDispatchTable中的一個函數(shù)。 該方法,在2007年由Ruben Santamarta[5]提及, 允許我們將執(zhí)行流轉(zhuǎn)向我們的攻擊利用代碼。
我們將攔截處理的是hal!HalQuerySystemInformation函數(shù)。該函數(shù)由未文檔化的NtQueryIntervalProfile[5][6]函數(shù)調(diào)用,而且調(diào)用函數(shù)也不常用。 要理解它為啥很重要,我們就需要詳細的談下windows中的內(nèi)存布局。
Windows將內(nèi)存劃分為兩個大區(qū)間:內(nèi)核態(tài)的內(nèi)存空間在MmUserProbeAddress之上, 其下是用戶態(tài)的內(nèi)存空間。 內(nèi)核態(tài)的內(nèi)存是所有進程共用的(雖然進程代碼無法訪問這些空間),而用戶態(tài)的內(nèi)存空間是因進程而異的。由于我們的攻擊利用代碼要運行在用戶進程空間中,而我們卻在掛鉤一個內(nèi)核函數(shù)指針, 如果其他進程調(diào)用了NtQueryIntervalProfile,很可能就會導(dǎo)致操作系統(tǒng)的崩潰。 因此,我們攻擊利用的第一步就是恢復(fù)原始的函數(shù)指針。

在我們之前的代碼里,你可以看到我們依賴額外的信息來確定函數(shù)指針入口所在,以及原始值。
現(xiàn)在,我們實際的攻擊利用觸發(fā)函數(shù)如下所示:

為了靈活性,我們的WriteWhatWhere()函數(shù)原型也包括了地址的原始值。最后一步就是找到利用點和攻擊利用掛鉤的地址。#p#
研究:確定操作系統(tǒng)的配置
這里,我們假設(shè)進行本地提權(quán)。我們可以在系統(tǒng)上以某用戶權(quán)限執(zhí)行任意代碼, 最終的目標是取得對系統(tǒng)的完全控制權(quán)。 在對內(nèi)核漏洞的遠程攻擊中,確定操作系統(tǒng)的配置會更復(fù)雜。
已經(jīng)確定需要知道如下的信息:
1) nt!HalDispatchTable的地址
2) hal!HaliQuerySystemInformation的地址
3) nt!SeAccessCheck 或者相關(guān)函數(shù)中要打補丁的代碼的地址
4) 要補丁的值
此外,我們也需要查找原始的值,以便我們恢復(fù)原始函數(shù)功能,進行不同的利用。畢竟,一旦我們完成了所需做的,干嘛還把門敞著呢?
我們需要知道兩個內(nèi)核模塊的基址——硬件抽象層(HAL)和NT內(nèi)核自身。為了獲取這些,我們還需要一個未文檔化的函數(shù)——NtQuerySystemInformation[6][7]。 因為我們需要知道兩個函數(shù),我們將提前創(chuàng)建函數(shù)原型,并從NTDLL中直接加載它們:

下一步就是確定所使用模塊的版本信息,以及它們在內(nèi)存中的實際位置。利用NtQuerySystemInformation獲取加載的模塊,然后遍歷查詢所需的模塊名稱。

下一步,在大多數(shù)情況是很壞的主意,但在這里不是。使用LoadLibraryEx已經(jīng)過時的特性, 加載我們找到的模塊的拷貝(duplicate copies)。

設(shè)置了這個標志, 我們就不會加載任何引用的模塊, 不執(zhí)行任何的代碼, 但仍然可以使用GetProcAddress()來搜尋模塊。這正是我們想要的, 因為我們要把這些加載的模塊當作我們的源碼,來查詢在實際運行的內(nèi)核代碼中所需的。
此時,我們已經(jīng)具備了查找偏移所需的一切。有了拷貝的內(nèi)核模塊的基址,系統(tǒng)的實際基址, 所以我們可以將拷貝中的相對虛擬地址(RVA)轉(zhuǎn)換為實際的系統(tǒng)地址。同時我們又擁有對拷貝代碼的讀取權(quán),所以可以掃描代碼查詢所需的函數(shù)。 所剩的最后一件事,就是一個windows調(diào)用,用GetVersionEx()來決定運行的windows的版本。
有些很容易, 因為地址已經(jīng)導(dǎo)出了:

但我們所需的大部分,還是要通過搜索獲取。要搜索的兩個函數(shù),其中hal!HaliQuerySystemInformation沒有導(dǎo)出符號, 另一個是nt!SeAccessCheck或者它直接調(diào)用的函數(shù)。
再看最后一個例子,來看看是如何處理導(dǎo)出函數(shù)和那些完全私有函數(shù)的。首先是nt!SeAccessCheck:

然后看一下將要進行補丁處理的nt!SeAccessCheckWithHint:

這里,兩個函數(shù)看上去是相鄰的, 但我們還是要使用公開的函數(shù)來跟蹤對這些內(nèi)部函數(shù)的引用,然后據(jù)此確定我們的補丁位置。 代碼如下所示:

這里的PatternScan函數(shù)是一個簡單的輔助處理,根據(jù)給定的指針、掃描的大小范圍,要掃描的模式及其大小,來查找模式匹配的起始點(如果沒有找到就是NULL)。
在上面的代碼中,首先搜索到nt!SeAccessCheckWithHint的相對跳轉(zhuǎn),并獲取到偏移。
將其用于計算我們的拷貝模塊中nt!SeAccessCheckWithHint的實際起始位置,然后掃描查找我們要替換的條件分支的模式部分。 一旦定位成功, 就可以確定它的實際地址——首先將其轉(zhuǎn)換為相對虛擬地址(RVA),然后根據(jù)實際加載的內(nèi)核映像進行重定位。最后,相應(yīng)的替換值還是依賴于具體的操作系統(tǒng)版本的。這里對JZ(0x0f 0×84)的替換是NOP(0×90)和JMP(0xe9)。
通過從拷貝的系統(tǒng)模塊中收集所需信息,對多個不同版本的Windows操作系統(tǒng)的處理都可以放在同一個框架下進行。 通過在目標函數(shù)中搜索有關(guān)模式, 只要不是我們要查找的函數(shù)發(fā)生了變化,其他的我們都可以有效的應(yīng)對。#p#
最后的麻煩
目前我們所做的一切都可以正常運行,直到Windows 8, 更確切的說,是NT6.2內(nèi)核。為方便起見,我們將實際的攻擊利用代碼運行在用戶態(tài)中。
在Ivy-Bridge架構(gòu)中,Intel引入了一種叫做管態(tài)執(zhí)行保護(Supervisor Mode Execute Protection, SMEP)[9]的功能。如果開啟了此項功能,當處理器在管態(tài)模式下,我們試圖執(zhí)行用戶態(tài)地址空間中的指令時,處理器就會出錯。這樣將控制權(quán)從內(nèi)核中攔截的函數(shù)指針轉(zhuǎn)換給我們的代碼時,就會發(fā)生異常。Windows在Windows8/Server 2012中支持SMEP,而且在支持的處理器中是默認開啟的。要解決這個問題,我們要么把攻擊利用代碼移到內(nèi)核空間中(這在NT6.2中也比較難), 要么就是禁用SMEP[11][12]。
最后存在的問題出現(xiàn)在Windows 8.1中。 要獲取Windows 8.1的真正版本號, 需要額外的處理,根據(jù)MSDN[13]:

包含了這個,就能正確的檢測Windows 8.1了,在確定偏移時適當?shù)恼{(diào)整一下我們的搜索參數(shù)即可。
結(jié)束語
當然了,這里有缺少的部分。利用框架來驗證攻擊利用是很有幫助的,我們還需要一個‘任意寫(write-what-where)’的案例來對對此進行驗證。
我們內(nèi)部就是在使用該框架在驗證那些漏洞,如果你有啥新的發(fā)現(xiàn),可以通過http://www.zerodayinitiative.com/提交給我們(有沒有攻擊負載都可以)。期待你的消息。
原文:http://h30499.www3.hp.com/t5/HP-Security-Research-Blog/Verifying-Windows-Kernel-Vulnerabilities/ba-p/6252649#.UyZi2T-Sy0f
注解
[1] 我對此能找到的最早使用是在Gerardo Richarte的論文“About Exploits Writing(GCON 1, 2002)”他將其劃分為“將任意的內(nèi)容寫到某處”(write-anything-somewhere)和“將任意的內(nèi)容寫到任意地方”(write-anything-anywhere)。這里,我們的“任意寫”(write-what-where)是指“將任意內(nèi)容寫到任意地方”(write-anything-anywhere)。
[2] Greg Hoglund, “A *REAL* NT Rootkit, patching the NT Kernel” (Phrack 55, 1999)
[3] John Heasman, “Implementing and Detecting an ACPI BIOS Rootkit” (Black Hat Europe, 2006)
[4] Barnaby Jack, “Remote Windows Kernel Exploitation – Step In To the Ring 0” (Black Hat USA, 2005) [White Paper]
[5] Ruben Santamarta, “Exploiting Common Flaws in Drivers” (2007)
[6] Windows NT/2000 Natvie API Reference (Gary Nebbett, 2000) 雖然沒有囊括較新的Windows 操作系統(tǒng)版本,但它對內(nèi)部Windows API函數(shù)和結(jié)構(gòu)仍然是一份很棒的參考
[7] Alex Ionescu, “I Got 99 Problems But a Kernel Pointer Ain’t One” (RECon, 2013)
[8] Raymond Chen, “LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES) is fundamentally flawed” (The Old New Thing)
[9] Varghese George, Tom Piazza, and Hong Jiang, “Intel Next Generation Microarchitecture Codename Ivy Bridge” (IDF, 2011)
[10] Ken Johnson and Matt Miller, “Exploit Mitigation Improvements in Windows 8” (Black Hat USA, 2012)
[11] Artem Shishkin, “Intel SMEP overview and partial bypass on Windows 8” (Positive Research Center)
[12] Artem Shisken and Ilya Smit, “Bypassing Intel SMEP on Windows 8 x64 using Return-oriented Programming” (Positive Research Center)
[13] MSDN, “Operating system version changes in Windows 8.1 and Windows Server 2012 R2”
參考讀物
Enrico Perla and Massimiliano Oldani, A Guide to Kernel Exploitation: Attacking the Core, (Syngress, 2010)
bugcheck and skape, “Kernel-mode Payloads on Windows”, (Uninformed Volume 3, 2006)
skape and Skywing, “A Catalog of Windows Local Kernel-mode Backdoor Techniques”, (Uninformed Volume 8, 2007)
mxatone, “Analyzing local privilege escalations in win32k”, (Uninformed Volume 10, 2008)