天玄安全实验室 02月08日
​CVE-2021-44707 Adobe Reader越界写漏洞分析与利用
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文分析了2021年天府杯中使用的Adobe Reader越界写漏洞,该漏洞位于字体解析模块CoolType.dll中,影响版本为21.007.20099。通过开启page heap并打开POC,发现Adobe崩溃于CoolType + 2013E处,明显是越界访问内存。深入分析发现,漏洞是由于MultiToWide函数在将字符串转化为宽字节字符串时,未正确校验字符串长度,导致后续遍历时越界访问,最终触发崩溃。文章还提出了利用思路,通过堆喷射ArrayBuffer对象制造内存空洞,覆盖ArrayBuffer对象的Length属性值,构造任意地址读写原语。

💥漏洞概述:该漏洞是2021年天府杯中使用的Adobe Reader越界写漏洞,存在于字体解析模块CoolType.dll中,影响Adobe Reader版本21.007.20099。

🛠️原理分析:通过调试发现,漏洞触发于函数MultiToWide中,该函数内部调用MultiByteToWideCharStub函数,将字符串转化为宽字节字符串时,由于对字符串长度校验不足,导致后续遍历时发生越界访问,最终导致程序崩溃。

💡利用思路:利用方式与大多数越界写漏洞类似,通过堆喷射ArrayBuffer对象,在MultByteStr大小的内存区域制造空洞,绕过sub_800C383的字符串长度校验。然后,通过覆盖相邻ArrayBuffer对象的Length属性值,构造越界写原语,最终实现任意地址读写。

原创 Joey 2022-11-07 20:17 北京

漏洞概述该漏洞为2021年天府杯中使用的Adobe Reader越界写漏洞

漏洞概述

该漏洞为2021年天府杯中使用的Adobe Reader越界写漏洞,漏洞位于字体解析模块:CoolType.dll中,对应的Adobe Reader版本为:21.007.20099。

原理分析

开启page heap后打开POC,Adobe崩溃于CoolType + 2013E处:

First chance exceptions are reported before any exception handling.This exception may be expected and handled.eax=00000046 ebx=00000002 ecx=a54d102f edx=5ab2f001 esi=34adeb2c edi=5ab2efd0eip=6cf9013e esp=34ade848 ebp=34adea70 iopl=0         nv up ei ng nz ac po cycs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010293CoolType!CTInit+0x1cb4e:6cf9013e 807aff00        cmp     byte ptr [edx-1],0         ds:002b:5ab2f000=??0:005> dd edx -315ab2efd0  0000d0c0 00000000 00000000 000000005ab2efe0  00000000 00000000 00000000 000000005ab2eff0  00000000 00000000 00000000 d0c000005ab2f000  ???????? ???????? ???????? ????????5ab2f010  ???????? ???????? ???????? ????????5ab2f020  ???????? ???????? ???????? ????????5ab2f030  ???????? ???????? ???????? ????????5ab2f040  ???????? ???????? ???????? ????????

从崩溃处可以明显看出越界访问了0x5ab2f000处的内存,崩溃函数为:CoolType +1FCB0,下断于该函数查看参数:

0:011> gBreakpoint 0 hiteax=0000002e ebx=34fff0e4 ecx=34fff310 edx=73006500 esi=59e06fd0 edi=00000001eip=6cf8fcb0 esp=34ffef0c ebp=34fff0a8 iopl=0         nv up ei ng nz ac pe cycs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000297CoolType!CTInit+0x1c6c0:6cf8fcb0 55              push    ebp0:011> dps esp+4 L734ffef10  59e06fd034ffef14  0000002e34ffef18  34ffefc434ffef1c  0000000134ffef20  0000000034ffef24  0000000134ffef28  000000000:011> dd 59e06fd059e06fd0  d6000800 50015001 51015101 6101610159e06fe0  62016201 31000100 7a540f51 01521d1859e06ff0  73006e18 74002000 73006500 d0c0740059e07000  ???????? ???????? ???????? ????????59e07010  ???????? ???????? ???????? ????????59e07020  ???????? ???????? ???????? ????????59e07030  ???????? ???????? ???????? ????????59e07040  ???????? ???????? ???????? ????????

