diff --git a/.github/workflows/publish-release.yaml b/.github/workflows/publish-release.yaml new file mode 100644 index 0000000..623a2ce --- /dev/null +++ b/.github/workflows/publish-release.yaml @@ -0,0 +1,19 @@ +name: Publish PowerShell Module +on: + release: + types: [created] + +jobs: + publish-powershell-gallery: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v2 + - name: publishing + run: | + curl -L https://github.com/wietze/ArgFuscator.net/archive/refs/heads/main.zip -o ArgFuscator.zip && unzip ArgFuscator.zip && rm ArgFuscator.zip + mv ./ArgFuscator.net-main/models . + rm -rf ./ArgFuscator.net-main + rm ./Invoke-ArgFuscator.ps1 + Publish-Module -Path '.' -NuGetApiKey ${{ secrets.PGALLERY }} + shell: pwsh diff --git a/Invoke-ArgFuscator.ps1 b/Invoke-ArgFuscator.ps1 index 7049a1c..f893066 100644 --- a/Invoke-ArgFuscator.ps1 +++ b/Invoke-ArgFuscator.ps1 @@ -14,12 +14,7 @@ if ($args.Length -eq 0) { $InputFile = Read-Host "Path to configuration file" if (!($n = Read-Host "Number of commands to generate [default=$n_default]")) { $n = $n_default } -} else { - $InputFile = $Args[0] - $n = if ($args.Length -eq 2) { $Args[1] } else { $n_default } - if ($args.Length -gt 2) { - throw "Unexpected argument count" - } + $Args = @{ InputFile = $InputFile; n = $n }; } -Invoke-ArgFuscator -InputFile $InputFile -n $n +Invoke-ArgFuscator @Args diff --git a/Invoke-ArgFuscator.psd1 b/Invoke-ArgFuscator.psd1 new file mode 100644 index 0000000..f3a1ccc --- /dev/null +++ b/Invoke-ArgFuscator.psd1 @@ -0,0 +1,52 @@ +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'Invoke-ArgFuscator.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0' + + # ID used to uniquely identify this module + GUID = '844d9edc-57ad-4fcc-9fd5-77a69d4bf569' + + # Author of this module + Author = 'wietze' + + # Description of the functionality provided by this module + Description = 'A PowerShell module that generate obfuscated command-lines for common system-native executables' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + ScriptsToProcess = @() + + # Functions to export from this module + FunctionsToExport = @('Invoke-ArgFuscator') + + # Cmdlets to export from this module + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = @() + + # Aliases to export from this module + AliasesToExport = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess + PrivateData = @{ + PSData = @{ + # A URL to the license for this module. + LicenseUri = 'https://github.com/wietze/Invoke-ArgFuscator/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/wietze/Invoke-ArgFuscator' + } + } + + NestedModules = @() + +} \ No newline at end of file diff --git a/Invoke-ArgFuscator.psm1 b/Invoke-ArgFuscator.psm1 index 07a038a..c69f8d2 100755 --- a/Invoke-ArgFuscator.psm1 +++ b/Invoke-ArgFuscator.psm1 @@ -10,10 +10,107 @@ using module "Modifiers\Shorthands.psm1" using module "Modifiers\UrlTransformer.psm1" $OutputEncoding = [ System.Text.Encoding]::UTF8 +function Invoke-TokeniseCommand { + param( + [string]$InputCommand + ) + + if ($null -eq $InputCommand) { return $null } + $SeparationChar = ' ' + $QuoteChars = @('"', "'") + $ValueChars = @('=', ':') + $CommonOptionChars = @('/', '-') + + $InQuote = $null + [Token[]]$Tokens = @() # Explicitly type the array as Token[] + $TokenContent = @() + $SeenValueChar = $false + + for ($i = 0; $i -lt $InputCommand.Length; $i++) { + if ($TokenContent.Count -eq 0) { $SeenValueChar = $false } + $Char = $InputCommand[$i].ToString() + + if (($null -eq $InQuote) -and ( + ($Char -eq $SeparationChar) -or ( + (-not $SeenValueChar) -and + ((($i -eq $InputCommand.Length) -or (-not @('\\', '/') -contains $InputCommand[$i + 1])) -and + $ValueChars.contains($Char)) + ) + )) { + if ($Char -ne $SeparationChar) { + $TokenContent += $Char + } + + if ($TokenContent.Count -gt 0) { + $Tokens += [Token]::new($TokenContent) + } + $TokenContent = @() + } + else { + if (($null -ne $InQuote) -and ($Char -eq $InQuote)) { + $InQuote = $null + } + elseif (($null -eq $InQuote) -and ($QuoteChars | Where-Object { $_ -eq $Char })) { + $InQuote = $Char + } + + $TokenContent += $Char + } + $SeenValueChar = $SeenValueChar -or ($ValueChars | Where-Object { $_ -eq $Char }) + } + + if ($TokenContent.Count -gt 0) { + $Tokens += [Token]::new($TokenContent) + } + + # Find matching template, if available + $Tokens[0].Type = "command" + + $Tokens | Select-Object -Skip 1 | ForEach-Object -Begin { $i = 0 } -Process { + $TokenText = $_.ToString() + $_TokenText = $TokenText -replace "(['`"])(.*?)\1", '$2' #Remove any surrounding quotes + + # If previous token ends with a ValueChar, assume this token denotes a 'value' type; + # or, if no option char present, designate it as 'value', unless overwritten further down + if (($ValueChars | Where-Object { $Tokens[$i].TokenContent[-1] -eq $_ }) -or -not ($CommonOptionChars | Where-Object { $_TokenText.StartsWith($_) })) { + $_.Type = 'value' + + # Special case: WMIC + if ($Tokens[0].ToString() -match 'wmic(\.exe)?'` + -and -not ($Tokens[1..($i + 1)] | Where-Object { $_.GetType() -eq 'disabled' })` + -and -not ($Tokens[$i].GetType() -eq 'argument' -and ($ValueChars | Where-Object { $Tokens[$i].TokenContent[-1] -eq $_ }))) { + Write-Host $Tokens[$i] + $_.Type = 'disabled' + } + } + + if ($_TokenText -match '^(?:\\\\[^\\]+|[a-zA-Z]:|\.[\\/])((?:\\[^\\]+)+\\)?([^<>:]*)$' -or $_TokenText -match '^[^<>:]+\.[a-zA-Z0-9]{2,4}$') { + $_.Type = 'path' # Windows file path format + } + if ($_TokenText -match '^(HKLM|HKCC|HKCR|HKCU|HKU|HKEY_(LOCAL_MACHINE|CURRENT_CONFIG|CLASSES_ROOT|CURRENT_USER|USERS))\\?') { + $_.Type = 'disabled' # Windows Registry + } + if ($_TokenText.StartsWith('http:') -or $_TokenText.StartsWith('https:') -or $_TokenText -match '[12]?\d?\d\.[12]?\d?\d\.[12]?\d?\d\.[12]?\d?\d') { + $_.Type = 'url' #URLs (including IP addresses) + } + + $i++ + } + + $result = @() + $Tokens | ForEach-Object { + $hashtable = @{} + $hashtable[$_.Type] = ($_.TokenContent -join '') + $result += $hashtable + } + + return $result +} + function Invoke-ArgFuscator { [CmdletBinding()] Param ( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = "FromFile")] [ValidateScript({ if ((Test-Path $_ -PathType 'Leaf') -and ((Get-Item $_ | Select-Object -Expand Extension) -eq ".json" )) { return $true } @@ -21,18 +118,40 @@ function Invoke-ArgFuscator { throw "Make sure the file exists, and has a '.json' extension." } })] [string]$InputFile, + + [Parameter(Mandatory = $true, ParameterSetName = "FromCommand")] + [ValidateNotNullOrEmpty()] + [string]$Command, + + [Parameter(ParameterSetName = "FromCommand")] + [ValidateScript({ + $platformPath = Join-Path $PSScriptRoot "models" $_ + if (Test-Path $platformPath -PathType Container) { + return $true + } + else { + throw "Platform '$_' not found. Make sure the platform directory exists in the models folder ($platformPath)." + } + })] + [string]$Platform = "windows", [int]$n = 1 ) <# .SYNOPSIS - Obfuscates a command provided in a JSON-formatted configuration file. + Obfuscates a command provided in a JSON-formatted configuration file or command string. .DESCRIPTION - Obfuscates a command provided in a JSON-formatted configuration file by applying specified obfuscation options to the provided command. + Obfuscates a command provided in a JSON-formatted configuration file/command string by applying specified obfuscation options to the provided command. .PARAMETER InputFile Specifies the path to the JSON-formatted config file. + .PARAMETER Command + Specifies the command as string. + + .PARAMETER Platform + Specifies the platform (windows, linux, macos). Default value is windows. + .PARAMETER n Specifies the number of obfuscated commands to generate. Default value is 1. @@ -46,7 +165,26 @@ function Invoke-ArgFuscator { https://www.github.com/wietze/Invoke-Argfuscator #> - $JSONData = Get-Content -Encoding UTF8 -Path $InputFile | ConvertFrom-Json; + if ($PSCmdlet.ParameterSetName -eq "FromFile") { + $JSONData = Get-Content -Encoding UTF8 -Path $InputFile | ConvertFrom-Json + } + else { + $CommandData = Invoke-TokeniseCommand $Command + $cmd = $CommandData[0]["command"] + $filePath = Join-Path -Path $PSScriptRoot -ChildPath "\models\$Platform\$cmd.json" + if (Test-Path $filePath) { + $ModelData = Get-Content -Encoding UTF8 -Path $filePath | ConvertFrom-Json + # Create a PSCustomObject that matches the expected format + $JSONData = [PSCustomObject]@{ + "command" = ($CommandData | ConvertTo-JSON | ConvertFrom-Json) + "modifiers" = $ModelData.modifiers + } + } + else { + Write-Error("Command '{0}' could not be found in models folder ({1} does not exist)" -f $cmd,$filePath) + return $null + } + } $ErrorModifiers = @(); for ($i = 0; $i -lt $n; $i++) { $Tokens = [System.Collections.ArrayList]@(); diff --git a/README.md b/README.md index 81527a4..0935076 100644 --- a/README.md +++ b/README.md @@ -25,34 +25,72 @@ This module works on any operating system supporting PowerShell/pwsh; this inclu * **macOS**: If you have `brew` preinstalled, run `brew install powershell/tap/powershell` to install the latest version of PowerShell. For alternative installation options, refer to Microsoft's [documentation](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-macos). * **Linux**: Refer to Microsoft's [documentation](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-linux) to see how you can install PowerShell on your distribution. -### Installation +### Installation & usage -Simply clone or download the contents of this repository to a folder of choice. +1. The simplest way to install this module is via the following PowerShell command: -## Usage - -1. Make sure you have a model file, in JSON format. These can be generated via [ArgFuscator.net](https://argfuscator.net/) via the 'Download' option; alternatively, you can obtain raw base files via [GitHub](https://github.com/wietze/Argfuscator.net/tree/main/models). -2. Call `Invoke-ArgFuscator.ps1` via PowerShell, e.g.: - - ```bash - # Windows - powershell .\Invoke-ArgFuscator.ps1 - - # macOS and Linux - pwsh ./Invoke-ArgFuscator.ps1 + ```pwsh + Install-Module -Name Invoke-ArgFuscator ``` - This will allow you to pass the path to the model file interactively. +2. To use the module, call the function `Invoke-ArgFuscator` from within PowerShell, for example: - ***Alternatively***, pass the path to the model file as command-line argument, e.g.: + a. To pass a command line you want to obfuscate as a command-line argument (assuming it is supported by [ArgFuscator.net](https://github.com/wietze/Argfuscator.net)): - ```bash - # Windows - powershell .\Invoke-ArgFuscator.ps1 "path\to\file.json" + ```bash + # Windows + powershell /c "Invoke-ArgFuscator -Command 'certutil /f /urlcache https://www.example.org/ homepage.txt'" - # macOS and Linux - pwsh ./Invoke-ArgFuscator.ps1 "path/to/file.json" - ``` + # macOS and Linux + pwsh -c "Invoke-ArgFuscator -Command 'certutil /f /urlcache https://www.example.org/ homepage.txt'" + ``` + + b. To use your own model files[^1]: + + ```bash + # Windows + powershell /c "Invoke-ArgFuscator -InputFile path\to\file.json" + + # macOS and Linux + pwsh -c "Invoke-ArgFuscator -InputFile path/to/file.json" + ``` + +## Local Development + +1. Clone this repository to your device. +2. Call `Invoke-ArgFuscator.ps1` via PowerShell, for example: + + a. To run interactively pass the path of a model file[^1] via the standard input (stdin): + + ```bash + # Windows + powershell .\Invoke-ArgFuscator.ps1 + + # macOS and Linux + pwsh ./Invoke-ArgFuscator.ps1 + ``` + + b. To pass the path to the model file[^1] as a command-line argument: + + ```bash + # Windows + powershell .\Invoke-ArgFuscator.ps1 -InputFile "path\to\file.json" + + # macOS and Linux + pwsh ./Invoke-ArgFuscator.ps1 -InputFile "path/to/file.json" + ``` + + c. To pass a command line you want to obfuscate as a command-line argument: + + *Note that this requires the [models/](https://github.com/wietze/ArgFuscator.net/tree/main/models) folder to be present in the same folder as `Invoke-ArgFuscator.ps1`.* + + ```bash + # Windows + powershell .\Invoke-ArgFuscator.ps1 -Command "certutil /f /urlcache https://www.example.org/ homepage.txt" + + # macOS and Linux + pwsh ./Invoke-ArgFuscator.ps1 -Command "certutil /f /urlcache https://www.example.org/ homepage.txt" + ``` ## Integration @@ -61,13 +99,19 @@ Because Invoke-ArgFuscator is a PowerShell module, you can add this project's fu To leverage Invoke-ArgFuscator, add ```pwsh -Import-Module ./Invoke-ArgFuscator.psm1 +Import-Module Invoke-ArgFuscator ``` -to your PowerShell file, and call it as follows: +to your PowerShell file, and call it as either of the following: ```pwsh Invoke-ArgFuscator -InputFile $InputFile -n $n +Invoke-ArgFuscator -Command $Command -Platform $Platform -n $n ``` -with `$InputFile` a `string` containing a (relative/absolute) file path to the model file, and `$n` an `integer` greater than 0 for the number of obfuscated command-line equivalents that should be produced. +with + +* `$InputFile` a `string` containing a (relative/absolute) file path to the model file, and `$n` an `integer` greater than 0 for the number of obfuscated command-line equivalents that should be produced (optional); or, +* `$Command` a `string` containing the command line you wish to obfuscate, `$Platform` a `string` with the relevant platform (e.g. `windows`, optional), and `$n` an `integer` greater than 0 for the number of obfuscated command-line equivalents that should be produced (optional). + +[^1]: These can be generated via [ArgFuscator.net](https://argfuscator.net/) via the 'Download' option, or downloaded from [GitHub](https://github.com/wietze/Argfuscator.net/tree/main/models).