深入考察微软Windows地址簿中的堆缓冲区溢出漏洞
2021-08-13 12:30:00 Author: www.4hou.com(查看原文) 阅读量:30 收藏

概述 

在本文中,我们将深入分析微软Windows地址簿中的堆缓冲区溢出漏洞。实际上,微软已经在2021年2月的周二补丁中发布了关于这个漏洞的通告。这篇文章将详细介绍什么是微软Windows地址簿,漏洞本身的情况,以及如何构造一个导致含有该漏洞的应用程序崩溃的PoC。

Windows地址簿

Windows地址簿是微软Windows操作系统的一部分,是一项为用户提供集中式联系人列表的服务,微软和第三方应用程序都可以访问和修改地址簿。实际上,Windows地址簿维护着一个本地数据库和接口,用于查找和编辑联系人的信息,并可以使用轻量级目录访问协议(LDAP)查询网络目录服务器。Windows地址簿于1996年推出,后来在Windows Vista中被Windows Contacts所取代,随后,后者在Windows 10中被People App所取代。

Windows地址簿提供了一个API,以便于其他应用程序能够直接使用其数据库和用户界面服务,同时,也方便各种服务访问和修改联系信息。虽然微软已经替换了提供地址簿功能的应用程序,但较新的替换程序仍然利用了原来的功能并确保向后兼容。Windows地址簿功能存在于几个Windows库中,这些库被Windows 10应用程序使用,包括Outlook和Windows Mail。通过这种方式,现代应用程序仍然可以使用Windows地址簿,甚至可以从旧版本的Windows中导入地址簿。

CVE-2021-24083

具体来说,这个堆缓冲区溢出漏洞位于SecurityCheckPropArrayBuffer()函数,该函数隶属于wab32.dll;当这个函数处理联系人的嵌套属性,就有可能触发该漏洞。通过网络攻击该缓冲区溢出漏洞时,只需诱使用户打开一个精心制作的.wab文件即可;为此,攻击者可以在该文件的WAB记录中包含一个恶意的复合属性。

漏洞分析

下面的漏洞分析是基于在Windows 10 x64上运行的10.0.19041.388版本的Windows Address Book Contacts DLL(wab32.dll)的。

Windows Address Book Contacts DLL(即wab32.dll)提供对地址簿API的访问功能;许多应用程序都是借助于这个库与Windows地址簿进行互动的。这个Contacts DLL用于处理与联系人和身份管理有关的操作。其中,Contacts DLL能够导入从早期版本的Windows地址簿中导出的地址簿(即WAB文件)。

实际上,早期版本的Windows地址簿是以.wab文件的形式来维护身份和联系人的数据库的。虽然当前版本的Windows默认不再使用.wab文件,但它们仍然允许从早期安装的Windows地址簿中导入WAB文件。

虽然将WAB文件导入Windows地址簿的方法有多种,但据观察,应用程序通常都是依靠Windows Contacts Import Tool(即C:\Program Files\Windows Mail\wabmig.exe)来导入地址簿的。这个导入工具会加载wab32.dll来处理WAB文件的加载过程,提取相关的联系人,并将其导入到Windows地址簿中。

WAB文件的格式

WAB文件格式(通常被称为Windows地址簿或Outlook地址簿)是一种尚未公开的专有文件格式,群主包含个人身份信息。同时,身份又可能包含联系人,而每个联系人还可能包含一个或多个属性。

尽管该格式尚未公开,但已经有人对该文件格式进行了部分的逆向分析。下面的结构是从一个公开的第三方应用程序和wab32.dll的反汇编代码中获得的。因此,在结构定义、字段名和字段类型方面可能存在不准确之处。

WAB文件具有以下结构:

火狐截图_2021-08-10T09-44-52.453Z.png

除非另有说明,否则所有多字节字段都以小端字节顺序表示。同时,所有字符串字段都是使用Unicode字符,并以UTF16-LE格式编码。

幻数字段包含以下16个字节:9c cb cb 8d 13 75 d2 11 91 58 00 c0 4f 79 56 a4。虽然有些资料将81 32 84 C1 85 05 D0 11 B2 90 00 AA 00 3C F6 76的字节序列列为WAB文件的有效幻数,但通过实验发现,替换该字节序列会妨碍Windows地址簿处理该文件。