传入的参数1为POC中构造的字符串,参数2则为字符串的长度,调试后发现调用了函数MultiToWide后,传入的字符串变成了崩溃时的内存布局:

if ( !a9 || a10_ff || a5 )    {      v49 = size;      MultiToWide(a5, v12, *(unsigned __int16 *)a3, (void *)v12, (int)&v49);// 内部调用MultiByteToWideCharStub      LOWORD(result) = v49;      *(_WORD *)a3 = v49;      result = (unsigned __int16)result;      goto LABEL_83;    }......LABEL_83:
    if ( (_WORD)result )    {      v44 = (_BYTE *)(v12 + 1);      v45 = ~v12;      v51 = ~v12;
      do      {
        if ( *(v44 - 1) || *v44 )               // crash

深入分析MultiToWide函数,内部调用了MultiByteToWideCharStub函数,将字符串转化为宽字节字符串:

bool __cdecl MultiToWide(int a1, int lpMultiByteStr, int cbMultiByte, void *MultByte, int MultByteSize){  _BYTE *v5; // edx  size_t size; // eax  int v7; // edx  int v8; // ecx  char v9; // al  bool v10; // zf  unsigned __int16 CodePage; // ax  int WideCharSize; // eax  int v14; // esi  int v15; // [esp+10h] [ebp-210h]  size_t MultByteSize_1; // [esp+18h] [ebp-208h]  char lpWideCharStr[512]; // [esp+1Ch] [ebp-204h] BYREF  v5 = (_BYTE *)lpMultiByteStr;  v15 = 0;  size = *(_DWORD *)MultByteSize;  *(_DWORD *)MultByteSize = 0;  MultByteSize_1 = size;
  if ( !cbMultiByte )  {LABEL_4:    v7 = cbMultiByte + lpMultiByteStr;    v8 = 2 * cbMultiByte;    *(_DWORD *)MultByteSize = 2 * cbMultiByte;
    if ( 2 * cbMultiByte )    {
      do      {        v9 = *(_BYTE *)--v7;        *((char *)MultByte + v8 - 1) = 0;        v10 = v8 == 2;        v8 -= 2;        *((_BYTE *)MultByte + v8) = v9;      }
      while ( !v10 );    }
    return 1;  }
  while ( (unsigned __int8)(*v5 - 0x20) <= 0x5Du )  {    ++v5;
    if ( ++v15 >= (unsigned int)cbMultiByte )      goto LABEL_4;  }  CodePage = GetCodePage(a1);  WideCharSize = off_82FF304(CodePage, 0, lpMultiByteStr, cbMultiByte, lpWideCharStr, 0x100);// 该函数为MultiByteToWideCharStub
  if ( WideCharSize && WideCharSize <= 0x100 )  {    v14 = 2 * WideCharSize;    sub_800C383(MultByte, MultByteSize_1, lpWideCharStr, 2 * WideCharSize);// 内部调用memcpy    *(_DWORD *)MultByteSize = v14;
    return 1;  }  *(_DWORD *)MultByteSize = MultByteSize_1;
  if ( sub_81443C0(a1, lpMultiByteStr, cbMultiByte, MultByte, (size_t *)MultByteSize) )
    return 1;  *(_DWORD *)MultByteSize = MultByteSize_1;
  return sub_81442A2(a1, lpMultiByteStr, cbMultiByte, (int)MultByte, MultByteSize) != 0;}

转换完毕后调用sub_800C383,检查当前MultByteSize大于等于2倍的WideCharSize时才会将转换后的宽字节字符串拷贝至原字符串的位置,否则将原字符串清空:

size_t __cdecl sub_800C383(void *MultByte, size_t MultByteSize, void *WideChar, size_t WideCharSize_double){  size_t v4; // esi  int *v5; // eax  int v7; // [esp-8h] [ebp-Ch]  v4 = WideCharSize_double;
  if ( WideCharSize_double )  {
    if ( MultByte )    {
      if ( WideChar && MultByteSize >= WideCharSize_double )      {        memcpy(MultByte, WideChar, WideCharSize_double);
        return 0;      }
      else      {        memset(MultByte, 0, MultByteSize);
        if ( WideChar )        {
          if ( MultByteSize >= WideCharSize_double )
            return 0x16;          v5 = errno();          v7 = 0x22;        }
        else        {          v5 = errno();          v7 = 0x16;        }        v4 = v7;        *v5 = v7;        invalid_parameter_noinfo();      }    }
    else    {      v4 = 0x16;      *errno() = 0x16;      invalid_parameter_noinfo();    }  }
  return v4;}

调试至MultiByteToWideCharStub函数,转化后WideChar字符串的字符数为0x23个:

0:008> gBreakpoint 2 hiteax=000003a8 ebx=1306e8a8 ecx=0b9aea08 edx=1306e8a8 esi=00000024 edi=0b9aec3ceip=724040e9 esp=0b9ae9d8 ebp=0b9aec0c iopl=0         nv up ei ng nz na po nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000282CoolType!CTGetVersion+0x58ab9:724040e9 ff1504f35b72    call    dword ptr [CoolType!CTGetVersion+0x213cd4 (725bf304)] ds:002b:725bf304={KERNEL32!MultiByteToWideCharStub (75603da0)}0:008> dps esp L60b9ae9d8  000003a8 //CodePage0b9ae9dc  00000000 //dwFlags0b9ae9e0  1306e8a8 //lpMultiByteStr0b9ae9e4  00000024 //cbMultiByte0b9ae9e8  0b9aea08 //lpWideCharStr0b9ae9ec  00000100 //cchWideChar0:008> dd 1306e8a8 Lc1306e8a8  5001d608 51015001 61015101 620161011306e8b8  31016201 7a540f51 01521d18 20736e181306e8c8  74736574 74747474 74747474 000074740:008> dd 0b9aea08 Lc0b9aea08  0c4ef818 0c4ef810 0b9aea4c 6f6c53a80b9aea18  1306ded0 0c4c5588 00000000 000000000b9aea28  0b9aea50 6dc1901f 6dc19024 1fbe9e770:008> peax=00000023 ebx=1306e8a8 ecx=c7dacb9e edx=0b9aea08 esi=00000024 edi=0b9aec3ceip=724040ef esp=0b9ae9f0 ebp=0b9aec0c iopl=0         nv up ei pl zr na pe nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246CoolType!CTGetVersion+0x58abf:
724040ef 85c0            test    eax,eax0:008> dd 0b9aea08 L23*2/4 //转换后0x23个字符的WideChar字符串0b9aea08  003f0008 00010050 00010050 000100510b9aea18  00010051 00010061 00010061 000100620b9aea28  00010062 00510031 0054000f 0018007a0b9aea38  0052001d 00180001 0073006e 007400200b9aea48  00730065

继续执行到sub_800C383,由于当前MultiByteSize小于WideCharSize * 2,会执行memset函数清空MultiByteStr:

0:008> pceax=0000002e ebx=1306e8a8 ecx=c7dacb9e edx=0b9aea08 esi=00000046 edi=0b9aec3ceip=7240410d esp=0b9ae9e0 ebp=0b9aec0c iopl=0         nv up ei ng nz na po cycs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000283CoolType!CTGetVersion+0x58add:7240410d e87182ecff      call    CoolType!CTInit+0x8d93 (722cc383)0:008> dps esp L40b9ae9e0  1306e8a8  //MultiByteStr0b9ae9e4  0000002e  //MultiByteSize0b9ae9e8  0b9aea08  //WideCharStr0b9ae9ec  00000046  //WideCharSize * 20:008> dd 1306e8a8 Lc //原MultiByte字符串1306e8a8  5001d608 51015001 61015101 620161011306e8b8  31016201 7a540f51 01521d18 20736e181306e8c8  74736574 74747474 74747474 000074740:008> peax=00000022 ebx=1306e8a8 ecx=7df11661 edx=00000000 esi=00000046 edi=0b9aec3ceip=72404112 esp=0b9ae9e0 ebp=0b9aec0c iopl=0         nv up ei pl nz ac po nccs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000212CoolType!CTGetVersion+0x58ae2:72404112 83c410          add     esp,10h0:008> dd 1306e8a8 Lc //执行函数后被清空1306e8a8  00000000 00000000 00000000 000000001306e8b8  00000000 00000000 00000000 000000001306e8c8  00000000 00000000 00000000 00000000

随后遍历被清空的MultByteStr,遍历的次数为转换后的WideChar的大小。当前MultByte的大小为0x2e,WideChar的大小为0x46。因此遍历到超过MultByte的大小时就造成了越界访问。

LABEL_83:
    if ( (_WORD)MultSize )    {      v44 = (_BYTE *)(EmptyMultByteStr + 1);      v45 = ~EmptyMultByteStr;      v51 = ~EmptyMultByteStr;
      do      {
        if ( *(v44 - 1) || *v44 )  //POC崩溃处        {
          if ( &v44[v45] != (_BYTE *)offset )          {            *(_BYTE *)(EmptyMultByteStr + offset) = *(v44 - 1);            *(_BYTE *)(offset + EmptyMultByteStr + 1) = *v44;          }          offset += 2;        }        MultSize = *(unsigned __int16 *)a3;        v44 += 2;        v46 = (int)&v44[v45] < MultSize;        v45 = v51;      }
      while ( v46 );    }    *(_WORD *)a3 = offset;
    return MultSize;  }

利用思路

该漏洞的利用方式和大部分越界写漏洞一致:

    通过堆喷射ArrayBuffer对象制造MultByteStr大小的内存空洞

    触发漏洞,MultByteStr位于两个ArrayBuffer对象之间

    绕过sub_800C383的字符串长度校验,将WideChar的字符串覆盖临近ArrayBuffer对象的Length属性值,构造越界写原语

    通过越界写原语修改下一个临近ArrayBuffer对象的Length属性为0xFFFFFFFF,构造任意地址读写原语

有了思路之后,首先需要确定控制MultByteStr大小的值位于POC中的位置,通过逆向可以得知该值通过一系列运算确定:

int __thiscall GetMultStr(int this, __int16 a2, __int16 a3, __int16 a4, __int16 a5, unsigned __int16 *a6){  int v7; // ebx  unsigned __int8 *OriginStr; // esi  __int16 v9; // cx  __int16 v10; // dx  unsigned __int16 MultiByteLength; // cx  int MultiByteStr; // esi  unsigned __int16 v14; // [esp+10h] [ebp-10h]  unsigned __int16 v15; // [esp+14h] [ebp-Ch]  unsigned __int16 v16; // [esp+18h] [ebp-8h]  unsigned __int8 v17; // [esp+1Eh] [ebp-2h]  unsigned __int8 v18; // [esp+1Fh] [ebp-1h]  v7 = 0;
  if ( !*(_DWORD *)(this + 8) )
    return 0;  OriginStr = *(unsigned __int8 **)(this + 0x18);
  if ( !*(_WORD *)(this + 0x14) )
    return 0;
  while ( 1 )  {    v9 = *OriginStr;    OriginStr += 0xC;    v10 = *(OriginStr - 11) | (unsigned __int16)(v9 << 8);    v16 = _byteswap_ushort(*((_WORD *)OriginStr - 5));    v15 = _byteswap_ushort(*((_WORD *)OriginStr - 4));    v14 = _byteswap_ushort(*((_WORD *)OriginStr - 3));    v18 = *(OriginStr - 2);    MultiByteLength = _byteswap_ushort(*((_WORD *)OriginStr - 2)); //获取MultiByteStr的长度    v17 = *(OriginStr - 1);
    if ( a2 == v10 && a3 == v16 && a4 == v15 && a5 == v14 )
      break;
    if ( (unsigned __int16)++v7 >= *(_WORD *)(this + 0x14) )
      return 0;  }  *a6 = MultiByteLength;  MultiByteStr = *(_DWORD *)(this + 4) + *(unsigned __int16 *)(this + 0x16) + (v17 | (v18 << 8)); //获取MultiByteStr
  if ( (unsigned __int8)((_DWORD (__stdcall *)(int, _DWORD))sub_801A99B)(MultiByteStr, MultiByteLength) )
    return MultiByteStr;
  else
    return 0;}

确定MultiByteStr的长度后,会调用malloc函数申请大小为MultiByteStr长度加1的内存空间,并将MultiByteStr拷贝到该块内存中:

while ( 1 )  {    oldMultStr = (void *)GetMultStr(3, v13, (__int16)Src, a4, MultiByte);// 循环获取到str
    if ( LOWORD(MultiByte[0]) )
      break;
    if ( ++v13 > 0xA )      goto LABEL_22;  }  v14 = (void *)alloc(LOWORD(MultiByte[0]) + 1);// 调用malloc申请大小为MultiByteStr长度加1的内存空间  memcpy(v14, oldMultStr, LOWORD(MultiByte[0]));// 拷贝MultiByteStr到新申请的内存中

修改POC使得MultiByteStr的长度为0x10f,通过JS代码堆喷射大小为0x100的ArrayBuffer对象同时制造内存空洞,使得MultiByteStr位于两个ArrayBuffer对象之中,修改后的内存布局如下:

0:009> gBreakpoint 0 hiteax=0000010f ebx=0b5aea10 ecx=0b5aec3c edx=0000010f esi=1db0a4a0 edi=00000001eip=709bfcb0 esp=0b5ae838 ebp=0b5ae9d4 iopl=0         nv up ei ng nz ac pe cycs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000297CoolType!CTInit+0x1c6c0:709bfcb0 55              push    ebp0:009> dps esp+4 L70b5ae83c  1db0a4a00b5ae840  0000010f0b5ae844  0b5ae8f00b5ae848  000000010b5ae84c  000000000b5ae850  000000010b5ae854  000000000:009> dd 1db0a4a0 L(110*2+8)/4//----------------MultiByteStr---------------1db0a4a0  41004100 41004100 41004100 410041001db0a4b0  41004100 41004100 41004100 410041001db0a4c0  41004100 41004100 41004100 410041001db0a4d0  41004100 41004100 41004100 410041001db0a4e0  41004100 41004100 41004100 410041001db0a4f0  41004100 41004100 41004100 410041001db0a500  41004100 41004100 41004100 410041001db0a510  41004100 41004100 41004100 410041001db0a520  41004100 41004100 41004100 410041001db0a530  41004100 41004100 41004100 410041001db0a540  41004100 41004100 41004100 410041001db0a550  41004100 41004100 41004100 410041001db0a560  41004100 41004100 41004100 410041001db0a570  41004100 41004100 41004100 410041001db0a580  41004100 41004100 41004100 410041001db0a590  41004100 41004100 41004100 414141001db0a5a0  41414141 41414141 41414141 00414141//-------------------------------------------1db0a5b0  399fd8af 88009700 00000000 00000100 //Arraybuffer.ByteLength = 0x1001db0a5c0  00000000 00000000 ffffffff ffffffff1db0a5d0  ffffffff ffffffff ffffffff ffffffff1db0a5e0  ffffffff ffffffff ffffffff ffffffff1db0a5f0  ffffffff ffffffff ffffffff ffffffff1db0a600  ffffffff ffffffff ffffffff ffffffff1db0a610  ffffffff ffffffff ffffffff ffffffff1db0a620  ffffffff ffffffff ffffffff ffffffff1db0a630  ffffffff ffffffff ffffffff ffffffff1db0a640  ffffffff ffffffff ffffffff ffffffff1db0a650  ffffffff ffffffff ffffffff ffffffff1db0a660  ffffffff ffffffff ffffffff ffffffff1db0a670  ffffffff ffffffff ffffffff ffffffff1db0a680  ffffffff ffffffff ffffffff ffffffff1db0a690  ffffffff ffffffff ffffffff ffffffff1db0a6a0  ffffffff ffffffff ffffffff ffffffff1db0a6b0  ffffffff ffffffff ffffffff ffffffff1db0a6c0  ffffffff ffffffff

执行完毕MultiToWide函数后,临近Arraybuffer对象的长度被覆盖为0x410041:

0:009> dd 1db0a4a0 L(110*2+8)/4//----------------MultiByteStr---------------1db0a4a0  00410041 00410041 00410041 004100411db0a4b0  00410041 00410041 00410041 004100411db0a4c0  00410041 00410041 00410041 004100411db0a4d0  00410041 00410041 00410041 004100411db0a4e0  00410041 00410041 00410041 004100411db0a4f0  00410041 00410041 00410041 004100411db0a500  00410041 00410041 00410041 004100411db0a510  00410041 00410041 00410041 004100411db0a520  00410041 00410041 00410041 004100411db0a530  00410041 00410041 00410041 004100411db0a540  00410041 00410041 00410041 004100411db0a550  00410041 00410041 00410041 004100411db0a560  00410041 00410041 00410041 004100411db0a570  00410041 00410041 00410041 004100411db0a580  00410041 00410041 00410041 004100411db0a590  00410041 00410041 00410041 004100411db0a5a0  00410041 00410041 00410041 00410041//-------------------------------------------1db0a5b0  00410041 00410041 00410041 00410041 //Arraybuffer.ByteLength = 0x4100411db0a5c0  00000000 00000000 ffffffff ffffffff1db0a5d0  ffffffff ffffffff ffffffff ffffffff1db0a5e0  ffffffff ffffffff ffffffff ffffffff1db0a5f0  ffffffff ffffffff ffffffff ffffffff1db0a600  ffffffff ffffffff ffffffff ffffffff1db0a610  ffffffff ffffffff ffffffff ffffffff1db0a620  ffffffff ffffffff ffffffff ffffffff1db0a630  ffffffff ffffffff ffffffff ffffffff1db0a640  ffffffff ffffffff ffffffff ffffffff1db0a650  ffffffff ffffffff ffffffff ffffffff1db0a660  ffffffff ffffffff ffffffff ffffffff1db0a670  ffffffff ffffffff ffffffff ffffffff1db0a680  ffffffff ffffffff ffffffff ffffffff1db0a690  ffffffff ffffffff ffffffff ffffffff1db0a6a0  ffffffff ffffffff ffffffff ffffffff1db0a6b0  ffffffff ffffffff ffffffff ffffffff1db0a6c0  ffffffff ffffffff

此时已经具有了越界写的能力,再次修改下一个临近Arraybuffer的对象的长度为0xFFFFFFFF即可完成读写原语的构造,剩下的利用过程大同小异就不再赘述了。终在Windows 10上完成了整个利用:


总结

该漏洞为Adobe Reader越界写漏洞,由于解析字体将字符串转化为宽字节字符串时没有进行完整的校验导致越界拷贝,利用的难度不大且触发稳定。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Adobe Reader 越界写漏洞 CoolType.dll 漏洞分析 漏洞利用
相关文章