Important notice: this blog is for educational purposes only. Do not use these techniques against targets without explicit permission of the system owner. This blog is part of the Blue Hat Hackers group insights and can be found here

In this blogpost I will detail a technique to inject a payload into a remote process from an Office macro, without being detected by Windows Defender. The technique uses uncommon functions for the injection of shellcode. This has been described before in articles such as “One thousand and one ways to copy your shellcode to memory (VBA Macros)“ by @TheXC3LL and “Abusing native Windows functions for shellcode execution” by @noottrak. However, when implemented in a VBA macro, it quickly becomes evident that using these functions comes with a caveat that makes them impractical for application in Red Teaming engagements. The document freezes, causing the victim to most likely kill the Office process and, consequently, our beacon. To work around this issue, I developed a two-stage loader which still takes advantage of Defender’s inability to detect these uncommon functions, while also resulting in clean injection into an external process.

Shellcode injection generically consists of three steps:

  1. Obtaining memory with read, write and execute permissions.
  2. Getting your shellcode into said memory.
  3. Diverting the control flow to the start of your shellcode.

Getting the shellcode into memory

Let’s break down an example of shellcode injection using alternative functions, taken from Adepts of 0xCC. We can use the combination HeapCreate and HeapAlloc to obtain a memory region with the right permissions. The combination of SetConsoleTitleA and GetConsoleTitleA allows us to write our shellcode there.

Finally, EnumSystemCodePagesW can be used to divert the control flow. The end goal for me is to inject a Cobalt Strike beacon using this method. Since raw payloads generated by Cobalt Strike include NULL bytes, we either need to encode the payload to eliminate those, or implement the Set- and GetConsoleTitleA functions in such a way that they are unsusceptible to them. I would opt for the second route by creating a VBA function which copies the payload one byte at a time:

The function takes an array of bytes as an argument and copies them into the specified buffer at the specified index. We can call the function multiple times if we need to inject a payload that exceeds the maximum length of a single VBA instruction (maximum line length * maximum number of line continuations). When implemented in a VBA macro, we are now able to receive a beacon in our Cobalt Strike Team server. However, the Office process on the victim’s machine will freeze.

So why does this happen? In ‘traditional’ shellcode injection, the control flow is diverted using the function CreateThread, which, as the name suggests, creates a new thread for the shellcode to run in. This means that the regular program flow will continue in its own thread. EnumSystemCodePagesW diverts control flow by calling the callback function invoked by the first argument. As such, the shellcode runs in the same thread, therefore blocking further execution of the macro itself. Since Office will block until execution of a macro is finished, this causes the program in which the macro is running to freeze. The victim will likely close the program, killing our beacon in the process.

Avoiding Office to freeze due to the macro process

To resolve the issue, we want to move away from the Office process as quickly as possible, preferably without the victim noticing anything strange about the document. To achieve this goal, I created a loader consisting of two stages. The first stage was implemented in VBA and uses uncommon functions to inject shellcode as described before. The sole purpose of this stage is being able to execute arbitrary code in a VBA macro, without Defender’s static analysis flagging the document. The second stage was implemented as shellcode and will inject our final payload (in this case a Cobalt Strike beacon) into a remote process and gracefully return to the macro.

This provided me the unique opportunity to dive deeper into writing shellcode. I used the techniques described in the article “From a C project, through assembly, to shellcode” by @hasherezade (https://twitter.com/hasherezade). On a high level, this method works as follows:

  1. Write your payload in C/C++.
  2. Refactor the code to load all import through PEB lookup.
  3. Use a C/C++ compiler to create an Assembly file.
  4. Refactor the Assembly to make valid shellcode.
  5. Compile and link the Assembly file into a PE file.
  6. Dump the section containing your shellcode.

My payload written in C++ looks as follows:

Using CreateToolHelp32Snaphot we create a snapshot of all processes. We then loop over all processes to find the process we want to inject into, in this case OneDrive.exe. If we find the process, we perform a remote process injection using VirtualAllocEx, WriteProcessMemory and CreateRemoteThread. Using these functions directly in a VBA macro will certainly get it flagged by static analysis of Windows Defender. The question is whether the dynamic analysis performs as well.

Using the PEB look-up from the Hasherezade article, we can look up the memory addresses of the functions we need and create function pointers for them as follows:

The function CreateToolhelp32Snapshot is now available to us as

_CreateToolhelp32Snapshot, which will execute without any external dependencies. Next, we use the Microsoft C/C++ compiler to create an Assembly file from the source code. You should have multiple instances of cl.exe on your system, located in folders named x86 and x64. The instance of cl.exe you use will determine whether you get a 32 or 64 bit Assembly file. The following command creates the required Assembly file:

Now that we have an assembly file, we need to make some modifications to make it valid shellcode. Hasherezade explains how the required steps can be performed manually, which is definitely a good exercise. However, for now we will use the tool masm_shc to automate most of the work:

Adjusting the assembly file to correctly return to the macro

We could compile and link the Assembly file now. If we include the resulting shellcode in our macro, we would get a beacon living in a different process, resolving our initial problem. However, the shellcode will not return correctly to the macro, making the Office program crash. To prevent our victims from becoming suspicious and alerting the SOC, we can add a small stub to the Assembly file to correctly return to the macro. I wrote a small C++ program that pauses just before execution of the EnumSystemCodePagesW function and prints the address of the callback function. This allows us to easily put a breakpoint at the callback function. The callback function returns the message FALSE to indicate that we do not want to enumerate any additional code pages.

Looking at the callback function in a debugger, we can see the following:

It is important to notice that the ‘add esp, 8’ instruction is merely there to counter the two push instructions that pushed the arguments for our function call onto the stack. In our stub we don’t need those instructions. The resulting stub looks as follows:

Next, we can compile and link the Assembly file into a valid PE file, using the Microsoft Assembler. To differentiate between a 32 and 64 bit PE, use ml.exe or ml64.exe, respectively. The following command creates a PE file that uses our stub as the entry point:

Finally, we can extract the shellcode by dumping the .text section, using PE-Bear. I wrote a Python script to convert the binary blob into a format that works with the VBA WriteMemory function and added the final payload to the macro. Now, when the macro runs, it injects a Cobalt Strike beacon into OneDrive.exe without crashing or giving any other signs to the user something might be off.

I hope you enjoyed this blogpost! If you have any questions, comments or suggestions, feel free to reach out to me at LinkedIn.

More information

For any further queries, please contact Sander Ubink.