Old school: evil Excel 4.0 macros (XLM)

In this post, I will dive into Excel 4.0 macros (also called XLM macros – not XML) for offensive purposes. If you grew up in the Windows 95 age or later, just as I did, you might have never heard of this technology that was introduced as early as 1992. Virtually all malicious macro documents for MS Office are based on Visual Basic for Applications (VBA). However, XLM macros are a hidden gem for red teamers and turn out to be a very good alternative to VBA macros for offensive purposes: XLM can be difficult to analyse and it appears that most antivirus solutions have trouble detecting XLM maldocs. And although the technology is 26 years old by now, Excel 4.0 macros are still supported in the most recent Microsoft Office versions (including Office 2016, at time of writing).

This blog post is a teaser for the presentation by Pieter Ceelen and Stan Hegt titled “The MS Office magic show” at DerbyCon 2018 (Sunday, 1 PM). After the presentation, the video and slide material will be published online.


It is 1992 when spreadsheet software Excel 4.0 is released for Windows 3.0 and 3.1. For automation, XLM macros can be used in this version of Excel via so-called macro worksheets. The concept of XLM is very different from Visual Basic for Applications (VBA) macros, which were introduced one year later in Excel 5.0. To give you an idea how old this stuff is: our long lost friend Clippy was not introduced before 1996.

Hello world XLM macro

Let’s get our feet wet. The following steps can be performed to create an XLM macro in recent versions of Excel:

Step 1: Insert a macro worksheet

Create a new Excel workbook. Right click “Sheet1” in the bottom of your screen and click “Insert”.

A window pops up that allows you to choose from various objects to insert. Select “MS Excel 4.0 Macro” and click “OK”.

Step 2: Write your macro

A new worksheet titled “Macro1” has been created. This is a special worksheet type in which XLM macros can be entered (a so-called macro sheet). Click on any cell and enter the formulas “=EXEC(“calc.exe”)”, “=ALERT(“Hello world”)” and “=HALT()” in this cell and the subsequent cells below.

Step 3: Run your macro

To test your macro, right click on the first cell containing your macro code and choose “Run”. A new window will popup. This should contain the name of the first cell containing your macro code (in my case the default “R1C1” for row 1, column 1). Click “Run” to execute the macro and subsequently watch calculator and a MessageBox popup.

If you want your macro to run automatically when the workbook is opened (similar to Sub AutoOpen() for VBA macros), rename the first cell of your macro to “Auto_open”.

Optionally, you can hide your macro worksheet by a right mouse click on the sheet name (“Macro1”) and selecting “Hide”. Auto_open XLM macros will still run from hidden worksheets.

Win32 API access using XLM

XLM macros are a very rich language that provides plenty of offensive opportunities. Many of the things that you can do with VBA can also be achieved using XLM macros. One big exception to this is COM, since the Component Object Model (COM) was only introduced in 1993, a year after Excel 4.0.

In our first example we have already explored the EXEC function, which can be used to create processes. In order to achieve more complex offensive actions you’ll want access to the Win32 API. This can be achieved via REGISTER and CALL functions. The code displayed below shows a proof of concept for shellcode injection using XLM macros. Amongst others, I have successfully tested it with a Cobalt Strike stager payload.

I assume that you are familiar with the concept of shellcode injection and I will therefore only discuss the XLM specifics here. Using the REGISTER function we can load an exported function of a DLL, including the Win32 API DLLs. Syntax for this function is as follows:

REGISTER(module_name, procedure_name, type, alias, argument, macro_type, category)

  • Module_name is the name of the DLL, for example “Kernel32” for c:\windows\system32\kernel32.dll.
  • Procedure_name is the name of the exported function in the DLL, for example “VirtualAlloc“.
  • Type is a string specifying the types of return value and arguments of the functions. More on this below.
  • Alias is a custom name that you can give to the function, by which you can call it later.
  • Argument can be used to name the arguments to the function, but is optional (and left blank in our code).
  • Macro_type should be 1, which stands for function.
  • Category is a category number (used in ancient Excel functionality). We can specify an arbitrary category number between 1 and 14 for our purpose.

