a shattered plate being reassembled

Reconstrucción de scripts PowerShell a partir de múltiples registros de eventos de Windows

Los adversarios siguen abusando de PowerShell para ejecutar comandos y scripts maliciosos. Es fácil entender su popularidad entre los atacantes: no sólo está presente en todas las versiones de Windows por defecto (y es crucial para tantas aplicaciones de Windows que pocos optan por desactivarlo), sino que este potente entorno interactivo de CLI y scripts puede ejecutar código en memoria sin que el malware llegue a tocar el disco. Esto supone un problema tanto para los defensores como para los investigadores.

En un artículo anterior, explicamos varios artefactos forenses dejados por PowerShell. Con el lanzamiento de PowerShell 5.0 en 2015, se activó por defecto el registro de bloques de scripts. Esta función registra comandos y scripts completos en los registros de eventos a medida que se ejecutan. Si un script es muy grande, PowerShell lo divide en múltiples partes antes de registrarlas bajo el ID de evento 4104, que será el foco de este artículo.

La comunidad de código abierto tiene una variedad de herramientas efectivas para usar cuando se analizan o buscan automáticamente eventos sospechosos. En un artículo reciente, hemos analizado paso a paso la decodificación de la actividad maliciosa de PowerShell en un incidente específico, utilizando dichas herramientas. Sin embargo, la capacidad de extraer o reconstruir (parcial o totalmente) un script de PowerShell muy grande a partir de múltiples registros de eventos aún no se encuentra en la mayoría de las herramientas disponibles.

Cuando se ejecuta un script de PowerShell de gran tamaño, da lugar a una serie de artefactos fragmentados depositados en múltiples registros. El filtrado del ID de evento 4104 devuelve una lista de esos artefactos. El contenido de uno de estos artefactos, incluido en el registro de eventos C:\Windows\System32\winevt\Logs\Microsoft-Windows-PowerShell%4Operational.evtx, se muestra en la parte inferior de la pantalla del Visor de Eventos en la Figura 1.

Figura 1: Eventos 4104 en el registro Operational.evtx

El ScriptBlock ID para este fragmento, 51baf005-40a5-4878-ab90-5ecc51cab9af, aparece a la derecha en la Figura 2.

Figura 2: Detalle que muestra el ScriptBlock ID del fragmento 97

Para crear un único objeto PowerShell que contenga todos los artefactos encontrados con este proceso, abre PowerShell ISE, sustituye la ubicación del EVTX fuera de línea (en nuestro ejemplo, Operational.evtx) y el ScriptBlock ID (en nuestro ejemplo, 51baf005-40a5-4878-ab90-5ecc51cab9af), y ejecuta lo siguiente para crear un único objeto PowerShell como se muestra en el ejemplo siguiente.

#Filtering out all the Event Records associated with the ScriptBlockID into a single PS Object
$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="C:\SampleEVTX\Microsoft-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

En la Figura 3, sólo una parte de la secuencia de comandos se registra en los registros de eventos, concretamente los segmentos 97 a 121. Debido a la rotación programada de los registros, decenas de segmentos ya no están disponibles.

Figura 3: Fragmentos que aparecen en el registro de eventos

Sin embargo, incluso los datos parciales pueden ser útiles durante una investigación de respuesta a incidentes, lo que hace que esta técnica de extracción sea útil incluso cuando no se puede determinar el estado de los datos del registro antes de la operación.

Para intentar el proceso de listado y extracción a través de un sencillo script disponible en GitHub, utiliza el script de PowerShell ExtractAllScripts.ps1 dándole el parámetro -List, tal y como se muestra en la Figura 4. (Por comodidad, enlazamos y mostramos el script completo al final de este post).

Figura 4: Concatenando los resultados

 

Para extraer los scripts seleccionados, asigna al script ExtractAllScripts.ps1 el parámetro -ScriptBlockID [ID]. Un extracto del script muestra lo que ocurre entre bastidores:

$StoreArrayHere = Get-WinEvent -FilterHashtable @{ Path="C:\SampleEVTX\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 } 
#Joining the specific property of all event records and exporting to a single file. 
$MergedScript = -join ($SortIt | % { $_.Properties[2].Value }) | Out-File 34CE.

La popularidad de PowerShell entre los atacantes se debe en parte a su ubicuidad y a su capacidad para ejecutar código malicioso en memoria. Por lo tanto, los defensores deben examinar cualquier rastro de script que se encuentre en los registros, incluso si dichos rastros pueden estar dispersos en múltiples ubicaciones. Dado que los intervalos de rotación de los registros y el tamaño de los scripts varían, el resultado final del proceso detallado en este artículo puede recuperar parte, la mayor parte o la totalidad del script en cuestión. Sin embargo, la técnica en sí misma permite a los defensores aprovechar al máximo lo que está disponible.

Apéndice: 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 = ''     
}