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 with FontBook
  • 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