Join PowerShell Script from Event Logs

Although not a strict pre-requisite, please check out the article below which explains about various types of artifacts left or generated by PowerShell script/module execution.

PowerShell Command History Forensics — Blog — Sophos Labs — Sophos Community

Sometimes while going through Microsoft-Windows-PowerShell/Operational Windows Event Logs, you may encounter the execution of suspicious PowerShell code logged Event ID 4104. The code could be simple commands or part of a bigger-badder .PS1 file.

C:\Windows\System32\winevt\Logs\Microsoft-Windows-PowerShell%4Operational.evtx

The PowerShell script (.ps1) might be deleted by the threat actor but Windows Script Block logging to the rescue!

But there’s just one challenge,

97 of whaaaaat?

I tried to use various open-source tools such as,

  1. DeepBlueCLI
  2. Countercept/chainsaw
  3. EVTXecmd

but I couldn’t find a point-and-shoot way to extract the complete PowerShell script from within the Event Logs.

The answer lay hidden here: Script Tracing and Logging — PowerShell | Microsoft Docs

$created = Get-WinEvent -FilterHashtable @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 } |
Where-Object { $_.<...> }
$sortedScripts = $created | sort { $_.Properties[0].Value }
$mergedScript = -join ($sortedScripts | % { $_.Properties[2].Value })

That’s it — Microsoft expects the on-looker to grasp the concept just like that. Well, that’s not the case for some audiences so I thought of writing this blog to hand-hold you through the entire process.

Say Hello to PowerShell ISE

The WHAT

Once you discover the Script which is spread across 121 Event Log entries, take a note of the ScriptBlock ID.

Open PowerShell ISE and execute the command after replacing the location of your Event Log (EVTX) & ScriptBlock ID:

$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="C:\EVTXSamples\Microsoft-Windows-PowerShell%4Operational.evtx"; ProviderName=“Microsoft-Windows-PowerShell”; Id = 4104 } | Where-Object { $_.Message -like '*51baf005-40a5-4878-ab90-5ecc51cab9af*' }$SortIt = $StoreArrayHere | sort { $_.Properties[0].Value }$SortIt | select ActivityId,Message
uh oh

That does not seem right. It looks like we only have a partial script recorded in the Event Logs i.e. 97 to 121. We seem to be missing 1–96

That’s OK. I wanted to show you that due to log rotation, you might miss a snippet or two, but for an Incident Responder, it’s important to extract and look for cues in whatever data you have.

Options:

  1. Remove the Where-Object filter and find out which script blocks have been completely recorded in the Event Logs.
$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="C:\EVTXSamples\Microsoft-Windows-PowerShell%4Operational.evtx"; ProviderName=“Microsoft-Windows-PowerShell”; Id = 4104 }$SortIt = $StoreArrayHere | sort { $_.Properties[0].Value }$SortIt | select ActivityId,Message

2. Using Event Log Explorer or Windows Event Viewer, find out another ScriptBlock ID of interest.

Turns out, we were able to capture a few scripts. We filtered using one of the ScriptBlock ID entries from the list,

$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="Microsoft-Windows-PowerShell%4Operational.evtx"; ProviderName=“Microsoft-Windows-PowerShell”; Id = 4104 } | Where-Object { $_.Message -like '*97b04021-6c0b-4fd2-8f57-39ada2599db8*' }$SortIt = $StoreArrayHere | sort { $_.Properties[0].Value }$SortIt | select ActivityId,Message
voila

The HOW

To restore this script to disk,

$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="Microsoft-Windows-PowerShell%4Operational.evtx"; ProviderName=“Microsoft-Windows-PowerShell”; Id = 4104 } | Where-Object { $_.Message -like '*97b04021-6c0b-4fd2-8f57-39ada2599db8*' }$SortIt = $StoreArrayHere | sort { $_.Properties[0].Value }$MergedScript = -join ($SortIt | % { $_.Properties[2].Value }) | Out-File SomeBadScript.ps1
happy huntin’

Update

Due to the amazing response I got on this, I wrote (and tested on real-world EVTX files) a PowerShell script which you can use to simply dump all the PS1/Commands contained in an offline EVTX:

Sample Output

Once again, thank you!