In late April we found and wrote a description of CVE-2018-8174, a new zero-day vulnerability for Internet Explorer that was picked up by our sandbox. The vulnerability uses a well-known technique from the proof-of-concept exploit CVE-2014-6332 that essentially “corrupts” two memory objects and changes the type of one object to Array (for read/write access to the address space) and the other object to Integer to fetch the address of an arbitrary object.
But whereas CVE-2014-6332 was aimed at integer overflow exploitation for writing to arbitrary memory locations, my interest lay in how this technique was adapted to exploit the use-after-free vulnerability. To answer this question, let’s consider the internal structure of the VBScript interpreter.
Debugging a VBScript executable is a tedious task. Before the script is executed, it is compiled into p-code, which is then interpreted by the virtual machine. There is no open source information about the internal structure of this virtual machine and its instructions. It took me a lot of effort to track down a couple of web pages with Microsoft engineer reports dated 1999 and 2004 that shed some light on the p-code. There was enough information there for me to fully reverse-engineer all the VM instructions and write a disassembler! The final scripts for disassembling VBScript p-code in the memory of the IDA Pro and WinDBG debuggers are available in our Github repository.
With an understanding of the interpreted code, we can precisely monitor the execution of the script: we have full information about where the code is being executed at any given moment, and we can observe all objects that are created and referenced by the script. All this greatly assists in the analysis.
The best place to run the disassembling script is the CScriptRuntime::RunNoEH function, which directly interprets the p-code.
The CScriptRuntime class contains all information about the state of the interpreter: local variables, function arguments, pointers to the top of the stack and the current instruction, plus the address of the compiled script.
The VBScript virtual machine is stack-oriented and consists of slightly more than 100 instructions.
All variables (local arguments and ones on the stack) are represented as a VARIANT structure occupying 16 bytes, where the upper word indicates the data type. Some of the type values are given on the relevant MSDN page.
Below is the code and disassembled p-code of class ‘Class1’:
Function 34 is a constructor of class ‘Class1’.
The OP_CreateClass instruction calls the VBScriptClass::Create function to create a VBScriptClass object.
The OP_FnBindEx and OP_CreateVar instructions try to fetch the variables passed in the arguments, and since they do not yet exist, they are created by the VBScriptClass::CreateVar function.
This diagram shows how variables can be fetched from a VBScriptClass object. The value of the variable is stored in the VVAL structure:
To understand the exploitation, it is important to know how variables are represented in the VBScriptClass structure.
When the OP_NamedSt ‘mem’ instruction is executed in function 36 (‘SetProp’), it calls the Default Property Getter of the instance of the class that was previously stacked and then stores the returned value in the variable ‘mem’.
***BOS(8292,8301)*** mem=Value *****
0002OP_LocalAdr -1 <-------- put argument on stack
0005OP_NamedSt ‘mem’ <-------- if it's a class dispatcher with Default Property Getter, call and store returned value in mem
Below is the code and disassembled p-code of function 30 (p), which is called during execution of the OP_NamedSt instruction:
The first basic block of this function is:
***BOS(8626,8656)*** P=CDbl(“174088534690791e-324”) *****
This block converts the string ‘174088534690791e-324’ to VARIANT and stores it in the local variable 0, reserved for the return value of the function.
After the return value is set but before it is returned, this function performs:
For IIIl=0 To 6
This calls the garbage collector for the ‘Class1’ instance and results in a dangling pointer reference due to the use-after-free vulnerability in Class_Terminate() that we discussed earlier.
In the line
***BOS(8855,8874)*** Set llII=New Class2 *****
the OP_InitClass ‘Class2’ instruction creates an “evil twin” instance of class ‘Class1’ at the location of the previously freed VBScriptClass, which is still referenced by the OP_NamedSt ‘mem’ instruction in function 36 (‘SetProp’).
Class ‘Class2’ is the “evil twin” of class ‘Class1’:
The location of variables in memory is predictable. The amount of data occupied by the VVAL structure is calculated using the formula 0x32 + the length of the variable name in UTF-16.
Below is a diagram that shows the location of ‘Class1’ variables relative to ‘Class2’ variables when ‘Class2’ is allocated in place of ‘Class1’.
When execution of the OP_NamedSt ‘mem’ instruction in function 36 (‘SetProp’) is complete, the value returned by function 30 (‘p’) is written to memory through the dangling pointer of VVAL ‘mem’ in Class1, overwriting the VARIANT type of VVAL ‘mem’ in Class2.
Thus, an object of type String is converted to an object of type Array, and data that was previously considered to be a string is treated as an Array control structure, allowing access to be gained to the entire address space of the process.
Our scripts for disassembling VBScript compiled into p-code enable VBScript debugging at the bytecode level, which helps to analyze exploits and understand how VBScript operates. They are available in our Github repository
The case of CVE-2018-8174 demonstrates that when memory allocations are highly predictable, use-after-free vulnerabilities are easy to exploit. The in-the-wild exploit targets older versions of Windows. The location of objects in memory required for its exploitation is most likely to occur in Windows 7 and Windows 8.1.
Automatic Exploit Protection (AEP), part of Kaspersky Lab products, blocks all stages of the exploit with the following verdicts: