空指標漏洞防護技術(提高篇)

綠盟發表於2015-09-15

在《空指標漏洞防護技術-初級篇》中我們介紹了空指標及空指標漏洞的概念,在這次高階篇中介紹空指標利用及相應的防護機制。

1 提高篇之:空指標的利用

前面主要介紹了空指標的一些概念和相關的知識,瞭解了什麼是空指標,對於由野指標導致的空指標漏洞不是今天的重點。接下來主要就針對指向零頁記憶體的空指標漏洞做詳細的介紹。

此類漏洞利用主要集中在兩種方式上:

  1. 利用NULL指標。
  2. 利用零頁記憶體分配可用記憶體空間

對於第一種情況可以利用NULL指標來繞過條件判斷或是安全認證。比如X.0rg空指標引用拒絕訪問漏洞(CVE-2008-0153 ),如下圖對比修改補丁前後的對比:

從程式碼補丁可以看出該漏洞利用NULL指標改變程式流程來觸發漏洞。

針對第二種情況,在某些情況下零頁記憶體也是可以被使用,比如下面兩種情況:

  1. 在windows16系統或是windows16虛擬系統中,零頁記憶體是可以使用的;在windows 32位系統上執行DOS程式就會啟動NTVDM程式,該程式就會使用到零頁記憶體。
  2. 通過ZwAllocateVirtualMemory等系統呼叫在程式中分配零頁記憶體(win7系統之前)。

接下來結合ZwAllocateVirtualMemory API函式的呼叫直觀感受在win7與win8系統中零頁記憶體分配的差異。

1.1 ZwAllocateVirtualMemory基本介紹

zwAllocateVirtualMemory函式在指定程式的虛擬空間中申請一塊記憶體,那是不是隻要在記憶體申請空間就會呼叫zwAllocateVirtualMemory函式呢?呼叫zwAllocateVirtualMemory需要根據實際的情況。

堆的分配、使用、回收都是通過微軟的API來管理的,最常見的API是malloc和new。在底層呼叫是HeapAlloc,同時通過HeapCreate來建立堆。對zwAllocateVirtualMemory函式的呼叫情況是:

. HeapCreate->RtlCreateHeap->ZwAllocateVirualMemory,這裡會直接申請一大塊記憶體,至於申請多大的記憶體,由程式PEB結構中的欄位決定,HeapSegmentReserve欄位指出要申請多大的虛擬記憶體,HeapSegmentCommit指明要提交多大記憶體。

圖中看出預設申請大小是0x100000,預設提交大小是0x2000。下圖是利用Heapcreate函式呼叫zwAllocatevirtualMemory時的函式呼叫棧關係。

圖中展示了函式的呼叫過程。

  1. HeapAlloc->RtlAllocateHeap-> ZwAllocateVirualMemory,這裡的記憶體申請是由Heapcreate已經提交的記憶體中申請。堆管理器從這片記憶體中劃分一塊出來以滿足申請的需要,僅當申請的記憶體不夠的時候,才會再次呼叫ZwAllocateVirualMemory函式。 **也就是說,我們平時使用**** malloc ****或是**** new *\*在堆上申請一塊小的記憶體是不會呼叫該函式的。** 這點大家要注意。下圖中展示了利用HeapAlloc呼叫ZwAllocateVirualMemory的過程:

c. 直接利用zwAllocateVirtualMemory分配記憶體,對於zwAllocateVirtualMemory函式,微軟沒有給出公開的文件,但是可以通過相關資料或是逆向來了解該函式的使用方式。直接利用zwAllocateVirtualMemory函式分配記憶體首先載入函式所在模組,同時獲取該函式的符號地址。由於該函式提供了比較全面的引數,利用此函式來分配記憶體空間更加靈活多變。

1.2 ZwAllocateVirtualMemory函式知識

zwAllocateVirtualMemory該函式在指定程式的虛擬空間中申請一塊記憶體,該塊記憶體預設將以64kb大小對齊。

兩個主要引數:

返回值

如果記憶體空間申請成功會返回0,失敗會返回各種NTSTATUS碼。

從zwAllocateVirtualMemory說明來看,本想利用BaseAddress引數在零頁記憶體中分配空間,但是當BaseAdress指定為0時,系統會尋找第一個未使用的記憶體塊來分配,而不是在零頁記憶體中分配。那麼如何才能分配到零頁記憶體呢?

