A few weeks ago, a vulnerability in the Office Equation 3.0 process (EQNEDT32.EXE) was discovered by Embedi. For a couple of reasons this event raised a few eyebrows.
First, the process was a 32bit application without ASLR even on a windows 10 machine.
Secondly, the patch released by Microsoft had been discussed to be a binary patch and not recompiled, suggesting that they might not actually have the source code.
Should this be the case, the equation process may well be fertile ground for other vulnerabilities, as Microsoft would find it harder to look around the code for vulnerabilities that may well have existed for the last 17 years.
With that risk in mind, we decided to look for vulnerabilities ourselves and alert Microsoft.
POC video of the vulnerability being exploited while bypassing the new ASLR mitigation
Although Microsoft has recently forced ASLR on the process, which would make the exploitation of any vulnerability trickier, in this paper we will disclose the way we were nevertheless still able to bypass that mitigation.
But first, let’s look at the original vulnerability (CVE-2017-11882).
Please take a look at figures 1 and 2 below that compare the code seen before and after the patch:
Figure 1: Before the patch
Figure 2: After the patch
The issue causing the problem here is the promiscuous call to the get_byte().
Figure 3: The get_byte() function
The get_byte() function increases the global variable dword_45BD3C and copies its pointed content into AL.
In the vulnerable function this went on looping until a null was encountered, which could lead to a stack based overflow when no null was present. In a nutshell, that’s pretty much all there is to this vulnerability.
When trying to hunt down other code locations with a similar issue, we xrefd the get_byte() function in order to find other functions using it, and found the following function in 0x00443F6C:
Figure 4: A call to get_byte()
The former function was found to be called twice from the function in 0x0443E34:
Figure 5: The vulnerable function caller
The basic objective of this function is to parse the SIZE record.
In terms of code however, we can see that it copies the amount of data sent to it (v12\v13) to the address that resides in two local variables (v6\v8 respectively).
The relevant variables are controlled from the Equation OLE object in the file:
Figure 6: The variables in hex stream
v12 and v13 (highlighted above in orange) are calculated using the following formula:
hex((realsize * 8 – 7) / 2)[2:].ljust(2, ‘0’)
The v6 and v8 are pointers to an address on the stack.
So when v12 or v13 are provided with a large enough value this would easily cause a stack based overflow and overwrite EIP.
Figure 7: EIP Control
To devise an exploitation plan we use BinScope to understand what mitigations are employed.
Figure 8: Old EQNEDT32.exe Binscope
Figure 9: New EQNEDT32.exe Binscope
Comparing the BinScope results of the binary before and after patching, it seems that the binary no longer fails the DBCheck (Dynamic Base). This indicates ASLR was added and would thus prevent us from returning to hardcode addresses, as performed by Embedi.
ASLR will cause eqnedt32 to load at a different base address every time it is executed. This mitigation usually poses a major challenge for attackers, but being a 32-bit process it turns out that the entropy is quite low.
How low? 8-bit to be exact.
The loader will only randomize the base address of the module to one of 256 options.
Another nice feature of eqnedt32 is that it crashes gracefully without prompting any indication to the user.
This behavior allows us to create an RTF with 256 equation objects, brute-forcing our way to a functioning ROP chain that will call winExec (that is imported by the developer for our convenience) with our supplied string.
Figure 10: embedded OLE objects ASLR brute-force
Each equation object in the RTF contains an ROP chain based on a different offset, covering all possibilities that the process might have loaded at.
Furthermore at the moment of crash it seems that EAX points just 0x32 bytes before our controlled input.
Figure 11: Our controlled input at the time of crash.
The controlled input is also part of the equation object inside the RTF file.
So, it seems that all our ROP chain should do is increment EAX and jump to winExec. However, we have one minor constraint.
Between the call to our vulnerable function and the return of its caller to the address that we corrupt, there is another call to function sub_4428f0.
Figure 12: The vulnerable function caller
sub_4428f0 requires arg0 to be a writeable address. We take care of that by pointing it to any writeable address within the module itself, and our first gadget is to jump over that address with add esp, 4 ret.
Figure 13: Our final ROP chain
We reported this bug to Microsoft and it was fixed in the last Patch Tuesday under CVE-2018-0802. Having reported it, it was determined that it was a duplicate, meaning it had been found in parallel by someone else.
However, we believed the exploitation technique (reported to Microsoft as well), also deserved to be discussed. Hence the above illustrates and presents the full exploitation of a new 17 year old vulnerability existing in all Office versions.
We would also like to thank our colleagues, Jonathan Jacobi, Nadav Grossman, Omri Herscovici and Yannay Livneh, for their help in this research.