Windows 蓝牙漏洞利用(CVE-2022-44675)
前言
去年,笔者留存的两个蓝牙漏洞被微软修补(CVE-2022-44674,CVE-2022-44675)。其中一个漏洞(CVE-2022-44675)笔者在修补之前已经完成了相对稳定的本地提权利用。
本文将会介绍:
- 该漏洞的细节
- 笔者如何完成的该漏洞的本地提权利用
- 堆风水布置的一些细节
漏洞细节(CVE-2022-44675)
该漏洞是一个位于 bthport.sys 的整形溢出导致的越界写漏洞。
bthport.sys 介绍
bthport.sys 是 Windows 蓝牙总线驱动。其用于
- 和上层总线驱动交互(如USB总线)
- 向下层驱动提供服务(如蓝牙枚举器)
- 向用户态程序提供服务(如 HCI 的 Inquiry 命令)
Bluetooth SDP 规范
Service Discovery Protocol (SDP) 为应用程序提供了一种方法来发现哪些服务可用并确定这些可用服务的特征。1
SDP 是 C/S 架构的。SDP 服务端会维护一个由服务记录组成的 SDP 数据库。SDP 客户端可以通过 SDP 请求向服务端请求服务记录的信息。
服务记录和服务属性
服务记录是一个服务属性列表。每一个服务属性都只描述关于服务的一个特征。一个服务属性包含两个部分:一个属性 ID 和一个属性值。
- 属性 ID 属性ID是一个可以在服务记录中区分服务属性的16位无符号整数。属性 ID 还标识着相关属性值的语义。
- 属性值 属性值是不定长的域。其意义由与其相关的属性 ID 决定。属性值可以包含多种不同类型的信息。
数据表现方式
SDP 定义了一种简单的机制来描述属性 ID、属性 ID 范围和属性值中包含的数据。其使用的基础是数据元素。2
数据元素由一个头区域和数据区域构成。头区域包含两部分,5位的类型描述符和3位的大小索引。
类型描述符
大小描述符
数据区域则是一段长度和意义由头区域定义的字节。
数据表现样例
在 Windows 实现中的漏洞
Windows 在 bthport.sys 中提供了几个 IOCTL 向用户态程序提供了有关于 SDP Service Records 的访问和修改功能。该漏洞便出现在这部分功能之中。
这里列出一些与漏洞有关的功能:
- SdpDB_AddRecord 该功能要求输入一个用数据表达方式呈现的服务记录。如果执行正确,则该服务记录会保存至 SDP 数据库中并返回一个服务记录句柄。
- SdpDB_RemoveRecord 该功能要求输入一个服务记录句柄。如果执行正确,则与当前句柄关联的服务记录会被删除。
- SdpInt_ServiceAndAttributeSearch 该功能要求输入服务 UUID 和属性 ID。正确的执行会返回完整的查询到的服务记录。 注:服务 UUID 是一类特殊的服务属性,其属性 ID 为0x0001,属性值是UUID。
漏洞细节
漏洞的根本原因是代码在累加被查询出来的属性值的大小时,没有对整形溢出进行判断。这会使后续代码分配出一个较小的内存块。该内存块的后续内存会被查询出来属性覆盖。
// ......
while ( v12 != a1 )
{
if ( (a7 || (*((_DWORD *)v12 + 5) & 2) == 0)
// 查找具有服务 UUID 的服务记录并返回。
&& SdpUS_SearchStream(v34, *((unsigned __int8 **)v12 + 5), *((_DWORD *)v12 + 9)) )
{
v14 = *((_DWORD *)v12 + 9);
v15 = (unsigned __int8 *)*((_QWORD *)v12 + 5);
v33 = 0i64;
// 在该服务记录中查找位于特定属性值范围中的属性并返回。
v10 = SdpFindAttributeInStreamInternal(
v15,
v14,
*(struct _SdpAttributeRange **)v35,
*((_DWORD *)v35 + 2),
&v33,
v13);
if ( v10 < 0 )
break;
v16 = v33;
if ( v33 )
{
v17 = v32;
// 将查询出来的结果保存在双链表中。
if ( *v32 != &P )
LABEL_32:
__fastfail(3u);
*((_QWORD *)v33 + 1) = v32;
*(_QWORD *)v16 = &P;
*v17 = v16;
v32 = (PVOID *)v16;
// ------------ BUG HERE ------------
v8 += *((_DWORD *)v16 + 4); // 累加已经保存的属性值的大小。
// 此处并没有对整形溢出进行判断,因此可以通过特殊构造的服务记录来使这里溢出。
}
}
v12 = *(struct _SDP_DATABASE **)v12;
}
// ......
v18 = 4;
if ( v8 <= 0xFFFF )
v18 = 2;
if ( v8 <= 0xFF )
v18 = 1;
v19 = v8 + v18 + 1;
// 溢出的整形会在这里作为内存分配函数的大小参数,然后分配出一个小于真实需要的大小的内存块。
v20 = (unsigned __int8 *)ExAllocatePoolWithTag(NonPagedPoolNx, v19, 0x44706453u);
v21 = v20; if ( !v20 )
{
v27 = 6;
v10 = 0xC000009A;
LABEL_26:
*v13 = v27;
goto LABEL_30;
}
memset(v20, 0, v19);
v22 = a5;
*a4 = v21;
*v22 = v19;
v23 = SdpWriteVariableSizeToStream(6, v8, v21);
v24 = (PVOID *)P;
v25 = v23;
if ( P != &P )
{
do
{
// ------------ Out of Bounds Write ------------
memmove(v25, (char *)v24 + 20, *((unsigned int *)v24 + 4));
v26 = *((unsigned int *)v24 + 4);
v24 = (PVOID *)*v24;
v25 += v26;
}
while ( v24 != &P );
// ......
因此,如果存在两个服务记录其某个属性值的大小为0x8000’0000,那么当我们在查询这两个服务记录中的那个属性值时,就会触发这个漏洞。
举例:
std::uint8_t data[] = {
0x37, 0x80, 0x00, 0x00, 0x1e, // total length minus the header size 5 bytes
0x09, 0x00, 0x01, // Attribute id 1
0x35, 0x11, 0x1c, 0xaa, 0xaa, 0xaa, 0xaa, 0xbb, 0xbb, 0xbb, 0xbb, 0xcc, 0xcc, 0xcc, 0xcc, 0xdd, 0xdd, 0xdd, 0xdd, // GUID
0x09, 0x01, 0x00, // Attribute id 0x100
0x27, 0x80, 0x00, 0x00, 0x00 // Attribute size 0x8000'0000
// ...... more data
};
以上为一个具有两个服务属性的服务记录。其中一个属性为服务 UUID,其属性 ID 为1,属性值为GUID{0xaaaaaaaa, 0xbbbb, 0xbbbb, { 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc } }
;另一个属性为属性 ID 为0x100属性值大小为0x8000’0000的服务属性。如果 SDP 数据库中存在两条这样的服务记录,当我们查询服务 UUID 为GUID{0xaaaaaaaa, 0xbbbb, 0xbbbb, { 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc } }
,且服务属性范围为[0x100,0x100]的属性值时,则两个属性值的大小累加后会上溢。
堆风水细节和漏洞利用
漏洞利用的过程主要的困难点在于,这是一个32位的整型溢出漏洞,这意味它会越界写4GB的内存。所以稳定的利用这个漏洞,需要在内核堆上布置4GB的内存来避免在利用流程在完成之前操作系统崩溃。
Windows 内核堆
网上有很多关于 Windows 内核堆的介绍文章3,并且如果展开的话这将是另一个话题。因此这里仅对其做简单介绍,并对 Segment Alloc 做进一步的介绍。
Windows 内核态的堆主要分为以下四种:
- Low Fragmentation Heap : <= 0x200
- Variable Size: <= 0x2’0000
- Segment Alloc: <= 0x7f’0000
- Large Alloc: > 0x7f’0000
Segment Alloc 是当分配内存的大小位于( 0x20'000, 0x7f0'000 ]
中时采用的分配器。Segment Alloc 由一个 HEAP_SEG_CONTEXT 结构体管理。
Segment Alloc 会分配出来大小可变的段(HEAP_PAGE_SEGMENT),每个段由多个可分配的页和一个 HEAP_PAGE_SEGMENT 头组成。
实际上 Segment Alloc 会根据不同的分配大小,也会有一些细节上的不同。
( 0x2'0000, 0x7'f000 ]
HEAP_PAGE_SEGMENT 大小0x2000,可分配的页紧随其后。( 0x7'f000, 0x7f'0000 ]
HEAP_PAGE_SEGMENT 大小0x2000,可分配的页会位于 HEAP_PAGE_SEGMENT 的起始位置后0x1’0000的位置。
如何布局堆
这个漏洞的堆布局需要满足几个要求。
- 快速 在布局时,因为要布局4GB的内存,布局应当速度够快。这样可以避免这4GB的内存中插入非预期的内存从而导致系统崩溃。
- 稳定 在布局时,需要保证布局的稳定性,保证尽量每次布局都能相同。这样可以保证后续利用顺利进行。
- 可用性好 在布局时,需要保证布局能够有足够的操作空间,降低后续进一步利用的难度。
为了满足快速,在堆布局时应当选择块更大的分配区间,但不应当选择 Large Alloc。因为在经过测试后,Large Alloc 不能满足稳定性要求。所以下一个可选择的也就是 Segment Alloc。如果用 Segment Alloc 这种方式,需要保证单次最大的堆布局块不应当大于0x7’f000。因为如果大于0x7’f000,header和可分配的页中间会存在(0x1’0000-0x2000=0xe000)大小的内存块,这部分内存块可能没有实际的物理内存映射。
最后,应当用0x7’f000大小的内存块来布局这4GB内存,这样可以最大程度满足上述三个要求。
漏洞利用
在克服了4GB堆喷这个难点之后,这个漏洞就会变成一个大小内容可控的越界写漏洞。进一步获取任意地址读取和任意地址写可以通过 NamedPipe4 来做。这里对该技术细节不做详细介绍,仅阐述三个可能遇到的坑点。
- 在堆布局开始以前,需要先留足足够的内存块给操作系统使用,这样可以最大程度上缩减4GB内存布局被打断的情况。
- 如果仅堆喷4GB的话,当最后一个 HEAP_PAGE_SEGMENT 被覆盖之后,操作系统可能在访问 FreeThunkTree 时崩溃。为了避免这种情况,应当在堆喷4GB完成后,再对内存进一步布局,使得 FreeThunkTree 上的非法指针不容易被访问到。
- 为了尽量保证4GB堆喷不被打断,还可以通过提高线程优先级等方式,减少线程被打断的情况。
不足
尽管为了让漏洞利用稳定已经做了非常多的努力,该利用还是无法做到99.9%的稳定性。这是因为堆喷4GB还是需要花费较长的时间,操作系统在这期间会做很多内存操作,这是不可预测的。
引用
[1]: https://www.bluetooth.com/specifications/specs/core-specification-5-3/ (Bluetooth Core Specification, Vol 3: Host, Part B: Service Discovery Protocol (SDP) Specification, 1 INTRODUCTION, 1.1 General Description)
[2]: https://www.bluetooth.com/specifications/specs/core-specification-5-3/ (Bluetooth Core Specification, Vol 3: Host, Part B: Service Discovery Protocol (SDP) Specification, 3 DATA REPRESENTATION)
[3]: https://speakerdeck.com/scwuaptx/windows-kernel-heap-segment-heap-in-windows-kernel-part-1 (Windows Kernel Heap: Segment heap in windows kernel Part 1)
[4]: https://github.com/vp777/Windows-Non-Paged-Pool-Overflow-Exploitation (Windows Non-Paged Pool Overflow Exploitation)