A VBA P-Code Disassembler
[Bontchev21b] Springer, Studies in Big Data 84, 2021, pp. 193–199.
1 Introduction
Recently we have observed a significant increase in the frequency at which Microsoft Office macros are being used as an attack vector. Microsoft Office uses a macro programming language called Visual Basic for Applications (VBA), which is powerful enough to do whatever the attacker needs. Usually, the malicious VBA macros are used to download the second stage of the malware (ransomware, banking Trojan, backdoor, etc.). They can be relatively small and are easy to modify or even to completely rewrite them each time, thus making them difficult to detect at the perimeter defenses (e.g., with an e-mail scanner) with known-malware detection tools.
So far we have seen malicious VBA macros being distributed with Microsoft Word, Excel, PowerPoint, Access, Visio, Project and Publisher documents.
Microsoft Office has built-in protections against execution of foreign macros, but unless properly administered, they are easy for the user to disable and the malicious documents usually use some form of social engineering to convince the user to do so.
Therefore, we need proper tools for inspecting the macro content of the received documents, in order to decide whether it contains any malicious code. During our research we have discovered that the publicly available tools lack the capability to discover all forms in which a malicious macro can exist. We have applied our findings are our knowledge from reverse-engineering the formats of Microsoft Office documents and have created a tool, which allows the disassembling of the p-code into which VBA is compiled.
2 Problem description
VBA macros are stored in OLE2 files. OLE2 was the native document format used by the older versions of Microsoft Office. The contemporary versions use a different, mostly text-based format. The information is stored into multiple files, most of which are text files, and these files are gathered together and compressed into a ZIP archive, which is the document. But even in this case, the VBA macros reside in a file named vbaProject.bin, which is an OLE2 file.
The format of the OLE2 files is extremely complex. It is basically a “file system in a file” – with its own clusters, file allocation table, “files” (called “streams”) and “directories” (called “storages”) [OLE2]. Each VBA module is stored in a separate OLE2 stream, called a “code module”. The exact position of this stream inside the OLE2 stream and storage structure varies among the different Office applications but, generally, all VBA-related streams (the code module streams as well as some other streams) reside under a storage named VBA.
It is not widely known but a VBA macro can exist in up to three different executable forms. Which one of them is actually executed when the document, containing the macro, is opened, depends on various circumstances.
These three forms are:
-
Source code. When a VBA module is created with the VBA Editor, its source code is compressed and stored at the end of the corresponding code module stream.
-
P-code. As soon as a single line of VBA code is entered (or modified), it is compiled into an assembler-like language, called “p-code” (from “pseudo code”) for a virtual stack machine. The p-code for each VBA line is also stored in the code module stream, before the compressed source code. The p-code of each line is stored in the order in which the lines have been created – which is not necessarily the order in which the VBA lines appear in the source. The p-code is preceded by a table, the entries in which point to each line of p-code. These entries appear in the same order as the lines in the VBA source.
-
Execodes. After any of the VBA modules has been executed at least once (even if just debugging has been started on a module), the p-code is compiled further and stored in some form of internal representation in OLE2 streams, the names of which begin with __SRP_, followed by a number.
The most “universal” form of internal VBA representation is the source code. It is one and the same in every version of VBA higher than 3. (Version 3, used only in Excel 95, has neither source code, nor execodes – only p-code.) That is, you can take a document containing VBA macros that was created with a version of Office using VBA5 (i.e., Office 97) and open it with a version of Office that uses a different VBA version (e.g., VBA7, used by Office 2013) and the VBA program will work just fine. This is despite the fact that each different version of VBA uses a different set of p-code instructions. (More exactly, the opcodes of many of the instructions change, because new instructions are inserted in the middle of the instruction set in every new VBA version.)
This is why Microsoft advised the anti-virus industry to locate the compressed macro source, decompress it, and scan it for known macro malware. Microsoft has documented the format of the module streams sufficiently to locate the compressed source code area [VBA]. Therefore, many of the publicly available open-source tools for VBA inspection (and even many anti-virus products) follow this advice and show the contents of the decompressed source. For the anti-virus products this has the additional benefit that a single detection entry will match all VBA forms (VBA5, VBA6, VBA7) of the same macro malware, since the source code remains the same.
Paradoxically, however, the source code is only rarely used by VBA. In particular, it is used only when a document created by a version of Office that uses one version of VBA is opened by a version of Office that uses a different VBA version. In such cases the source code is retrieved and it is compiled into p-code understandable by the VBA version of the Office application that has opened the document.
In practice, this means that if the VBA version does not change (Office 97 uses VBA5, Office 2000, 2002, 2003, 2005, 2007 and 2009 use VBA6, Office 2010, 2013 and 2016 use VBA7), the contents of the source code will be ignored and the contents of the p-code will dictate what actions will be actually performed.
This situation creates a window of opportunity for the macro malware author. He could modify the contents of the document so that the compressed source indicates some benign VBA program or is missing completely, while the p-code performs the desired malicious action. As long as such a doctored document is not opened with a version of Office that uses a different version of VBA, the malicious p-code will be executed while all source code inspecting tools will be fooled by the benign (or missing) source code.
We have created a proof-of-concept set of documents for each version of Microsoft Word that uses a different version of VBA (Word97.doc, Word2000-2009.doc and Word2010+.doc) [PoC]. Most of the free macro inspection tools will report that these documents contain no macros, because the source code in the code modules has been overwritten with zeroes. However, if opened with the corresponding version of Microsoft Word, these documents will display a short message and will start the Windows calculator.
The execodes pose a similar problem, but on a much smaller scale. While they, too, could contain a program that is different from what both the source code and the p-code suggest, the execodes are specific to the particular version of Office that has created them. They will not run on anything but on that particular version. Execodes created by Word 2000 will execute only on Word 2000 and not, for instance, on Word 2002, despite that both the same VBA version (VBA6). Furthermore, the format of the execodes is extremely complex and is outside the scope of this work.
There are some very real examples of the discrepancy between source code, p-code and execodes being used in macro malware:
-
A Word macro virus (W97M/Class.EZ) destroys the p-code area in the infected documents and sets the VBA version to an impossibly large number, forcing re-compilation from source, when the document is opened.
-
A variant of the X97M/Jini virus has been misdisinfected by some inferior anti-virus product. The code module streams have been removed (thus the source code and the p-code are absent) but the execodes remain. With a normal, parasitic virus, this would have prevented the virus from working, since the execodes wouldn’t be able to copy a module that is not present. However, Jini is a mass-mailer. It sends the infected document as an Outlook e-mail attachment, so the inability to infect other documents does not prevent it from replicating.
In fact, given that the possibility of discrepancy between the source code, the p-code and the execodes has been known since the 90s, it is surprising that malware authors are not exploiting it more often. However, the recent resurgence of macro malware suggests that they might start doing so in the future. Therefore, we need more powerful tools that let us inspect not only the source code but also the p-code of any VBA programs residing in suspicious documents.
3 Related work
Our work would have been impossible without the work done by other authors who have created tools that allowed us to easily access the contents of the OLE2 files and the module streams.
Of particular relevance are the following:
-
oledump by Didier Stevens [oledump] is a Python program for displaying the stream structure of OLE2 files and dumping the contents of any particular stream (including the VBA source code)
-
oletools by Philippe Lagadec is a Python library for accessing programmatically the stream structure of an OLE2 file and in particular the VBA-related stream and storage subtree.
4 P-code disassembler
While working in the anti-virus industry (the author has designed the macro malware scanning engine in the anti-virus product F-PROT), we did some extensive reverse-engineering of the VBA code module formats. We refused to accept Microsoft’s advice of looking at the source code, since it only rarely determines what would be actually executed and is effectively absent in VBA3. Instead, we concentrated on understanding how the p-code is stored and how to locate, extract and analyze it.
We used our knowledge and the results of our research to construct a Python program, which is able to locate and disassemble the p-code instructions of any VBA module. It is built upon Philippe Lagadec’s excellent package oletools, which allows to obtain access to the VBA code module streams without having to handle ourselves such things as the OLE2 file formats. We have released the resulting program, a VBA p-code disassembler, as an open-source project on GitHub [pcodedmp].
The program takes as a command-line argument a list of one or more names of files or directories. If the name is an OLE2 document, it will be inspected for VBA code and the p-code of each code module will be disassembled. If the name is a directory, all the files in this directory and its subdirectories will be similarly processed. In addition to the disassembled p-code, by default the script also displays the parsed records of the dir stream, as well as the identifiers (variable and function names) used in the VBA modules and stored in the _VBA_PROJECT stream.
The script supports VBA5 (Office 97, MacOffice 98), VBA6 (Office 2000 to Office 2009 inclusive) and VBA7 (Office 2010 and higher).
The script also accepts the following command-line options:
-h, --help Displays a short explanation how to use the script and what the command-line options are.
-v, --version Displays the version of the script.
-n, --norecurse If a name specified on the command line is a directory, process only the files in this directory; do not process the files in its subdirectories.
-d, --disasmonly Only the p-code will be disassembled, without the parsed contents of the dir stream, the contents of the PROJECT stream, or the identifiers in the _VBA_PROJECT stream.
--verbose The contents of the dir and _VBA_PROJECT streams is dumped in hex and ASCII form. In addition, the raw bytes of each compiled into p-code VBA line is also dumped in hex and ASCII.
For instance, using the script on the proof-of-concept document [PoC] produces the following results:
python pcodedmp.py -d poc2b.doc Processing file: poc2b.doc ======================================================================= Module streams: Macros/VBA/ThisDocument - 1949 bytes Line #0: FuncDefn (Sub / Property Set) func_00000078 Line #1: LitStr 0x001D "This could have been a virus!" Ld vbInformation Ld vbOKOnly Add LitStr 0x0006 "Virus!" ArgsCall MsgBox 0x0003 Line #2: LitStr 0x0008 "calc.exe" Paren ArgsCall Shell 0x0001 Line #3: EndSub
For reference, it is the result of compiling the following VBA code:
Private Sub Document_Open()
MsgBox "This could have been a virus!", vbInformation+vbOKOnly, "Virus!"
Shell("calc.exe")
End SubWe have also created a plug-in for Didier Stevens’ tool oledump [dumper]. Unfortunately, this plug-in works only for documents that use VBA6 and only dumps the p-code in hex and ASCII, without doing any disassembling. This is because, in order to determine the VBA version (and thus the location of the p-code and the meaning of each p-code opcode), we need access to the _VBA_PROJECT and dir streams and oledump simply does not provide access to that to its plug-ins via the plug-in interface.
5 Future work
Our tool requires further testing, in order to make sure that it is compatible with some marginal cases (like MacOffice, which uses big-endian format for storing the various structures in the VBA-related streams, as opposed to the little-endian format used by the PC version of Microsoft Office).
We also plan on extending the tool to support VBA3. Although VBA3 uses p-code too, the OLE2 stream format structure is quite different. While VBA3 was used only in a now-obsolete version of Excel (Excel 95), Excel documents produced by later versions of Excel can be saved in a “compatible” format (which contains both the VBA3 and VBA5 forms of the same VBA macros), so VBA3 macros pose a non-negligible threat.
6 References
- [OLE2] Microsoft, Microsoft Compound Document File Format, https://www.openoffice.org/sc/compdocfileformat.pdf
- [VBA] Microsoft, Office VBA File Format Structure, https://msdn.microsoft.com/en-us/library/cc313094%28v=office.12%29.aspx
- [PoC] Vesselin Bontchev, http://bontchev.my.contact.bg/poc2.zip
- [oledump] Didier Stevens, Oledump, https://blog.didierstevens.com/programs/oledump-py/
- [oletools] Philippe Lagadec, Oletools – Python tools to analyze OLE and MS Office files, http://www.decalage.info/python/oletools
- [pcodedmp] Vesselin Bontchev, A VBA p-code disassembler, https://github.com/bontchev/pcodedmp
- [dumper] Vesselin Bontchev, A p-code dumper plugin for oledump, https://github.com/DidierStevens/DidierStevensSuite/blob/master/plugin_pcode_dumper.py