Exploit-DB #47091
Description: Microsoft DirectWrite / AFDKO - Interpreter Stack Underflow in OpenType Font Handling Due to Missing CHKUFLOW
Type: dos
Platform: windows
Date: 2019-07-10
Author: Google Security Research

Exploit:
-----=====[ 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.

At the time of this writing, based on the available source code, we conclude that AFDKO was originally developed to only process valid, well-formatted font files. It contains very few to no sanity checks of the input data, which makes it susceptible to memory corruption issues (e.g. buffer overflows) and other memory safety problems, if the input file doesn't conform to the format specification.

We have recently discovered that starting with Windows 10 1709 (Fall Creators Update, released in October 2017), Microsoft's DirectWrite library [3] includes parts of AFDKO, and specifically the modules for reading and writing OpenType/CFF fonts (internally called cfr/cfw). The code is reachable through dwrite!AdobeCFF2Snapshot, called by methods of the FontInstancer class, called by dwrite!DWriteFontFace::CreateInstancedStream and dwrite!DWriteFactory::CreateInstancedStream. This strongly indicates that the code is used for instancing the relatively new variable fonts [4], i.e. building a single instance of a variable font with a specific set of attributes. The CreateInstancedStream method is not a member of a public COM interface, but we have found that it is called by d2d1!dxc::TextConvertor::InstanceFontResources, which led us to find out that it can be reached through the Direct2D printing interface. It is unclear if there are other ways to trigger the font instancing functionality.

One example of a client application which uses Direct2D printing is Microsoft Edge. If a user opens a specially crafted website with an embedded OpenType variable font and decides to print it (to PDF, XPS, or another physical or virtual printer), the AFDKO code will execute with the attacker's font file as input.

-----=====[ Description ]=====-----

The afdko/c/public/lib/source/t2cstr/t2cstr.c file in AFDKO implements the Type 2 CharString interpreter for OpenType fonts. The interpreter stack is represented by the following structure in the t2cCtx object:

--- cut ---
    70      struct /* Operand stack */
    71      {
    72          long cnt;
    73          float array[CFF2_MAX_OP_STACK];
    74          unsigned short numRegions;
    75          long blendCnt;
    76          abfOpEntry blendArray[CFF2_MAX_OP_STACK];
    77          abfBlendArg blendArgs[T2_MAX_STEMS];
    78      } stack;
--- cut ---

Values are popped off the stack in the instruction handlers in t2Decode() using the POP() macro:

--- cut ---
   152  #define POP() (h->stack.array[--h->stack.cnt])
--- cut ---

As the macro assumes that the stack is non-empty, another macro in the form of CHKUFLOW() is required to verify this requirement:

--- cut ---
   137  /* Check stack contains at least n elements. */
   138  #define CHKUFLOW(h, n)                                       \
   139      do {                                                     \
   140          if (h->stack.cnt < (n)) return t2cErrStackUnderflow; \
   141      } while (0)
--- cut ---

As a result, it is essential for the interpreter's memory safety to invoke CHKUFLOW() with an appropriate "n" argument before using POP() the corresponding number of times. In a majority of cases, the interpreter operates on the stack correctly; however, we have found several instances where the CHKUFLOW() calls are missing. The problems were identified in the handling of the following instructions:

- tx_callgrel
- tx_rmoveto
- tx_vmoveto
- tx_hmoveto
- tx_SETWVN

For example, the handler of the "rmoveto" instruction is shown below:

--- cut ---
  1484              case tx_rmoveto:
  1485                  if (callbackWidth(h, 1))
  1486                      return t2cSuccess;
  1487                  {
  1488                      float y = POP();
  1489                      float x = POP();
  1490                      if ((h->flags & IS_CFF2) && (h->glyph->moveVF != NULL))
  1491                          popBlendArgs2(h, &INDEX_BLEND(0), &INDEX_BLEND(1));
  1492                      callbackMove(h, x, y);
  1493                  }
  1494                  break;
--- cut ---

It's clear that the two POP() invocations in lines 1488 and 1489 are not preceded by CHKUFLOW(). Such bugs may have two kinds of security-relevant consequences:

1. Out-of-bounds data is read and used as arguments to the affected instructions,
2. The stack index becomes negative, which could facilitate overwriting memory residing directly before the stack array.

In this particular case, the stack counter itself is placed before the stack array. This means that consequence #1 is not really a problem as the out-of-bounds data is initialized and its value is known to the attacker. As for item #2 -- an attack would require the CharString execution loop to continue to the next instruction while preserving the negative value of h->stack.cnt. For the rmoveto, vmoveto and hmoveto instructions, the stack counter is reset back to 0 in line 2303, because their handlers end with a "break;" statement:

--- cut ---
  2301          } /* End: switch (byte0) */
  2302          clearBlendStack(h);
  2303          h->stack.cnt = 0; /* Clear stack */
  2304      }                     /* End: while (cstr < end) */
--- cut ---

This leaves us with callgrel and SETWVN. In both cases, the out-of-bounds argument would have to be valid in the context of those instructions in order for