Getting PowerShell Script Block Logging events with context like who, when, and how  run the code

Getting PowerShell Script Block Logging events with context like who, when, and how run the code

Aug 17, 2023ยท

5 min read

Play this article

Recently we've enabled the PowerShell Script Block logging security feature in our environment to be able to see what PowerShell commands were run on our computers. But what was my surprise when I found out there is no easy way to get such events with some context like who run such command, how the console was invoked when it started/ended etc.

So I've created a function Get-PSHScriptBlockLoggingEvent to solve this issue ๐Ÿ™‚

What is PowerShell Script Block logging?

There are already a lot of good articles about PowerShell Script Block logging feature, but in a nutshell:

  • it is available since PowerShell v5

  • allows you to log every command run on your host in PSH 5.x, 6.x or 7.x

  • events with ID 4104 are logged into the special Microsoft-Windows-PowerShell/Operational (for Windows PowerShell) or PowerShellCore/Operational (for PowerShell Core) event logs and contain deobfuscated data

  • events can be encrypted using a certificate, so you don't have to worry about leaking sensitive data


  • Install the newest version of my module CommonStuff and call function Get-PSHScriptBlockLoggingEvent

  •               Install-Module CommonStuff -force
                  Import-Module CommonStuff
                  # get Script Block logging events with all possible details for last 24 hours

Retrieval of Script Block logging events

The simplest way to get Script Block logging events (ID 4104) looks like this ๐Ÿ‘‡

# for Windows PowerShell 
Get-WinEvent -FilterHashtable @{logname = "Microsoft-Windows-PowerShell/Operational"; id = 4104 } | select -ExpandProperty message
# for PowerShell Core
Get-WinEvent -FilterHashtable @{logname = "PowerShellCore/Operational"; id = 4104 } | select -ExpandProperty message

And the result can be like ๐Ÿ‘‡

As you can see the returned result contains nothing more than invoked commands. The commands text is split into several events and contains not just the command text itself, but also some additional data that has to be cut off.

Now compare it with the result returned via my function Get-PSHScriptBlockLoggingEvent ๐Ÿ‘‡

Get-PSHScriptBlockLoggingEvent -startTime "10.3.2023 13:35" | Out-GridView

Run commands are grouped by PSH console ProcessId and a lot of additional data are shown.

Advantages of my Get-PSHScriptBlockLoggingEvent function

  • Supports reading of

    • Both Windows PowerShell and PowerShell Core event logs

    • Local computer event logs

    • Remote computer event logs (as a path to exported evtx files)

    • Forwarded event log (on event collector machine)

    • Protected event logs

  • Among commands that were run, returns context like

    • When the PSH process started/ended

    • How the PSH process was invoked

    • Who invoked the PSH process

    • What scripts were invoked during the session

    • Type of the session (local/remote)

    • Type of the PSH console (Windows PowerShell/PowerShell Core)

    • Computer name where commands were run

  • Checks included

    • Required event logs are enabled

    • Script Block Logging is enabled

    • Certificate with a private key is available (in case Protected Event logging is enabled)

    • Event logging manifest is enabled (if PowerShell Core is detected)

    • ...

  • Offers option to enable Script Block logging if it is not enabled already

What event logs are used to get the context

To be able to show the context of the command, Get-PSHScriptBlockLoggingEvent function combines data from several event logs:

  • Microsoft-Windows-PowerShell/Operational (for Windows PowerShell) and PowerShellCore/Operational (for PowerShell Core)

    • 4104 event

      • contain content of the invoked commands
    • 40961 event

      • contain start time of the invoked PSH session
  • Microsoft-Windows-WinRM/Operational

    • 91 event

      • contain start time of the session, by who and from which host it was started
  • Windows PowerShell (data only for Windows PowerShell)

    • 400 event

      • contain details about how the session was invoked and by whom (is logged a few milliseconds (or seconds :D) after the 40961 event)
    • 403 event

      • contain when the session ended and can be found through (correlates with 400 event through HostId value)

What obstacles had to be solved?

  • There is no unique identifier that can be used to correlate all PowerShell-related events!

    • 4104 and 40961 events contain ProcessId, but start/stop events (400) don't

    • To get the corresponding start event (400), you have to find the closest one (by comparing CreateTime property)

      • Sometimes these are created exactly at the same time
    • To get stop event (403), you need HostId property which is the same as for the start event (400)

  • The PowerShell start event (400) is sometimes not logged at all for some reason

  • ProcessId is surprisingly often reused which complicates the grouping of the events from one session


How Script Block logging can be bypassed?

  • As an administrator delete the registry value EnableScriptBlockLogging saved in

    • HKLM\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging (for Windows PowerShell)

    • HKLM:\SOFTWARE\Policies\Microsoft\PowerShellCore\ScriptBlockLogging (for PowerShell Core 6.x)

    • HKLM:\SOFTWARE\WOW6432Node\Policies\Microsoft\PowerShellCore\ScriptBlockLogging (for PowerShell Core 7.x)

  • And start a new PowerShell console (the old one will be still logged!)

  • Use PowerShell 2.0 if it is enabled in the system (by default it is)

  • Use PowerShell Core without registering "Windows Event Logging Manifest" during the installation a.k.a. don't create PowerShell Core event log

  • As an administrator clear the following event logs (this won't help if they are forwarded to some SIEM in the realtime though) or (probably a better solution) is to disable them

    • Microsoft-Windows-PowerShell/Operational

    • PowerShellCore/Operational

    • Windows PowerShell (cannot be disabled)

    • Microsoft-Windows-WinRM/Operational

How certificate for Protected Event Logging (PEL) can be created?

  • Save the following text into the PEL.inf file (encoded as UTF8!).

    • You can edit Subject and ValidityPeriodUnits parts as you like

        Signature = "$Windows NT$"
        Subject = ""
        MachineKeySet = false
        KeyLength = 4096
        KeySpec = AT_KEYEXCHANGE
        HashAlgorithm = Sha256
        Exportable = true
        RequestType = Cert
        ValidityPeriod = "Years"
        ValidityPeriodUnits = "1000"
  • In the working directory where PEL.inf is stored open Command Prompt and call the following command

      certreq -new PEL.inf PEL.cer
  • A new certificate PEL.cer will be created in the working directory and also in your Certificate Personal store

  • Export the newly created certificate from your certificate store (including the private key!) as PEL.pfx

  • PEL.cer can be used for events content encrypting, PEL.pfx for decrypting

  • Official tutorial for enabling PEL

Did you find this article valuable?

Support Ondrej Sebela by becoming a sponsor. Any amount is appreciated!