Reconstructing PowerShell scripts from multiple Windows event logs
Credit to Author: Vikas Singh| Date: Tue, 29 Mar 2022 18:27:50 +0000
Adversaries continue to abuse PowerShell to execute malicious commands and scripts. It is easy to understand its popularity among attackers: Not only is it present on all versions of Windows by default (and crucial to so many Windows applications that few choose to disable it), this powerful interactive CLI and scripting environment can execute code in-memory without malware ever touching the disk. This poses a problem for defenders and researchers alike.
In a previous post, we explained various forensic artifacts left behind by PowerShell. With the release of PowerShell 5.0 back in 2015, Script Block Logging was enabled by default. This feature records commands and entire scripts in event logs as they execute. If a script is very large, PowerShell breaks it into multiple parts before logging those under Event ID 4104, which will be the focus of this article.
The open-source community has a variety of effective tools to use when parsing or automatically hunting for suspicious events. In a recent post, we took a step-by-step look at decoding malicious PowerShell activity in a specific incident, using such tools. However, the ability to extract or reconstruct (partially or in full) a very large PowerShell script from multiple event records is still lacking in most of the tools available.
When a large PowerShell script runs, it results in a number of fragmented artifacts deposited across multiple logs. Filtering for event ID 4104 returns a list of those artifacts. The content of one of these artifacts, contained in the C:WindowsSystem32winevtLogsMicrosoft-Windows-PowerShell%4Operational.evtx
event log, is shown in the lower portion of the Event Viewer screen in Figure 1.
Figure 1: 4104 events in the Operational.evtx log
The ScriptBlock ID for this fragment, 51baf005-40a5-4878-ab90-5ecc51cab9af
, appears on the right in Figure 2.
Figure 2: Detail showing ScriptBlock ID for fragment 97
To create a single PowerShell object containing all the artifacts found with this process, open PowerShell ISE, replace the location of the offline EVTX (in our example, Operational.evtx
) and ScriptBlock ID (in our example, 51baf005-40a5-4878-ab90-5ecc51cab9af
), and execute the following to create a single PowerShell object as shown in the example below.
#Filtering out all the Event Records associated with the ScriptBlockID into a single PS Object $StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="C:SampleEVTXMicrosoft-Windows-PowerShell%4Operational.evtx"; ProviderName=“Microsoft-Windows-PowerShell”; Id = 4104 } | Where-Object { $_.Message -like '*51baf005-40a5-4878-ab90-5ecc51cab9af*' } #Sorting the objects in the sequence to maintain the order of script. $SortIt = $StoreArrayHere | sort { $_.Properties[0].Value } #Display a few columns of interest. $SortIt | select TimeCreated,ActivityId,Id,Message
In Figure 3, only a portion of the script is recorded in the event logs, specifically segments 97 to 121. Due to scheduled log rotation, dozens of segments are no longer available.
Figure 3: Fragments appearing in the event log
However, even partial data may be helpful during an incident-response investigation, making this extraction technique useful even when the condition of the log data cannot be ascertained prior to the operation.
To attempt the Listing and Extraction process via a simple script available on GitHub, use the PowerShell script ExtractAllScripts.ps1
by giving it the -List
parameter, as shown in Figure 4. (As a convenience, we link to and show the full script at the end of this post.)
Figure 4: Concatenating the results
To extract selected scripts, give the ExtractAllScripts.ps1
script the -ScriptBlockID [ID]
parameter. An excerpt from the script shows what happens behind the scenes:
$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="C:SampleEVTXMicrosoft-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 } #Joining the specific property of all event records and exporting to a single file. $MergedScript = -join ($SortIt | % { $_.Properties[2].Value }) | Out-File 34CE.
PowerShell’s popularity among attackers stems in part from its ubiquity and its ability to run malicious code in memory. Defenders must therefore examine whatever script traces may be found in logs, even if such traces may be scattered across multiple locations. Since log-rotation intervals and script sizes both vary, the ultimate output of the process detailed in this post may retrieve some, most, or all of the script in question. The technique itself, however, enables defenders to make the most of what is available.
Appendix: ExtractAllScripts.ps1
https://gist.github.com/vikas891/841ac223e69913b49dc2aa9cc8663e34.js
#Usage: #Usage: # #NOTE: Remember to include the path to Microsoft-Windows-PowerShell%4Operational.evtx below. # #C:>ExtractAllScripts.ps1 #The default behavior of the script is to assimilate and extract every script/command to disk. # #C:ExtractAllScripts -List #This will only list Script Block IDs with associated Script Names(if logged.) # #C:>ExtractAllScripts.ps1 -ScriptBlockID aeb8cd23-3052-44f8-b6ba-ff3c083e912d #This will only extract the script corresponding to the user specified ScriptBlock ID # #Twitter: @vikas891 param ($ScriptBlockID, [switch]$List) $StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="Microsoft-Windows-PowerShell%4Operational.evtx"; ProviderName=“Microsoft-Windows-PowerShell”; Id = 4104 } $Desc = $StoreArrayHere | sort -Descending { $_.Properties[1].Value } $ArrayofUniqueIDs = @() if(!$ScriptBlockID) { $Desc | %{ $ArrayofUniqueIDs += $_.Properties[3].Value } } else { $Desc | %{ $ArrayofUniqueIDs += $_.Properties[3].Value } if($ScriptBlockID -in $ArrayofUniqueIDs) { $ArrayofUniqueIDs = $ScriptBlockID } else { "" Write-Host "[!] Specified Script Block ID does not exist. Exiting.." -ForegroundColor Red break } } $ArrayofUniqueIDs = $ArrayofUniqueIDs | select -Unique if($List) { foreach ($a in $ArrayofUniqueIDs) { $Temp = $StoreArrayHere | Where-Object { $_.Message -like "*$a*" } $SortIt = $Temp | sort { $_.Properties[0].Value } "" if($SortIt[0].Properties[4].Value) { $OriginalName = Split-Path -Path $SortIt[0].Properties[4].Value -Leaf $FileName = "$($a)_$($OriginalName)" $DisplayName = $SortIt[0].Properties[4].Value } else { $OriginalName = '' $FileName = $a $DisplayName = 'NULL' } Write-Host -NoNewline "Script ID: " Write-Host -NoNewline $a -ForegroundColor Yellow Write-Host -NoNewline " | " -ForegroundColor White Write-Host -NoNewline "Script Name:" Write-Host -NoNewline $DisplayName -ForegroundColor Magenta $NumberOfRecords = $Temp.Count $MessageTotal = $Temp[0] | % {$_.Properties[1].Value} if($NumberOfRecords -eq $MessageTotal) { Write-Host -NoNewline " | Complete Script " -ForegroundColor Green Write-Host -NoNewline " | Event Records Logged"$NumberOfRecords/$MessageTotal "" } else { Write-Host -NoNewline " | InComplete Script Logged" -ForegroundColor Red Write-Host -NoNewline " | Event Records Logged"$NumberOfRecords/$MessageTotal "" } } break } foreach ($a in $ArrayofUniqueIDs) { $Temp = $StoreArrayHere | Where-Object { $_.Message -like "*$a*" } $SortIt = $Temp | sort { $_.Properties[0].Value } "" if($SortIt[0].Properties[4].Value) { $OriginalName = Split-Path -Path $SortIt[0].Properties[4].Value -Leaf $FileName = "$($a)_$($OriginalName)" $DisplayName = $SortIt[0].Properties[4].Value } else { $OriginalName = '' $FileName = $a $DisplayName = 'NULL' } Write-Host -NoNewline "Extracting " Write-Host -NoNewline $a -ForegroundColor Yellow if ($OriginalName) { Write-Host -NoNewline _$OriginalName -ForegroundColor Magenta } Write-Host -NoNewline " | " -ForegroundColor White Write-Host -NoNewline "ScriptName:" Write-Host -NoNewline $DisplayName -ForegroundColor Magenta $MergedScript = -join ($SortIt | % { $_.Properties[2].Value }) | Out-File $FileName $NumberOfRecords = $Temp.Count $MessageTotal = $Temp[0] | % {$_.Properties[1].Value} if($NumberOfRecords -eq $MessageTotal) { Write-Host -NoNewline " | Complete Script Logged " -ForegroundColor Green Write-Host -NoNewline " | Event Records Exported"$NumberOfRecords/$MessageTotal Write-Host -NoNewline " | Number of lines" (Get-Content $FileName).Length "" } else { Write-Host -NoNewline " | InComplete Script Logged" -ForegroundColor Red ren $FileName "$FileName.partial" Write-Host -NoNewline " | Event Records Exported"$NumberOfRecords/$MessageTotal Write-Host -NoNewline " | Number of lines" (Get-Content "$FileName.partial").Length "" } $FileName = '' }