- A Missed 0day?
- Vulnerability Analysis
- Code Analysis
- Dynamic Debugging
- Analysis of the Patch
- Correlation with APT-C-06
- Reader’s Takeaways
Recently, the Advanced Threat Response Team of 360 Core Security has discovered several VBScript vulnerabilities exploited in the wild, including CVE-2016-0189, CVE-2018-8373 and another previously unknown vulnerability (we have not yet known which CVE number it corresponds). These three vulnerabilities, plus the one we discovered in April this year (CVE-2018-8174), there are in total four in the wild. After analysis, we found that the obfuscation and exploitation of these four files are highly consistent. We suspect that they are from the same hacker (or hacking group) that has been developing 0day exploit of VBScript and used it for attacks.
Here are the in-the-wild exploits of the four vulnerabilities we discovered:
We and other security vendors have uncovered the other three vulnerabilities in previous analysis reports, so in this article, we will focus on the fourth VBScript 0day that has not been published.
The exploit has been hidden very concealed. We found that the vulnerability was fixed in the Microsoft March 2017 Security Update. Microsoft did not mention that the vulnerability was exploited. We speculated that’s because the vulnerability might be fixed by Microsoft without finding any evidence of being exploited. The latest version we located that can trigger this vulnerability is VBScript.dll 5.8.9600.18538, which was fixed in VBScript.dll 5.8.9600.18616. What’s interesting is that our analysis shows the relevant exploit can be dates back to earlier than March 2017, which also means that the vulnerability was a 0day at the time. Unfortunately, we haven’t seen any analysis of the vulnerability from other vendors.
Below we will share with you the root causes and exploitation of this vulnerability.
This vulnerability is in function VBScript!rtJoin. While executing the join statement of VBScript, function VbsJoin will call rtJoin. Then rtJoin will first traverse each element in incoming array and calculates the total length of the stitched string (including the stitching characters, the default is unicode space 0x0020). Then call SysAllocStringLen to allocate the corresponding space for saving the stitched string.
The actual allocated space size = (number of bytes to be allocated + 0x15) & 0xfffffff0 (see the implementation of oleaut32!SysAllocStringLen and oleaut32!CbSysStringSize)
The first 4 bytes of the string initial address are the byte length of the string (see the BSTR structure). The pseudo code for the entire process above is as follows:
The corresponding stack backtrace is as below:
The parsing process then copies the strings one by one to the newly allocated space. In this process, the length of each string is obtained using the string address stored on the stack, as the size parameter of memcpy. When there is a class object in the array elements, it will trigger the Default Property Get callback of the class object to get the default property. In the callback, you can operate on other members in the array, such as changing the string size. As long as the string size before and after the change is precisely controlled, the data size copied through memcpy (the first one in the figure below) may exceed the space allocated by SysAllocStringLen, resulting in heap overflow. The pseudo code for the entire process above is as follows:
We constructed the PoC of this vulnerability for researcher to use:
The original code first prepares the memory and then exploits the vulnerability (exp_1) for the first time, overwriting the length field of a BSTR object and obtaining a very long BSTR object. Then it will use this BSTR to obtain a previously prepared memory address; After it succeeds, the vulnerability (exp_2) is used again to overwrite the object type of a forged string from 0008 to 200c, thus obtaining a unidimensional length-exceeding array with starting address being 0, element size being 1, and element number being 0x7fffffff.
With the first obtained memory address and the second obtained length-exceeding array, arbitrary address reading is then realized. The subsequent exploitation is basically the same as the previously disclosed details.
Below, it shows the first half of memory preparing code:
In this part of the code, the string length of str_h is 0x4fec bytes. Its actual space allocated by SysAllocStringLen is 0x5000 bytes ((0x4fec+0x15) & 0xfffffff0 = 0x5000) while that of str_o (actual sting length is 0x4ff6 bytes) is 0x5000 bytes ((0x4ff6+0x15) & 0xfffffff0 = 0x5000). Array_a and array_b are two arrays, and the actual memory each array occupies is 0xa00*0x10 = 0xa000 bytes (each element is a VAR structure).
Note that 0x4fec2 + 0x18 + 0x22 = 0x9ff4, (0x9ff4+0x15) & 0xfffffff0 = 0xa000, these values will be mentioned in later part of this report.
Below is the second half of memory preparing code:
Str_left_0 is 0x4ffa bytes (get_left_str_a_by_size will reduce the incoming parameter by 6 bytes), and SysAllocStringLen allocates 0x5000 bytes for it ((0x4ffa + 0x15) & 0xfffffff0 = 0x5000); Str_left_1 is 0x9ffa bytes, and the memory allocated by SysAllocStringLen is 0xa000 bytes ((0x9ffa + 0x15) & 0xfffffff0 = 0x5000).
Each element of array2 is then assigned with value str_left_1 (the actual memory size is 0xa000); Each element of array3 is assigned with value array_b whose actual memory size is 0xa000 (see above).
Now, the memory preparation is complete. Afterwards, as long as some elements of array2 (operating in exp_1) or array3 (operating in exp_2) are released, there will be a large number of memory holes with a size of 0xa000 for each. At this point, immediately request for 0xa000 bytes can make it highly possible to reuse the released memory.
As long as the requested size by SysAllocStringLen in function rtJoin is 0xa000 bytes, together with the above vulnerabilities, the data overwriting on certain str_left_1 object in array2 or an array_b object in array3 can be achieved, which will be described in details later.
In exp_1, it is the first time to trigger the vulnerability, and the length of a BSTR object is rewritten to 0xfffffffe.
First, assign the first and second elements of array_c with str_h (the string length is 0x4fec bytes, the actual occupied space is 0x5000 bytes, see above), and the third element is assigned the object of class_a whose Default Property Get can return a length of 0x18 bytes (0x1a-0x6+0x4 = 0x18). As a result, the spliced length of the three elements of the array plus the separator byte is 0x9ff4 (0x4fec+0x4fec+0x18+0x2+) 0x2 = 0x9ff4)
Before triggering the vulnerability, release half of the elements in array2 before calling make_hole_of_array2 to generate enough memory holes with the size of 0xa000.
The corresponding memory before and after calling make_hole_of_array2 is as follows. It can be seen that that half of the string memory in array2 is released. For the elements whose subscripts are in the range 0x00-0x7F, the even parts are released; for the elements whose subscripts are in the range of 0x80-0xFF, the odd parts are released:
Subsequently, SysAllocStringLen in function rtJoin will request a BSTR with a total length of 0xa000 bytes ((0x9ff4 + 0x15) & 0xfffffff0 = 0xa000). Due to the heap allocation algorithm of Windows, this memory will reuse one from the free heap block on the right side of the above figure.
In the Default Property Get of class, the first and second elements of array_c (set to Nothing) will be released first, and immediately assigned with value str_o (the string length is 0x4ff6 bytes, the actual occupied space is 0x5000 bytes).
There are two things to be noted here:
- 1) The two operations with str_o 2 will reuse the 2 0x5000 memory blocks just released (that is, the memory occupied by the original two str_h).
- 2) After the reuse, the length and content of the string at the same address have changed (the original is str_h, length 0x4fec bytes, now it’s changed to str_o, length 0x4ff6). So the string length retrieved before memcpy in rtJoin are 0x4ff6, 0x4ff6, 0x18; plus two separator character 0x4, memcpy totally copies data of 0xa008, with 0x14 more bytes than 0x9ff4, the last 4 of the extra bytes will overwrite the length field of the adjacent str_left_1 object in array2. In the exploit code, the attacker overwrote the length of the original str_left_1 to 0xfffffffe.
The mismatching process is shown below:
Afterwards, the previously prepared string address is obtained with the length-exceeding string, for subsequent use.
The following figure shows the string prepared in function prepare:
In exp_2, the vulnerability is triggered for the second time. The type of the string corresponding to fake_array is changed to 0x200c. The method is the same as overwriting the length of the string. So it will not be repeated here.
Fake_array is a string that is actually a fake tagSAFEARRAY structure.
The following is a length-exceeding array after retrieving for type obfuscation, for later use:
Later, the sample encapsulates a set of functions for arbitrary address reading with the string address and the length-exceeding array obtained earlier, for later use.
With the ability to read arbitrary addresses, the exploit also encapsulates several auxiliary functions:
Then leak the virtual table address of the CScriptEntryPoint object through the following method:
After that, use the encapsulated auxiliary function to get the base address of VBScript.dll. Next, get the msvcrt.dll base address by traversing the VBScript.dll import table. msvcrt.dll introduces kernelbase.dll, ntdll.dll. Finally, the function addresses of NtContinue, VirtualProtect, etc. are obtained. The whole process is as follows:
In Windows 7 and Windows 8, the way the original exploit executes shellcode is the same as the previous CVE-2018-8174, while in Windows 8.1 and Windows 10, it is slightly different from that in earlier version systems.
In function prepare, when the memory preparation is completed, array2, array3 and the pvData of array_c is shown below:
Firstly, here is the memory reuse of string str_h by string str_o in the Public Default Property Get callback. After the reuse, the overall memory size does not change but the length of the string changes.
Then, there is the reuse of a released 0xa000 string in array2 when SysAllocStringLen requests 0xa000 sized memory. As you can see from the figure below, before the vulnerability was triggered for the first time, the memory that was reused is the array2 (0x81) that was just released. Then the length of the string corresponding to array2 (0x82) will be overwritten.
The first time the vulnerability was triggered in exp_1, the length field of a str_left_1 string was overwritten.
Memory reuse is performed again in exp_2. This time, the 0xa000 memory requested by SysAllocStringLen is the memory that has just been released by array3 (0x81) (the release mode is the same as array2), and then the header of the associated memory of array3 (0x82) will be overwritten.
When the vulnerability is triggered for the second time, the type of the carefully prepared string fake_array is rewritten from 0008 to 200c, resulting in a unidimensional length-exceeding array.
The shellcode implementation details in Windows 7 and Wndows 8 can be found in the CVE-2018-8174 analysis article we wrote earlier. In Windows 8.1 and Windows 10, the sample used other tricks to bypass CFG (in our tests, this method was successful in earlier versions of Windows 8.1 and Windows 10). More details on this section will be disclosed later.
Below is the comparison of the Bindiff tool before and after the patch:
It can be seen that before copying each array element to the join_list in the patch file, the string data is firstly saved by SysAllocString, so that even if the initial string length is changed in the later callback, the data saved using function SysAllocString will be copied when excute memcpy for memory copycopying. In this way, the memory size requested by SysAllocStringLen is the same as the data size of the memcpy copy, thus fixing the vulnerability.
We analyzed the shellcode of the four VBScript vulnerabilities. We found that the shellcode used by CVE-2016-0189, this vulnerability in this article and CVE-2018-8174 are basically the same except the configured CC. The shellcode of CVE-2018-8373 is slightly different, but also very similar on the whole. We speculate that this vulnerability is also one of the APT-C-06 (aka Darkhotel) arsenals.
Have you noticed that there is an integer overflow point in the rtJoin function?
We retrieved the integer overflow vulnerability related to the join series function in VBScript, and found a vulnerability CVE-2017-11869. We made a patch comparison of VBScript.dll before and after the fix and found some interesting modifications, as follows:
If you are interested in them, you can do your own research on CVE-2017-11869.
In this article, we shared the details of the third VBScript vulnerability discovered this year of which the exploitation is as impressive as the previous ones. We believe that there are other similar issues in VBScript, and speculate that there are other similar exploits are under the control of the hacker or hacking group. Here we would like to warn you to stay sharp on these kinds of vulnerabilities out there.