0x00 前言
漏洞分析第一篇,在讲UAF之前,先简单过一下HEVD代码的逻辑,方便后续的分析。
0x01 HEVD代码分析
1.1 驱动程序逻辑分析
驱动程序的主函数文件是HackSysExtremeVulnerableDriver.c
主要包含5个函数
DriverEntry: 驱动程序入口函数,类似于exe的main、dll的DllMain
IrpDeviceIoCtlHandler: 设备操作处理函数,DeviceIoControl请求处理,重要函数,根据请求会调用不同漏洞代码模块。
DriverUnloadHandler: 驱动卸载处理函数,可忽略
IrpCreateCloseHandler:驱动设备打开关闭处理函数,通常来说就是CreateFile、CloseHandle的请求处理,可忽略
IrpNotImplementedHandler:可忽略
除了DriverEntry是固定函数名,其他都是自定义的,只有传参和返回类型是有要求的,那么是怎么将相关请求绑定相关函数的呢。
ring3在访问驱动(通过驱动符号链接)进行操作时,会产生相应的IRP(I/O Request Package)事件,在驱动内对IRP进行操作,实现用户层对驱动的操作。
实现对IRP事件的处理需要使用到派遣函数,这时就是通过驱动对象的MajorFunction属性进行IRP请求类型和派遣函数绑定,所以派遣函数其实也是回调函数,也是为啥传参和返回类型是有要求的。
如下所示
IRP_MJ_CREATE对应CreateFile请求。
IRP_MJ_CLOSE对应CloseHandle请求。
IRP_MJ_DEVICE_CONTROL则对应DeviceIoControl请求,绑定的派遣函数是IrpDeviceIoCtlHandler。
IrpDeviceIoCtlHandler里获取IRP请求里设置的控制码,通过switch-case来调用不同的漏洞代码模块。当然控制码是自定义的。
再来看一下exp里如何访问漏洞模块的,像UAF,通过IOCTL_ALLOCATE_UAF_OBJECT和IOCTL_FREE_UAF_OBJECT等控制码来访问驱动里的漏洞模块。
对应到驱动代码里就是这块分支函数,也是后续的分析重点。
我们需要理解的逻辑大概就这些了,关于更多驱动开发的知识可参考下面这个,可以快速掌握一些驱动知识。
https://bbs.pediy.com/thread-266038.htm
1.2 UAF漏洞代码分析
1.2.1 UAF漏洞介绍
申请出一个堆块保存在一个指针中,在释放后,没有将该指针清空,形成了一个悬挂指针(danglingpointer),而后再申请出堆块时会将刚刚释放出的堆块申请出来,并复写其内容,而悬挂指针此时仍然可以使用,使得出现了不可控的情况。攻击者一般利用该漏洞进行函数指针的控制,从而劫持程序执行流。
当应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放回内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放回内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。(这块可能不太准确,大概看看就行)
但是其实这里有以下几种情况
- 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
- 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
- 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
漏洞利用的过程可以分为以下4步:
- 申请堆块,保存指针。
- 释放堆块,形成悬挂指针。
- 再次申请堆块,填充恶意数据。
- 使用悬挂指针,实现恶意目的。
下面我们去HEVD项目中具体看如何体现。
1.2.2 UAF漏洞代码
漏洞代码位于UseAfterFreeNonPagedPool.c
里面包括4个重要函数以及这4个IRP处理函数,IRP处理函数会分别调用这4个重要函数
AllocateUaFObjectNonPagedPool
用于创建内核对象PUSE_AFTER_FREE_NON_PAGED_POOL
如下调用ExAllocatePoolWithTag在内核非分页池申请内存,并填充数据
_USE_AFTER_FREE_NON_PAGED_POOL是一个0x58大小的结构体。
这边将该结构体对象存放在全局变量g_UseAfterFreeObjectNonPagedPool
中了
FreeUaFObjectNonPagedPool
用于释放内核对象,这个函数里有两段代码,上面会修复代码,可以看到比g_UseAfterFreeObjectNonPagedPool
被释放后,多了一个置NULL的动作,这样就可以防止悬挂指针的重利用。
AllocateFakeObjectNonPagedPool
将用户模式传入UserFakeObject指向内容拷贝给核心对象。
先根据FAKE_OBJECT_NON_PAGED_POOL结构体分配一个非分页池的内存,然后将UserFakeObject的内容拷贝给KernelFakeObject。
这里重点在于ExAllocatePoolWithTag分配内核池内存,如果存在一个已释放的相同大小或者大一点的内存,那么重新申请就有概率申请到该段内存,然后再为该段内存写入恶意代码,这样就会导致之前的悬挂指针再被调用时,访问的是被覆盖的内存内容,从而执行恶意代码。
为了增大申请到该段内存的概率,会使用一种池喷射的技巧,可参考扩展知识部分。
查看这个FAKE_OBJECT_NON_PAGED_POOL可以看到大小与之前的机构体一致。这个结构体没有如上分成callback和buffer,这个其实不影响的,只要大小一样,把结构体前4字节设置成恶意代码地址即可。
UseUaFObjectNonPagedPool
该函数作用是调用全局变量g_UseAfterFreeObjectNonPagedPool,执行他的回调函数。
1.2.3 小结
分析了漏洞代码,其实会对漏洞成因更加了解,我们只要按照AllocateUaFObjectNonPagedPool->FreeUaFObjectNonPagedPool->AllocateFakeObjectNonPagedPool->UseUaFObjectNonPagedPool的顺序调用,就可以触发漏洞,在第三步传入包含恶意代码地址的结构体,覆盖原来的内存,再二次调用原来的结构体指针即可访问恶意代码,也就是UAF(use after free)的含义。
而修复方案在于Free之后将引用指针置位NULL,来避免二次访问已释放内存块。
后续对于漏洞的利用除了上述流程还需要考虑如何提高申请到相同内存块的几率,这个涉及到内核池管理,也用到内核池漏洞常用的池喷射技术。
0x02 漏洞利用
先在用户空间的堆中分配FakeObject,将前4字节指向漏洞利用后运行的payload EopPayload地址。
2.1 池喷射代码
再强调下为啥需要池喷射,UAF需要重新申请到相同的内存块并覆盖成恶意代码,而内核池中可能会有许多空间的内存块,如果释放的内存块刚好和其他空闲的内存块相邻,系统就会将这两个内存块合并,那么再申请内存时, 无法保证刚好用到我们释放的那个内存块。
NtAllocateReserveObject可用于在内核池分配两个可选的内核对象,这里是调用NtAllocateReserveObject在内核空间分配IoCompletionReserve内核对象,IoCompletionReserve的内核对象大小为0x60,刚好比我们需要重利用的结构体0x58大一点。
池喷射第一步,先申请10000个IoCompletionReserve对象,用于将内核池中空闲、零散的池块都申请完。
第二步,然后再申请5000个该对象,这时申请出来的池块很大概率是连续的。
第三步,每隔一个内核对象释放一个对象,这样就会留下很多间隔0x60的空闲池块,那么在申请_USE_AFTER_FREE_NON_PAGED_POOL结构体时用到的池块的前一个池块就不会是空闲的,释放的时候就不会被合并,这样出意外的可能性就很低了。
这里可能会有一个疑问,上面释放了那么多池块,为啥不会申请到其他,一个原因是申请是优先使用池块大小相同或更相近的,我们在漏洞代码里看到的两个结构体都是0x58是最相近的,另一个原因是越晚释放的池块会更优先被使用,也就是后入先出的概念。
2.2 UAF利用代码
接着就是UAF利用的常规几步,
第一步:访问驱动,发送申请UAF_OBJECT结构体的请求。
第二步:访问驱动,发送释放UAF_OBJECT结构体的请求。
第三步:访问驱动,发送申请FAKE_OBJECT结构体的请求,这里循环了1000次,也是池喷射的概念,一次可能不一定申请到上面释放的内存块,所以增大概率,申请1000次。
第四步:也就是漏洞触发恶意代码执行的一步,调用原来已释放结构体的悬挂指针,访问被覆盖的内存块,触发恶意代码执行。
中间有一个FreeReserveObjects,用于释放之前池喷射申请的所有内存块,不然太占用内存空间了,因为运行在内核,不释放的话即使你当前漏洞利用程序退出也不会释放。
2.3 payload代码
这段payload的作用是将SYSTEM进程的token复制到当前进程,这样当前进程则为system权限。
fs寄存器在Ring0中指向一个称为KPCR的数据结构,即FS段的起点与 KPCR 结构对齐,而在Ring0中fs寄存器一般为0x30,这样fs:[124h]就指向KPRCB数据结构的第四个字节。由于 KPRCB 结构比较大,在此就不列出来了。查看其数据结构可以看到第四个字节指向CurrentThead
(KTHREAD类型)。这样fs:[124h]其实是指向当前线程的_KTHREAD
1 2 3 4 5 6 7 8 9 10 11 12 13 | kd> dt nt!_KPCR +0x000 NtTib : _NT_TIB ...... +0x0dc KernelReserved2 : [17] Uint4B +0x120 PrcbData : _KPRCB kd> dt _KPRCB nt!_KPRCB +0x000 MinorVersion : Uint2B +0x002 MajorVersion : Uint2B +0x004 CurrentThread : Ptr32 _KTHREAD +0x008 NextThread : Ptr32 _KTHREAD +0x00c IdleThread : Ptr32 _KTHREAD |
_KTHREAD:[0x50]
指向 _KPROCESS
, 即 nt!_KTHREAD.ApcState.Process
,_EPROCESS
的第一个成员就是_KPROCESS
,表示两个数据结构地址一样,则可以通过_KPROCESS
访问_EPROCESS
数据
再来看看_EPROCESS
的结构,+0xb8处是进程活动链表,用于储存当前进程的信息,我们通过对它的遍历,可以找到system的token
(+0xf8),我们知道system的PID一直是4,通过这一点我们就可以遍历了,遍历到系统token
之后替换就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | kd> dt nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x098 ProcessLock : _EX_PUSH_LOCK +0x0a0 CreateTime : _LARGE_INTEGER +0x0a8 ExitTime : _LARGE_INTEGER +0x0b0 RundownProtect : _EX_RUNDOWN_REF +0x0b4 UniqueProcessId : Ptr32 Void +0x0b8 ActiveProcessLinks : _LIST_ENTRY +0x0c0 ProcessQuotaUsage : [2] Uint4B +0x0c8 ProcessQuotaPeak : [2] Uint4B +0x0d0 CommitCharge : Uint4B +0x0d4 QuotaBlock : Ptr32 _EPROCESS_QUOTA_BLOCK +0x0d8 CpuQuotaBlock : Ptr32 _PS_CPU_QUOTA_BLOCK +0x0dc PeakVirtualSize : Uint4B +0x0e0 VirtualSize : Uint4B +0x0e4 SessionProcessLinks : _LIST_ENTRY +0x0ec DebugPort : Ptr32 Void +0x0f0 ExceptionPortData : Ptr32 Void +0x0f0 ExceptionPortValue : Uint4B +0x0f0 ExceptionPortState : Pos 0, 3 Bits +0x0f4 ObjectTable : Ptr32 _HANDLE_TABLE +0x0f8 Token : _EX_FAST_REF |
2.4 执行
执行效果如下,效果是通过UAF在内核进行system token复制,让当前进程的token已切换为system,接着创建一个新进程如cmd.exe则也是system权限。
然后看下内核的变化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 搜索HEVD lm m H* # 查看符号表 kd> x /D HEVD!u* A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 9a3e77f2 HEVD!UaFObjectCallbackNonPagedPoolNx (void) 9a3e7806 HEVD!UseUaFObjectNonPagedPoolNx (void) 9a3e74e8 HEVD!UseUaFObjectNonPagedPoolIoctlHandler (struct _IRP *, struct _IO_STACK_LOCATION *) 9a3e742c HEVD!UseUaFObjectNonPagedPool (void) 9a3e78c2 HEVD!UseUaFObjectNonPagedPoolNxIoctlHandler (struct _IRP *, struct _IO_STACK_LOCATION *) 9a3e7108 HEVD!UninitializedMemoryStackObjectCallback (void) 9a3e6fe6 HEVD!UninitializedMemoryPagedPoolObjectCallback (void) 9a3e70e8 HEVD!UninitializedMemoryStackIoctlHandler (struct _IRP *, struct _IO_STACK_LOCATION *) 9a3e6fc6 HEVD!UninitializedMemoryPagedPoolIoctlHandler (struct _IRP *, struct _IO_STACK_LOCATION *) 9a3e7418 HEVD!UaFObjectCallbackNonPagedPool (void) |
通过上述操作可找到UseUaFObjectNonPagedPool函数的地址,然后分析该函数调用g_UseAfterFreeObjectNonPagedPool结构体的回调函数位置,定位到9a3e749b
在9a3e749b下断点, 然后再运行exp
1 | bp 9a3e749b |
此处跳转的内存地址是00ab39d0
再步入之前,我们先看下nonPagedPool,看池喷射的效果
g_UseAfterFreeObjectNonPagedPool保存着内核对象_USE_AFTER_FREE_NON_PAGED_POOL的地址
1 | 9a3e4014->8757b948 |
通过dd 8757b948可以看到,当前释放的内核对象_USE_AFTER_FREE_NON_PAGED_POOL->CallBack已经指向ac39d0,后面连续的41(即A),其实这段就是FakeObject。
然后查看内核对象所在的nonPagedPool,这里很明显可以看到每个pool chunk大小都是60,并且每隔一个就是释放的状态,也正好符合我们刚才池喷射的理论。
1 | !pool 8757b948 |
最后一列TAG中,Hack即表示AllocateFakeObjectNonPagedPool调用分配给fakeObject的内存
我们继续跟踪步入该段代码,可以看到和之前分析的payload一致,说明覆盖悬挂指针的内存块成功。
1 2 3 4 5 | kd> !dml_proc Address PID Image file name 86cf38a8 4 System .... 88a8e460 368 HackSysEVDExpl |
断到最后token复制的位置,可以看到将system的token=0x8da01277拷贝给当前进程了。
1 2 3 4 5 6 7 8 | kd> r ecx ecx=88a8e460 #当前进程句柄 kd> r edx edx=8da01277 #sytem进程的token kd> dt nt!_EX_FAST_REF 86cf38a8+f8 # 通过句柄查看system进程的token +0x000 Object : 0x8da01277 Void +0x000 RefCnt : 0y111 +0x000 Value : 0x8da01277 |
0x03 扩展知识
3.1 windows API
3.1.1 DeviceIoControl
DeviceIoControl 将控制代码直接发送到指定的设备驱动程序,使相应的设备执行相应的操作。
语法
1 2 3 4 5 6 7 8 9 10 | BOOL WINAPI DeviceIoControl( _In_ HANDLE hDevice, _In_ DWORD dwIoControlCode, _In_opt_ LPVOID lpInBuffer, _In_ DWORD nInBufferSize, _Out_opt_ LPVOID lpOutBuffer, _In_ DWORD nOutBufferSize, _Out_opt_ LPDWORD lpBytesReturned, _Inout_opt_ LPOVERLAPPED lpOverlapped ); |
- hDevice [in]
需要执行操作的设备句柄。该设备通常是卷,目录,文件或流,使用 CreateFile 函数打开获取设备句柄。
- dwIoControlCode [in]
操作的控制代码,该值标识要执行的特定操作以及执行该操作的设备的类型,每个控制代码的文档都提供了lpInBuffer,nInBufferSize,lpOutBuffer和nOutBufferSize参数的使用细节。
- lpInBuffer [in, optional]
(可选)指向输入缓冲区的指针。这些数据的格式取决于dwIoControlCode参数的值。如果dwIoControlCode指定不需要输入数据的操作,则此参数可以为NULL。
- nInBufferSize [in]
输入缓冲区以字节为单位的大小。单位为字节。
- lpOutBuffer [out, optional]
(可选)指向输出缓冲区的指针。这些数据的格式取决于dwIoControlCode参数的值。如果dwIoControlCode指定不返回数据的操作,则此参数可以为NULL。
- nOutBufferSize [in]
输出缓冲区以字节为单位的大小。单位为字节。
- lpBytesReturned [out, optional]
(可选)指向一个变量的指针,该变量接收存储在输出缓冲区中的数据的大小。如果输出缓冲区太小,无法接收任何数据,则GetLastError返回ERROR_INSUFFICIENT_BUFFER,错误代码122(0x7a),此时lpBytesReturned是零。
- lpOverlapped [in, out, optional]
(可选)指向OVERLAPPED结构的指针。
返回值:
如果操作成功完成,DeviceIoControl将返回一个非零值。
如果操作失败或正在等待,则DeviceIoControl返回零。 要获得扩展的错误信息,请调用GetLastError。
3.1.2 ExAllocatePoolWithTag
ExAllocatePoolWithTag用于内核模式,在堆
中分配指定类型的池内存,并返回指向已分配内存空间的首地址的指针。
1 2 3 4 5 | PVOID ExAllocatePoolWithTag( __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType, SIZE_T NumberOfBytes, ULONG Tag ); |
- PoolType
该参数用来指定想要申请的内存的类型(内核空间中的内存主要分成两类;分页内存区,和未分页内存区)。查询可选的内存区类型可以到MSDN查询POOL_TYPE结构。
如果此值为NonPagedPool,则分配非分页内存。
如果为PagedPool, 则分配内存为分页内存。
- NumberOfBytes
通过该参数指定想要分配的内存的字节数,最好是4的倍数。
- Tag
为将要被分配的空间指定标志(就是给你得到的空间取个独一无二的名字)。
进一步解释:赋给该参数的内容是一个字符串常量,最多可以包含四个字母,该字符串应该放到单引号当中(比如:‘tag1’‘tag2’)。另外,这个字符串常常是逆序的,如,‘1gaT’(所以大家会发现输入这个参数的串确实都是倒过来的。。。)。输入到这个参数中的每一个字符的ASCII值都必须在0-127之间。每次的申请空间的时候都最好应该使用一个独一无二的标识,这样可以帮助调试器和检查器辨认和分析。
- 返回值
如果该函数发现目前系统的自由空间不足,就会返回NULL。否则,将返回指向被分配出来的空间的首地址。
3.1.3 ProbeForRead
检查用户模式缓冲区是否确实驻留在地址空间的用户部分中,并且是否正确对齐。简而言之,就是看看这块内存是否是Ring3的内存,并不检查内存是否可读。如果不存在ring3内存地址空间范围内,则抛出异常。
1 2 3 4 5 | void ProbeForRead( const volatile VOID *Address, SIZE_T Length, ULONG Alignment ); |
- Address
[in] 指定用户模式缓冲区的开始
- Length
[in] 指定用户模式缓冲区的长度(以字节为单位)
- Alignment
[in] 指定用户模式缓冲区开头所需的对齐方式(以字节为单位)。
- 返回值
None
3.1.4 UNREFERENCED_PARAMETER
作用:告诉编译器,已经使用了该变量,不必检测警告!
在VC编译器下,如果您用最高级别进行编译,编译器就会很苛刻地指出您的非常细小的警告。当你声明了一个变量,而没有使用时,编译器就会报警告。
3.1.4 NtAllocateReserveObject
系统调用,负责在内核端创建保留对象–在内核池上执行内存分配,返回适当的Handle等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | #define APC_OBJECT 0 #define IO_COMPLETION_OBJECT 1 #define MAX_OBJECT_ID 1 NTSTATUS STDCALL NtAllocateReserveObject( OUT PHANDLE hObject, IN POBJECT_ATTRIBUTES ObjectAttributes, IN DWORD ObjectType ) { PVOID ObjectBuffer; HANDLE hOutputHandle; NTSTATUS NtStatus; if ( PreviousMode == UserMode ) { /* Validate hObject */ } if ( ObjectType > MAX_OBJECT_ID ) { /* Bail out: STATUS_INVALID_PARAMETER */ }else { NtStatus = ObCreateObject( PreviousMode, PspMemoryReserveObjectTypes[ObjectType], ObjectAttributes, PreviousMode, 0, PspMemoryReserveObjectSizes[ObjectType], 0, 0, &ObjectBuffer ); if ( !NT_SUCCESS( NtStatus ) ) /* Bail out: NtStatus */ memset( ObjectBuffer, 0, PspMemoryReserveObjectSizes[ObjectType] ); if ( ObjectType == IO_COMPLETION ) { /* * * Perform some ObjectBuffer initialization * */ ObjectBuffer[0x0C] = 3; ObjectBuffer[0x20] = PspIoMiniPacketCallbackRoutine; ObjectBuffer[0x24] = ObjectBuffer; ObjectBuffer[0x28] = 0; } NtStatus = ObInsertObjectEx( ObjectBuffer, &hOutputHandle, 0, 0xF0003, 0, 0, 0 ); if ( !NT_SUCCESS( NtStatus ) ) /* Bail out: NtStatus */ *hObject = hOutputHandle; } return(NtStatus); } |
hObject: 分配的内核对象句柄
ObjectType: 目前该参数只有两个值0/1,用于标识两个内核对象UserApcReserve和IoCompletionReserve,IoCompletionReserve对象大小为0x60。
3.2 Preparing Pool Memory
翻译自 Kernel Pool Exploitation on Windows 7 (BlackHat_DC_2011_Mandt_kernelpool-wp)
内核池利用的一个重要方面是能够一致地覆盖所需的内存。 由于内核池的碎片状态使分配的位置无法预测,因此攻击者必须首先使用内核对象或其他可控制的内存分配对内核池进行碎片整理。 在这方面的目标是分配所有空闲块,以使池分配器返回一个新页面。 用相同大小的分配填充新分配的页面,并释放第二个分配,这使攻击者可以为易受攻击的缓冲区创建漏洞。 反过来,这将使攻击者能够溢出用于填充内核池的对象或内存分配。
3.3 池喷射
尝试利用内核池漏洞时,必须处理块(chunks)和池(pool)的元数据。如果你想避免蓝屏,你需要控制一切,因为在块头上会有一些额外的检查。
内核池喷射是一项使池中分配位置可预测的艺术。这意味着你可以知道一个块将被分配到哪里,哪些块在其附近。
如果您想要泄露某些精确的信息或覆盖特定的数据,利用内核池喷射是必须的。
池喷射的基础是分配足够的对象,以确保您控制分配的位置。 Windows为我们提供了许多在不同类型的池中分配对象的工具。例如,我们可以在NonPagedPool(非分页池)中分配ReservedObjects或Semaphore 。关键是要找到与您要控制的池类型相匹配的对象。您选择的对象大小也很重要,因为它与创建后所留的空隙大小直接相关。一旦您选择了对象,您将首先通过大量分配该对象使得池非随机化。
上面申请的对象是属于内核对象,针对内核漏洞的堆喷射,微软有一个内核对象列表,我们可以通过调用用户模式功能来创建内核对象,尽管它不是很完整。
https://msdn.microsoft.com/library/windows/desktop/ms724485(v=vs.85).aspx
有些细节仍然需要注意,否则可能会遇到麻烦:
- 如果您选择的对象的大小不超过0x200字节,这很可能会在lookaside列表中存储相应的释放块,这样这些块的不会被合并。为避免这种情况,您必须释放足够多的对象填充满lookaside列表。
- 您的释放的块可能会落在DeferredFree列表中,并且不会立即合并。所以你必须释放足够多的对象来填充满这个列表,这样才能释放出块制造空隙。
- 最后,你在池中分配对象,这对于整个内核是很常见的。这意味着您刚创建的空隙可能随时被您无法控制的东西分配填充。所以你必须要快!
上述步骤的要点是:
- 通过使用对象的句柄,选择需要释放的块
- 释放足够的块填满lookaside列表
- 释放选定的块
- 免释放足够的块填充DeferredFree列表
- 尽可能快地使用你制造的空隙!
该技术实际应用中会有些改动。
先了解下UAF中的步骤
1.首先申请0x10000个该对象并将指针保存下来;
2.然后再申请0x5000个对象,将指针保存下来;
3.第二步中的0x5000个对象,每隔一个对象释放一个对象;
第一步的操作是将现有的空余堆块都申请出来,第二步中申请出来的堆块应该都是连续的,通过第三步的操作,使得我们申请UAE_AFTER_FREE结构体其前面的堆块应该不是空闲的,因此在释放的时候不会合并,从而再分配的时候出现意外的可能性基本为0。
参考
Windows内核池喷射1-偏内核池介绍 https://www.anquanke.com/post/id/86188
3.4 内核对象
3.4.1 内核对象类型查询
池喷射需要找到适合大小的内核对象
这里使用windbg分析
首先,获取更全面的对象列表
1 | !object \ObjectTypes |
这是一个可以在内核空间中分配的对象的列表。我们可以通过查看更多的细节来探索几个关于它们的重要属性。使用命令 dt nt!_OBJECT_TYPE _OBJECT_TYPE_INITIALIZER 结构的偏移量,它将给我们带来极大的方便。让我们看看它为我们提供了 Mutant 对象的哪些我想要的信息:
1 | dt nt!_OBJECT_TYPE 8521a838 |
然后阅读下 _OBJECT_TYPE_INITIALIZER
1 | dt nt!_OBJECT_TYPE_INITIALIZER 8521a838+28 |
_OBJECT_TYPE_INITIALIZER中有两个关键信息
此对象被分配给的池类型 – 在这里是非分页池(NonPagedPool)
功能偏移(这在实际的漏洞利用部分十分重要)
上面就是可获取到内核对象的一些信息,在实际过程中,分配到内核池的时候大小可能会有一些偏差。
像 用户模式 CreateMutexA调用可在内核中创建Mutant对象,而未命名和命名的mutex大小是不一样的,提供的名称会占用Mutant对象大小。
3.4.2 内核对象实际分析
接下来我们再测试下实际分配到内核池中时的大小怎么查看。
如下运行一段代码,会创建IoCompletionReserve内核对象,并保持不退出。
PS:这边获取到的句柄为0x70
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | from ctypes import * from ctypes.wintypes import * kernel32 = windll.kernel32 ntdll = windll.ntdll def alloc_iocreserve(): IO_COMPLETION_OBJECT = 1 hHandle = HANDLE(0) ntdll.NtAllocateReserveObject(byref(hHandle), 0x0, IO_COMPLETION_OBJECT) hHandle = hHandle.value if hHandle == None: print "[-] Error while creating IoCompletionReserve" return 0 print "Handle: " + hex(hHandle) return hex(hHandle) alloc_iocreserve() variable = raw_input('Press any key to exit...') |
搜索名称为python.exe的进程信息
1 2 3 4 | kd> !process 0 0 python.exe PROCESS 88bd0030 SessionId: 1 Cid: 0b00 Peb: 7ffd3000 ParentCid: 019c DirBase: bf2d0580 ObjectTable: 969c1de8 HandleCount: 40. Image: python.exe |
切换到该进程上下文
1 2 3 4 | kd> .process 88bd0030 ReadVirtual: 88bd0048 not properly sign extended Implicit process is now 88bd0030 WARNING: .cache forcedecodeuser is not enabled |
在当前上下文查询句柄,其中可看到IoCompletionReserve内核对象的地址
1 2 3 4 5 6 7 8 9 | kd> !handle 70 PROCESS 88bd0030 SessionId: 1 Cid: 0b00 Peb: 7ffd3000 ParentCid: 019c DirBase: bf2d0580 ObjectTable: 969c1de8 HandleCount: 40. Image: python.exe Handle table at 969c1de8 with 40 entries in use 0070: Object: 8843a4b8 GrantedAccess: 000f0003 Entry: 8a2780e0 Object: 8843a4b8 Type: (86cf3be0) IoCompletionReserve ObjectHeader: 8843a4a0 (new version) HandleCount: 1 PointerCount: 1 |
这样就可以找到池的位置,如下,每行是一个pool chunk,注意到带*号的 pool chunk就是IoCompletionReserve内核对象在内核池块中实际地址,并且可以看到大小为0x60,根据这个大小我们就可以选取相应的内核对象进行池喷射了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | kd> !pool 8843a4b8 Pool page 8843a4b8 region is Unknown 8843a000 size: 30 previous size: 0 (Allocated) Mmdi 8843a030 size: 18 previous size: 30 (Allocated) MmSi 8843a048 size: 30 previous size: 18 (Allocated) Icp 8843a078 size: 18 previous size: 30 (Allocated) MmSi 8843a090 size: 68 previous size: 18 (Allocated) EtwR (Protected) 8843a0f8 size: 48 previous size: 68 (Allocated) Vad 8843a140 size: 68 previous size: 48 (Allocated) FMsl 8843a1a8 size: 40 previous size: 68 (Allocated) Even (Protected) 8843a1e8 size: 20 previous size: 40 (Allocated) ReTa 8843a208 size: 50 previous size: 20 (Allocated) Vadm 8843a258 size: c8 previous size: 50 (Allocated) Ntfx 8843a320 size: 48 previous size: c8 (Allocated) Vad 8843a368 size: 40 previous size: 48 (Allocated) VM3D 8843a3a8 size: 38 previous size: 40 (Allocated) AlIn 8843a3e0 size: a8 previous size: 38 (Allocated) File (Protected) *8843a488 size: 60 previous size: a8 (Allocated) *IoCo (Protected) Owning component : Unknown (update pooltag.txt) 8843a4e8 size: 40 previous size: 60 (Allocated) Even (Protected) |
如果想看pool chunk具体信息,可如下,PreviousSize BlockSize在32位系统中是实际大小>>3,64位是>>4,所以这里BlockSize=0xc*8=0x60,和上面获取到的一致
1 2 3 4 5 6 7 8 9 10 | kd> dt _POOL_HEADER 8843a488 nt!_POOL_HEADER +0x000 PreviousSize : 0y000010101 (0x15) +0x000 PoolIndex : 0y0000000 (0) +0x002 BlockSize : 0y000001100 (0xc) +0x002 PoolType : 0y0000010 (0x2) +0x000 Ulong1 : 0x40c0015 +0x004 PoolTag : 0xef436f49 +0x004 AllocatorBackTraceIndex : 0x6f49 +0x006 PoolTagHash : 0xef43 |
3.4.3 参考
Windows内核池喷射1-偏内核池介绍 https://www.anquanke.com/post/id/86188
Windows内核池喷射2-合适的内核对象获取 https://www.anquanke.com/post/id/86896
堆喷射 http://huntercht.51vip.biz:8888/zblog/?id=35
0x04 参考
https://gloomyer.com/2019/01/17/uaf-2019-1-17/
Windows Kernel Exploit 内核漏洞学习(1)-UAF https://bbs.pediy.com/thread-252310.htm
https://www.freesion.com/article/9516423134/
Windows堆喷射 https://www.anquanke.com/post/id/85586
Windows堆喷射 https://www.anquanke.com/post/id/85592
内核池块分析: https://www.cnblogs.com/flycat-2016/p/5449738.html
windows 内核池原理: https://blog.csdn.net/qq_38025365/article/details/106259634
内核池利用文档 BlackHat_DC_2011_Mandt_kernelpool-wp.pdf