PowerShell forensics in the registry: logging and execution policy
9 min read
PowerShell is the attacker's interpreter of choice on Windows: signed, present everywhere, scriptable, and capable of doing nearly anything without dropping a binary. So when you work a Windows intrusion, two questions come up early. First, what was the host's PowerShell execution policy — and does it matter? Second, was PowerShell logging turned on — ScriptBlockLogging, module logging, transcription — because that single fact decides whether the event logs are full of the attacker's commands or empty. Both answers are sitting in the SOFTWARE hive, and you can read them offline without ever booting the machine.
This post is about reading the registry's record of the PowerShell auditing posture: where the values live, what they mean, and why the logging configuration is a piece of evidence in its own right.
Execution policy: where it lives, and why it is not a security boundary
The per-machine execution policy is stored under the PowerShell shell-id key. The powershell_execpolicy plugin reads it from:
SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell
SOFTWARE\Microsoft\PowerShell\3\ShellIds\Microsoft.PowerShell
The value name is ExecutionPolicy, a string. The 1 and 3 are PowerShell engine major versions: 1 covers the older 1.0/2.0 engine, 3 covers 3.0 and later (which is what every modern Windows install actually runs). The plugin walks both and emits a row per version that has the value set, so you can see, for example, PowerShell 3 → RemoteSigned. The typical values you will encounter are Restricted, AllSigned, RemoteSigned, Unrestricted, and Bypass.
Here is the part that trips up people new to this: the execution policy is not a security control. Microsoft says so explicitly, and it is worth internalising before you over-read it in a report. The policy governs whether script files (.ps1) run by default and whether they need signing. It does nothing to stop an interactive session, a one-liner pasted into a console, a command passed with -Command, a script piped through stdin, or the single most common bypass of all — powershell.exe -ExecutionPolicy Bypass, which sets the policy for that process from the command line, no registry edit required. The flag is not even hidden; it is in every "living off the land" cheat sheet.
So treat the stored ExecutionPolicy as configuration context, not as a barrier the attacker had to defeat. A host showing Restricted did not stop anyone who knew -ExecutionPolicy Bypass. What the value is good for: establishing the administrative baseline of the machine, and occasionally catching a clumsy change. A workstation flipped from the GPO-pushed RemoteSigned to Unrestricted or Bypass in the registry is a deviation worth a sentence — someone, at some point, wanted scripts to run freely on that box. It is a weak signal, but it is free.
Logging policy: the values that decide whether you have evidence
This is the part that matters far more, and it is a different key entirely. The logging configuration is Group Policy state, so it lives under the Policies branch, not the shell-id branch. The pslogging plugin reads three sibling keys under:
SOFTWARE\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging
SOFTWARE\Policies\Microsoft\Windows\PowerShell\ModuleLogging
SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription
and pulls, respectively:
EnableScriptBlockLogging(underScriptBlockLogging)EnableModuleLogging(underModuleLogging)EnableTranscripting(underTranscription) — note the value name isEnableTranscripting, with the awkward spelling Microsoft actually shipped
Each is effectively a boolean DWORD: 1 for on, absent or 0 for off. The plugin surfaces them as a three-row table — Setting / Enabled — and its own note states the doctrine plainly: "If these are off, expect little PowerShell evidence elsewhere — frames what the host could have recorded." That sentence is the whole reason this plugin exists. The registry is not where the PowerShell commands are recorded; it is where you learn whether they were recorded at all.
What each one buys you, when it is on:
ScriptBlockLogging is the big one. With EnableScriptBlockLogging=1, PowerShell writes the full text of every script block it compiles to the Microsoft-Windows-PowerShell/Operational event log as Event ID 4104. Crucially, this captures the code as the engine sees it — after the layers of -EncodedCommand base64, string concatenation, and Invoke-Expression wrapping that obfuscation frameworks pile on. It is the closest thing to a transcript of what actually ran. When this is enabled, an intrusion that leaned on PowerShell is often reconstructable command by command. When it is off, those 4104 events do not exist, and you are reconstructing from process-creation logs, prefetch, and AMSI telemetry instead.
ModuleLogging (EnableModuleLogging=1) records pipeline execution details for configured modules as Event ID 4103 in the same Operational log. Note that module logging is usually scoped by a ModuleNames subkey listing which modules to log (commonly * for all). It is noisier and less complete than script-block logging for reconstructing intent, but it captures parameter binding and pipeline data that 4104 does not always make obvious.
Transcription (EnableTranscripting=1) writes a flat-file transcript of each session — input and output — to disk. The output directory is typically configured in an OutputDirectory value under the same Transcription key. This is a different evidence source again: not an event log but text files, which means it survives event-log rollover and clearing, but also means it can be deleted by anyone with write access to the output path. If transcription was on, go find those files.
A practical caveat: these three policy keys are populated by Group Policy. On a domain box they reflect the GPO that applied; on a standalone box an admin may have set them by hand or via DSC. Either way, the value in the hive is the policy that was in force at the time the hive was captured — which is exactly what you want to know.
The defender angle: the registry as the auditing-state-of-record
Tie the two halves together from a blue-team and DFIR standpoint, because this is where the registry earns its keep.
The reason an analyst checks these keys first is to calibrate expectations before spending hours hunting for events that were never written. If you load a SOFTWARE hive, run pslogging, and all three rows come back empty or 0, you now know that the absence of PowerShell 4104/4103 events in your collected logs is expected, not suspicious — the host was never configured to record them. That is the difference between "the attacker covered their tracks" and "there were never any tracks here to begin with." Reporting one as the other is an easy way to mislead a case.
It also cuts the other way, and this is the more interesting investigative angle. Disabling logging is a defense-evasion move, mapped in MITRE ATT&CK to T1562.001, Impair Defenses: Disable or Modify Tools — and we have written about that broader technique in the context of the registry's defense-evasion footprint. An attacker who lands on a well-instrumented host with EnableScriptBlockLogging=1 has a motive to flip it to 0 before doing the noisy work. If you can establish from a baseline, a GPO export, or a prior image that script-block logging was enabled, and the captured hive now shows it disabled — with a matching LastWrite on the ScriptBlockLogging key around your intrusion window — that is a defense-evasion finding, not a configuration footnote.
Hedge appropriately here. A single hive is a snapshot. "Logging is off" in one image does not by itself prove an attacker turned it off; plenty of environments simply never enabled it (it is off by default on older builds). The strong finding requires a before state to compare against — VSS snapshots, a golden image, the domain GPO, or an earlier triage collection. Without that, the honest report says "PowerShell script-block logging was disabled at time of capture; unable to determine whether this was the standing configuration or a change," and you note the key's LastWrite as a lead to run down. The registry hands you the posture; corroboration of when it changed comes from comparing states.
Reading the auditing state from an offline SOFTWARE hive
The workflow on a mounted or copied SOFTWARE hive is short:
- Execution policy — read
Microsoft\PowerShell\3\ShellIds\Microsoft.PowerShell(and the1variant for completeness) and note theExecutionPolicyvalue. Record it as context. Do not over-weight it. - Logging policy — read the three keys under
Policies\Microsoft\Windows\PowerShell\and record whetherEnableScriptBlockLogging,EnableModuleLogging, andEnableTranscriptingare set to1. For transcription, also grabOutputDirectoryso you know where the files should be. For module logging, check theModuleNamessubkey to see what scope was logged. - Note the
LastWritetimestamps on those policy keys. They tell you when the auditing configuration was last touched, which is the lead for a "was logging disabled during the incident" question. - Calibrate your event-log expectations off the result, then go pull (or explain the absence of) the Operational log 4104/4103 events and the transcript files.
You can do all of this without a live system. Drop the SOFTWARE hive into the parser on this site and the powershell_execpolicy and pslogging plugins surface exactly these values in the tree view — the execution policy per engine version, and the three logging toggles as an enabled/disabled table — so you can read the machine's PowerShell auditing state in the browser, with nothing uploaded, before you commit to a log-collection strategy.
Tools
- RegRipper has long-standing plugins covering the PowerShell keys; check your plugin set for the execution-policy and logging coverage and read the source to confirm exactly which values your version emits. For the full catalogue of what RegRipper pulls from each hive, see the RegRipper plugins reference.
- Eric Zimmerman's RECmd with a batch file targeting
Policies\Microsoft\Windows\PowerShell— same values, CSV out, trivially diffable between two hives or two VSS snapshots, which is precisely how you establish a before/after on the logging posture. - The parser on this site decodes both the execution policy and the ScriptBlockLogging / ModuleLogging / Transcription toggles as part of its standard tree view, so you can analyze a SOFTWARE hive in your browser.
The bottom line
The execution policy in the registry is configuration, not a control — read it for context, never report it as a barrier, and remember -ExecutionPolicy Bypass defeats it from the command line without touching the hive. The logging policy under Policies\Microsoft\Windows\PowerShell is the load-bearing artifact: EnableScriptBlockLogging, EnableModuleLogging, and EnableTranscripting tell you, before you go looking, whether the event logs hold the attacker's commands or hold nothing. Read the toggles, note the LastWrite, calibrate your expectations, and — when you can find a before-state to compare against — treat a disabled logging policy as the T1562 defense-evasion finding it may well be.