1.3 零頁記憶體分配之例項win7 vs win8

瞭解了zwallocatevirtualmemory的用法,就結合例項來看看如何利用該函式進行零頁記憶體分配。前面介紹將BaseAdress設定為0時,並不能在零頁記憶體中分配空間,就需要利用其它方式在零頁記憶體分配空間。在AllocateType引數中有一個分配型別是MEM_TOP_DOWN,該型別表示記憶體分配從上向下分配記憶體。那麼此時指定 BaseAddress為一個 低地址,例如 1,同時指定分配記憶體的大小 大於這個值 ,例如8192(一個記憶體頁),這樣分配成功後 地址範圍就是 0xFFFFE001(-8191) 到 1把0地址包含在內了,此時再去嘗試向 NULL指標執行的地址寫資料,會發現程式不會異常了 。通過這種方式我們發現在0地址分配記憶體的同時,也會在高地址(核心空間)分配記憶體(當然此時使用高地址肯定會出錯,因為這樣在使用者空間)。

下面就舉個例子看如何在零頁分配記憶體

瞭解windows程式設計的情況下,對上面的程式碼不難理解,獲取模組控制程式碼,獲取zwAllocateVirtualMemory函式地址,並傳遞引數呼叫該函式,其中baseaddress的值為4,引數型別中包含了MEM_TOP_DOWN,即記憶體由上向下分配。所以如果成功的話,將把零頁記憶體中的地址4-0都會分配出去;函式返回0表示記憶體分配成功。然後再給0地址賦值列印,對0地址重新賦值後再次列印,檢視結果。

為了能對比win7與win8系統執行結果的差異,需要準備兩個乾淨的系統win7和win8,這裡採用的都是32位系統。

在win7系統中直接編譯執行該程式,執行結果如下

從顯示結果來看,利用zwAllocateVirtualMemory函式,確實在零記憶體頁分配了4-0地址的空間,也就是說我們可以利用zwAllocateVirtualMemory在零頁記憶體中成功分配空間。

同時在win8系統中同時編譯該程式,如果F5除錯執行結果如下圖:

或是CTRL+F5非除錯執行,其結果如下圖:

在win8系統中在呼叫zwallocatevirtualmemory後,函式返回了非0,返回值為0Xc00000f0。最終沒能在零頁記憶體分配空間。也就是說在win8系統中對零頁記憶體做了安全防護,導致在零頁地址分配記憶體時失敗。

接下來看看win8系統中到底對NULL Pointer也就是零頁記憶體做了哪些防護。

2 提高篇:windows零頁記憶體防護機制

win8系統對零頁記憶體防護機制通過搜尋引擎可以查到: **在**** WIN8 ****系統中利用了核心程式結構**** EPROCESS ****中的**** flags ****欄位的**** Vdmallowed *\*標誌來判斷是否允許訪問零頁記憶體。** 但是知其然而不知其所以然不是我們追求的目標。我們要做的就是在不依靠其他文獻的條件下通過逆向和動態除錯技術來剖析win8對零頁記憶體的防護機制。

2.1 搭建核心除錯環境

探索Win8的零頁記憶體保護機制在核心中的實現,首先需要搭建一個能除錯核心的環境。利用上面給出的例子結合核心除錯來分析安全機制。關於如何搭建核心環境在之前的文章中已經介紹過,這裡再累贅一下。

5.1.1 虛擬機器及除錯環境

搭建核心除錯環境,採用雙機除錯模式,在虛擬機器中安裝win8系統同時配置win8除錯模式。

Vmware安裝win8系統就不在詳解。系統啟動後:

管理員許可權開啟CMD

按照上面的步驟配置好後,關閉虛擬機器,開啟win8虛擬機器配置選項,刪除並行埠,新增串列埠,同時配置串列埠,如下圖:

配置好vmware後啟動虛擬機器進入除錯模式,如下圖:

5.1.2 配置啟動windbg

雙機模式除錯核心需要配置windbg工具,現在主機中安裝windbg;建立一個windbg的快捷方式,並在屬性->目標中填入如下內容:

Windbg路徑根據安裝路徑不同而調整。

