CVE: CVE-2021-1758
Tested Versions:
- macOS Catalina 10.15.4 (19E287)
Product URL(s):
Description of the vulnerability
This vulnerability exists in libFontParser.dylib
, a part of CoreText
library is widely used in macOS, iOS, iPadOS to parse, and draw text. This vulnerability allows attacker to read memory of application which uses API from CoreText
.
macOS/iOS creates a font format structure that is a wrapper of Type 1 Postscript Font and TrueType Font is Mac Resource Fork Font. CoreText
is a framework to draw text that supports load Mac Resource Fork Font through API CoreText
CTFontManagerCreateFontDescriptorsFromURL
.
The first 12 bytes of Mac Resource Fork Font is header length, resource data offset, resource data length. The Mac Resource Fork Font Header is calculated by adding the resource data offset with the begin of file. The vulnerability exists in libFontParser.dylib
CheckMapCommon
which handles Mac Resource Fork Font Header validation.
__int64 __fastcall CheckMapCommon(unsigned int *buf, __int64 rfork_header_ptr)
{
...
rfork_len_map = _byteswap_ulong(buf[3]);
rfork_len = _byteswap_ulong(buf[2]);
v4 = rfork_len + _byteswap_ulong(*buf);
rfork_headr_ptr = rfork_len_map + _byteswap_ulong(buf[1]);
if ( v4 > rfork_headr_ptr )
rfork_headr_ptr = v4;
if ( rfork_headr_ptr > 0xFFFFFE )
goto LABEL_25;
offset_to_typelist = *(unsigned __int16 *)(rfork_header_ptr + 24);
if ( _bittest(&offset_to_typelist, 8u) )
goto LABEL_25;
offset_to_typelist_ = __ROL2__(offset_to_typelist, 8);
end_map_ptr = rfork_header_ptr + rfork_len_map;
cur_rfork_header_ptr = rfork_header_ptr + offset_to_typelist_ + 2;
if ( cur_rfork_header_ptr > end_map_ptr )
goto LABEL_25;
v10 = (_WORD *)(rfork_header_ptr + offset_to_typelist_);
n_in_type = __ROL2__(*v10, 8) + 1; // n_in_type = 0xFFFF + 1 = 0
if ( n_in_type < 0 || end_map_ptr < cur_rfork_header_ptr + 8LL * n_in_type )
goto LABEL_25;
offset_typelist = *(_WORD *)(rfork_header_ptr + 26);
if ( offset_typelist == -1 )
{
v13 = 0LL;
}
else
{
v13 = (unsigned __int16)__ROL2__(offset_typelist, 8) + rfork_ptr;
if ( v13 > end_map_ptr )
goto LABEL_25;
}
if ( n_in_type <= 0 ) // bug here will skip the rest of validation
return 0; // return true
...
The Mac Resource Header has 3 values: offset_to_typelist, offset_to_name_type_list, num_type_list are \*(\_WORD \*)(rfork_header_ptr + 24)
, \*(\_WORD \*)(rfork_header_ptr + 26)
, \*(\_WORD \*)(rfork_header_ptr + 28)
The function libFontParser.dylib
CheckMapCommon
can be corrupted by modified offset_to_typelist
point to memory pointer contains 0xFFFF (n_in_type) and then n_in_type is automatically added by 1, as result n_in_type is zero. After that libFontParser.dylib
CheckMapCommon
will check n_in_type less or equal to zero, if it is return true.
Later, libFontParser.dylib
will invoke GetResourcePtrCommon
to get resource from resource map.
_DWORD *__fastcall GetResourcePtrCommon(__int64 a1, void *tag_name, __int16 a3, int a4, _QWORD *a5, _DWORD *a6, __int128 a7)
{
num_typelist = __ROL2__(rfork_header_ptr[14], 8) + 1;
typelist_ptr = (signed __int64)(a7 == 0 ? 0LL : (_WORD *)((char *)rfork_header_ptr + (unsigned __int16)__ROL2__(rfork_header_ptr[13], 8)));
if ( num_typelist <= 0 )
return 0LL;
v10 = num_typelist;
cur_ptr = rfork_header_ptr + 18;
result = 0LL;
while ( _byteswap_ulong(*(_DWORD *)(cur_ptr - 3)) != (_DWORD)tag_name )
{
cur_ptr += 4;
v13 = __OFSUB__(v10--, 1);
if ( (unsigned __int8)((v10 < 0) ^ v13) | (v10 == 0) )
return result;
}
v29 = a6;
v28 = a1;
v30 = a5;
v14 = (unsigned __int16)__ROL2__(*(cur_ptr - 1), 8) + 1;
relist_offset = (_DWORD *)((char *)rfork_header_ptr + (unsigned __int16)__ROL2__(*cur_ptr, 8) + 32);
v16 = v31 - 1;
while ( !(_QWORD)a7 )
{
if ( v31 == -1 )
{
if ( __ROL2__(*((_WORD *)relist_offset - 2), 8) == attribue )
goto LABEL_21;
}
else if ( !v16 )
{
goto LABEL_21;
}
LABEL_19:
relist_offset += 3;
--v16;
v13 = __OFSUB__(v14--, 1);
if ( (unsigned __int8)((v14 < 0) ^ v13) | (v14 == 0) )
return 0LL;
}
v17 = *((_WORD *)relist_offset - 1);
if ( v17 == -1 || !(unsigned __int8)_EqualString(a7, v9 + (unsigned __int16)__ROL2__(v17, 8), 0LL, 1LL) )
goto LABEL_19;
LABEL_21:
v18 = *vptr & 0xFFFFFF00;
if ( v29 )
*v29 = (signed __int16)__ROL2__(*((_WORD *)vptr - 2), 8);
off_1 = _byteswap_ulong(v18);
if ( *((_QWORD *)&a7 + 1) )
{
typelist_name_offset = *((_WORD *)vptr - 1);
if ( typelist_name_offset == -1 )
**((_BYTE **)&a7 + 1) = 0;
else
memmove( // abuse be OOB read
*((void **)&a7 + 1),
(const void *)(typelist_ptr + (unsigned __int16)__ROL2__(typelist_name_offset, 8)),
*(unsigned __int8 *)(typelist_ptr + (unsigned __int16)__ROL2__(typelist_name_offset, 8)) + 1LL);// sadly length of typelist_name is only maxium 0x100
}
}
Because libFontParser.dylib
CheckMapCommon
does not validate typelist_name_offset
for each resource entry, so an attacker can modify this value as they want, leading to an Out-Of-Bounds read.
Reproduce vulnerability
Validate vulnerability with harness.cc
clang++ harness.cc -o harness -framework ApplicationServices -framework CoreFoundation -framework CoreText -framework CoreGraphics --std=c++14 -fsanitize=address
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib ./harness ./sfnt_patch.dfont
GuardMalloc[harness-1905]: Allocations will be placed on 16 byte boundaries.
GuardMalloc[harness-1905]: - Some buffer overruns may not be noticed.
GuardMalloc[harness-1905]: - Applications using vector instructions (e.g., SSE) should work.
GuardMalloc[harness-1905]: version 064535.38
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1905==ERROR: AddressSanitizer: SEGV on unknown address 0x640004baf6d2 (pc 0x7fff54430de6 bp 0x7ffee440d890 sp 0x7ffee440d840 T0)
==1905==The signal is caused by a READ memory access.
#0 0x7fff54430de5 in GetResourcePtrCommon (libFontParser.dylib:x86_64+0x70de5)
#1 0x7fff54430e7b in FPRMGetIndexedResource (libFontParser.dylib:x86_64+0x70e7b)
#2 0x7fff543c469e in TResourceForkFileReference::GetIndexedResource(unsigned int, unsigned int, short*, unsigned long*, unsigned char*) const (libFontParser.dylib:x86_64+0x469e)
#3 0x7fff543c4626 in TResourceFileDataReference::TResourceFileDataReference(TResourceForkSurrogate const&, unsigned int, unsigned int) (libFontParser.dylib:x86_64+0x4626)
#4 0x7fff543c454f in TResourceFileDataSurrogate::TResourceFileDataSurrogate(TResourceForkSurrogate const&, unsigned int, unsigned int) (libFontParser.dylib:x86_64+0x454f)
#5 0x7fff54413914 in TFont::CreateFontEntities(char const*, bool, bool&, short, char const*, bool) (libFontParser.dylib:x86_64+0x53914)
#6 0x7fff54416482 in TFont::CreateFontEntitiesForFile(char const*, bool, bool, short, char const*) (libFontParser.dylib:x86_64+0x56482)
#7 0x7fff543c103d in FPFontCreateFontsWithPath (libFontParser.dylib:x86_64+0x103d)
#8 0x7fff381a75ee in create_private_data_array_with_path (CoreGraphics:x86_64h+0xa5ee)
#9 0x7fff381a730b in CGFontCreateFontsWithPath (CoreGraphics:x86_64h+0xa30b)
#10 0x7fff381a6f56 in CGFontCreateFontsWithURL (CoreGraphics:x86_64h+0x9f56)
#11 0x7fff39b842ad in CreateFontsWithURL(__CFURL const*, bool) (CoreText:x86_64+0xe2ad)
#12 0x7fff39c82024 in CTFontManagerCreateFontDescriptorsFromURL (CoreText:x86_64+0x10c024)
#13 0x10b7f1d7c in load_font_from_path(char*) (harness:x86_64+0x100001d7c)
#14 0x10b7f2dd1 in main (harness:x86_64+0x100002dd1)
#15 0x7fff71ce6cc8 in start (libdyld.dylib:x86_64+0x1acc8)
==1905==Register values:
rax = 0x000000000000dead rbx = 0x0000000000000400 rcx = 0x00007ffee440d8cc rdx = 0x0000000000000010
rdi = 0x0000640004ba3fc0 rsi = 0x0000640004baf6d2 rbp = 0x00007ffee440d890 rsp = 0x00007ffee440d840
r8 = 0x00007ffee440d8c0 r9 = 0x00007ffee440d8cc r10 = 0x0000640004ba1825 r11 = 0xffffffffffffffff
r12 = 0x0000000000000000 r13 = 0x0000640004ba188f r14 = 0x0000640004ba3fc0 r15 = 0x0000640004ba1825
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (libFontParser.dylib:x86_64+0x70de5) in GetResourcePtrCommon
==1905==ABORTING
[1] 1905 abort DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib ./harness
Validate with FontBook by run this command
DYLD_INSERT_LIBRARIES=/usr/lib/libgmalloc.dylib <path>/Font\ Book.app/Contents/MacOS/Font\ Book
Open with sfnt_patch.dfont, then open Console.app in tab Crash Reports we can see that process FontValidator is crashed
Process: FontValidator [67246]
Path: /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ATS.framework/Versions/A/Support/FontValidator
Identifier: FontValidator
Version: 493.0.4.1
Code Type: X86-64 (Native)
Parent Process: ??? [1]
Responsible: FontValidator [67246]
User ID: 501
Date/Time: 2020-04-18 03:16:40.406 -0700
OS Version: Mac OS X 10.15.4 (19E266)
Report Version: 12
Anonymous UUID: 650A797B-F8B4-2013-399D-4036C2748CEA
Sleep/Wake UUID: 2FFD7BD7-F3AC-4628-9F52-8126DDA57B60
Time Awake Since Boot: 630000 seconds
System Integrity Protection: disabled
Crashed Thread: 0 Dispatch queue: com.apple.main-thread
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x000000010a5c36d2
Exception Note: EXC_CORPSE_NOTIFY
Termination Signal: Segmentation fault: 11
Termination Reason: Namespace SIGNAL, Code 0xb
Terminating Process: exc handler [67246]
VM Regions Near 0x10a5c36d2:
mapped file 000000010a5b4000-000000010a5b6000 [ 8K] r--/rwx SM=COW
-->
Dispatch continuations 000000010a600000-000000010aa00000 [ 4096K] rw-/rwx SM=PRV
Suggested Mitigations
- Make sure to enable Apple Sandbox.
- Avoid open untrusted font with API
CTFontManagerCreateFontDescriptorsFromURL
and install font withFontBook
- Avoid install untrusted font from unknow source
Timeline
- 2020-09-17 Reported to Vendor, Vendor acknowledged on same day
- 2021-02-01 Vendor patched the vulnerability