PowerShell Fundamentals
If you cannot read PowerShell, you cannot investigate a Windows breach. Mimikatz, PowerSploit, Empire, Covenant, and almost every public red-team toolkit are PowerShell — and almost every EDR alert from Microsoft Defender for Endpoint in the last five years is a 4104 Script Block event with the actual command the attacker ran. The language is also the cleanest way to query WMI, run remote commands via WinRM, and pull forensic artifacts from a fleet. This module covers the pipeline object model, PSRemoting, Script Block Logging, AMSI, and Constrained Language Mode — the five things every Windows-side operator needs.
The Core Concept
Section titled “The Core Concept”PowerShell is built on .NET. Every cmdlet returns one or more .NET objects, not text; the pipeline passes object references between cmdlets, not bytes. Get-Process | Where-Object {$_.CPU -gt 10} | Select-Object Name,CPU filters in-memory objects, not by parsing stdout. This is the single biggest difference from bash and the reason PowerShell scripts are short.
┌──────────────────────────────────────────────────────────┐│ PowerShell pipeline ││ Get-Process -> Where-Object -> Select-Object -> Out-Host││ each step passes PSObject references, not bytes │├──────────────────────────────────────────────────────────┤│ Underneath ││ .NET / CoreCLR -> System.Management.Automation.dll ││ PSHost -> RunspacePool -> WinRM (5985/5986) │└──────────────────────────────────────────────────────────┘How It Works
Section titled “How It Works”- Verb-noun cmdlet convention. Approved verbs (
Get-,Set-,New-,Remove-,Invoke-,ConvertTo-,Out-,Select-,Where-,ForEach-). Singular noun = single object; plural = collection.Get-ProcessreturnsSystem.Diagnostics.Process[]. - Pipeline objects.
$_(alias$PSItem) is the current pipeline object.Where-Object {<# predicate #>}filters;ForEach-Object {<# action #>}maps;Select-Object -First 10slices. Property access with dot notation:$p.Name,$p.WorkingSet64. - PSRemoting / WinRM:
- Server: WinRM service (
WinRM); HTTP listener on 5985, HTTPS on 5986. - Auth: Negotiate (Kerberos in-domain, NTLM fallback), Basic (cleartext username/password over TLS), CredSSP (delegation, dangerous), Kerberos (SPN
HOST/server.domain.com). - Session configurations (endpoints):
Get-PSSessionConfiguration—Microsoft.PowerShell(default 64-bit),Microsoft.PowerShell32(32-bit),Microsoft.PowerShell.Workflow. - One-to-one:
Enter-PSSession -ComputerName dc01 -Credential $c - One-to-many:
Invoke-Command -ComputerName srv1,srv2 -ScriptBlock { Get-Process } - Connection via
New-PSSessionfor reuse; close withRemove-PSSession.
- Server: WinRM service (
- CIM vs WMI:
Get-WmiObject Win32_Process— DCOM, deprecated, slow.Get-CimInstance Win32_Process— WS-Man (same protocol as WinRM), modern.Invoke-CimMethodfor method calls;Set-CimInstancefor writes.
- PowerShell versions:
- Windows PowerShell 5.1 — built into Windows Server 2016/2019/2022 and Windows 10/11. Runs on .NET Framework 4.x. Path
%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe. - PowerShell 7.x — cross-platform, .NET Core/5+. Side-by-side install at
C:\Program Files\PowerShell\7\pwsh.exe. Many security tools still target 5.1.
- Windows PowerShell 5.1 — built into Windows Server 2016/2019/2022 and Windows 10/11. Runs on .NET Framework 4.x. Path
- Logging — Script Block Logging is the single most important defensive control:
- Registry:
HKLM\SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging\EnableScriptBlockLogging=1. - Event log:
Microsoft-Windows-PowerShell/Operational, Event ID 4104 (Script Block), 4103 (Module), 4100 (parse error), 800 (Pipeline Execution), 400 (Engine state). - 4104 fires for every script block (function, loop body, conditional). Includes the de-obfuscated text after
IEXdecoding if the script block ended in a decoded eval. - 4104 is logged regardless of CLM; even when an attacker obfuscates, the script block contains the deobfuscated source because the PowerShell engine logs at compile time, not at parse time.
- Registry:
- AMSI (Anti-Malware Scan Interface) —
amsi.dllexposed to PowerShell, .NET, VBA, JavaScript. PowerShell callsAmsiScanBuffer(content)beforeInvoke-Expressionevaluates any script block. IfAMSI_RESULT_DETECTED(0x80070079), execution is blocked. - Constrained Language Mode (CLM) —
$ExecutionContext.SessionState.LanguageMode. Modes:FullLanguage(default),ConstrainedLanguage(set by WDAC policy),RestrictedLanguage,NoLanguage. CLM blocks direct .NET method invocation ([Activator]::CreateInstance), type accelerators, and Add-Type — but allows cmdlets and basic language constructs. WDAC enforces CLM system-wide; a script written before WDAC was enabled will fail mid-execution.
Common Techniques
Section titled “Common Techniques”| Technique | Description | Detection Difficulty |
|---|---|---|
| AMSI memory patch | Overwrite AmsiScanBuffer prologue with mov eax, 0x80070057; ret | Medium — EID 8 thread into amsi.dll + EID 4104 with amsiInitFailed |
| Reflection-based AMSI bypass | Set amsiInitFailed=true field via reflection | Easy — EID 4104 with literal amsiInitFailed |
| CLM bypass via COM-interop | Use New-Object -ComObject from CLM context (CLM does not block COM) | Hard — by-design gap, fixed in recent Windows |
Encoded command -enc | powershell -EncodedCommand <base64-utf16le> | Easy — EID 4104 logs decoded script block |
| AMSI provider unregistration | Unregister AMSI provider via registry / amsi.dll unload | Medium — registry write alert on HKLM\Software\Microsoft\AMSI\Providers |
| Runspace-based payloads | System.Management.Automation.Runspaces.Pipeline directly | Hard — no powershell.exe host process |
PowerShell-7 use of pwsh.exe | Defenders usually log powershell.exe but not pwsh.exe | Easy once alerted — add pwsh.exe to monitoring |
Hands-On Commands
Section titled “Hands-On Commands”# Get the current language mode and execution policy$ExecutionContext.SessionState.LanguageModeGet-ExecutionPolicy -List
# List every local session configuration (PSRemoting endpoints)Get-PSSessionConfiguration | Select Name, PSVersion, Permission, RunAsUser
# Run a command on multiple hosts with implicit credentials (Kerberos)Invoke-Command -ComputerName srv01,srv02,srv03 -ScriptBlock { Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'lsass.exe' } | Select-Object Name, ProcessId, CommandLine}
# Capture all logons in the last 24h from a DCGet-WinEvent -LogName Security -MaxEvents 10000 | Where-Object { $_.Id -eq 4624 -and $_.TimeCreated -gt (Get-Date).AddDays(-1) } | ForEach-Object { [PSCustomObject]@{ Time = $_.TimeCreated User = $_.Properties[5].Value SrcIP = $_.Properties[18].Value Type = $_.Properties[8].Value } } | Sort-Object Time -Descending
# Use the AMSI bypass (reflection trick) — for the lab; do not use on production hosts$a = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')$b = $a.GetField('amsiInitFailed','NonPublic,Static')$b.SetValue($null, $true)
# Confirm CLM is active (set by WDAC); this throws if CLM is on[Activator]::CreateInstance([Type]::GetTypeFromProgID('Scripting.FileSystemObject'))
# Pipe to Out-GridView for spreadsheet-like filteringGet-Process | Where-Object { $_.CPU -gt 10 } | Out-GridView
# Encode a script for use with -EncodedCommand (UTF-16LE base64)$cmd = 'Get-Process | Where-Object {$_.CPU -gt 5}'$bytes = [System.Text.Encoding]::Unicode.GetBytes($cmd)[Convert]::ToBase64String($bytes)
# Decode an encoded command (defender side) — see what attackers ran$enc = "RwBlAHQALQBQAHIAbwBjAGUAcwBzAA=="[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($enc))Detection — Defender View
Section titled “Detection — Defender View”Sysmon Event IDs (the same numbers used on F3 — Sysmon for both Windows and Linux uses the same schema):
- 1 — ProcessCreate (catch
powershell.exe -enc,pwsh.dll,powershell.exe -ExecutionPolicy Bypass). - 3 — NetworkConnect (catch
powershell.execonnecting to external IPs — usually suspicious). - 8 — CreateRemoteThread (catch AMSI patching into
amsi.dll). - 10 — ProcessAccess (catch mimikatz-style LSASS access from
powershell.exe). - 11 — FileCreate (catch payload extraction from base64).
Windows PowerShell operational log:
- 4104 — Script Block (the deobfuscated source — even for
-enc). - 4103 — Module (which module was loaded — catch
PowerSploit,PowerView,Nishang). - 800 — Pipeline Execution (catch pipeline commands).
- 400 — Engine state (catch
pwshstartup; includes the version field).
MITRE ATT&CK T1059.001 — Command and Scripting Interpreter: PowerShell.
MITRE ATT&CK T1021.006 — Remote Services: Windows Remote Management (Invoke-Command, Enter-PSSession to remote hosts).
MITRE ATT&CK T1055 — Process Injection (AMSI patch via CreateRemoteThread from PowerShell host).
MITRE ATT&CK T1562.001 — Impair Defenses: Disable or Modify Tools (AMSI bypass fits here).
MITRE ATT&CK T1006 — Direct Volume Access (PowerShell’s Get-Content -Raw \\.\C: reads raw disk — used by some credential-dumping scripts).
Wazuh rule 60004 / 920 — Suspicious PowerShell command line containing bypass, -enc, DownloadString, IEX [verify — requires custom decoder].
Wazuh rule 92600 — PowerShell 4104 event with AmsiUtils or amsiInitFailed [verify — custom rule against the Windows PowerShell operational log].
Evasion Techniques
Section titled “Evasion Techniques”AMSI bypass — memory patching. A CreateRemoteThread from powershell.exe into amsi.dll!AmsiScanBuffer patching the first 7 bytes to mov eax, 0x80070057; ret causes all subsequent script scans to return E_INVALIDARG (clean). Detection: Sysmon EID 8 with TargetImage containing amsi.dll, OR EID 7 with ImageLoaded=amsi.dll AND EID 8 within 5 seconds.
AMSI bypass — reflection. No CreateRemoteThread needed; the .NET reflection API can flip amsiInitFailed to true:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils') .GetField('amsiInitFailed','NonPublic,Static') .SetValue($null,$true)This works because PowerShell checks the static field before calling AmsiScanBuffer. Detection: EID 4104 with the literal string amsiInitFailed or AmsiUtils — alert on any script block containing these.
PowerShell 7 (pwsh.exe) vs PowerShell 5.1 (powershell.exe). Many EDR rules and Wazuh decoders only check for powershell.exe. An attacker who installs PowerShell 7 can use pwsh.exe and bypass legacy logging if Script Block Logging is only enabled for Windows PowerShell. Fix: enable Script Block Logging for both via Group Policy and ensure EDR monitors pwsh.exe (now in the Sysmon default configuration).
Runspace-based payloads. Using System.Management.Automation.Runspaces.Pipeline from a C# loader that does not load powershell.exe at all. The PowerShell engine (System.Management.Automation.dll) is invoked directly from any .NET process. Detection: alert on any process loading System.Management.Automation.dll that is not powershell.exe, pwsh.exe, or wsmprovhost.exe.
Script Block Logging bypass via selective disabling. An attacker with admin rights can clear the Microsoft-Windows-PowerShell/Operational event log (wevtutil cl Microsoft-Windows-PowerShell/Operational). Detection: monitor for wevtutil cl invocations from any process.
Constrained Language Mode bypass via COM. New-Object -ComObject ScriptControl or MSScriptControl.ScriptControl lets you run VBScript from within CLM. Recent Windows versions block this. Detection: alert on ScriptControl ProgID instantiation.
| Evasion | What it bypasses | How to detect |
|---|---|---|
| AMSI memory patch | Antimalware scan of script content | EID 8 thread into amsi.dll; EID 4104 with amsiInitFailed |
| AMSI reflection bypass | Antimalware scan | EID 4104 with literal amsiInitFailed |
pwsh.exe vs powershell.exe | Legacy decoders | Monitor both; Group Policy for both Script Block Logs |
| Runspace direct | powershell.exe process alert | Alert on System.Management.Automation.dll load outside known hosts |
wevtutil cl | Forensic trail | Alert on event-log clear operations (Security EID 1102) |
| CLM COM bypass | Constrained Language Mode | Alert on ScriptControl ProgID |
⚠️ What caught me: A red team left a long-running PowerShell script that enumerated the domain every 30 minutes. The customer had Script Block Logging on and 4104 events in their SIEM. They missed the activity because their 4104 rule excluded anything with severity=Information (the default level for 4104). The script was 8 KB of legitimate-looking cmdlet calls, including a benign-looking loop, but the inner pipeline had
Get-ADUser -Filter * -Properties *and dumped to a CSV. Lesson: 4104 events are Information by design; severity is set by your SIEM, not by Windows. Filter on content, not on severity.
Lab Practice
Section titled “Lab Practice”VM: Windows Server 2019 with PowerShell 5.1 + PowerShell 7.4 installed side-by-side. Enable Script Block Logging via Group Policy (Computer Configuration → Administrative Templates → Windows Components → Windows PowerShell → Turn on PowerShell Script Block Logging = Enabled).
Objective:
- Run
powershell.exe -EncodedCommand <base64-of 'Get-Process | Out-GridView'>and confirm the decoded text appears in EID 4104 of the PowerShell Operational log. - Run the AMSI reflection bypass in a fresh PowerShell 5.1 host; then run
Invoke-Mimikatz(use a benign variant for the lab). Confirm Defender blocks the second invocation if AMSI is enabled; confirm the bypass defeats the block. - Demonstrate a one-to-many
Invoke-Commandagainst three lab hosts; show the EID 4624 LogonType=3 on each target. - Use
pwsh.exe(PowerShell 7) to run the same encoded command; verify that the 4104 event is logged for bothpowershell.exeandpwsh.exe.
Expected flag format: flag{eid_4104_decoded_observed_amsi_bypassed_then_blocked_psr_4624_type3_pwsh_logged_too}.
Detection that should fire: EID 4104 with the decoded command (regardless of -enc); EID 8 if the AMSI memory-patch path was used; Security EID 4624 LogonType=3 on each target; if Defender was on, the Invoke-Mimikatz script block triggers a Defender alert.
Interview Questions
Section titled “Interview Questions”Conceptual: explain why Get-Process | Where-Object {$_.CPU -gt 10} is structurally different from ps aux | awk '$3 > 10'. What is the cost and what is the benefit?
Technical: walk through what happens when Invoke-Command -ComputerName srv01 -ScriptBlock {...} runs. Name every authentication protocol attempted, every endpoint used, and the WinRM HTTP verb.
Scenario: a customer disabled Script Block Logging “for performance.” List three events they will no longer see, and propose an alternative control for each.
What is Constrained Language Mode, what triggers it (name the policy mechanism), and what is the one feature that CLM blocks but PowerShell cmdlets remain able to do? Why does that matter?
Technical: explain the AMSI reflection bypass (amsiInitFailed = true). Why does it work, and what single rule on Event ID 4104 catches it in every variant of the technique?
Related Modules
Section titled “Related Modules”→ F3 (Windows Internals) · F5 (Python for Security) · F8 (Git & Version Control for Security)