啟動windbg,由於我的主機是win7系統如果直接雙擊windbg快捷方式啟動

這是缺少必要的許可權,所以在啟動windbg快捷方式時,需要以管理員的許可權啟動,啟動完成後就可以和虛擬機器中的win8系統進行核心級除錯,啟動之後的結果如下圖:

到此核心除錯的基本環境建立好了,為了能夠更加直觀的檢視windows系統函式及呼叫關係需要為windbg配置符號表,這樣windbg可以識別出windows標準的匯出符號,同時為了能夠更方便的除錯nullpointer,也需要載入該程式的符號表。

到此核心除錯環境搭建完成。

2.2 使用者態及核心態跨棧除錯

該部分是動態除錯的關鍵部分,也是注意事項最多的一節。

5.2.1 核心除錯使用者態程式

在上面配置好環境後,在windbg中執行G命令,啟動除錯。啟動程式後,同時在windbg中按住CRTL+BREAK斷到偵錯程式中。此時win8系統處於中斷狀態,不會再執行任何指令。

偵錯程式斷下來

我們知道在win7與win8中呼叫zwallocatevirtualmemory時由於零頁記憶體保護機制的原因,win8系統不能在零頁記憶體分配空間。那麼就從除錯nullpointer入手,通過呼叫zwallocatevirtualmemory除錯核心態程式來剖解安全機制。

但是現在windbg處於核心除錯狀態,檢視所有啟動的程式

需要切換到nullpointer程式中。載入了nullpointer符號表。檢視main函式的反彙編如下圖所示:

5.2.2 使用者態進入核心態

跟進該函式,最後進入

進入了sysenter指令之前,對windows系統有所瞭解的話,就知道該函式利用該呼叫進入快速系統呼叫也就是說此時將由使用者態進入核心態。

SYSENTER用來快速呼叫一個0層的系統過程。SYSENTER是SYSEXIT的同伴指令。該指令經過了優化,它可以使將由使用者程式碼(執行在3層)向作業系統或執行程式(執行在0層)發起的系統呼叫發揮最大的效能。

在呼叫SYSENTER指令前,軟體必須通過下面的MSR暫存器,指定0層的程式碼段和程式碼指標,0層的堆疊段和堆疊指標:

  1. IA32_SYSENTER_CS:一個32位值。低16位是0層的程式碼段的選擇子。該值同時用來計算0層的堆疊的選擇子。

2.IA32_SYSENTER_EIP:包含一個32位的0層的程式碼指標,指向第一條指令。

3.IA32_SYSENTER_ESP:包含一個32位的0層的堆疊指標。

MSR暫存器可以通過指令RDMSR/WRMSR來進行讀寫。暫存器地址如下表。這些地址值在以後的intel 64和IA32處理器中是固定不變的。

為了能保證我們除錯的**** NT!NtAllocatevirtualMemory ****函式剛好是**** nullpointer ****呼叫的,需要給**** NT!NtAllocatevirtualMemory ****函式下斷點,意思是隻在指定程式呼叫該函式時才斷下來。** 待函式斷下來後,同時檢視函式呼叫棧如下圖 **:

從圖中呼叫棧可知,此時斷下來的NT!NtAllocatevirtualMemory剛好是nullpointer引用的核心函式。此時已經進入核心除錯狀態。

5.3 逆向分析nt!NtAllocateVirtualMemory

進入win8核心除錯,就要結合靜態分析和動態除錯來挖掘有用的資訊。首先找到win8核心檔案。 **需要注意的是此時分析的是虛擬機器中**** win8 ****系統的核心檔案,不是**** win7 *\*主機的核心檔案。**

5.3.1 NtAllocatevirtualMemory引數確認

在前面的除錯中,windbg斷在了NT! NtAllocatevirtualMemory函式的入口處。對比NtAllocatevirtualMemory函式在windbg和IDA反編譯的結果:

可知,在執行完指令call __SEH_prolog4_GS,後EBP+8為第一個引數,EBP+C為第二個引數,那就看看在呼叫call __SEH_prolog4_GS後EBP的值,如下圖:

第一個引數是程式控制程式碼,本程式控制程式碼剛好是-1,第二個引數基地址指標,之前我們在程式中基地址是4,也就是0x00000004,檢視基地址指標的值:

