Detecting Obfuscated PowerShell
Edit: If you want to see how deep this rabbit hole goes, check out our Black Hat / DEF CON presentation: https://www.youtube.com/watch?v=x97ejtv56xw
I was recently looking at a sample that was encoded using MSF’s basic template obfuscation (stolen without attribution from Matt Graeber of course):
For example:
function mtKZ {
Param ($l7PpJu1SE4VO, $qhnBBk5lHo)
$pcE6VKGt = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
return $pcE6VKGt.GetMethod('GetProcAddress').Invoke($null, @([System.Runtime.InteropServices.HandleRef](New-Object System.Runtime.InteropServices.HandleRef((New-Object IntPtr), ($pcE6VKGt.GetMethod('GetModuleHandle')).Invoke($null, @($l7PpJu1SE4VO)))), $qhnBBk5lHo))
}
For detection purposes, attempted obfuscation like this (i.e.: the variable names) are themselves an indicator to malicious activity.
PowerShell’s AST APIs make detection of stuff like this a breeze. For example, here’s a way to get all of the variables in $Path:
$tokens = @()
$null = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $null)
$tokens | ? VariablePath | % { $_.VariablePath.UserPath }
With that, we can start to do some variable analysis. Basic entropy is a pretty good start. When you combine that with letter frequency distribution, this creates a pretty good obfuscation metric:
14 [C:\temp]
>> dir *.ps1 | % { Measure-VariableObfuscation.ps1 $_.FullName } | sort ObfuscationMetric
Path Entropy TopFourLetters ObfuscationMetric
---- ------- -------------- -----------------
C:\temp\hello.ps1 0 0 0
C:\temp\foo2.ps1 0 1 0
C:\temp\2.ps1 0 0 0
C:\temp\1.ps1 0 0 0
C:\temp\3.ps1 0 0 0
C:\temp\verbose.ps1 3.17281073351987 0.666666666666667 1.05760357783996
C:\temp\msf_template.ps1 3.07084709362252 0.631578947368421 1.13136471870303
C:\temp\foo.ps1 3.65719253292414 0.588235294117647 1.50590280767465
C:\temp\pester.temp.tests.ps1 3.47972685963298 0.5625 1.52238050108943
C:\temp\configtest.ps1 3.75004181130572 0.495934959349594 1.89026497805654
C:\temp\sendmailmessagetest.ps1 3.79012121177685 0.48780487804878 1.94128159627595
C:\temp\Repro.ps1 4.16910660776366 0.46583850931677 2.22697620042034
C:\temp\sttest.ps1 4.27537906345103 0.469194312796209 2.26939552183183
C:\temp\TranscriptTest.ps1 4.17394102071541 0.425629290617849 2.39738946498757
C:\temp\Invoke-ActiveScriptEventConsumer.ps1 4.21710521416516 0.415384615384615 2.46538458674271
C:\temp\mywatch-command.ps1 4.2973293816282 0.42159383033419 2.48560182741991
C:\temp\Burn-Console.ascii.ps1 4.45029315016471 0.352501867064974 2.88155650574519
C:\temp\Burn-Console.ps1 4.45029315016471 0.352501867064974 2.88155650574519
C:\temp\Invoke-TokenManipulation.ps1 4.91011096435002 0.384693390598902 3.02122372925736
C:\temp\Invoke-TokenManipulationNonAdmin.ps1 4.91011096435002 0.384693390598902 3.02122372925736
C:\temp\stager.ps1 5.32866566677047 0.244131455399061 4.02777076220679
MSF could of course adapt to this, but its algorithm would continue to have predictable and detectable output. All you’ve got to do is look J
And of course, Measure-VariableObfuscation:
#requires -Module PowerShellArsenal
[CmdletBinding()]
param(
[Parameter(Mandatory)]
$Path
)
$tokens = @(); $null = [Management.Automation.Language.Parser]::ParseFile(
$Path, [ref] $tokens, [ref] $null)
$bytes = [byte[]][char[]]-join ($tokens | ? VariablePath |
% { $_.VariablePath.UserPath })
$entropy = 0
$top4 = 0
if($bytes)
{
$entropy = Get-Entropy $bytes
$letterFrequency = Measure-LetterFrequency (-join ($tokens |
? VariablePath | % { $_.VariablePath.UserPath })) -Raw
$top4 = $letterFrequency[1..4] | Measure-Object -Sum Percent | % Sum
}
[PSCustomObject] @{
Path = $Path
Entropy = $entropy
TopFourLetters = $top4
ObfuscationMetric = $entropy * (1 - $top4)
}