编号为1到6的六个表描述符字段都具有以下结构:

火狐截图_2021-08-10T09-47-04.792Z.png

下面是一些已知的表描述符类型的示例:

文本记录(类型:0x84d0):一个包含Unicode字符串的记录。

索引记录(类型:0xFA0):可能包含WAB记录的多个描述符的记录。

每个文本记录都具有以下结构:

火狐截图_2021-08-10T09-48-13.633Z.png

同样,所有索引记录则具有以下结构:

火狐截图_2021-08-10T09-48-57.991Z.png

索引记录中的每个条目(即连续存放的索引记录结构)都有一个偏移量,指向一个WAB记录。

WAB记录

WAB记录用于描述联系人。它包含存储在属性中的电子邮件地址和电话号码等字段,这些字段可以是各种类型,如字符串、整数、GUID和时间戳。每个WAB记录都具有以下结构:

火狐截图_2021-08-10T09-49-30.639Z.png

以下字段是相关的:

RecordProperties字段含有一系列记录属性结构。

PropertyCount字段表示RecordProperties字段内属性的数量。

需要注意的是,记录属性可以是简单的,也可以是复合的。

简单属性

简单属性具有以下结构:

火狐截图_2021-08-10T09-50-30.332Z.png

简单属性的标签小于0x1000,并包括以下内容:

火狐截图_2021-08-10T09-51-41.878Z.png

不过,这里需要注意以下几点:

◼前面列出的属性清单并不完整。关于更多的属性标签定义,请看这里。

◼PtypBinary的值前有一个COUNT字段,用于对16位字进行计数。

◼除上述内容外,还存在以下属性;它们在WAB中的用法尚不清楚:

        ◾PtypEmbeddedTable(0x0000000D):该属性值是一个组件对象模型(COM)对象。

        ◾PtypNull (0x00000001):None:该属性是一个占位符。

        ◾PtypUnspecified (0x00000000):Any:该属性的类型值匹配任何类型。

复合属性

复合属性具有以下结构:

火狐截图_2021-08-10T09-52-53.053Z.png

复合属性的标签大于或等于0x1000,并包括以下内容:

火狐截图_2021-08-10T09-53-41.901Z.png

每个复合属性的Value字段包含NestedPropCount数量的相应类型的简单属性。

如果是固定长度的属性(PtypMultipleInteger16、PtypMultipleInteger32、PtypMultipleGuid和PtypMultipleTime),那么,复合属性的Value字段则包含NestedPropCount数量的相应简单属性的Value字段。

例如,在一个NestedPropCount为4的PtypMultipleInteger32结构中:

◼Size总是16。

◼Value包含四个32位的整数。

对于长度可变的属性(如PtypMultipleString8、PtypMultipleBinary和PtypMultipleString),那么复合属性的Value字段包含NestedPropCount数量的相应简单属性的Size和Value字段。

例如,在一个NestedPropCount为2的PtypMultipleString结构中,包含Unicode字符串“foo”和“bar”:

◼Size为14 00 00 00。

◼Value字段由以下两个字节字符串连接而成:

        ◾“foo”以四个字节的长度进行编码:06 00 00 00 66 00 6f 00 6f 00。

        ◾“bar”以四个字节的长度进行编码:06 00 00 00 62 00 61 00 72 00。

技术细节

当以WAB文件形式导入畸形的Windows地址簿时,就会触发这个漏洞。当用户试图将WAB文件导入Windows地址簿时,会调用WABObjectInternal::Import()方法,该方法又会调用ImportWABFile()方法。对于WAB文件中的每个联系人,ImportWABFile()方法都会执行以下嵌套调用:ImportContact()、CWABStorage::ReadRecord()、ReadRecordWithoutLocking()以及HrGetPropArrayFromFileRecord(). 后面这个函数会接收一个文件的指针作为参数,读取联系人头部,并提取PropertyCount和DataLen字段。函数HrGetPropArrayFromFileRecord()则继续调用SecurityCheckPropArrayBuffer()对导入的文件进行安全检查,并调用HrGetPropArrayFromBuffer()将联系人属性读入一个属性数组。