剛好是0x00000004,使用者態傳入的第三個引數是0,這裡的第三個引數也剛好是0。其他引數不在一一列舉;也就是是說核心函式NtAllocatevirtualMemory與使用者態函式zwallocatevirtualmemory引數是一致的。

5.3.2 查詢NtAllocatevirtualMemory 零頁記憶體安全機制

到了最重要的時刻,接下來動態除錯NtAllocatevirtualMemory, 一路單步執行,看到eax=0xc00000f0時停下:

從圖中可知並EAX來自於ESI的賦值。那就繼續往上看,看ESI的值是從哪裡來的

從圖中看出,在執行test[edi+0C4],1000000h指令後,如果相等就執行了mov esi,0xc00000f0。

5.3.3 確認NtAllocatevirtualMemory零頁記憶體安全機制

前面已經找到了返回0xc00000f0的地方,接下來就要看看條件判斷的地方, EDI=0x84b2ac80是EPROCESS程式的地址,檢視EPROCESS結果可知:

在結構eprocess偏移0xc4的位置剛好是標誌Vdmallowed,該值是0,並且[edi=0xc4] 與上1000000後剛好是零。導致ESI=0xc00000f0,進而導致EAX=0xc00000f0。

**到此基本上已經確認了**** NtAllocateVirtualMemory ****中對**** NULLPage ****的安全機制,就是檢查**** EPROCESS ****中的**** VdmAllowed *\*的標誌位。**

確定條件判斷確定位置

從判斷語句來看V76應該是引數中的基地址,V14應該是EPROCESS的地址。看一下這兩個值的來源

從上圖可知v76就是baseaddress; V14追溯到keGetCurrentThread,在核心模式下FS卻指向KPCR(Kernel’s Processor Control Region)結構。即FS段的起點與KPCR結構對齊。看一下KPCR結構偏移124的位置

圖中可知偏移124的位置剛好的當前執行緒的結構地址。通過檢視函式KeGetCurrentThread的實現也能證實這一點,如下圖:

執行緒結構地址+128(0x80)的位置剛好是當前程式的核心地址,如下圖所示:

之後NtAllocateVirtualMemory函式在將程式結構地址偏移0xC4值與0x1000000做比較判斷做安全檢查。

到目前為止,NtAllocateVirtualMemory函式對零頁記憶體的保護機制剖析完成。總結一下:

  1. 判斷基地址是否小於**** 0x1000000,
  2. 判斷核心結構**** EPROCESS ****的**** Vdmallowed ****標誌是否為**** 0

5.3.4 查詢核心中其他對零頁記憶體保護的函式

通過對NtAllocateVirtualMemory逆向分析和動態跟蹤瞭解了win8中對零頁記憶體的保護機制;那麼除了NtAllocateVirtualMemory函式,在核心中是否還有其他的函式也對零頁記憶體進行了安全檢查呢?

通過在整個NT核心檔案中查詢零頁記憶體保護機制,又發現了幾個包含零頁記憶體包含的函式,搜尋結果如下圖:

也就是說在核心檔案中除了NTAllocateVirtualMemory外,還有四個函式對零頁記憶體做檢測:

  1. MiIsVaRangeAvailable:
  2. MiMapViewOfPhySicalSection
  3. MiMapLockedPagesInUserSpace
  4. MiCreatePebOrTeb

這四個函式不都是匯出函式,也就是說在核心程式設計中有可能我們使用的一些匯出函式中內部呼叫了這四個函式中的某一個,其內部已經做了零頁記憶體檢測。

6 總結

本文先是介紹了什麼是空指標漏洞,之後對windows系統的零頁記憶體保護機制做了剖析。

空指標漏洞:

對零頁記憶體的安全防護:

  1. 檢測分配頁是否在零頁記憶體。
  2. **檢測**** EPROCESS ****結構中的**** VdmAllowed *\*標誌。**

Win8**** 以後的零頁記憶體防護也只是保證了不會在零頁記憶體分配空間,緩解分配零頁記憶體空間來利用漏洞;從上圖可知,利用零頁記憶體分配導致的空指標漏洞也只是眾多空指標漏洞型別中的一種。通過零頁記憶體保護機制並不能緩解所有的空指標漏洞。

相關文章