danielbohannon / Invoke-Obfuscation

PowerShell Obfuscator
Apache License 2.0
3.62k stars 764 forks source link

Out-ObfuscatedTokenCommand fails on 'HelpMessage' and 'ConfirmImpact' ParameterBindings #2

Closed cobbr closed 7 years ago

cobbr commented 7 years ago

Problem

When Out-ObfuscatedTokenCommand.ps1 attempts to determine whether to '$EncapsulateAsScriptBlockInsteadOfParentheses', it misses the 'HelpMessage' and 'ConfirmImpact' ParameterBinding options.

I discovered this issue while attempting to obfuscate modules in the Empire project.

This error specifically was found while attempting to obfuscate Get-SPN.ps1, Invoke-EventVwrBypass.ps1, and Invoke-ShellCode.ps1.

Steps to reproduce

PS> git clone https://github.com/danielbohannon/Invoke-Obfuscation.git
PS> wget https://github.com/adaptivethreat/Empire/raw/master/data/module_source/situational_awareness/network/Get-SPN.ps1
PS> wget https://github.com/adaptivethreat/Empire/raw/master/data/module_source/privesc/Invoke-EventVwrBypass.ps1
PS> wget https://github.com/adaptivethreat/Empire/raw/master/data/module_source/code_execution/Invoke-Shellcode.ps1
PS> Import-Module .\Invoke-Obfuscation\Invoke-Obfuscation.psm1

[*] Validating necessary commands are loaded into current PowerShell session.

[*] Function Loaded :: Out-ObfuscatedTokenCommand
[*] Function Loaded :: Out-ObfuscatedStringCommand
[*] Function Loaded :: Out-EncodedAsciiCommand
[*] Function Loaded :: Out-EncodedHexCommand
[*] Function Loaded :: Out-EncodedOctalCommand
[*] Function Loaded :: Out-EncodedBinaryCommand
[*] Function Loaded :: Out-SecureStringCommand
[*] Function Loaded :: Out-EncodedBXORCommand
[*] Function Loaded :: Out-PowerShellLauncher
[*] Function Loaded :: Invoke-Obfuscation

[*] All modules loaded and ready to run Invoke-Obfuscation

PS> Out-ObfuscatedTokenCommand -Path .\Get-SPN.ps1 | Out-File out
Obfuscating Get-SPN.ps1

[*] Obfuscating 27 Comment tokens.

[*] Obfuscating 69 String tokens.
Exception calling "Create" with "1" argument(s): "At line:14 char:21
+ ... HelpMessage=("{8}{12}{14}{1}{11}{13}{5}{10}{3}{0}{7}{4}{2}{6}{9}{15}" ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Attribute argument must be a constant or a script block.
At line:19 char:21
+ ... HelpMessage=("{8}{11}{5}{9}{2}{0}{1}{10}{4}{6}{7}{3}"-f 'r Domain','  ...
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Attribute argument must be a constant or a script block.
(errors continue)

The error message makes it pretty clear that there is a problem with the 'HelpMessage' ParameterBinding. The other two files show a similar error message, but for the 'ConfirmImpact' ParameterBinding.

Reducing the Get-SPN.ps1 file to this minified version still reproduces the error.

function Get-SPN
{   
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory=$false,
        HelpMessage="Credentials to use when connecting to a Domain Controller.")]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty

    )
}

Solution

It seems that these are similar issues to the one described in the source here.

# For Parameter Binding the value has to either be plain concatenation or must be a scriptblock in which case we will encapsulate with {} instead of ().
# The encapsulation will occur later in the function. At this point we're just setting the boolean variable $EncapsulateAsScriptBlockInsteadOfParentheses.
# Actual error that led to this is: "Attribute argument must be a constant or a script block."
# ALLOWED     :: [CmdletBinding(DefaultParameterSetName={"{1}{0}{2}"-f'd','DumpCre','s'})]
# NOT ALLOWED :: [CmdletBinding(DefaultParameterSetName=("{1}{0}{2}"-f'd','DumpCre','s'))]

The only difference being that it is for 'HelpMessage' and 'ConfirmImpact' instead of 'DefaultParameterSetName'.

The code that follow this explanation seems to only cover the case of 'DefaultParameterSetName'.

$SubStringStart = 30
If($Token.Start -lt $SubStringStart)
{
    $SubStringStart = $Token.Start
}

$SubString = $ScriptString.SubString($Token.Start-$SubStringStart,$SubStringStart).ToLower()
If($SubString.Contains('parametersetname') -AND $SubString.Contains('='))
{
    $EncapsulateAsScriptBlockInsteadOfParentheses = $TRUE
}

Replacing that code with the following code remediates the issue for 'HelpMessage' and 'ConfirmImpact'. (Though, maybe some work should be done to determine if others should join this list.)

$LastEndBracketIndex = $ScriptString.LastIndexOf(']', $Token.Start)
$LastBeginBracketIndex = $ScriptString.LastIndexOf('[', $Token.Start)
$LastEndParenIndex = $ScriptString.LastIndexOf(')', $Token.Start)
$LastBeginParenIndex = $ScriptString.LastIndexOf('(', $Token.Start)

If($LastBeginBracketIndex -gt $LastEndBracketIndex -AND $LastBeginParenIndex -gt $LastEndParenIndex)
{
    $LastCommaIndex = $ScriptString.LastIndexOf(',', $Token.Start)
    $BeginSubStringIndex = [math]::max($LastBeginBracketIndex, $LastCommaIndex)
    $ScriptSubString = $ScriptString.SubString($BeginSubStringIndex, $Token.Start-$BeginSubStringIndex).ToLower()

    If(($ScriptSubString.Contains('parametersetname') -OR $ScriptSubString.Contains('confirmimpact') -OR $ScriptSubString.Contains('helpmessage')) -AND $ScriptSubString.Contains('='))
    {
        $EncapsulateAsScriptBlockInsteadOfParentheses = $TRUE
    }
}

This is also a more systematic way of grabbing the ParameterBinding option's name, as opposed to assuming a length of the attribute itself.

danielbohannon commented 7 years ago

Issue fixed in d419d0b4a0592c0cd48d7a22d462477f4b976970 release.