实际上,函数HrGetPropArrayFromBuffer()在很大程度上依赖于SecurityCheckPropArrayBuffer()执行的检查的正确性。然而,该函数未能对某些属性类型实施安全检查。具体来说,SecurityCheckPropArrayBuffer()可能会跳过检查属性标签未知的嵌套属性的内容,而函数HrGetPropArrayFromBuffer()则继续处理所有的嵌套属性,而不管安全检查结果如何。因此,攻击者可以利用这一点来欺骗函数HrGetPropArrayFromBuffer()解析一个未通过检查的联系人属性。而解析这样的属性的结果是,函数HrGetPropArrayFromBuffer()可能导致堆缓冲区溢出。

代码分析

下面的代码块显示了与此漏洞相关的方法的受影响部分。代码片段用[N]表示的引用标记来进行划分。与此漏洞无关的行被[Truncated]标记所取代。

以下是函数HrGetPropArrayFromFileRecord的伪代码:

[1]
if ( !(unsigned int)SecurityCheckPropArrayBuffer(wab_buffer_full, HIDWORD(uBytes[1]), wab_buffer[3]) )
  {
[2]
    result = 0x8004011b;        // Error
    goto LABEL_25;              // Return prematurely
  }
[3]
  result = HrGetPropArrayFromBuffer(wab_buffer_full, HIDWORD(uBytes[1]), wab_buffer[3], 0, a7);

在[1]处调用函数SecurityCheckPropArrayBuffer()时,会对收到的缓冲区和其中包含的属性进行一系列的安全检查。如果检查顺利通过,那么就认为输入是可以信任的,并通过调用HrGetPropArrayFromBuffer()在[3]处进行相应的处理。否则,该函数在[2]处返回一个错误。

