首页
社区
课程
招聘
CVE-2020-17087整数溢出漏洞分析
2022-3-15 14:04 20974

CVE-2020-17087整数溢出漏洞分析

2022-3-15 14:04
20974

看到GPZ很久之前披露的一个在野cng.sys驱动漏洞,本菜鸟尝试对其进行简单分析,希望能从中学习到一些知识

漏洞描述

漏洞概述:

 

内核驱动模块cng.sys中存在整数溢出漏洞,利用此漏洞进行越界读写,最终可实现本地提取,此漏洞被黑客用于Chrome Sandbox Escape(CVE-2020-15999)

 

影响的windows版本:

 

Windows 10 2004以及之前的版本

漏洞分析

Windows版本:win10 1903 10.0.18362.836

 

根据GPZ给出的Poc,可以定位到漏洞函数cng!CfgAdtpFormatPropertyBlocksize可以被用户控制,6 * size的结果强行转换为无符号的16位整数,0x10000 // 0x6的结果为0x2aa,0x10000 / 6的结果为10922.666666666666,假设控制size的大小为0x2ab,将其乘上6就溢出为2,BCryptAlloc函数内部会调用ExAllocatePoolWithTag或者SkAllocatePool在类型为NonPagedPoolNx池动态申请池空间,此时NumberOfBytes为2,申请的空间就十分小,然而do-while循环的次数为size(即0x2ab),每次向池空间写入6字节,最终导致越界写入,触发BSOD

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
63
64
65
66
__int64 __fastcall CfgAdtpFormatPropertyBlock(char *a1, unsigned __int16 size, __int64 a3)
{
  unsigned int v3; // ebx
  char *v6; // r14
  __int16 v7; // di
  _WORD *pool_ptr; // rax
  _WORD *v9; // rdx
  _WORD *pool_ptr_w; // rcx
  __int64 count; // r8
  _WORD *v12; // rcx
  char v13; // al
 
  v3 = 0;
  v6 = a1;
  if ( a1 && size && a3 )
  {
    v7 = 6 * size;
    pool_ptr = BCryptAlloc((unsigned __int16)(6 * size));// 6 * 0x2aab = 2
    v9 = pool_ptr;
    if ( pool_ptr )
    {
      pool_ptr_w = pool_ptr;
      if ( size )
      {
        count = size;
        do
        {
          // store 6 bytes every time
          *pool_ptr_w = (unsigned __int8)a0123456789abcd[(unsigned __int64)(unsigned __int8)*v6 >> 4];
          v12 = pool_ptr_w + 1;
          v13 = *v6++;
          *v12++ = (unsigned __int8)a0123456789abcd[v13 & 0xF];
          *v12 = 0x20;
          pool_ptr_w = v12 + 1;
          --count;
        }
        while ( count );
      }
      *(_QWORD *)(a3 + 8) = v9;
      *(_WORD *)(a3 + 2) = v7;
      *(_WORD *)a3 = v7 - 2;
    }
    else
    {
      return 0xC000009A;
    }
  }
  else
  {
    return 0xC000000D;
  }
  return v3;
}
 
PVOID __fastcall BCryptAlloc(SIZE_T NumberOfBytes)
{
  char DeviceContext; // al
 
  DeviceContext = (char)WPP_MAIN_CB.Queue.Wcb.DeviceContext;
  if ( !LODWORD(WPP_MAIN_CB.Queue.Wcb.DeviceContext) )
    DeviceContext = GetTrustedEnvironment();
  if ( (DeviceContext & 2) != 0 )
    return (PVOID)SkAllocatePool(0x200i64, NumberOfBytes, 'bgnC');// NonPagedPoolNx
  else
    return ExAllocatePoolWithTag((POOL_TYPE)0x200, NumberOfBytes, 'bgnC');// NonPagedPoolNx
}

漏洞调试

cng!CngDispatch

从驱动的cng!DriverEntry入口函数入手,发现好几个MajorFunction都设置为cng!CngDispatch

1
2
3
4
5
6
7
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)CngDispatch;
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)CngDispatch;
DriverObject->MajorFunction[3] = (PDRIVER_DISPATCH)CngDispatch;
DriverObject->MajorFunction[4] = (PDRIVER_DISPATCH)CngDispatch;
DriverObject->MajorFunction[5] = (PDRIVER_DISPATCH)CngDispatch;
DriverObject->MajorFunction[0xA] = (PDRIVER_DISPATCH)CngDispatch;
DriverObject->MajorFunction[0xE] = (PDRIVER_DISPATCH)CngDispatch;

