TL;DR Macro code in Office documents can be digitally signed, and Office can be configured to restrict macro execution to digitally signed documents. We found a method to alter digitally signed VBA projects to execute our own, arbitrary code under the right conditions, without invalidating the digital signature.
When we recommend clients to harden their corporate environment, we often have to address macro code execution within Microsoft Office applications.
A possible recommendation is to configure Microsoft Office to restrict macro code execution to VBA projects with a digital signature: VBA code without a signature, or with an invalid signature, can not execute.
At the end of 2019, I wondered how secure the digital signature for VBA projects is. I started to research code signing in Office documents, and soon found an answer: the digital signature for VBA projects covers the source code, and not the compiled code. Thus when you alter the compiled code the signature will remain valid, and when you setup the right conditions, your altered, compiled code will be executed without recompilation of the digitally signed source code.
An Office document, like a Word document, with VBA code, contains a VBA project. This project contains streams, and each VBA code module is contained in its own stream (aka module stream). Your typical module stream will contain compiled VBA code followed by compressed VBA source code.
This is documented in Microsoft technical document [MS-OVBA]: the correct technical term for “compiled VBA code” is PerformanceCache and for “compressed VBA source code” it is CompressedSourceCode. The name PerformanceCache also hints at its purpose: to improve performance. The idea is that under the right conditions, the VBA engine will directly execute the PerformanceCache code, without compiling the CompressedSourceCode into PerformanceCache code first, and thus gain time by omitting the compilation step.
Remark that module streams are not the only streams containing compiled code: _VBA_PROJECT and __SRP_* streams also contain compiled code.
When code signing VBA code with a code signing certificate, a cryptographic digest is created: the contents hash. This is explained in section 2.4.2 of document [MS-OVBA]:
The Contents Hash is a cryptographic digest of a subset of the information stored in the VBA Storage (section 2.3.4).
Take notice of the word subset. We have to look at the algorithm (pseudo code) as documented in section 22.214.171.124 to understand what is meant by subset. Here is the part pertaining to module streams:
Only the CompressedSourceCode is taken into account when calculating the contents hash. The PerformanceCache is ignored. Thus, it is possible to alter the compiled VBA code without changing the contents hash, and thus without invalidating the digital signature applied on the VBA project.
We created a proof-of-concept document: a Word document with VBA code that displays a message box upon opening, and digitally signed the VBA project. Then we replaced the original compiled code with compiled code that launched calculator (what else? 😉 ):
We did this for the PerformanceCache of each module stream, _VBA_PROJECT and __SRP_*.
The result is a document with a valid signature, that will launch calculator under the right conditions. Right conditions meaning: have the compiled code executed directly, without recompilation of the VBA source code (otherwise a message box would be displayed). Since compiled code is version and architecture dependent (there is no official, public documentation of the data structures of compiled code), one has to make sure to produce code on a machine with the same versions and architecture as the target machine (this is the same problem faced by VBA stomping).
But when these conditions are met, our altered code will execute, even when digital signatures are enforced.
When analyzing this PoC document with oledump.py, we see the message box code:
When analyzing this PoC document with pcodedmp, we see the calculator code:
Does this create a risk for my organisation?
The short answer is: most likely not.
First case, you have implemented VBA macro restrictions, allowing only signed code (default is to accept any certificate from trusted root CAs), then a dedicated attacker can obtain a code signing certificate (legal purchase, stolen certificate, …) and create a document that will execute in your environment. This approach is easier and more reliable than tampering.
Second case: you have implemented VBA macro restrictions, allowing only signed code for your own code signing certificate and no other trusted root CAs. In that case, a dedicated attacker can use this method to create a document that will execute in your environment. But there are several restrictions:
1) The attacker needs to obtain a document with VBA code and a valid digital signature from your organisation
2) This document needs to be suitable for the attacker’s purpose: have the right modules and references (these can not be altered without invalidating the digital signature)
3) The attacker needs to figure out the software versions and architecture of the target machine, to provision a similar machine to be able to produce the correct compiled code
Because of these restrictions, malware authors that create common malicious documents will not adopt this technique.
Only dedicated adversaries that target your organisation (like pentesters 😉, APT crews, …) might use this technique.
Make your own PoC
We will not share our PoC, mainly because of the restrictions explained above. Our PoC will most likely not work in your environment.
If you want to create your own PoC, follow these steps:
1) Create a Word document with a VBA module with this simple code:
2) Apply your digital signature to this document
3) Run the macro
4) Save this document as a .doc file
5) Then open this .doc file with a binary editor, search for ascii string Hello by searching for hexadecimal sequence 05 00 48 65 6C 6C 6F, and replace the e (65) with a (61): Hallo
6) Save this new document (your own PoC), and test it
7) If the PoC test is successful, you will see a message box with Hallo in stead of Hello (FYI: Hallo is Hello in Dutch)
Will Microsoft fix this?
No. The fact that compiled code is ignored for code signing, is by design: this is documented in MS-OVBA.
We did inform Microsoft of our technique out of courtesy, but as we expected, they did close our case. It will only be reopened if a method is found that works remotely and does not rely on social engineering or man-in-the-middle style attacks. Because even for signed documents, the user still has to enable macros.
Be sure to post a comment if you have an idea to use this technique without social engineering or man-in-the-middle.
About the authors
Didier Stevens is a malware expert working for NVISO. Didier is a SANS Internet Storm Center senior handler and Microsoft MVP, and has developed numerous popular tools to assist with malware analysis. You can find Didier on Twitter and LinkedIn.