下面是函数SecurityCheckPropArrayBuffer()的伪代码:

    __int64 __fastcall SecurityCheckPropArrayBuffer(unsigned __int8 *buffer_ptr, unsigned int buffer_length, int header_dword_3)
    {
      unsigned int security_check_result; // ebx
      unsigned int remaining_buffer_bytes; // edi
      int l_header_dword_3; // er15
      unsigned __int8 *ptr_to_buffer; // r9
      int current_property_tag; // ecx
      __int64 c_dword_2; // r8
      unsigned int v9; // edi
      int VA; // ecx
      int VB; // ecx
      int VC; // ecx
      int VD; // ecx
      int VE; // ecx
      int VF; // ecx
      int VG; // ecx
      int VH; // ecx
      signed __int64 res; // rax
      _DWORD *ptr_to_dword_1; // rbp
      unsigned __int8 *ptr_to_dword_0; // r14
      unsigned int dword_2; // eax
      unsigned int v22; // edi
      int v23; // esi
      int v24; // ecx
      unsigned __int8 *c_ptr_to_property_value; // [rsp+60h] [rbp+8h]
      unsigned int v27; // [rsp+68h] [rbp+10h]
      unsigned int copy_dword_2; // [rsp+70h] [rbp+18h]
 
      security_check_result = 0;
      remaining_buffer_bytes = buffer_length;
      l_header_dword_3 = header_dword_3;
      ptr_to_buffer = buffer_ptr;
      if ( header_dword_3 )                     
      {
        while ( remaining_buffer_bytes > 4 )       
        {
 
[4]
 
          if ( *(_DWORD *)ptr_to_buffer & 0x1000 ) 
          {
 
[5]
 
            current_property_tag = *(unsigned __int16 *)ptr_to_buffer;
            if ( current_property_tag == 0x1102 ||                   
                 (unsigned int)(current_property_tag - 0x101E) < = 1 )
            {                        
 
[6]
                                      
              ptr_to_dword_1 = ptr_to_buffer + 4;                    
              ptr_to_dword_0 = ptr_to_buffer;
              if ( remaining_buffer_bytes < 0xC )                    
                return security_check_result;                        
              dword_2 = *((_DWORD *)ptr_to_buffer + 2);
              v22 = remaining_buffer_bytes - 0xC;
              if ( dword_2 > v22 )                                    
                return security_check_result;                        
              ptr_to_buffer += 12;
              copy_dword_2 = dword_2;
              remaining_buffer_bytes = v22 - dword_2;
              c_ptr_to_property_value = ptr_to_buffer;               
              v23 = 0;                                               
              if ( *ptr_to_dword_1 > 0u )
              {
                while ( (unsigned int)SecurityCheckSingleValue(
                                        *(_DWORD *)ptr_to_dword_0,
                                        &c_ptr_to_property_value,
                                        ©_dword_2) )
                {
                  if ( (unsigned int)++v23 >= *ptr_to_dword_1 )      
                  {                                                  
                    ptr_to_buffer = c_ptr_to_property_value;
                    goto LABEL_33;
                  }
                }
                return security_check_result;
              }
            }
            else                                                       
            {
             
[7]
 
              if ( remaining_buffer_bytes < 0xC )
                return security_check_result;
              c_dword_2 = *((unsigned int *)ptr_to_buffer + 2);      
              v9 = remaining_buffer_bytes - 12;
              if ( (unsigned int)c_dword_2 > v9 )                    
                return security_check_result;                        
              remaining_buffer_bytes = v9 - c_dword_2;
              VA = current_property_tag - 0x1002;                    
              if ( VA )
              {
                VB = VA - 1;
                if ( VB && (VC = VB - 1) != 0 )
                {
                  VD = VC - 1;
                  if ( VD && (VE = VD - 1) != 0 && (VF = VE - 1) != 0 && (VG = VF - 13) != 0 && (VH = VG - 44) != 0 )
                    res = VH == 8 ? 16i64 : 0i64;
                  else
                    res = 8i64;
                }
                else
                {
                  res = 4i64;
                }
              }
              else
              {
                res = 2i64;
              }
              if ( (unsigned int)c_dword_2 / *((_DWORD *)ptr_to_buffer + 1) != res )
                return security_check_result;                                       
                                                                                      
 
              ptr_to_buffer += c_dword_2 + 12;
            }
          }
          else                                   
          {                                       
 
[8]
 
            if ( remaining_buffer_bytes < 4 )      
              return security_check_result;
            v24 = *(_DWORD *)ptr_to_buffer;        
            c_ptr_to_property_value = ptr_to_buffer + 4;// new exe: v13 = buffer_ptr + 4;
            v27 = remaining_buffer_bytes - 4;      
            if ( !(unsigned int)SecurityCheckSingleValue(v24, &c_ptr_to_property_value, &v27) )
              return security_check_result;
            remaining_buffer_bytes = v27;
            ptr_to_buffer = c_ptr_to_property_value;
          }
    LABEL_33:
          if ( !--l_header_dword_3 )
            break;
        }
      }
      if ( !l_header_dword_3 )
        security_check_result = 1;
      return security_check_result;
    }

在[4]处,将对待处理的属性的标签进行相应的安全检查。这里执行的检查,主要取决于在每个迭代中处理的属性是一个简单属性还是一个复合属性。对于简单属性(即标签低于0x1000的属性),在[8]处继续执行。对简单属性进行以下检查:

◼如果缓冲区内的剩余字节数少于4,该函数将返回一个错误。

◼获得一个指向属性值的指针,并调用SecurityCheckSingleValue()函数对简单属性及其值进行安全检查。函数SecurityCheckSingleValue()会执行安全检查,并增加指针的值,使其指向缓冲区中的下一个属性,这样SecurityCheckPropArrayBuffer()函数就可以在下一次迭代中检查下一个属性了。

◼属性总数将被递减并与零进行比较。如果等于零,那么函数成功返回。如果不同,则循环的下一次迭代会检查下一个属性。

同样,对于复合属性(即标签等于或高于0x1000的属性),将在[5]处继续执行,并进行以下操作。

对于可变长度的复合属性(如果属性标签等于0x1102(PtypMultipleBinary)或等于或小于0x101f(PtypMultipleString)),则在[6]处的代码将进行下面的处理:

◼缓冲区内剩余的可读字节数与0xC进行比较,以避免缓冲区越界。