跟进cng!CngDispatch函数,可以看到switch中以Major Function Code作为条件,当前案例中关注的是case 0xe,宏IRP_MJ_DEVICE_CONTROL的值为0xe,用户态调用DeviceIoControl这个API最终执行到内核驱动模块中的DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]

 

IoCode即为IO控制码,从崩溃的栈帧回溯的执行路径获悉IoCode为0x390400,但是很遗憾,我的分析属于一种根据结果来推断过程(根据崩溃的栈帧),这也是四哥痛骂的那波人之一(我太菜了),代码审计的逻辑是从cng!CngDispatch函数开始,进入到cng!CngDeviceControl函数中根据不同IoCode进行递归,然后进行代码审计。本身来说这个漏洞成因相对简单,需要学习的是寻找触发漏洞路径

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
switch ( CurrentStackLocation->MajorFunction )
{
    ...
    case 0xEu:
        IoCode = CurrentStackLocation->Parameters.Read.ByteOffset.LowPart;
        if ( (IoCode & 3) == 2 && CurrentStackLocation->Parameters.Read.Length )
        {
            ...
        }
        else
        {
            OutputBuffer = irp->AssociatedIrp.MasterIrp;
            InputBuffer = OutputBuffer;
            OutputLen = CurrentStackLocation->Parameters.Read.Length;
        }
        irp->IoStatus.Status = CngDeviceControl(
            InputBuffer,
            CurrentStackLocation->Parameters.Create.Options,// InputLen
            OutputBuffer,
            &OutputLen,
            IoCode,
            irp->RequestorMode);
        irp->IoStatus.Information = OutputLen;
        break;
}

cng!CngDeviceControl

在cng!CngDispatch函数中下断点,然后查看一下cng!CngDeviceControl函数的6个参数,分别为输入缓冲区、输入缓冲区长度、输出缓冲区、输出缓冲区长度的指针、IO控制码、请求模式

1
1: kd> ba e1 /w "@$curprocess.Name == \"poc.exe\"" cng!CngDispatch+85

CVE-2020-17087-1.png

 

从上图的第5个参数可以看到传入的IoCode为0x390400,因为抵达漏洞函数需要执行过函数cng!ConfigIoHandler_Safeguarded,cng!CngDeviceControl函数中存在这样的判断

1
2
if ( IoCode == 0x390400 )
      return ConfigIoHandler_Safeguarded(InputBuffer, NumberOfBytes, (IRP *)OutputBuffer, OutputLen);

cng!ConfigIoHandler_Safeguarded

cng!ConfigIoHandler_Safeguarded使用的参数为cng!CngDeviceControl前四个参数:rcx,rdx,r8,r9

 

