-
-
Save antonioCoco/0ce9d3e1367506a05645d65318cc92ae to your computer and use it in GitHub Desktop.
Invoke-PowerTrashUnpacker - Automatically unpack Powertrash loaders
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function Invoke-PowerTrashUnpacker{ | |
<# | |
.SYNOPSIS | |
Invoke-PowerTrashUnpacker - Automatically unpack Powertrash loaders | |
Author: Antonio Cocomazzi @ SentinelOne | |
License: MIT | |
.DESCRIPTION | |
Invoke-PowerTrashUnpacker allows you to automatically unpack Powertrash loaders. | |
NOTE: Powershell AST and partial runtime evaluation are used for the unpacking. | |
Run this unpacker only on a testing virtual machine. | |
.PARAMETER InputPath | |
The path of the Powertrash loader or a directory containing Powertrash loaders | |
.EXAMPLE | |
PS>Invoke-PowerTrashUnpacker 86533fff7813bc140c89bd2ed09b8484afe7e4ac.ps1 | |
Description | |
----------- | |
Unpack a ps1 powertrash sample | |
#> | |
Param | |
( | |
[Parameter(Position = 0, Mandatory = $True)] | |
[String] | |
$InputPath | |
) | |
# Manage both cases if InputPath is a folder or a file | |
$files_to_unpack = @() | |
if(Test-Path -Path $InputPath -PathType Container){ | |
Get-ChildItem -Path $InputPath -File -ErrorAction SilentlyContinue | ForEach-Object { $files_to_unpack += $_.FullName} | |
} | |
else{ | |
$files_to_unpack += (Resolve-Path $InputPath).ToString() | |
} | |
foreach ($file_to_unpack in $files_to_unpack){ | |
$script_code = Get-Content -Path $file_to_unpack -Raw | |
# Checking the PowerTrash signature. We handle also the cases when the script is UTF16-LE encoded | |
if($script_code -match 'function\s[0-9a-zA-Z]{3,7}\r?\n\{\r?\n(\$[0-9a-zA-Z]{3,7}=.*\r?\n){10}'){ | |
Write-Output "[*] The file '$file_to_unpack' matched the PowerTrash signature. Trying to unpack it." | |
} | |
else{ | |
# Trying to read the file as UTF16-LE | |
$script_code = [System.IO.File]::ReadAllText($file_to_unpack, [System.Text.Encoding]::Unicode) | |
if($script_code -match 'function\s[0-9a-zA-Z]{3,7}\r?\n\{\r?\n(\$[0-9a-zA-Z]{3,7}=.*\r?\n){10}'){ | |
Write-Output "[*] The file '$file_to_unpack' matched the PowerTrash signature. Trying to unpack it." | |
} | |
else{ | |
Write-Output "[-] The file '$file_to_unpack' is not PowerTrash. Skipping..." | |
Continue | |
} | |
} | |
# Using Powershell Abstract Syntax Tree (AST) to parse the code | |
$AST_powertrash = [System.Management.Automation.Language.Parser]::ParseInput($script_code, [ref]$null, [ref]$null) | |
# Search for all functions in the powershell script | |
$functions_all = $AST_powertrash.FindAll({$args[0].GetType().Name -like "*FunctionDefinitionAst"}, $true) | |
# Search for all commands in the powershell script | |
$commands_all = $AST_powertrash.FindAll({$args[0].GetType().Name -like "*CommandAst"}, $true) | |
# Last function invocation is the main Powertrash function | |
$main_func = $commands_all[-1].Extent.Text | |
# Dinamically import all of the function definitions in our current process | |
$functions_all | ForEach-Object { Invoke-Expression $_.Extent.Text} | |
# We start to do our magic powershell unpacking from here, using AST to locate the base64 payload starting from the main | |
$base64_packed_payload = "" | |
$offset_start = 0x0 | |
$unpacked_payload_size = 0x0 | |
foreach ($function in $functions_all){ | |
# if we locate the main function name | |
if ($function.Name -eq $main_func){ | |
# We use AST a second time only on the main function | |
$AST_main = [System.Management.Automation.Language.Parser]::ParseInput($function.Extent.Text, [ref]$null, [ref]$null) | |
# Getting all assignments from the main function | |
$assignments = $AST_main.FindAll({$args[0].GetType().Name -like "*AssignmentStatementAst"}, $true) | |
# The base64 payload is dinamically retrieved by invoking the function (right operand) of the first assignment in the main | |
$base64_packed_payload = Invoke-Expression $assignments[0].Right.Extent.Text | |
# Second assignment right operator contains the offset of the start address for the unpacked code | |
$offset_start = $assignments[1].Right.Extent.Text | |
# Third assignment right operator contains the size of the unpacked payload | |
$unpacked_payload_size = $assignments[2].Right.Extent.Text | |
} | |
} | |
# Base64 payload retrieved, now extracting the binary payload, just replicating what PowerTrash does | |
$decoded_payload = [System.Convert]::FromBase64String($base64_packed_payload) | |
$bytearray = [IO.MemoryStream][Byte[]]$decoded_payload | |
$deflate_stream = New-Object IO.Compression.DeflateStream($bytearray, [IO.Compression.CompressionMode]::Decompress) | |
$unpacked_payload = New-Object Byte[]($unpacked_payload_size) | |
$bytes_read=$deflate_stream.Read($unpacked_payload, 0, $unpacked_payload_size) | |
if($bytes_read -gt 0 -and $bytes_read -eq ([int]$unpacked_payload_size)){ | |
Write-Output "[+] Powershell code unpacked!" | |
} | |
else{ | |
Write-Output "[-] Powershell code couldn't be unpacked. Skipping..." | |
continue | |
} | |
# Powershell code unpacked. Now we check if the unpacked payload is a pure PE | |
$unpack_success = $false | |
$pe_header_offset = [BitConverter]::ToInt32($unpacked_payload[60..63],0) | |
if($pe_header_offset -lt $unpacked_payload.Length -and [char]$unpacked_payload[0] -eq 'M' -and [char]$unpacked_payload[1] -eq 'Z'){ | |
if(([char]$unpacked_payload[$pe_header_offset] -eq 'P') -and ([char]$unpacked_payload[$pe_header_offset+1] -eq 'E')){ | |
$output_path = $file_to_unpack + ".dll" | |
Write-Output "[*] Unpacked content is a PE" | |
Set-Content $output_path -Value $unpacked_payload -Encoding Byte | |
Write-Output "[+] PE dumped to file $output_path" | |
$unpack_success = $true | |
} | |
} | |
# If not a pure PE, second we check if the unpacked payload is a PE with some prepended junk data | |
if(-not $unpack_success){ | |
Write-Output "[!] Unpacked content is not a PE file, trying to walk the content for searching PE signatures..." | |
for ($i=0; $i -lt $unpacked_payload.Length; $i++) | |
{ | |
if([char]$unpacked_payload[$i] -eq 'M' -and [char]$unpacked_payload[$i+1] -eq 'Z'){ | |
$mz_offset_candidate = $i | |
$pe_header_offset_candidate = [BitConverter]::ToInt32($unpacked_payload[($mz_offset_candidate+60)..($mz_offset_candidate+63)],0) | |
if($pe_header_offset_candidate -lt $unpacked_payload.Length -and ([char]$unpacked_payload[$mz_offset_candidate+$pe_header_offset_candidate] -eq 'P') -and ([char]$unpacked_payload[$mz_offset_candidate+$pe_header_offset_candidate+1] -eq 'E')){ | |
Write-Output "[*] PE file found at offset $mz_offset_candidate" | |
Write-Output "[*] Unpacked content was prepended with $mz_offset_candidate bytes of junk data, removing..." | |
$unpacked_payload = $unpacked_payload[$mz_offset_candidate..$unpacked_payload.Length] | |
$output_path = $file_to_unpack + ".dll" | |
Set-Content $output_path -Value $unpacked_payload -Encoding Byte | |
Write-Output "[+] PE dumped to file $output_path" | |
$unpack_success = $true | |
break | |
} | |
} | |
} | |
} | |
# The extracted payload from powershell is a PIC, probably a Core Impact loader | |
if(-not $unpack_success) | |
{ | |
$pic_offset_string = ([int]$offset_start).ToString('X') | |
Write-Output "[!] Unpacked content appears to be a packed Position Independent Code (PIC) starting at offset 0x$pic_offset_string, run Invoke-CoreImpactUnpacker to unpack it further" | |
$output_path = $file_to_unpack + ".pic" | |
Set-Content $output_path -Value $unpacked_payload -Encoding Byte | |
Write-Output "[+] Packed PIC extracted and dumped to file $output_path" | |
} | |
# We remove all of the dinamically imported functions to avoid a SessionStateOverflowException FunctionOverflow exception at the next iteration | |
$functions_all | ForEach-Object { $func_path="Function:\"+$_.Name; Remove-Item -Path $func_path -Force -ErrorAction SilentlyContinue} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment