Skip to content

Instantly share code, notes, and snippets.

@antonioCoco
Created May 6, 2023 05:10
Show Gist options
  • Save antonioCoco/0ce9d3e1367506a05645d65318cc92ae to your computer and use it in GitHub Desktop.
Save antonioCoco/0ce9d3e1367506a05645d65318cc92ae to your computer and use it in GitHub Desktop.
Invoke-PowerTrashUnpacker - Automatically unpack Powertrash loaders
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