跟进cng!ConfigIoHandler_Safeguarded函数,可以看到函数根据输入缓冲区的长度申请了两个大小相同的池块,姑且称为pool1、pool2,将输入缓冲区的内容拷贝至pool1,pool2初始化为0,在后续的分析过程,将pool1的首地址称为pool_ptr1,pool2的首地址称为pool_ptr2

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
__int64 __fastcall ConfigIoHandler_Safeguarded(
        PVOID InputBuffer,
        SIZE_T InputLen,
        PVOID OutputBuffer,
        ULONG *OutputLen)
{
 ...
 
  size = InputLen;
  if ( OutputBuffer )
    v7 = *OutputLen; // v7 = 8
  else
    v7 = 0;
  *OutputLen = 0;
  v8 = 8;
  len = v7; // len = 8
  if ( v7 < 8 )
  {
    if ( v7 )
      memset(OutputBuffer, 0, v7);
    return 0xC0000023;
  }
  else
  {
    v9 = (unsigned int)InputLen;
    pool_ptr1 = BCryptAlloc((unsigned int)InputLen); // allocate pool1
    v11 = BCryptAlloc(v9); // allocate pool2
    pool_ptr2 = v11;
    if ( pool_ptr1 && v11 )
    {
      memmove(pool_ptr1, InputBuffer, v9); // copy InputBuffer to pool1
      memset(pool_ptr2, 0, v9); // clear pool2
      v13 = IoUnpack_SG_ParamBlock_Header(pool_ptr1, size, &v19, pool_ptr2);
      if ( v13 )
      {
        v15 = WinErrorToNtStatus(v13);
      }
      else
      {
        v14 = ConfigFunctionIoHandler(
                v19,
                (int)pool_ptr1,
                size,
                (struct _CRYPT_CONTEXT_FUNCTION_PROVIDERS *)OutputBuffer,
                &len,
                (__int64)pool_ptr2);
        ...
 
      }
      ...
    }
    ...
}

在调试器中查看执行memmove前后的变化,验证以上说法

 

CVE-2020-17087-2.png

cng!IoUnpack_SG_ParamBlock_Header

从伪代码中可以得知调用cng!IoUnpack_SG_ParamBlock_Header的返回值必须为0,否则函数直接返回,于是继续跟进cng!IoUnpack_SG_ParamBlock_Header,函数首先进行pool_ptr1 + 2 < pool_ptr1的判断,然后又进行pool_ptr1 + 2 > (_DWORD )((char )pool_ptr1 + size这样的判断,不太清楚这样做的意义,获取pool1的前4字节,与0x1A2B3C4D进行比较,因此输入缓冲区的前4字节设置为0x1A2B3C4D,第3个参数所指内存会保存pool1的第二个4字节,数值为0x10400,for循环内会将pool2的前8字节置位

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
__int64 __fastcall IoUnpack_SG_ParamBlock_Header(_DWORD *pool_ptr1, unsigned int size, _DWORD *a3, _QWORD *pool_ptr2)
{
  ...
  v4 = 0;
  if ( !pool_ptr1 )
    return 1i64;
  if ( pool_ptr1 + 2 < pool_ptr1 )
    return 1i64;
  v5 = size;
  if ( pool_ptr1 + 2 > (_DWORD *)((char *)pool_ptr1 + size) || *pool_ptr1 != 0x1A2B3C4D )
    return 1i64;
  if ( a3 )
    *a3 = pool_ptr1[1];
  if ( !pool_ptr2 )
    return 0i64;
  v6 = pool_ptr2 + 1;
  if ( pool_ptr2 + 1 >= pool_ptr2 )
  {
    if ( v6 <= (_QWORD *)((char *)pool_ptr2 + size) )
    {
      v7 = 0;
      for ( i = pool_ptr2; !*i; ++i )
      {
        if ( (unsigned int)++v7 >= 8 )
        {
          *pool_ptr2 = 0xFFFFFFFFFFFFFFFFui64;
          return 0i64;
        }
        ...
      }
      ...
    }
    ...
  }
  ...
}

查看pool2在执行完循环前后的变化

1
2
3
4
5
6
1: kd> dq @rax l4
ffffbb0f`7e160000  00000000`00000000 00000000`00000000
ffffbb0f`7e160010  00000000`00000000 00000000`00000000
1: kd> dq @r9 l4
ffffbb0f`7e160000  ffffffff`ffffffff 00000000`00000000
ffffbb0f`7e160010  00000000`00000000 00000000`00000000

cng!ConfigFunctionIoHandler

回到cng!ConfigIoHandler_Safeguarded函数,函数继续调用cng!ConfigFunctionIoHandler函数,第一个参数传入的值为0x10400,HIWORD(a1)返回的结果为1,就会进入到cng!ConfigurationFunctionIoHandler函数,a2、a3、a4、a5、a6对应的是pool_ptr1、InputLen、OutputBuffer、&OutputLen、pool_ptr2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
NTSTATUS __fastcall ConfigFunctionIoHandler(
        unsigned int a1,
        int a2,
        unsigned int a3,
        struct _CRYPT_CONTEXT_FUNCTION_PROVIDERS *a4,
        _DWORD *a5,
        __int64 a6)
{
  switch ( HIWORD(a1) )
  {
    case 0u:
      return RegistrationFunctionIoHandler(a1, a2, a3, (int)a4, (ULONG)a5, a6);
    case 1u:
      return ConfigurationFunctionIoHandler(a1, a2, a3, a4, a5, a6);
    case 2u:
      return ResolutionFunctionIoHandler(a1, a2, a3, (int)a4, (ULONG)a5, a6);
  }
  return 0xC00000AF;
}

在调试器中查看cng!ConfigurationFunctionIoHandler函数的参数

 

CVE-2020-17087-3.png

cng!ConfigurationFunctionIoHandler

cng!ConfigurationFunctionIoHandler函数开头调用cng!IoUnpack_SG_Configuration_ParamBlock,从函数名跟上文的分析来看,该函数会对池块的某些部分进行一些必要的检查并写入一些值,函数的返回值依然需要为0

 

CVE-2020-17087-4.png

cng!IoUnpack_SG_Configuration_ParamBlock

跟进cng!IoUnpack_SG_Configuration_ParamBlock函数,先对pool1的大小与8进行对比,由于我们使用的大小为0x3aab,还不能直接返回0,将pool1的首地址加上0x68得到v18,当前实例中保证pool1 < v18 < (pool1 + poolsize),紧接着就是一连串的判断后决定是否调用cng!IoUnpack_SG_SzString、cng!IoUnpack_SG_ContextFunctionConfig、cng!IoUnpack_SG_Buffer函数,由于a4、a6、a7等都为变量的地址(不为0),故都会进入分支执行这些函数

 

CVE-2020-17087-5.png

 

在调试器中查看一下第一次执行完cng!IoUnpack_SG_SzString函数后的变化

 

CVE-2020-17087-6.png

 

在调试器中查看一下最后一次执行完cng!IoUnpack_SG_ContextFunctionConfig函数后的变化

 

CVE-2020-17087-8.png

 

在调试器中查看一下执行完cng!IoUnpack_SG_Buffer函数后的变化

 

CVE-2020-17087-7.png

 

从上面几张图片可以看到,执行完cng!IoUnpack_SG_SzString、cng!IoUnpack_SG_ContextFunctionConfig、cng!IoUnpack_SG_Buffer函数后,缓冲区偏移0x10、0x18、0x20、0x28、0x30、0x40、0x48、0x58的内容在原有的基础上加上了pool1的首地址

cng!IoUnpack_SG_Buffer

对cng!IoUnpack_SG_Buffer函数进行简单的分析,可以发现函数内向pool_ptr2偏移0x58字节写入8字节的0xff,在pool_ptr2偏移poi(pool_ptr1 + 0x58)(当前数值为0x1000)处开始写入poi(pool_ptr1 + 0x50)(当前数值为0x2ab)字节的0xff

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
63
64
65
...
if ( pool_ptr1_off58 )
  {
    v9 = (__int64)pool_ptr1_off58 - pool_ptr1;
    if ( pool_ptr2 )
    {
      v10 = (char *)(v9 + pool_ptr2);           // v10 = pool_ptr2 + 0x58
      if ( v9 + pool_ptr2 >= pool_ptr2 )
      {
        if ( v10 + 8 >= v10 && (unsigned __int64)(v10 + 8) <= pool_ptr2 + size )
        {
          v11 = 0;
          v12 = (char *)(v9 + pool_ptr2);       // v12 = pool_ptr2 + 0x58
          while ( !*v12 )
          {
            ++v11;
            ++v12;
            if ( v11 >= 8 )
            {
              *(_QWORD *)v10 = 0xFFFFFFFFFFFFFFFFui64;// *(pool_ptr2+0x58) = 0xFFFFFFFFFFFFFFFF
              goto LABEL_10;
            }
            ...
          }
          ...
        }
        ...
      }
      ...
LABEL_10:
    v13 = 0i64;
    if ( *pool_ptr1_off58 != 0xFFFFFFFFFFFFFFFFui64 )
      v13 = *pool_ptr1_off58 + pool_ptr1;
    *pool_ptr1_off58 = v13;                     // *(pool_ptr1 + 0x58) += pool_ptr1
    
 
if ( !v13 )
    return 0i64;
  if ( v13 >= pool_ptr1 && pool_ptr1_off50_val + v13 >= v13 && pool_ptr1_off50_val + v13 <= size + pool_ptr1 )
  {
    v17 = v13 - pool_ptr1;                      // v17 = *(pool_ptr1 + 0x58) - pool_ptr1 = 0x1000
    if ( pool_ptr2 )
    {
      v18 = (void *)(v17 + pool_ptr2);
      if ( v17 + pool_ptr2 >= pool_ptr2 )
      {
        v19 = pool_ptr2 + pool_ptr1_off50_val + v17;// v19 = pool_ptr2 + 0x1000 + 0x2aab
        if ( v19 >= (unsigned __int64)v18 && v19 <= v6 + pool_ptr2 )
        {
          v20 = 0;
          if ( !(_DWORD)pool_ptr1_off50_val )
          {
LABEL_37:
            memset((void *)(v17 + pool_ptr2), 0xFF, pool_ptr1_off50_val);
            return 0i64;
          }
          ...
        }
        ...
      }
      ...
    }
    ...
  }
  ...

在调试器中查看pool2的内容进行验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1: kd> dq 0xffffbb0f7e160058 l1
ffffbb0f`7e160058  ffffffff`ffffffff
1: kd> dq 0xffffbb0f7e161000
ffffbb0f`7e161000  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161010  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161020  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161030  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161040  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161050  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161060  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e161070  ffffffff`ffffffff ffffffff`ffffffff
1: kd> dq 0xffffbb0f7e163aab-10
ffffbb0f`7e163a9b  ffffffff`ffffffff ffffffff`ffffffff
ffffbb0f`7e163aab  dcde6800`00000000 000000ff`8d6f0e3e
ffffbb0f`7e163abb  00000000`00000000 15fab900`00000000
ffffbb0f`7e163acb  000000ff`ffbb0f7e 00000000`00000000
ffffbb0f`7e163adb  00000000`00000000 00000000`00000000
ffffbb0f`7e163aeb  00000000`00000000 00000000`00000000
ffffbb0f`7e163afb  00000000`00000000 00000000`00000000
ffffbb0f`7e163b0b  00000000`00000000 00000000`00000000

执行完以上部分,紧接着就是一组赋值操作,然后返回0

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
if ( a3 )
    *a3 = *(_DWORD *)(pool_ptr1 + 8);
if ( a4 )
    *a4 = *(_QWORD *)(pool_ptr1 + 0x10);
if ( a5 )
    *a5 = *(_DWORD *)(pool_ptr1 + 0x18);
if ( a6 )
    *a6 = *(_QWORD *)(pool_ptr1 + 0x20);
if ( a7 )
    *a7 = *(_QWORD *)(pool_ptr1 + 0x28);
if ( a8 )
    *a8 = *(_QWORD *)(pool_ptr1 + 0x30);
if ( a9 )
    *a9 = *(_DWORD *)(pool_ptr1 + 0x38);
if ( a10 )
    *a10 = *(_QWORD *)(pool_ptr1 + 0x40);
if ( a11 )
    *a11 = *(_QWORD *)(pool_ptr1 + 0x48);
if ( a12 )
    *a12 = *(_DWORD *)(pool_ptr1 + 0x50);
if ( a13 )
    *a13 = *(_QWORD *)(pool_ptr1 + 0x58);
if ( a14 )
    *a14 = *(_QWORD *)(pool_ptr1 + 0x60);
return 0i64;

返回到cng!ConfigurationFunctionIoHandler函数后,由于输入缓冲区的第二个四字节0x10400被截断为0x400,就会执行到这样的分支

1
2
3
4
5
6
7
8
9
if ( a1 == 0x400 )
        return BCryptSetContextFunctionProperty(
                 dwTable,    //*a3=*(_DWORD *)(pool_ptr1 + 8)
                 pszContext, //*a4=*(_QWORD *)(pool_ptr1 + 0x10)
                 dwInterface,//*a5=*(_DWORD *)(pool_ptr1 + 0x18)
                 pszFunction,//*a6=*(_QWORD *)(pool_ptr1 + 0x20)
                 pszProperty,//*a8=*(_QWORD *)(pool_ptr1 + 0x30)
                 cbValue,    //*a12=*(_DWORD *)(pool_ptr1 + 0x50)
                 pbValue);   //*a13=*(_QWORD *)(pool_ptr1 + 0x58)

cng!BCryptSetContextFunctionProperty

跟进cng!BCryptSetContextFunctionProperty函数,继续分析此函数,cng!ValidateTableId所在的分支无法满足条件,利用cbValue和pbValue的值初始化DestinationString,利用此DestinationString调用cng!CfgReg_Acquire

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
NTSTATUS __stdcall BCryptSetContextFunctionProperty(
        ULONG dwTable,                          // pool_ptr1_off_8
        LPCWSTR pszContext,                     // pool_ptr1_off_10
        ULONG dwInterface,                      // pool_ptr1_off_18
        LPCWSTR pszFunction,                    // pool_ptr1_off_20
        LPCWSTR pszProperty,                    // pool_ptr1_off_30
        ULONG cbValue,                          // pool_ptr1_off_50
        PUCHAR pbValue)                         // pool_ptr1_off_58
{
  ...
 
  *(_QWORD *)&DestinationString.Length = 0i64;
  v36 = 0;
  v34 = 0;
  v35 = 0;
  v37 = 0i64;
  v38 = 0i64;
  v39 = 0i64;
  DestinationString.Buffer = 0i64;
  v42 = 0i64;
  if ( !ValidateTableId(dwTable)                // dwTable = 1, false
    || !(unsigned int)ValidateInterfaceId(dwInterface, 0)// dwInterface=3, false
    || !v13                                     // pszFunction, false
    || *v13 == (_WORD)v12                       // *pszFunction != 0, false
    || !pszProperty                             // pszProperty, false
    || *pszProperty == (_WORD)v12 )             // *pszProperty != 0, false
  {
    v16 = 0x57;
LABEL_36:
    if ( !v10 )
      goto LABEL_38;
    goto LABEL_37;
  }
  if ( cbValue && pbValue )                     // true
  {
    *(_QWORD *)&DestinationString.Length = pbValue;
    LODWORD(DestinationString.Buffer) = cbValue;
    HIDWORD(DestinationString.Buffer) = cbValue;
    LODWORD(v42) = v12;
  }
  v14 = CfgReg_Acquire(v11, dwTable, 0x3001Fi64);
  ...
}

cng!CfgReg_Acquire

进一步跟进cng!CfgReg_Acquire,发现cng!VerifyRegistryAccess函数尝试对System\CurrentControlSet\Control\Cryptography\Configuration\Local注册表项执行操作,cng!VerifyRegistryAccess内部调用cng!KeRegOpenKey尝试打开注册表表项

 

CVE-2020-17087-9.png

 

根据cng!VerifyRegistryAccess的返回值为5得知打开失败,函数将5返回给cng!BCryptSetContextFunctionProperty

 

CVE-2020-17087-10.png

 

紧接着在cng!BCryptSetContextFunctionProperty的执行流程大体如下,分别使用pool1_ptr+0x100、pool1_ptr+0x200、pool1_ptr+0x400处的字符初始化三个新的UnicodeString

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
...
 
v14 = CfgReg_Acquire(v11, dwTable, 0x3001Fi64);// v14 = 5
v16 = v14;
if ( v14 )
{
    v17 = v14 == 5; // v17 = true
    goto LABEL_43;
}
 
...
 
LABEL_43:
    if ( !v17 )
      goto LABEL_49;
  }
 
  *(_QWORD *)&DestinationString.Length = 0i64;
  DestinationString.Buffer = 0i64;
  *(_QWORD *)&v40.Length = 0i64;
  v40.Buffer = 0i64;
  *(_QWORD *)&v43.Length = 0i64;
  v43.Buffer = 0i64;
 
  RtlInitUnicodeString(&DestinationString, pszContext);// pool_ptr1_off_10
  RtlInitUnicodeString(&v40, pszFunction); // pool_ptr1_off_20
  RtlInitUnicodeString(&v43, pszProperty); // pool_ptr1_off_30

cng!CfgAdtReportFunctionPropertyModification

在初始化三个UnicodeString后,进入下面的分支调用cng!CfgAdtReportFunctionPropertyModification函数

 

CVE-2020-17087-11.png

 

在调试器中查看一下函数的参数,最少有10个,IDA反编译出来的结果中有11个

 

CVE-2020-17087-12.png

 

从上图中可以看到,在rsp+40的位置存储了0x2aab,这个数值来自于pool_ptr1 + 0x50的位置,查看一下寄存器r14w的数据来源,可以看到函数序言部分将pool_ptr1 + 0x50处的值赋给r14d

 

CVE-2020-17087-13.png

 

cng!CfgAdtReportFunctionPropertyModification函数内部调用最终的漏洞函数cng!CfgAdtpFormatPropertyBlock,调试器中查看一下cng!CfgAdtpFormatPropertyBlock的参数,可以看到第二个参数来自于cng!CfgAdtReportFunctionPropertyModification的第九个参数,数值为0x2aab

 

CVE-2020-17087-14.png

cng!CfgAdtpFormatPropertyBlock

由于漏洞原理在开篇提及,此处就对一些关键位置查看下。查看一下cng!BCryptAlloc的参数,果然溢出为2

 

CVE-2020-17087-15.png

 

调用完该函数申请到的池块大小为0x20字节

 

CVE-2020-17087-16.png

 

然而循环的次数为0x2aab次,每次向池块中写入6字节,最终就会覆写到相邻的内存,最终导致BSOD

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
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000030 rbx=0000000000000000 rcx=ffffda02a29ff000
rdx=ffffda02a29fe610 rsi=0000000000000000 rdi=0000000000000000
rip=fffff80063ae2904 rsp=ffffd7804ace5f20 rbp=0000000000002aab
 r8=0000000000002903  r9=0000000000000002 r10=fffff80063b1ce70
r11=0000000000000002 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr ac po nc
cng!CfgAdtpFormatPropertyBlock+0x88:
fffff800`63ae2904 668901          mov     word ptr [rcx],ax ds:ffffda02`a29ff000=????
Resetting default scope
 
STACK_TEXT: 
ffffd780`4ace5348 fffff800`62cadef2     : ffffda02`a29ff000 00000000`00000003 ffffd780`4ace54b0 fffff800`62b292f0 : nt!DbgBreakPointWithStatus
ffffd780`4ace5350 fffff800`62cad5e7     : fffff800`00000003 ffffd780`4ace54b0 fffff800`62bdc3f0 ffffd780`4ace59f0 : nt!KiBugCheckDebugBreak+0x12
ffffd780`4ace53b0 fffff800`62bc7de7     : fffff800`62e694f8 fffff800`62cd7a45 ffffda02`a29ff000 ffffda02`a29ff000 : nt!KeBugCheck2+0x947
ffffd780`4ace5ab0 fffff800`62c0d19e     : 00000000`00000050 ffffda02`a29ff000 00000000`00000002 ffffd780`4ace5d90 : nt!KeBugCheckEx+0x107
ffffd780`4ace5af0 fffff800`62a9a59f     : fffff800`62a05000 00000000`00000002 00000000`00000000 ffffda02`a29ff000 : nt!MiSystemFault+0x19dcee
ffffd780`4ace5bf0 fffff800`62bd5d5e     : 00000000`00000000 00000000`00000000 ffffda02`a2f84000 00000000`00000fff : nt!MmAccessFault+0x34f
ffffd780`4ace5d90 fffff800`63ae2904     : ffffd780`4ace5fe0 ffffd780`4ace6508 ffffda02`a2f84000 ffff062f`1ac23f52 : nt!KiPageFault+0x35e
ffffd780`4ace5f20 fffff800`63ae224e     : 00000000`00000000 ffffd780`4ace6050 ffffda02`a2f84000 00000000`00000001 : cng!CfgAdtpFormatPropertyBlock+0x88
ffffd780`4ace5f50 fffff800`63ae0282     : 00000000`00000005 ffffd780`4ace6720 ffffda02`a2f84000 ffffda02`a2f83200 : cng!CfgAdtReportFunctionPropertyOperation+0x23e
ffffd780`4ace6470 fffff800`63ac9580     : ffffd780`4ace6720 ffffda02`a2f83100 ffffd780`4ace65f0 ffffda02`a2f83200 : cng!BCryptSetContextFunctionProperty+0x3a2
ffffd780`4ace6570 fffff800`63a92e86     : 00000000`00003aab 00000000`00000008 00000000`00003aab ffffda02`a2f7f000 : cng!_ConfigurationFunctionIoHandler+0x3bd5c
ffffd780`4ace6660 fffff800`63a92d22     : 00000000`00003aab fffff800`62a5c339 ffffda02`a2f82fe0 00000000`00000004 : cng!ConfigFunctionIoHandler+0x4e
ffffd780`4ace66a0 fffff800`63a91567     : 00000000`00000000 fffff800`00003aab 00000000`00000000 00000000`00010400 : cng!ConfigIoHandler_Safeguarded+0xd2
ffffd780`4ace6710 fffff800`63a8e0ea     : 00000000`00000000 ffffda02`a2295ed0 ffffda02`a2295e00 00000000`00000000 : cng!CngDeviceControl+0x97
ffffd780`4ace67e0 fffff800`62a314e9     : ffffda02`a2295e00 00000000`00000000 00000000`00000002 00000000`00000001 : cng!CngDispatch+0x8a
ffffd780`4ace6820 fffff800`62fd6a55     : ffffd780`4ace6b80 ffffda02`a2295e00 00000000`00000001 ffffda02`a2a34820 : nt!IofCallDriver+0x59
ffffd780`4ace6860 fffff800`62fd6860     : 00000000`00000000 ffffd780`4ace6b80 ffffda02`a2295e00 ffffd780`4ace6b80 : nt!IopSynchronousServiceTail+0x1a5
ffffd780`4ace6900 fffff800`62fd5c36     : 000002e2`391a3000 00000000`00000000 00000000`00000000 00000000`00000000 : nt!IopXxxControlFile+0xc10
ffffd780`4ace6a20 fffff800`62bd9558     : 00000000`00000000 00000000`00000000 00000000`00000000 000000fa`99aff5e8 : nt!NtDeviceIoControlFile+0x56
ffffd780`4ace6a90 00007ffc`3233c1a4     : 00007ffc`2fe7eaa7 00000000`00000000 0000ed39`9d02f136 00000000`00000000 : nt!KiSystemServiceCopyEnd+0x28
000000fa`99aff918 00007ffc`2fe7eaa7     : 00000000`00000000 0000ed39`9d02f136 00000000`00000000 000000fa`99aff940 : ntdll!NtDeviceIoControlFile+0x14
000000fa`99aff920 00007ffc`32016430     : 00000000`00390400 00000000`00000024 00007ff7`98f322b8 000000fa`99aff9e8 : KERNELBASE!DeviceIoControl+0x67
000000fa`99aff990 00007ff7`98f311fb     : 000002e2`391a13c0 000002e2`391970a0 00000000`00000000 00000000`00000000 : KERNEL32!DeviceIoControlImplementation+0x80
000000fa`99aff9e0 000002e2`391a13c0     : 000002e2`391970a0 00000000`00000000 00000000`00000000 000000fa`99affa20 : poc+0x11fb
000000fa`99aff9e8 000002e2`391970a0     : 00000000`00000000 00000000`00000000 000000fa`99affa20 00000010`00000008 : 0x000002e2`391a13c0
000000fa`99aff9f0 00000000`00000000     : 00000000`00000000 000000fa`99affa20 00000010`00000008 000000fa`99affa28 : 0x000002e2`391970a0

补丁分析

对比一下补丁前后的文件变化,可以看到只有cng!CfgAdtpFormatPropertyBlock函数发生了变化

 

CVE-2020-17087-17.png

 

查看一下函数的前后变化,发现补丁后函数调用cng!RtlUShortMult函数对传进来的size进行了处理,然后再传给cng!BCryptAlloc函数申请池空间

 

CVE-2020-17087-18.png

 

cng!RtlUShortMult函数十分简短,就是对size * 6的结果进行溢出判断(与0xffff比较)

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall RtlUShortMult(unsigned __int16 a1, __int64 a2, unsigned __int16 *a3)
{
  unsigned int v3; // ecx
  unsigned __int16 v4; // dx
 
  v3 = 6 * a1;
  if ( v3 > 0xFFFF )
    v4 = 0xFFFF;
  else
    v4 = v3;
  *a3 = v4;
  return v3 > 0xFFFF ? 0xC0000095 : 0;
}

CVE-2020-17087-19.png

参考链接

  • https://googleprojectzero.github.io/0days-in-the-wild/0day-RCAs/2020/CVE-2020-17087.html
  • https://msrc.microsoft.com/update-guide/vulnerability/CVE-2020-17087
  • https://www.anquanke.com/post/id/221964

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2022-3-16 07:53 被Jimpp编辑 ,原因:
上传的附件:
收藏
点赞2
打赏
分享
最新回复 (2)
雪    币: 958
活跃值: (349)
能力值: ( LV9,RANK:141 )
在线值:
发帖
回帖
粉丝
hackoflpf 2022-3-16 16:58
2
0
并不觉得这个洞能轻易rce啊,因为能写的内容非常有限,而且不能连续写,并且会溢出超长内容,如何控制这个真是问题
雪    币: 50
活跃值: (945)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
Zero~ 2022-11-10 09:10
3
0
0x10000/0x6的结果为0x2aaa,假设控制size的大小为0x2aab,将其乘上6就溢出为2
游客
登录 | 注册 方可回帖
返回