◼属性的Size字段与剩余的缓冲区长度进行比较,以避免缓冲区越界。

◼对于每个嵌套的属性,都会调用函数SecurityCheckSingleValue()进行如下处理:

     ◾对嵌套属性进行安全检查;

     ◾将指针移动到由调用者持有的缓冲区,以便指向下一个嵌套属性;

◼循环运行,直到联系中人的总属性数(在每次迭代时都会递减)为零为止。

对于固定长度的复合属性(如果有关的属性标签不同于0x1102(PtypMultipleBinary),并且大于0x101f(PtypMultipleString)),从[7]处开始执行下列操作:

1、将缓冲区内剩余的读取字节数与0xC比较,以避免缓冲区越界。

2、将Size与剩余的缓冲区长度进行比较,以避免缓冲区的越界。

3、每个嵌套属性的长度,只取决于属性标签,并且通过父属性标签进行计算。

4、Size除以NestedPropCount,得到每个嵌套属性的大小。

5、如果计算出的子属性长度与从父属性标签推导出的属性大小不同,该函数会返回错误。

6、缓冲区指针根据父属性值的长度递增,以指向下一个属性。

将未知或不可处理的属性类型的嵌套属性长度指定为0x0。

根据我们的观察,如果计算出的属性长度为0,缓冲区指针就会按头部描述的属性值的长度前移。无论属性长度如何,缓冲区都会被推进,通过推进缓冲区,安全检查允许父属性(可能包括子属性)的值保持不被检查的状态。为了使安全检查顺利通过,在步骤4中对固定长度的复合属性进行的除法结果必须为零。因此,为了让一个未知的或不可处理的属性通过安全检查,NestedPropCount必须大于Size。请注意,由于任何属性的字节长度至少为2,所以,NestedPropCount必须始终不能大于Size的二分之一,因此,在正常情况下,上述除法的结果绝不能为零。

检查结束后,如果检查失败,该函数返回0,如果检查通过,则返回1。

随后,函数HrGetPropArrayFromFileRecord()将调用HrGetPropArrayFromBuffer()函数,其目的是将属性收集到_SPropValue结构数组中,并将其返回给调用者。在这里,_SPropValue数组的长度等于属性的数量(由联系人头部给出),并通过调用LocalAlloc()函数在堆中分配内存空间。另外,属性的数量乘以sizeof(_SPropValue),就能得出总的缓冲区大小,具体如下所示:

    if ( !property_array_r )
    {
        ret = -2147024809;
        goto LABEL_71;
    }
    *property_array_r = 0i64;
    header_dword_3_1 = set_to_zero + header_dword_3;
 
[9]
 
    if ( (unsigned int)header_dword_3_1 < header_dword_3      
      || (unsigned int)header_dword_3_1 > 0xAAAAAAA           
      || (v10 = (unsigned int)header_dword_3_1,                              
          property_array = (struct _SPropValue *)LocalAlloc(
                                                   0x40u,
                                                   0x18 * header_dword_3_1),
                                                   // sizeof(_SPropValue) * n_properties_in_binary
        (*property_array_r = property_array) == 0i64) )
    {
        ERROR_INSUFICIENT_MEMORY:
        ret = 0x8007000E;
        goto LABEL_71;
    }

在[9]处,可以看到分配的内存长度为sizeof(_SPropValue) * n_properties_in_binary。紧接着,每个属性结构都会被初始化,其属性标签成员被设置为1。在初始化之后,已经对其进行了安全检查的缓冲区,将对各个属性进行相应的处理:让属性的指针指向一个属性,其属性头部和值的大小由相关的属性提供。

