漏洞概述
该漏洞为2021年天府杯中使用的Adobe Reader越界写漏洞,漏洞位于字体解析模块:CoolType.dll中,对应的Adobe Reader版本为:21.007.20099。
原理分析
开启page heap后打开POC,Adobe崩溃于CoolType + 2013E
处:
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000046 ebx=00000002 ecx=a54d102f edx=5ab2f001 esi=34adeb2c edi=5ab2efd0
eip=6cf9013e esp=34ade848 ebp=34adea70 iopl=0 nv up ei ng nz ac po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010293
CoolType!CTInit+0x1cb4e:
6cf9013e 807aff00 cmp byte ptr [edx-1],0 ds:002b:5ab2f000=??
0:005> dd edx -31
5ab2efd0 0000d0c0 00000000 00000000 00000000
5ab2efe0 00000000 00000000 00000000 00000000
5ab2eff0 00000000 00000000 00000000 d0c00000
5ab2f000 ???????? ???????? ???????? ????????
5ab2f010 ???????? ???????? ???????? ????????
5ab2f020 ???????? ???????? ???????? ????????
5ab2f030 ???????? ???????? ???????? ????????
5ab2f040 ???????? ???????? ???????? ????????
从崩溃处可以明显看出越界访问了0x5ab2f000
处的内存,崩溃函数为:CoolType +1FCB0
,下断于该函数查看参数:
0:011> g
Breakpoint 0 hit
eax=0000002e ebx=34fff0e4 ecx=34fff310 edx=73006500 esi=59e06fd0 edi=00000001
eip=6cf8fcb0 esp=34ffef0c ebp=34fff0a8 iopl=0 nv up ei ng nz ac pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297
CoolType!CTInit+0x1c6c0:
6cf8fcb0 55 push ebp
0:011> dps esp+4 L7
34ffef10 59e06fd0
34ffef14 0000002e
34ffef18 34ffefc4
34ffef1c 00000001
34ffef20 00000000
34ffef24 00000001
34ffef28 00000000
0:011> dd 59e06fd0
59e06fd0 d6000800 50015001 51015101 61016101
59e06fe0 62016201 31000100 7a540f51 01521d18
59e06ff0 73006e18 74002000 73006500 d0c07400
59e07000 ???????? ???????? ???????? ????????
59e07010 ???????? ???????? ???????? ????????
59e07020 ???????? ???????? ???????? ????????
59e07030 ???????? ???????? ???????? ????????
59e07040 ???????? ???????? ???????? ????????
传入的参数1为POC中构造的字符串,参数2则为字符串的长度,调试后发现调用了函数MultiToWide
后,传入的字符串变成了崩溃时的内存布局:
if ( !a9 || a10_ff || a5 )
{
v49 = size;
MultiToWide(a5, v12, *(unsigned __int16 *)a3, (void *)v12, (int)&v49);// 内部调用MultiByteToWideCharStub
LOWORD(result) = v49;
*(_WORD *)a3 = v49;
result = (unsigned __int16)result;
goto LABEL_83;
}
......
LABEL_83:
if ( (_WORD)result )
{
v44 = (_BYTE *)(v12 + 1);
v45 = ~v12;
v51 = ~v12;
do
{
if ( *(v44 - 1) || *v44 ) // crash
深入分析MultiToWide
函数,内部调用了MultiByteToWideCharStub
函数,将字符串转化为宽字节字符串:
bool __cdecl MultiToWide(int a1, int lpMultiByteStr, int cbMultiByte, void *MultByte, int MultByteSize)
{
_BYTE *v5; // edx
size_t size; // eax
int v7; // edx
int v8; // ecx
char v9; // al
bool v10; // zf
unsigned __int16 CodePage; // ax
int WideCharSize; // eax
int v14; // esi
int v15; // [esp+10h] [ebp-210h]
size_t MultByteSize_1; // [esp+18h] [ebp-208h]
char lpWideCharStr[512]; // [esp+1Ch] [ebp-204h] BYREF
v5 = (_BYTE *)lpMultiByteStr;
v15 = 0;
size = *(_DWORD *)MultByteSize;
*(_DWORD *)MultByteSize = 0;
MultByteSize_1 = size;
if ( !cbMultiByte )
{
LABEL_4:
v7 = cbMultiByte + lpMultiByteStr;
v8 = 2 * cbMultiByte;
*(_DWORD *)MultByteSize = 2 * cbMultiByte;
if ( 2 * cbMultiByte )
{
do
{
v9 = *(_BYTE *)--v7;
*((char *)MultByte + v8 - 1) = 0;
v10 = v8 == 2;
v8 -= 2;
*((_BYTE *)MultByte + v8) = v9;
}
while ( !v10 );
}
return 1;
}
while ( (unsigned __int8)(*v5 - 0x20) <= 0x5Du )
{
++v5;
if ( ++v15 >= (unsigned int)cbMultiByte )
goto LABEL_4;
}
CodePage = GetCodePage(a1);
WideCharSize = off_82FF304(CodePage, 0, lpMultiByteStr, cbMultiByte, lpWideCharStr, 0x100);// 该函数为MultiByteToWideCharStub
if ( WideCharSize && WideCharSize <= 0x100 )
{
v14 = 2 * WideCharSize;
sub_800C383(MultByte, MultByteSize_1, lpWideCharStr, 2 * WideCharSize);// 内部调用memcpy
*(_DWORD *)MultByteSize = v14;
return 1;
}
*(_DWORD *)MultByteSize = MultByteSize_1;
if ( sub_81443C0(a1, lpMultiByteStr, cbMultiByte, MultByte, (size_t *)MultByteSize) )
return 1;
*(_DWORD *)MultByteSize = MultByteSize_1;
return sub_81442A2(a1, lpMultiByteStr, cbMultiByte, (int)MultByte, MultByteSize) != 0;
}
转换完毕后调用sub_800C383
,检查当前MultByteSize大于等于2倍的WideCharSize时才会将转换后的宽字节字符串拷贝至原字符串的位置,否则将原字符串清空:
size_t __cdecl sub_800C383(void *MultByte, size_t MultByteSize, void *WideChar, size_t WideCharSize_double)
{
size_t v4; // esi
int *v5; // eax
int v7; // [esp-8h] [ebp-Ch]
v4 = WideCharSize_double;
if ( WideCharSize_double )
{
if ( MultByte )
{
if ( WideChar && MultByteSize >= WideCharSize_double )
{
memcpy(MultByte, WideChar, WideCharSize_double);
return 0;
}
else
{
memset(MultByte, 0, MultByteSize);
if ( WideChar )
{
if ( MultByteSize >= WideCharSize_double )
return 0x16;
v5 = errno();
v7 = 0x22;
}
else
{
v5 = errno();
v7 = 0x16;
}
v4 = v7;
*v5 = v7;
invalid_parameter_noinfo();
}
}
else
{
v4 = 0x16;
*errno() = 0x16;
invalid_parameter_noinfo();
}
}
return v4;
}
调试至MultiByteToWideCharStub
函数,转化后WideChar字符串的字符数为0x23个:
0:008> g
Breakpoint 2 hit
eax=000003a8 ebx=1306e8a8 ecx=0b9aea08 edx=1306e8a8 esi=00000024 edi=0b9aec3c
eip=724040e9 esp=0b9ae9d8 ebp=0b9aec0c iopl=0 nv up ei ng nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000282
CoolType!CTGetVersion+0x58ab9:
724040e9 ff1504f35b72 call dword ptr [CoolType!CTGetVersion+0x213cd4 (725bf304)] ds:002b:725bf304={KERNEL32!MultiByteToWideCharStub (75603da0)}
0:008> dps esp L6
0b9ae9d8 000003a8 //CodePage
0b9ae9dc 00000000 //dwFlags
0b9ae9e0 1306e8a8 //lpMultiByteStr
0b9ae9e4 00000024 //cbMultiByte
0b9ae9e8 0b9aea08 //lpWideCharStr
0b9ae9ec 00000100 //cchWideChar
0:008> dd 1306e8a8 Lc
1306e8a8 5001d608 51015001 61015101 62016101
1306e8b8 31016201 7a540f51 01521d18 20736e18
1306e8c8 74736574 74747474 74747474 00007474
0:008> dd 0b9aea08 Lc
0b9aea08 0c4ef818 0c4ef810 0b9aea4c 6f6c53a8
0b9aea18 1306ded0 0c4c5588 00000000 00000000
0b9aea28 0b9aea50 6dc1901f 6dc19024 1fbe9e77
0:008> p
eax=00000023 ebx=1306e8a8 ecx=c7dacb9e edx=0b9aea08 esi=00000024 edi=0b9aec3c
eip=724040ef esp=0b9ae9f0 ebp=0b9aec0c iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
CoolType!CTGetVersion+0x58abf:
724040ef 85c0 test eax,eax
0:008> dd 0b9aea08 L23*2/4 //转换后0x23个字符的WideChar字符串
0b9aea08 003f0008 00010050 00010050 00010051
0b9aea18 00010051 00010061 00010061 00010062
0b9aea28 00010062 00510031 0054000f 0018007a
0b9aea38 0052001d 00180001 0073006e 00740020
0b9aea48 00730065
继续执行到sub_800C383
,由于当前MultiByteSize小于WideCharSize * 2,会执行memset
函数清空MultiByteStr:
0:008> pc
eax=0000002e ebx=1306e8a8 ecx=c7dacb9e edx=0b9aea08 esi=00000046 edi=0b9aec3c
eip=7240410d esp=0b9ae9e0 ebp=0b9aec0c iopl=0 nv up ei ng nz na po cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000283
CoolType!CTGetVersion+0x58add:
7240410d e87182ecff call CoolType!CTInit+0x8d93 (722cc383)
0:008> dps esp L4
0b9ae9e0 1306e8a8 //MultiByteStr
0b9ae9e4 0000002e //MultiByteSize
0b9ae9e8 0b9aea08 //WideCharStr
0b9ae9ec 00000046 //WideCharSize * 2
0:008> dd 1306e8a8 Lc //原MultiByte字符串
1306e8a8 5001d608 51015001 61015101 62016101
1306e8b8 31016201 7a540f51 01521d18 20736e18
1306e8c8 74736574 74747474 74747474 00007474
0:008> p
eax=00000022 ebx=1306e8a8 ecx=7df11661 edx=00000000 esi=00000046 edi=0b9aec3c
eip=72404112 esp=0b9ae9e0 ebp=0b9aec0c iopl=0 nv up ei pl nz ac po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000212
CoolType!CTGetVersion+0x58ae2:
72404112 83c410 add esp,10h
0:008> dd 1306e8a8 Lc //执行函数后被清空
1306e8a8 00000000 00000000 00000000 00000000
1306e8b8 00000000 00000000 00000000 00000000
1306e8c8 00000000 00000000 00000000 00000000
随后遍历被清空的MultByteStr,遍历的次数为转换后的WideChar的大小。当前MultByte的大小为0x2e,WideChar的大小为0x46。因此遍历到超过MultByte的大小时就造成了越界访问。
LABEL_83:
if ( (_WORD)MultSize )
{
v44 = (_BYTE *)(EmptyMultByteStr + 1);
v45 = ~EmptyMultByteStr;
v51 = ~EmptyMultByteStr;
do
{
if ( *(v44 - 1) || *v44 ) //POC崩溃处
{
if ( &v44[v45] != (_BYTE *)offset )
{
*(_BYTE *)(EmptyMultByteStr + offset) = *(v44 - 1);
*(_BYTE *)(offset + EmptyMultByteStr + 1) = *v44;
}
offset += 2;
}
MultSize = *(unsigned __int16 *)a3;
v44 += 2;
v46 = (int)&v44[v45] < MultSize;
v45 = v51;
}
while ( v46 );
}
*(_WORD *)a3 = offset;
return MultSize;
}
利用思路
该漏洞的利用方式和大部分越界写漏洞一致:
-
通过堆喷射ArrayBuffer对象制造MultByteStr大小的内存空洞 -
触发漏洞,MultByteStr位于两个ArrayBuffer对象之间 -
绕过 sub_800C383
的字符串长度校验,将WideChar的字符串覆盖临近ArrayBuffer对象的Length属性值,构造越界写原语 -
通过越界写原语修改下一个临近ArrayBuffer对象的Length属性为0xFFFFFFFF,构造任意地址读写原语
有了思路之后,首先需要确定控制MultByteStr大小的值位于POC中的位置,通过逆向可以得知该值通过一系列运算确定:
int __thiscall GetMultStr(int this, __int16 a2, __int16 a3, __int16 a4, __int16 a5, unsigned __int16 *a6)
{
int v7; // ebx
unsigned __int8 *OriginStr; // esi
__int16 v9; // cx
__int16 v10; // dx
unsigned __int16 MultiByteLength; // cx
int MultiByteStr; // esi
unsigned __int16 v14; // [esp+10h] [ebp-10h]
unsigned __int16 v15; // [esp+14h] [ebp-Ch]
unsigned __int16 v16; // [esp+18h] [ebp-8h]
unsigned __int8 v17; // [esp+1Eh] [ebp-2h]
unsigned __int8 v18; // [esp+1Fh] [ebp-1h]
v7 = 0;
if ( !*(_DWORD *)(this + 8) )
return 0;
OriginStr = *(unsigned __int8 **)(this + 0x18);
if ( !*(_WORD *)(this + 0x14) )
return 0;
while ( 1 )
{
v9 = *OriginStr;
OriginStr += 0xC;
v10 = *(OriginStr - 11) | (unsigned __int16)(v9 << 8);
v16 = _byteswap_ushort(*((_WORD *)OriginStr - 5));
v15 = _byteswap_ushort(*((_WORD *)OriginStr - 4));
v14 = _byteswap_ushort(*((_WORD *)OriginStr - 3));
v18 = *(OriginStr - 2);
MultiByteLength = _byteswap_ushort(*((_WORD *)OriginStr - 2)); //获取MultiByteStr的长度
v17 = *(OriginStr - 1);
if ( a2 == v10 && a3 == v16 && a4 == v15 && a5 == v14 )
break;
if ( (unsigned __int16)++v7 >= *(_WORD *)(this + 0x14) )
return 0;
}
*a6 = MultiByteLength;
MultiByteStr = *(_DWORD *)(this + 4) + *(unsigned __int16 *)(this + 0x16) + (v17 | (v18 << 8)); //获取MultiByteStr
if ( (unsigned __int8)((_DWORD (__stdcall *)(int, _DWORD))sub_801A99B)(MultiByteStr, MultiByteLength) )
return MultiByteStr;
else
return 0;
}
确定MultiByteStr的长度后,会调用malloc函数申请大小为MultiByteStr长度加1的内存空间,并将MultiByteStr拷贝到该块内存中:
while ( 1 )
{
oldMultStr = (void *)GetMultStr(3, v13, (__int16)Src, a4, MultiByte);// 循环获取到str
if ( LOWORD(MultiByte[0]) )
break;
if ( ++v13 > 0xA )
goto LABEL_22;
}
v14 = (void *)alloc(LOWORD(MultiByte[0]) + 1);// 调用malloc申请大小为MultiByteStr长度加1的内存空间
memcpy(v14, oldMultStr, LOWORD(MultiByte[0]));// 拷贝MultiByteStr到新申请的内存中
修改POC使得MultiByteStr的长度为0x10f,通过JS代码堆喷射大小为0x100的ArrayBuffer对象同时制造内存空洞,使得MultiByteStr位于两个ArrayBuffer对象之中,修改后的内存布局如下:
0:009> g
Breakpoint 0 hit
eax=0000010f ebx=0b5aea10 ecx=0b5aec3c edx=0000010f esi=1db0a4a0 edi=00000001
eip=709bfcb0 esp=0b5ae838 ebp=0b5ae9d4 iopl=0 nv up ei ng nz ac pe cy
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000297
CoolType!CTInit+0x1c6c0:
709bfcb0 55 push ebp
0:009> dps esp+4 L7
0b5ae83c 1db0a4a0
0b5ae840 0000010f
0b5ae844 0b5ae8f0
0b5ae848 00000001
0b5ae84c 00000000
0b5ae850 00000001
0b5ae854 00000000
0:009> dd 1db0a4a0 L(110*2+8)/4
//----------------MultiByteStr---------------
1db0a4a0 41004100 41004100 41004100 41004100
1db0a4b0 41004100 41004100 41004100 41004100
1db0a4c0 41004100 41004100 41004100 41004100
1db0a4d0 41004100 41004100 41004100 41004100
1db0a4e0 41004100 41004100 41004100 41004100
1db0a4f0 41004100 41004100 41004100 41004100
1db0a500 41004100 41004100 41004100 41004100
1db0a510 41004100 41004100 41004100 41004100
1db0a520 41004100 41004100 41004100 41004100
1db0a530 41004100 41004100 41004100 41004100
1db0a540 41004100 41004100 41004100 41004100
1db0a550 41004100 41004100 41004100 41004100
1db0a560 41004100 41004100 41004100 41004100
1db0a570 41004100 41004100 41004100 41004100
1db0a580 41004100 41004100 41004100 41004100
1db0a590 41004100 41004100 41004100 41414100
1db0a5a0 41414141 41414141 41414141 00414141
//-------------------------------------------
1db0a5b0 399fd8af 88009700 00000000 00000100 //Arraybuffer.ByteLength = 0x100
1db0a5c0 00000000 00000000 ffffffff ffffffff
1db0a5d0 ffffffff ffffffff ffffffff ffffffff
1db0a5e0 ffffffff ffffffff ffffffff ffffffff
1db0a5f0 ffffffff ffffffff ffffffff ffffffff
1db0a600 ffffffff ffffffff ffffffff ffffffff
1db0a610 ffffffff ffffffff ffffffff ffffffff
1db0a620 ffffffff ffffffff ffffffff ffffffff
1db0a630 ffffffff ffffffff ffffffff ffffffff
1db0a640 ffffffff ffffffff ffffffff ffffffff
1db0a650 ffffffff ffffffff ffffffff ffffffff
1db0a660 ffffffff ffffffff ffffffff ffffffff
1db0a670 ffffffff ffffffff ffffffff ffffffff
1db0a680 ffffffff ffffffff ffffffff ffffffff
1db0a690 ffffffff ffffffff ffffffff ffffffff
1db0a6a0 ffffffff ffffffff ffffffff ffffffff
1db0a6b0 ffffffff ffffffff ffffffff ffffffff
1db0a6c0 ffffffff ffffffff
执行完毕MultiToWide
函数后,临近Arraybuffer对象的长度被覆盖为0x410041:
0:009> dd 1db0a4a0 L(110*2+8)/4
//----------------MultiByteStr---------------
1db0a4a0 00410041 00410041 00410041 00410041
1db0a4b0 00410041 00410041 00410041 00410041
1db0a4c0 00410041 00410041 00410041 00410041
1db0a4d0 00410041 00410041 00410041 00410041
1db0a4e0 00410041 00410041 00410041 00410041
1db0a4f0 00410041 00410041 00410041 00410041
1db0a500 00410041 00410041 00410041 00410041
1db0a510 00410041 00410041 00410041 00410041
1db0a520 00410041 00410041 00410041 00410041
1db0a530 00410041 00410041 00410041 00410041
1db0a540 00410041 00410041 00410041 00410041
1db0a550 00410041 00410041 00410041 00410041
1db0a560 00410041 00410041 00410041 00410041
1db0a570 00410041 00410041 00410041 00410041
1db0a580 00410041 00410041 00410041 00410041
1db0a590 00410041 00410041 00410041 00410041
1db0a5a0 00410041 00410041 00410041 00410041
//-------------------------------------------
1db0a5b0 00410041 00410041 00410041 00410041 //Arraybuffer.ByteLength = 0x410041
1db0a5c0 00000000 00000000 ffffffff ffffffff
1db0a5d0 ffffffff ffffffff ffffffff ffffffff
1db0a5e0 ffffffff ffffffff ffffffff ffffffff
1db0a5f0 ffffffff ffffffff ffffffff ffffffff
1db0a600 ffffffff ffffffff ffffffff ffffffff
1db0a610 ffffffff ffffffff ffffffff ffffffff
1db0a620 ffffffff ffffffff ffffffff ffffffff
1db0a630 ffffffff ffffffff ffffffff ffffffff
1db0a640 ffffffff ffffffff ffffffff ffffffff
1db0a650 ffffffff ffffffff ffffffff ffffffff
1db0a660 ffffffff ffffffff ffffffff ffffffff
1db0a670 ffffffff ffffffff ffffffff ffffffff
1db0a680 ffffffff ffffffff ffffffff ffffffff
1db0a690 ffffffff ffffffff ffffffff ffffffff
1db0a6a0 ffffffff ffffffff ffffffff ffffffff
1db0a6b0 ffffffff ffffffff ffffffff ffffffff
1db0a6c0 ffffffff ffffffff
此时已经具有了越界写的能力,再次修改下一个临近Arraybuffer的对象的长度为0xFFFFFFFF即可完成读写原语的构造,剩下的利用过程大同小异就不再赘述了。最终在Windows 10上完成了整个利用:
总结
该漏洞为Adobe Reader越界写漏洞,由于解析字体将字符串转化为宽字节字符串时
没有进行完整的校验导致越界拷贝,利用的难度不大且触发稳定。原文始发于微信公众号(天玄安全实验室):CVE-2021-44707 Adobe Reader越界写漏洞分析与利用