The biggest challenge of getting Win32 API functions to work with XLM macros is matching the types that are expected by the Win32 API functions with available Excel 4.0 data types. An overview of available data types can be found here. Every data type is marked with a letter from “A” to “R”. The first letter in the type argument specifies the type for the return value while the subsequents letters specify the argument types. So the string “JJJCJJ” used in our example in the registration of WriteProcessMemory, specifies that the return value for this function is a long int (“J”) and is passed 5 arguments of Excel 4.0 data types long int, long int, string (“C”), long int, long int respectively.

There are a few caveats that we encounter in this proof of concept code. First of all, there is the concept of variables. Variables in XLM macros exist via the concept of values in cells. We need a variable (hence, a cell) to store our shellcode, but cells cannot contain various non-printable characters. Luckily, we can use the =CHAR() function to encode these special characters. Also, cells cannot contain null bytes so we have to remove these from our payload (this can be achieved using various tools, such as msfvenom). Lastly, we encounter the issue of maximum cell length in XLM macro sheets. Hence, this is why our proof of concept iterates the WriteProcessMemory call over all cells in the second column until the value “END” is encountered using a WHILE loop.

The PoC is dirty, but it works and clearly demonstrates the offensive possibilities that can be achieved using XLM and Excel 4.0 macro functions. And I am pretty sure that there are elegant solutions for the issues we encountered. For example, our payload can also be stored in other places than cells (hint: have a look at the GET.WORKBOOK function for obtaining Workbook metadata).

An excellent reference document on functions that are supported in Excel 4.0 macros can be found here.

Support for XLM

Excel 4.0 macros are still supported by default in all recent Microsoft Office versions, including Office 2016. Although files with the .xlm file extension are blocked for opening by default in recent Office versions, XLM macrosheets can be used in .xls and .xlsm files (amongst others, there are some interesting file types that we will discuss in our DerbyCon presentation). As far as I know, XLM macros are only supported in Excel, not in other MS Office applications such as Word or PowerPoint. The usual macro warning (yellow bar) that you know from VBA macros also applies to XLM macros.

Antivirus struggles with XLM

Compared to VBA macros, XLM macros are stored in a completely different way in Excel files. In the newer .xlsm Excel file format which is an XML based ZIP container, the XLM macrosheet is stored in an XML file under subdirectory “macrosheets” (hint: read this sentence again and notice the difference between XML and XLM).

In the Excel 97 – 2003 format (.xls, Compound File Binary Format), data is stored in OLE streams under containers. As can be seen in the following screenshot, VBA macros are stored in a separate container (left screen), while XLM macros are embedded in the Workbook OLE stream. This is a big difference!

Because of this file structure difference, XLM macros are probably more difficult to analyse for security products. We have performed some limited testing and conclude that XLM macros are a blind spot for the antivirus industry. As proof, note the following screenshot that shows our shellcode injecting XLM macro is not detected by any AV engine in VirusTotal at the time of writing (similar code in VBA macros usually scores very high detection ratios):

AMSI circumvention

Microsoft recently announced that the Antimalware Scanning Interface (AMSI) now integrates with Office 365 for scanning VBA macros. Since XLM macros have nothing to do with the VBA engine, we suspected that XLM could be used to circumvent AMSI. We have confirmed this by hooking our debug AMSI provider with Office 2016 on Windows 10: AMSI does catch VBA calls to COM and Win32 API, but has no optics for XLM macros. A big plus for XLM from the red teamer’s perspective!

It is interesting to see that technology from 1992 can be abused to circumvent the most recent Office security features. This blog post only scratches the surface of what is possible with Excel 4.0 XLM macros. I am very curious to see what other interesting things our community can find with regard to this ancient technology.