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 = ''       }

http://feeds.feedburner.com/sophos/dgdY