原文鏈接:http://bobao.360.cn/learning/detail/4066.html
本文將介紹如何利用HackSys
Team Extremely Vulnerable Driver中的釋放後重用和池溢出問題。爲此我們需要對Windows內核內存管理有所瞭解。因此,本文將涵蓋以下內容:
1. Windows內核內存分配概述
2. Windows內核池風水演練
3. 利用HackSys Team Extremely Vulnerable Driver的釋放後重用
4. 通過兩種不同的方法利用HackSys Team Extremely Vulnerable Driver的池溢出
本文專注於windows 7 sp1(32位)。
Windows內核池
瞭解內存管理的基礎知識有所幫助,如果你不曾瞭解虛擬內存和分頁,那麼有必要快速閱讀以下內容:
1. 內存程序剖析
2. 內核如何管理你的內存
Windows內核使用兩種動態大小的“池”來分配系統內存,這些內核等同於用戶模式下的堆。我只介紹理解利用方法原理所需的詳情,更多信息請查看:
1. Windows 7 內核池利用,作者:Tarjei Mandt
2. 《Windows Internals》第7版第1部分第5章或《Windows Internals》第6版第2部分第10章——內存管理
Windows中有兩種關鍵類型的池——非分頁池和分頁池。還有特殊池(我將在介紹釋放後重用利用方法時介紹)和win32k使用的會話池(本文不作介紹)。
分頁池對比非分頁池
非分頁池由保證總是存儲在物理內存中的內存組成,而分頁池中分配的內存可以被分頁。這是必需的,因爲某些內核結構需要在高於可滿足缺頁中斷的IRQL可訪問。有關IRQL的更多詳細信息以及各級別支持的操作,請參閱“管理硬件優先級”。
這意味着非分頁池用於存儲進程、線程、信號量等關鍵控制結構。而分頁池用於存儲文件映射、對象句柄等。分頁池實際上由幾個單獨的池組成,而在Windows 7中,只有一個非分頁池。
爲了分配池內存,驅動程序和內核通常使用ExAllocatePoolWithTag函數,其定義如下:
PoolType參數包含一個POOL_TYPE枚舉中的值。這定義了正在請求什麼類型的池內存,我們將主要看到其用0調用,這對應於非分頁池。
第二個參數是所需的池內存的字節數,最後的PoolTag參數是一個32位值,其被完全視爲用於標記內存用途的4個字符,這在調試時非常方便,並且也被大量內核內存instrumentation使用——跟蹤使用某個標籤進行了多少分配,當內存分配到某個標籤時中斷,等等。
爲了釋放分配的池內存,通常使用ExFreePoolWithTag函數。
這只需要一個指向有效池分配的指針,池元數據將給予所有其他所需的東西,在標準條件下,提供的池標籤將不會被驗證。但是,啓用正確的調試設置後,標籤將被驗證,如果其不匹配,則會觸發一個BSOD。現在我們來看看這些函數的工作原理。
分配內存
反編譯器 ExAllocatePoolWithTag 乍看之下很嚇人。
還好,Tarjei Mandt已經在其論文中將函數轉化爲僞代碼,這可以作爲一個很好的指導。我將使用他的僞代碼和IDA中的一些檢查等,並通過windbg來解釋函數的工作原理。他的解釋可能更好、更準確,本節中的所有代碼片段都來自其論文。
首先,函數檢查請求的字節數是否超過4080字節,如果是,則調用Big Pool 分配器。
此處,esi包含請求的字節數,如果高於0xff0,則轉到nt!ExpAllocateBigPool。否則採取true分支,處理繼續。
在這一點上,[esp+48h+var_20]持有末尾爲1的PoolType。所以如果該值等於0,則其是一個非分頁池,跳過上面的if語句並轉到隨即顯示的else,同時,如果類型是用於分頁池內存,則採取true分支。
在true分支上,其檢查池類型是否用於會話池。
其隨後立即檢查請求的字節數是否高於32。
同時,在false分支上,其還檢查分配是否高於32字節。
如果任一檢查通過,邏輯會有點麻煩,更多詳情可見Tarjei的論文。該函數將嘗試通過在相關池的Lookaside列表中找到一個條目來分配請求的塊。Lookaside列表是每個池的每處理器結構,對它的引用存儲在內核處理器控制塊中。Lookaside列表由通常請求的內存大小的單鏈表組成,對於一般池內存,這是頻繁進行的小分配。使用Lookaside列表可以使這些頻繁的分配更快地進行。對於非常頻繁進行的固定大小的分配,存在其他更具體的lookaside列表。
如果兩個大小檢查均未通過,或者從lookaside列表分配內存失敗,則分頁池描述符被鎖定,這與用於非分頁池的結構相同,並且以相同的方式使用,所以我稍後將對此進行描述。
現在我們有了請求的分配是非分頁池類型時運行的代碼,此處我們在上面的loc_518175處採取了false分支。
接下來,代碼將檢查請求的塊大小是小於還是等於32字節,如下所示。如上所述,如果分配足夠小,其將嘗試使用lookaside列表,如果成功則返回true。
如果lookaside列表不能使用或請求的塊大小大於32字節,則非分頁池描述符將被鎖定。首先將獲取非分頁池描述符的指針,如果有超過1個的非分頁池,將進行查找。
首先,將根據可用的非分頁池數量和“本地節點”(論文解釋了這一點,但出於性能原因,多核系統中的每個處理器都可以有首選本地內存)來計算ExpNonPagedPoolDescriptor表中的索引:
此處eax最終持有所選索引。然後從表中讀取引用:
這與分頁池的邏輯相同,計算索引然後獲得引用:
此時,分頁和非分頁分配的代碼路徑已達到同一點。分配器將檢查頁面描述符是否被鎖定,如果沒有鎖定則獲取鎖定。
現在描述符結構實際上包含什麼?還好,其包含在Windows 7的公共符號中。
我們剛剛看到,(Non)PagedLock字段在函數明確獲取描述符鎖定之前被檢查。PoolType是自解釋的,PoolIndex字段指示可以在內核導出的ExpPagedPoolDescriptor或ExpNonPagedPoolDescriptors表中找到哪些條目。我們真正關心的其他字段是PendingFrees 和PendingFreeDepth(在下一節中解釋),以及我們需要現在看一看的ListHeads。
ListHeads是8個字節倍數到大分配的空閒內存塊列表。每個條目包括一個LIST_ENTRY結構,其是相同大小的塊的鏈表的一部分。列表由請求的塊大小+ 8(以給POOL_HEADER留出空間,稍後描述)索引,除以8以獲得字節數。分配器將從所需的確切大小的條目開始通覽列表,查找要使用的有效塊,如果不能精確匹配,則其查找更大的條目並將其拆分。僞代碼如下:
因篇幅限制,此處我們有所刪減,不過我們可以更詳細地介紹函數實際上成功找到正確大小的內存塊時會發生什麼。分配器進行的分配是請求的數量+8字節,以給之前提到的POOL_HEADER留出空間。該結構包含在Windows 7的公共符號中,如下所示:
PreviousSize字段是內存中先前分配的大小,這是在釋放分配以檢查損壞時使用的。如前所述,PoolIndex字段可用於查找分配的POOL_DESCRIPTOR。BlockSize是包括header在內的分配的總大小,最後,PoolType是來自分配的POOL_TYPE枚舉的值,如果塊不空閒,則爲2。PoolTag是自解釋的。
最後,如果函數在已分配的內存頁中找不到分配空間,則其將調用MiAllocatePoolPages,以創建更多,並返回新內存中的地址。
如下所示:
釋放內存
這一次我只提供了一些關於Tarjei
Mandt的反轉代碼的評論,我不知道程序集片段有多大用處,希望我的補充有作用。這隻包括與漏洞利用有關的組件,所有代碼和細節請參閱原論文。
塊大小應等於下一個池對象頭中的上一個大小字段,如果不是,則內存已損壞,BugCheck被觸發。當覆蓋這個結構時,我們需要確保用正確的值覆蓋塊大小,否則會藍屏。
然後檢查分頁池類型,我跳過了會話部分。
如果啓用了延遲釋放,則查看等待列表是否有>= 32個條目,如果有,則全部釋放,並將當前條目添加到列表中。
我們只查看允許DefferedFree的系統,所以我將跳過舊的合併邏輯。ExDeferredFreePool中的邏輯相當直觀,函數定義如下。
其接收一個指向POOL_DESCRIPTOR的指針,該指針先前被ExFreePoolWithTag鎖定。然後其循環通過PendingFrees,並釋放每個條目。如果上一個或下一個條目被釋放,則其將與當前被釋放的塊合併。
Windows內核池風水
爲了執行內核池風水,我們需要在正確類型的池中分配對象,及哪些是對我們有用的大小。我們知道,關鍵的內核數據結構(如信號量)存儲在非分頁池(也因所有基於池的挑戰而被HackSys驅動程序使用)中。要開始,我們需要找出一些在非分頁池中分配的內核結構及其大小。實現此目標的簡單方法是分配一些控件對象,然後使用內核調試器來查看相應的池分配。我使用以下代碼來做到這一點。
編譯並運行此代碼得到如下輸出,然後敲擊回車鍵後,我們附帶的內核調試器應該中斷。
使用調試器,我們可以找到每個結構駐留在內存中的位置以及爲其分配了多少內存。在windbg中,可以輸入!handle命令來獲取對象的詳細信息。此處我正在檢索Reserve對象的詳細信息。
一旦我們知道對象地址,我們就可以使用!pool命令查找其池詳細信息。作爲其第二個參數解析2意味着其只顯示我們感興趣的確切分配,刪除2將顯示內存頁內的周圍分配。
這裏我們可以看到,Reserve對象被分配了一個'IoCo'標籤,佔用了60個字節。爲其他對象重複此過程得到以下結果。
知道對象大小將在稍後我們需要確保確定大小的目標對象被可靠地分配內存空間中時有用。現在我們嘗試使用Event對象進行池修飾,這些對象爲我們提供了一個空閒和分配的0x40字節池塊的模式。
因爲分配器開始在空閒頁上分配內存之前通過查找空閒塊爲對象分配內存,因此我們需要先填充現有的0x40字節空閒塊。
比如下面的代碼將分配五個事件對象。
現在,如果我們構建這個代碼並使用附帶的內核調試器來運行它,我們可以看到五個事件對象的句柄。
檢查windbg中的最後兩個句柄發現,其沒有被分配到彼此接近之處。
進一步查看分配了倒數第二個Event對象的頁面的池信息後發現,其剛好被放置在兩個隨機對象之間的第一個可用間隙中。
但是,如果我們將DEFRAG_EVENT_COUNT增加到更大的數,結果大不相同。
再次運行它並查看最後的五個句柄。
檢查windbg中的句柄可以看到,其被連續分配在內存中。
檢查分配有兩個Event對象的頁面的池佈局可以發現,一長串Event對象被連續分配。內存分配器的確定性表明,如果我們分配足夠的Event對象,這最終總會發生。
現在我們要在受控大小的地址空間中創建“孔”。此時我們知道,分配的任何更多事件對象將大部分被連續分配,所以,通過分配大量對象,然後間隔釋放,我們應該得到一個空閒和分配對象的模式。
我將以下代碼添加到了上面的示例(循環打印最後五個句柄的位置)中。
運行後,我們得到一個示例句柄,該句柄從一個模糊隨機索引打印到其餘句柄中。
檢查windbg中的句柄後可以找到其在內存中的地址。
知道分配地址後,我們可以再次查看其分配的頁的池佈局。此處我們可以看到,我們已經成功地創建了一個空閒和分配的事件對象的模式。
對於我們無法找到相同大小的相應內核對象的對象/分配,我們可以使用分割大小的對象的多個副本,或嘗試更精細的東西。
HackSysTeam極其脆弱的驅動程序釋放後重用利用
內存在釋放後被使用時存在釋放後重用(UAF)漏洞。通過查找代碼執行此操作的地方,可能可以用其他內容替換釋放的內存。那麼當引用內存並且代碼認爲一個結構/對象在那裏時,另一個是。通過在可用內存中放置正確的新數據,可以獲得代碼執行。
漏洞
正如我剛纔所解釋的,爲了利用UAF,我們需要以下幾點:
1. 一種創建對象的方式
2. 一種釋放對象的方式
3. 一種替換其的方法
4. 一種導致替換對象作爲原始對象被引用的方式
和以前一樣,簡要看一下IDA中的驅動程序表明了我們的所有需求,我將從第1、2及4點開始,因爲這些讓我們開發了一個崩潰PoC。首先,我們需要一種使用驅動程序在內核內存中創建一個對象的方法,查看IOCTL分派函數給我們呈現了一個通過記錄以下字符串進行的函數調用:****** HACKSYS_EVD_IOCTL_CREATE_UAF_OBJECT ******。這看似正是我們所尋找的。
查看函數本身後可以看到在非分頁池上分配了0x58字節的內存。
如果此分配成功,則其繼續將值加載到內存中,並在全局變量中保存對其的引用。
在1處,函數將所有分配的內存設置爲用“0x41”字節填充。然後將0字節加載到內存的最後一個字節。在3處加載到對象的前四個字節的函數指針是一個記錄其被調用的簡單函數。
最後在4處,驅動程序在名爲P的全局變量中保存指向內存的指針。
現在我們可以創建對象,我們需要一種方法來釋放它。記錄****** HACKSYS_EVD_IOCTL_FREE_UAF_OBJECT ******之後的IOCTL分派函數中的函數調用可能是一個很好的調用。
查看函數本身可以看到,其不需要任何輸入,而是在我們查看的最後一個函數存儲的引用之上操作。
一旦被調用,函數在1處檢查在create函數中引用的全局指針“P”是否爲空,然後在2處繼續在其上調用ExFreePoolWithTag。
到我們的第三個需求——一種使驅動程序以某種方式引用釋放的對象的方法,****** HACKSYS_EVD_IOCTL_USE_UAF_OBJECT ******似乎可以做到這一點。
查看函數後可知,其嘗試通過create函數調用加載到UAF對象的前四個字節的函數指針。
在1處,其確保P包含指向對象的指針,且不是空指針。然後其將前四個字節的內存加載到eax中,並在2處確保其不是空字節。如果這兩個檢查都成功,則在3處進行回調。
敲定所需的IOCTL代碼爲我們提供了我們需要的三種IOCTL代碼。
編寫崩潰PoC
爲了可靠地檢測是否已發生UAF,我使用了一些Windows內核池調試功能。在這種情況下,使用以下命令啓用HackSysExtremeVulnerableDriver的專用池。
如果這成功運行,我們應會看到以下輸出。
當啓用了特殊池的二進制程序調用ExAllocatePoolWithTag函數時,其將使用ExAllocatePoolWithTagSpecialPool函數來分配內存,而不是遵循其標準邏輯。如下所示。
ExFreePoolWithTag函數具有匹配的邏輯。特殊池作爲由單獨的內存頁支持的文字分離內存池工作。特殊池有一些不同的選項。默認情況下,其處於驗證結束模式,簡言之,這意味着由驅動程序所作的所有分配被放置在儘可能靠近內存頁末尾處,後續和之前頁面被標記爲不可訪問。這意味着,如果驅動程序嘗試在分配結束後訪問內存,將會觸發錯誤。此外,頁面上未使用的內存用特殊模式標記,因此如果這些內存損壞,則該內存釋放後可檢測到錯誤。
此外,特殊池將標記其釋放的內存,並儘可能長時間地避免重新分配該內存。如果釋放的內存被引用,其將觸發錯誤。這會對驅動程序產生巨大的性能影響,因此其只在調試內存問題時啓用。
在特殊池爲啓用狀態下,我們可以爲此漏洞創建一個簡單的崩潰概念證明。下面的代碼將創建UAF對象、釋放該對象,然後導致其被引用。如果驅動程序引用釋放的內存,這應該因特殊池調試功能而觸發藍屏。
現在編譯並運行,然後...
使用附帶的內核調試器重新啓動系統,重新啓用特殊池並重新運行PoC,這樣我們可以確認崩潰是否由被引用的釋放的內存引起。
!analyze -v輸出立即告訴我們,崩潰可能是由被引用的釋放的內存引起的,進一步查看分析輸出可知,崩潰指令是之前在調用UAF對象回調函數的IOCTL中看到的push [eax]指令。
檢查驅動程序嘗試再次訪問的內存地址的池詳細信息後確認,內存可能之前已被釋放。
將其轉化爲利用方法
有了崩潰後,我們需要用可讓我們在引用時實現代碼執行的東西代替對象使用的內存。通常,我們必須尋找一個適當的對象,並可能使用一個基本的原語來讓我們獲得一個我們可以用於提升我們的權限的更有用的原語。不過幸運的是,HackSys驅動程序有一個讓這更容易的函數。日誌消息****** HACKSYS_EVD_IOCTL_CREATE_FAKE_OBJECT ******之後暴露的函數可以實現我們需要的功能。
查看函數實現後可知,其分配0x58字節的數據,然後檢查分配是否成功。
一旦其分配了所需的內存,其便將數據從IOCTL輸入緩衝區複製到其中。
在1處,指向分配的內存的指針爲ebx,在2處,其驗證從輸入緩衝區讀取數據是否是安全的,然後在3處,其在返回之前將0x16, 4字節塊從輸入緩衝區複製到新分配的內存中。
僞分配的對象與我們可以釋放並導致被引用的對象大小相同,這一事實是理想的場景。通過使用先前描述的內核池按摩技術,我們可以導致僞對象分配到UAF對象的地址。通過加載一個指向僞對象開頭的某些令牌竊取shellcode的指針,我們可以觸發使用UAF對象IOCTL代碼處理程序,從而使驅動程序執行我們的payload。
與我在池風水示例中使用的Event對象不同,UAF對象不是0x40字節,所以我們將使用Reserve對象,因爲我們早先發現,當包括8字節POOL_HEADER時,這些是匹配0x58字節的UAF對象的內存中的0x60字節。首先,我們需要添加以下header。
接下來,我們添加以下代碼來執行實際的池風水,這將填充任何現有的空閒0x60字節區域,然後創建一個分配和空閒的0x60字節塊的模式。
現在我們可以強制我們的僞對象分配到我們需要製作僞對象的UAF對象之前所在的位置。我們首先將本系列前面部分中使用的令牌竊取器添加到我們的用戶空間代碼中。
接下來我們來創建我們的僞對象,我們知道其需要是0x58字節,前四個包含一個函數指針,其餘的字節我們不關心。將函數指針設置爲我們的令牌竊取shellcode的地址後,其將在驅動程序引用我們的僞對象並觸發其所認爲的原始對象回調時執行。這緊隨用於釋放UAF對象的DeviceIOControl調用。
我創建了0x250的僞對象,用於填充我們之前創建的所有間隙。另外,我們需要在我們文件的頂部定義HACKSYS_EVD_IOCTL_ALLOCATE_FAKE_OBJECT。
最後一些清理代碼和調用系統啓動calc.exe適合代碼的末尾。
構建然後運行代碼(特殊池爲禁用狀態)給我們提供了一個作爲SYSTEM運行的良好計算器。
漏洞利用的最終/完整代碼見Github。
HackSysTeam極其脆弱的驅動程序池溢出
觸發驅動程序池溢出漏洞的IOCTL代碼很容易找到,****** HACKSYS_EVD_IOCTL_POOL_OVERFLOW ******記錄後隨即進行的函數調用是明顯的目標。
查看處理程序函數後可知,其在非分頁池上進行大小爲0x1F8字節的池分配(edi 在函數的開始與自身xor)。
如果分配成功,則處理程序將數據從用戶提供的緩衝區複製到池中。然而,複製的數據量由IOCTL中提供的大小控制。
這意味着,如果一個調用者提供的長度大於0x1F8個字節,就會發生越界寫入,這也可稱爲池溢出。我們將再次啓用特殊池,從而使觸發漏洞更容易。
以下代碼將提供一個IOCTL請求,其將在池分配結束後寫入4個字節,這應該導致其訪問標記爲不可訪問的頁面,並導致系統藍屏。
編譯然後運行,我們得到了我們想要的。
調試崩潰後可以看到,和預期的一樣,驅動程序嘗試在分配結束後寫入。
查看崩潰詳情後可知,其是在我們之前在HACKSYS_EVD_IOCTL_POOL_OVERFLOW處理程序中看到的rep movs指令處崩潰。
檢查損壞的內存地址後可以看到,和預期一樣,一連串0x41字節後是無法訪問的內存。
池溢出池風水
與UAF利用一樣,我們需要能確保我們的內存在分配時位置正確。在這種情況下,我們要確保另一個對象在內存中緊隨其後。這一次,我們分配的內存大小爲0x200字節(0x1F8 + 8字節header),Reserve對象分配總大小爲60個字節,這太小,並清楚地分開了我們想使其不切實際的數量,但是,我們之前看過的Event對象是0x40字節的分配。這一清楚的劃分分配到8是理想的。
爲了修整堆,這次我們再次使用Event對象對其進行碎片整理,然後我們將分配大量連續的Event對象,並以8個塊的形式釋放它們。這應該使我們獲得分配200字節的模式,然後分配非分頁池內存。下面的代碼在觸發調試器中斷之前執行池修飾,這樣我們可以檢查它是否有效。
這個運行後我們便可看到打印的指針值,然後按下Enter鍵觸發斷點。
在內核調試器中,我轉儲了句柄信息以獲取對象的詳細信息。
查看對象分配周圍的池內存,可以看到一個很好的重複模式——8個分配的事件對象,隨後是8個空閒的事件對象,與計劃的完全一致。
現在我們可以觸發我們的溢出,40字節的Event對象肯定將跟隨我們控制的內存,所以我們可以開始整合利用方法。
池溢出利用第一回合
現在我們可以可靠地覆蓋一個Event對象的header,我們需要實際覆蓋一些東西。我將使用兩種不同的方法,一種是最初在“Windows
7 內核池利用”中討論的,另一種是在“純數據Pwning微軟Windows內核:微軟Windows
8.1內核池溢出利用”中討論的。首先,我將使用Object Type索引覆蓋技術。
如Code Machine博文中所述,Windows內核內存中的每個對象都由幾個結構以及對象結構本身組成。第一個是我們之前討論的POOL_HEADER結構。以下是一個Event對象的例子,這次我們不會破壞該結構,所以當我們在內存中進一步重寫另一個結構時,我們將重用我們的利用方法中的值,以使其保持原樣。
接下來有一個或多個可選結構,存在哪些可選結構可通過查看出現在實際對象OBJECT_HEADER之前的最後一個結構找到。來自Event對象的示例OBJECT_HEADER佈局如下所示:
InfoMask字段只有0x8位設置,這意味着,如Code Machine文章中所述,池header和對象header之間的唯一可選結構是OBJECT_HEADER_QUOTA_INFO。該文章還告訴我們,其大小爲0x10字節,所以我們可以通過回看0x10字節在內存中查看它。
OBJECT_HEADER結構是我們將破壞的結構,所以當我們覆蓋這個結構時,我們將使用其默認值使其保持原樣。
OBJECT_HEADER結構包含用於管理對象的對象元數據,用於指示可選header、存儲調試信息等。如Nikita的幻燈片中所述,該header包含“TypeIndex”字段,這用作ObTypeIndexTable(用於存儲指向OBJECT_TYPE結構的指針,這些結構提供有關每個內核對象的重要細節)的索引。查看Windbg中的ObTypeIndexTable,我們可以看到條目。
將條目0xc視作OBJECT_TYPE結構使我們獲得以下內容:
所以我們肯定有正確的對象類型,但沒有什麼可以明顯讓我們實現代碼執行。進一步查看結構後我們看到TypeInfo字段,在windbg中更仔細檢查該字段後發現了一系列很好的函數指針。
這意味着正根據結構跳轉到函數。如果我們可以控制其中的一個,我們應該能夠讓內核在我們選擇的地址處執行shellcode。通過回看可以看到, ObTypeIndexTable的第一個條目是一個NULL指針,所以我們用0覆蓋OBJECT_HEADER中的TypeIndex字段,然後,當內核嘗試執行時,內核應該嘗試從NULL頁面讀取函數指針。因爲我們是在Windows 7 32位上執行此操作,所以我們可以分配NULL頁,從而可以控制內核執行跳轉到的位置,這樣我們便可使用與我之前所用相同的shellcode來提升我們的權限。
現在我們要覆蓋TypeIndex字段,保持緩衝區末尾和和Event對象之間的所有其他字段不變。我們從增加我們之前使用的InBuffer的大小開始。額外的0x28字節將覆蓋POOL_HEADER(0x8字節)、OBJECT_HEADER_QUOTA_INFO(0x10字節)及OBJECT_HEADER,直到幷包括TypeIndex(0x10字節)。
首先,我們使用之前看到的默認值覆蓋POOL_HEADER和OBJECT_HEADER_QUOTA_INFO結構。
最後,我們覆蓋了OBJECT_HEADER結構,主要使用其默認值,但TypeIndex值設置爲0。
現在讓我們運行代碼(確保特殊池已禁用),我們應該會得到因內核嘗試在地址0x0處訪問OBJECT_TYPE結構而導致的崩潰。我立即在我附帶的調試器中獲得了一個BugCheck,在發生異常的時候查看指令和寄存器,我們看到的正是我們所希望的。
一個名爲ObpCloseHandleTableEntry的函數在嘗試從ebx+0x74讀取內存時出錯(ebx爲0)。這應對應於OBJECT_TYPE結構中的DeleteProcedure條目(如果其按照計劃從NULL頁讀取)。現在我們只需要使用與本系列中之前使用的相同的方法分配NULL頁,並設置一個函數指針偏移量,以指向我們的令牌竊取shellcode。
在main的開始添加了以下代碼,以分配NULL頁。
成功分配NULL頁後,我們只需要放置一個指向我們的shellcode的指針,以代替其中一個函數指針。我嘗試在每個函數的偏移量處放置一個shellcode指針,發現Delete、OkayToClose及Close程序會導致shellcode以一種直接的方式被執行。我決定覆蓋Delete程序,因爲b33f使用了OkayToClose,Ashfaq使用了Close。
最後,我們需要稍微修改shellcode,因爲Delete程序預期4字節的參數需要從棧中刪除,以避免事情變得不穩定。將ret 4;添加到shellcode的末尾即可搞定。最後,在我們開始整理內存前,添加一個不錯的system("calc.exe");。現在我們再次運行代碼,應該會得到一個作爲SYSTEM運行的計算器,如下所示。
漏洞利用的最終/完整代碼見Github。
池溢出利用第二回合
我將使用的利用該漏洞的第二種技術是PoolIndex覆蓋技術——作爲例子在“Windows
7 內核池利用”中使用,並在“First
Dip Into the Kernel Pool : MS10-058”中通過示例代碼使用。
這次我們只覆蓋相鄰Event對象的POOL_HEADER結構,所以我們的緩衝區可以小一些。
我們將要覆蓋的字段是PoolIndex字段。默認情況下,Windows 7主機將只有一個非分頁池,這意味着該字段將不會被實際使用。所以首先我們將覆蓋PoolType字段,使塊看起來是分頁池的一部分。如前所述,該字段中需要的值可以在POOL_TYPE枚舉中找到,最終爲3。
PoolIndex字段用於索引 nt!ExpPagedPoolDescriptor 數組,以便在對象被釋放時爲其找到正確的PoolDescriptor。查看windbg中的數組可以看到:
你會注意到,僅前五個條目是有效的指針,其餘的是NULL,這意味着,如果我們用大於或等於5的值覆蓋POOL_HEADER的PoolIndex字段,當對象被釋放時,內核將嘗試從NULL頁開始引用 一個POOL_DESCRIPTOR。像以前一樣,我們可以從用戶空間分配NULL頁,並以可以實現代碼執行的方式設置結構值。首先,我們來覆蓋PoolIndex字段,並確保內核按預期崩潰。
現在編譯並運行二進制文件,我們得到了崩潰。
內核成功崩潰,嘗試在釋放池分配時訪問0x0 + 0x80地址的內存。現在我們如何從控制池描述符轉到代碼執行?
如前所述,池描述符包括一個PendingFrees列表,如果其包含32個或更多條目,其將被釋放。通過僞造一個Pool Descriptor對象,我們可以使PendingFrees列表指向我們控制的僞池分配,如果我們將PendingFreesDepth設置爲32或更多,則內核將嘗試釋放它們。釋放的對象地址將被添加到ListHeads列表中,通過在該列表中創建指向要覆蓋的目標地址的僞條目,剛剛被釋放的僞對象的地址將被寫到ListHeads列表中第一個條目的Blink地址。
這使我們可將受控用戶模式地址寫入內存中的任何地址。現在,我們讓內核將僞對象地址寫到0x41414141。
希望一些代碼會使這個更清楚。所有這些代碼都放在池噴射代碼之前。
首先我們像之前一樣分配NULL頁。
現在我們需要從0x0開始創建僞POOL_DESCRIPTOR結構。我基本上是通過逆向Jeremy的解決方案來說明如何做到這一點,所以我使用了他的值。
最後我們在0x1208創建僞塊,相應的POOL_HEADER需要爲0x1200。
0x1208處的內存是一個NULL指針,這一事實意味着DeferedFree將釋放它然後停止,因爲沒有後續條目。
我們還需要在對象釋放後立即創建另一個僞POOL_HEADER,因爲當內存管理器釋放前一個塊時,其將驗證其大小是否等於下一個塊前一個大小字段。
現在構建和運行代碼,我們得到了預期的錯誤。
這裏我們可以看到,0x1208由ExDeferredFreePool寫入[esi+4],等於0x41414141。現在我們需要覆蓋內存中的一些內容,這讓我們可實現代碼執行。爲此,我選擇覆蓋HalDispatchTable中的一個條目,和我利用任意覆蓋漏洞時一樣。
一旦條目被覆蓋,觸發正確的函數將導致使用分派表條目和內核代碼執行被重定向到僞池分配之前的位置(0x1208)。
首先,我們需要找到HalDispatch表地址和我們要覆蓋的目標條目,在這種情況下是ntdll中的NtQueryIntervalProfile函數被調用時使用的第二個條目。
接下來我們更新僞ListHeads條目,以指向 where。
最後,我們在0x1208處放置一個0xcc字節(int 3操作碼)來觸發斷點,並增加一個對NtQueryIntervalProfile的調用,以便在我們清理所有東西后調用該函數。放置0xCC字節的原因是,如果不這樣做,0x1208處的字節是clc(0xf8)的操作碼,後跟ret(0xc3),這意味着什麼都不會發生,操作系統保持正常。
我們還沒有設置我們的shellcode,但現在我們應該可在0x1208處實現代碼執行。再次運行代碼,我們得到了這一結果。
最後一步是設置shellcode。執行將從0x1208開始,所以我們不能只是在此處放置一個指針,相反,我們在調用NtQueryIntervalProfile之前設置了以下數據。
現在重新編譯並運行代碼,我們得到如下結果:
該漏洞利用的最終/完整代碼見Github。