如果特定的循环迭代处理的属性是一个简单属性,那么将执行以下代码:

    if ( !_bittest((const signed int *)¤t_property_tag, 0xCu) )
    {
      if ( v16 < 4 )
        break;
      dword_1 = wab_ulong_buffer_full[1];
      ptr_to_dword_2 = (char *)(wab_ulong_buffer_full + 2);
      v38 = v16 - 4;
      if ( (unsigned int)dword_1 > v38 )
        break;
      current_property_tag = (unsigned __int16)current_property_tag;
      if ( (unsigned __int16)current_property_tag > 0xBu )
      {
 
[10]
 
        v39 = current_property_tag - 0x1E;
        if ( !v39 )
          goto LABEL_79;
        v40 = v39 - 1;
        if ( !v40 )
          goto LABEL_79;
        v41 = v40 - 0x21;
        if ( !v41 )
          goto LABEL_56;
        v42 = v41 - 8;
        if ( v42 )
        {
          if ( v42 != 0xBA )
            goto LABEL_56;
          v43 = dword_1;
          (*property_array_r)[p_idx].Value.bin.lpb = (LPBYTE)LocalAlloc(0x40u, dword_1);
          if ( !(*property_array_r)[p_idx].Value.bin.lpb )
            goto ERROR_INSUFICIENT_MEMORY;
          (*property_array_r)[p_idx].Value.l = dword_1;
          v44 = *(&(*property_array_r)[p_idx].Value.at + 1);
        }
        else
        {
    LABEL_79:
 
[11]
                                           
          v43 = dword_1;
          (*property_array_r)[p_idx].Value.cur.int64 = (LONGLONG)LocalAlloc(0x40u, dword_1);
          v44 = (*property_array_r)[p_idx].Value.dbl;
          if ( v44 == 0.0 )
            goto ERROR_INSUFICIENT_MEMORY;
        }
        memcpy_0(*(void **)&v44, ptr_to_dword_2, v43);
        wab_ulong_buffer_full = (ULONG *)&ptr_to_dword_2[v43];
      }
      else
      {
    LABEL_56:              
 
[12]
           
        memcpy_0(&(*property_array_r)[v15].Value, ptr_to_dword_2, dword_1);
        wab_ulong_buffer_full = (ULONG *)&ptr_to_dword_2[dword_1];
      }
      remaining_bytes_to_process = v38 - dword_1;
      goto NEXT_PROPERTY;
    }
 
[Truncated]
 
    NEXT_PROPERTY:
        ++p_idx;
        processed_property_count = (unsigned int)(processed_property_count_1 + 1);
        processed_property_count_1 = processed_property_count;
        if ( (unsigned int)processed_property_count >= c_header_dword_3 )
          return 0;
      }

在[10]处,属性标签被提取出来并与几个常量进行比较。如果属性标签是0x1e (PtypString8)、0x1f (PtypString)或0x48 (PtypGuid), 那么,将从[11]处继续执行。如果属性标签是0x40(PtypTime)或者无法识别,则从[12]处继续执行。不过,在[12]处的memcpy调用很容易发生堆溢出。

反之,如果在特定的循环迭代中被处理的属性并非简单属性,则执行下面的代码。值得注意的是,当下面的代码被执行时,指针DWORD* wab_ulong_buffer_full指向被处理的属性标签。无论哪个复合属性被处理,在属性标签被识别之前,缓冲区都会提前指向属性值的起始地址,即在第4个32位整数处。

[13]
 
    if ( v16 < 4 )
    break;
    c_dword_1 = wab_ulong_buffer_full[1];
    v19 = v16 - 4;
    if ( v19 < 4 )
    break;
    dword_2 = wab_ulong_buffer_full[2];
    wab_ulong_buffer_full += 3;                    
 
    remaining_bytes_to_process = v19 - 4;
 
[14]
 
    if ( (unsigned __int16)current_property_tag >= 0x1002u )
    {
    if ( (unsigned __int16)current_property_tag < = 0x1007u || (unsigned __int16)current_property_tag == 0x1014 )
        goto LABEL_80;
    if ( (unsigned __int16)current_property_tag == 0x101E )
    {
        [Truncated]
        
    }
    if ( (unsigned __int16)current_property_tag == 0x101F )
    {
        [Truncated]       
    }
    if ( ((unsigned __int16)current_property_tag - 0x1040) & 0xFFFFFFF7 )
    {
        if ( (unsigned __int16)current_property_tag == 0x1102 )
        {
        [Truncated]
        }
    }
    else
    {
    LABEL_80:
 
[15]
 
        (*property_array_r)[p_idx].Value.bin.lpb = (LPBYTE)LocalAlloc(0x40u, dword_2);
        if ( !(*property_array_r)[p_idx].Value.bin.lpb )
        goto ERROR_INSUFICIENT_MEMORY;
        (*property_array_r)[p_idx].Value.l = c_dword_1;
        if ( (unsigned int)dword_2 > remaining_bytes_to_process )
        break;
        memcpy_0((*property_array_r)[p_idx].Value.bin.lpb, wab_ulong_buffer_full, dword_2);
        wab_ulong_buffer_full = (ULONG *)((char *)wab_ulong_buffer_full + dword_2);
        remaining_bytes_to_process -= dword_2;
    }
    }
 
    NEXT_PROPERTY:
        ++p_idx;
        processed_property_count = (unsigned int)(processed_property_count_1 + 1);
        processed_property_count_1 = processed_property_count;
        if ( (unsigned int)processed_property_count >= c_header_dword_3 )
        return 0;
    }

缓冲区在[13]处被推进后,属性标签将与[14]处的几个常量进行比较。最后,[15]处的代码片段将试图处理一个复合属性(即>=0x1000),该属性带有前面的常量没有考虑到的标签。

尽管每种类型的属性的处理逻辑是不相关的,但一个有趣的事实是,如果属性标签无法识别,缓冲区指针仍然被推进到其头部的末端,而且永远不会被收回。实际上,只要满足以下所有条件就会发生这种情况:

属性标签大于或等于0x1002。

属性标签大于0x1007。

属性标签不等于0x1014。

属性标签不等于0x101e。

属性标签不等于0x101f。

属性标签不等于0x1102。

从属性标签中减去0x1040,并将结果与0xFFFFFFF7进行逐位AND运算,其结果为非零。

有趣的是,如果满足上述所有条件,复合属性的属性头部就会被跳过,而下一次循环迭代会将其属性体解释为不同的属性。

因此,通过以下观察结果,我们发现可以使HrGetPropArrayFromBuffer()在堆中分配的_SPropValue数组发生溢出:

如果NestedPropCount大于Size,可以通过精心构造一个未知的或不可处理的属性来绕过安全检查。

可以设法让HrGetPropArrayFromBuffer()将一个精心构造的属性的值解释为一个单独的属性。

PoC

为了根据良性的WAB文件来构造恶意的WAB文件,可以从Windows地址簿的实例中导出一个有效的WAB文件。值得注意的是,Windows XP的Outlook Express提供了将联系人导出为WAB文件的功能。

通过更改WAB文件中的联系人使其具有以下特征,可以修改该良性WAB文件,使其成为恶意文件:

一个包含以下内容的嵌套属性。

一个未知或不可处理类型的标签,例如标签0x1058,并满足以下条件:

必须大于或等于0x1002。

必须大于0x1007。

必须与0x1014、0x101e、0x101f和0x1102不同。

从属性标签中减去0x1040,并使其与0xFFFFFFF7进行逐位AND操作的结果是非零的。

必须不等于0x1002、0x1003、0x1004、0x1005、0x1006、0x1007、0x1014、0x1040和0x1048。

NestedPropCount必须大于Size。

复合属性的值是空的。

一个恶意的简单属性,包含以下内容:

一个不等于0x1e、0x1f、0x40和0x48的属性标签。例如,标签0x0。

Size值大于0x18 x NestedPropCount,以便溢出_SPropValue数组。

未指定数量的尾部字节,以便溢出_SPropValue数组。

最后,当攻击者欺骗毫无戒心的用户导入特制的WAB文件时,就会触发该漏洞,并可能实现代码执行。失败的漏洞利用尝试很可能会导致Windows地址簿导入工具崩溃。

由于ASLR的存在以及脚本引擎的缺失,我们无法在Windows 10环境中通过这个漏洞实现任意代码执行攻击。

小结

在本文中,我们对CVE-2021-24083漏洞进行了深入的考察,希望本文对读者了解这个漏洞能够有所帮助!

本文翻译自:https://blog.exodusintel.com/2021/08/05/analysis-of-a-heap-buffer-overflow-vulnerability-in-microsoft-windows-address-book/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/g8RZ
如有侵权请联系:admin#unsafe.sh