DecodeSource.psm1
function decodesource ($encodedsource, [string]$mode = 'urldecode', [int]$number, [switch]$save, $outfile, [switch]$help) {# Decode a file or string to screen based on mode, with optional file save. function usage {Write-Host -f cyan "`nUsage: decodesource `"source string/file`" <auto/base64/deflate/gzip/hex/htmlentity/reverse/unicode/urldecode/quotedprintable/'regex','search','log','artifact','viewer','gzip','forensics','PowerShell','find','security','SOC','cybersecurity'> <number for urldecode iterations> -save <outfile> -help`n"; return} if ($help) {# Inline help. # Modify fields sent to it with proper word wrapping. function wordwrap ($field, $maximumlinelength) {if ($null -eq $field -or $field.Length -eq 0) {return $null} $breakchars = ',.;?!\/ '; $wrapped = @() if (-not $maximumlinelength) {[int]$maximumlinelength = (100, $Host.UI.RawUI.WindowSize.Width | Measure-Object -Maximum).Maximum} if ($maximumlinelength) {if ($maximumlinelength -lt 60) {[int]$maximumlinelength = 60} if ($maximumlinelength -gt $Host.UI.RawUI.BufferSize.Width) {[int]$maximumlinelength = $Host.UI.RawUI.BufferSize.Width}} foreach ($line in $field -split "`n") {if ($line.Trim().Length -eq 0) {$wrapped += ''; continue} $remaining = $line.Trim() while ($remaining.Length -gt $maximumlinelength) {$segment = $remaining.Substring(0, $maximumlinelength); $breakIndex = -1 foreach ($char in $breakchars.ToCharArray()) {$index = $segment.LastIndexOf($char) if ($index -gt $breakIndex) {$breakChar = $char; $breakIndex = $index}} if ($breakIndex -lt 0) {$breakIndex = $maximumlinelength - 1; $breakChar = ''} $chunk = $segment.Substring(0, $breakIndex + 1).TrimEnd(); $wrapped += $chunk; $remaining = $remaining.Substring($breakIndex + 1).TrimStart()} if ($remaining.Length -gt 0) {$wrapped += $remaining}} return ($wrapped -join "`n")} function scripthelp ($section) {# (Internal) Generate the help sections from the comments section of the script. ""; Write-Host -f yellow ("-" * 100); $pattern = "(?ims)^## ($section.*?)(##|\z)"; $match = [regex]::Match($scripthelp, $pattern); $lines = $match.Groups[1].Value.TrimEnd() -split "`r?`n", 2; Write-Host $lines[0] -f yellow; Write-Host -f yellow ("-" * 100) if ($lines.Count -gt 1) {wordwrap $lines[1] 100| Out-String | Out-Host -Paging}; Write-Host -f yellow ("-" * 100)} $scripthelp = Get-Content -Raw -Path $PSCommandPath; $sections = [regex]::Matches($scripthelp, "(?im)^## (.+?)(?=\r?\n)") if ($sections.Count -eq 1) {cls; Write-Host "$([System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) Help:" -f cyan; scripthelp $sections[0].Groups[1].Value; ""; return} $selection = $null do {cls; Write-Host "$([System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)) Help Sections:`n" -f cyan; for ($i = 0; $i -lt $sections.Count; $i++) { "{0}: {1}" -f ($i + 1), $sections[$i].Groups[1].Value} if ($selection) {scripthelp $sections[$selection - 1].Groups[1].Value} $input = Read-Host "`nEnter a section number to view" if ($input -match '^\d+$') {$index = [int]$input if ($index -ge 1 -and $index -le $sections.Count) {$selection = $index} else {$selection = $null}} else {""; return}} while ($true); return} if (-not $encodedsource) {usage; return} # Set default for URLDecoding and verify sourcetype. if (-not $number) {$number = 3} if (Test-Path $encodedsource) {$fileContent = Get-Content -Path $encodedsource -Raw; $source = (Resolve-Path $encodedsource).Path} else {$fileContent = $encodedsource; $source = "String input"} $decodedString = $fileContent # Define decoders. function AutoDetect {param([string]$s); $scores = @{} # Base64 if ($s -match '^[A-Za-z0-9+/]+={0,2}$' -and ($s.Length % 4 -eq 0)) {try {$bytes = [Convert]::FromBase64String($s); $decoded = [System.Text.Encoding]::UTF8.GetString($bytes) if ($decoded.Length -gt 0) {$printables = ($decoded.ToCharArray() | Where-Object {$_ -match '[\x20-\x7E]'}).Count; $ratio = $printables / $decoded.Length if ($ratio -ge 0.8) {$scores['base64'] = 5} elseif ($ratio -ge 0.5) {$scores['base64'] = 3} else {$scores['base64'] = 1}}} catch {$scores['base64'] = 0}} # GZip, ZLib, Deflate if ($s -match '^[A-Za-z0-9+/]+={0,2}$' -and ($s.Length % 4 -eq 0)) {try {$bytes = [System.Convert]::FromBase64String($s); $header = ($bytes[0..1] -join ' '); switch ($header) {'31 139' { $scores['gzip'] = 5 }; '120 156' { $scores['zlib'] = 5 }; '120 1' { $scores['zlib'] = 5 }}; $scores['deflate'] = 2} catch {}} # Hex if ($s -match '^[0-9A-Fa-f]+$' -and ($s.Length % 2 -eq 0)) {try {$bytes = for ($i = 0; $i -lt $s.Length; $i += 2) {[Convert]::ToByte($s.Substring($i,2),16)}; $decoded = [System.Text.Encoding]::UTF8.GetString($bytes); $printables = ($decoded.ToCharArray() | Where-Object {$_ -match '[\x20-\x7E]'}).Count; $ratio = $printables / $decoded.Length; if ($ratio -ge 0.8) {$scores['hex'] = 5} else {$scores['hex'] = 3}} catch {}} # URL encoding if ($s -match '%[0-9A-Fa-f]{2}') {$scores['urldecode'] = ($s -split '%[0-9A-Fa-f]{2}').Count} # HTML Entities if ($s -match '&[a-z]+;') {$scores['htmlentity'] = ($s -split '&[a-z]+;').Count} # Quoted-printable if ($s -match '=[0-9A-Fa-f]{2}') {$scores['quotedprintable'] = ($s -split '=[0-9A-Fa-f]{2}').Count} # Unicode escape if ($s -match '\\u[0-9A-Fa-f]{4}') {$scores['unicode'] = ($s -split '\\u[0-9A-Fa-f]{4}').Count} # Reverse string: low score, last resort $scores['reverse'] = 0.5 # Return best match if ($scores.Count -gt 0) {return $scores.GetEnumerator() | Sort-Object Value -Descending | Select-Object -First 1} return $null} function Base64Decode {param([string]$s); try {$bytes = [Convert]::FromBase64String($s); return [System.Text.Encoding]::UTF8.GetString($bytes)} catch {Write-Host -f red "`nBase64 decode error: $_.`n"; return $s}} function DeflateDecode {param([string]$s); $bytes = [System.Convert]::FromBase64String($s); if ($bytes.Length -ge 2) {$b0 = $bytes[0]; $b1 = $bytes[1] if ($b0 -eq 0x78 -and ($b1 -eq 0x01 -or $b1 -eq 0x9C -or $b1 -eq 0xDA -or $b1 -eq 0x5E -or $b1 -eq 0xBB)) {Write-Host -f cyan "`nDetected zlib header, using zlib decode."; return DecodeZlib -Bytes $bytes}} Write-Host -f cyan "`nNo zlib header detected, using raw deflate decode."; return DecodeDeflateRaw -Bytes $bytes} function DecodeZlib {param([byte[]]$Bytes); $deflateBytes = $Bytes[2..($Bytes.Length - 1)]; $ms = New-Object IO.MemoryStream(, $deflateBytes); $ds = New-Object IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Decompress); $sr = New-Object IO.StreamReader($ds); try {return $sr.ReadToEnd()} catch {Write-Host -f red "`nZlib decode failed: $_.`n"; return $null} finally {$sr.Close(); $ds.Close(); $ms.Close()}} function DecodeDeflateRaw {param([byte[]]$Bytes); try {$ms = New-Object IO.MemoryStream(, $Bytes); $ds = New-Object IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Decompress); $sr = New-Object IO.StreamReader($ds); $result = $sr.ReadToEnd(); $sr.Close(); $ds.Close(); $ms.Close(); return $result} catch {Write-Host -f red "`nRaw deflate decode error: $_.`n"; return ""}} function GZipDecode {param([string]$s); try {$bytes = [System.Convert]::FromBase64String($s); $ms = New-Object System.IO.MemoryStream(,$bytes); $gzip = New-Object System.IO.Compression.GzipStream($ms, [IO.Compression.CompressionMode]::Decompress); $reader = New-Object System.IO.StreamReader($gzip, [System.Text.Encoding]::UTF8); return $reader.ReadToEnd()} catch {Write-Host -f red "`nGZipDecode failed: $_.`n"; return $null}} function HexDecode {param([string]$s); try {if ($s.Length % 2 -ne 0) {throw "Hex string must have even length"}; $bytes = for ($i = 0; $i -lt $s.Length; $i += 2) {[Convert]::ToByte($s.Substring($i,2),16)}; return [System.Text.Encoding]::UTF8.GetString($bytes)} catch {Write-Host -f red "`nHex decode error: $_.`n"; return $s}} function HtmlEntityDecode {param([string]$s); return [System.Net.WebUtility]::HtmlDecode($s)} function QuotedPrintableDecode {param([string]$s); try {$cleaned = ($s -replace "=\r?\n", ""); return [regex]::Replace($cleaned, "=([0-9A-Fa-f]{2})", {param($m) [char][System.Convert]::ToInt32($m.Groups[1].Value,16)})} catch {Write-Host -f red "`nQuotedPrintable decode error: $_.`n"; return $s}} function ReverseString {param([string]$s); $chars = $s.ToCharArray(); [Array]::Reverse($chars); return -join $chars} function UnicodeEscapeDecode {param([string]$s); try {return ([regex]::Replace($s, '\\u([0-9A-Fa-f]{4})', {param($m) [char]([convert]::ToInt32($m.Groups[1].Value, 16))}))} catch {Write-Host -f red "`nUnicode escape decode error: $_.`n"; return $s}} function URLDecode {param([string]$s); try{$cleaned = $s -replace '(?<=\w|\%)\+(?=\w|\%)', ' '; return [uri]::UnescapeDataString($cleaned)} catch {Write-Host -f red "`nURLDecode error: $_.`n"; return $s}} function ZlibDecode {param([string]$s); try {$bytes = [Convert]::FromBase64String($s); $ms = [System.IO.MemoryStream]::new(); $ms.Write($bytes, 2, $bytes.Length - 2); $ms.Position = 0; $zlib = [System.IO.Compression.DeflateStream]::new($ms, [IO.Compression.CompressionMode]::Decompress); $reader = [System.IO.StreamReader]::new($zlib); return $reader.ReadToEnd()} catch {Write-Host -f red "`nZlib decode error: $_.`n"; return $s}} # Set decoder based on user input. if ($mode.ToLower() -eq 'auto') {$guess = AutoDetect -s $decodedString; if ($guess) {Write-Host -f green "`nAuto-detect: Likely encoding is '$($guess.Key)' (score: $($guess.Value))"; $mode = $guess.Key} else {Write-Host -f red "`nAuto-detect failed: Could not confidently determine encoding."; return}} switch ($mode.ToLower()) { 'base64' {$decodedString = Base64Decode -s $decodedString} 'deflate' {$decodedString = DeflateDecode -s $decodedString} 'gzip' {$decodedString = GZipDecode -s $decodedString} 'hex' {$decodedString = HexDecode -s $decodedString} 'htmlentity' {$decodedString = HtmlEntityDecode -s $decodedString} 'quotedprintable' {$decodedString = QuotedPrintableDecode -s $decodedString} 'reverse' {$decodedString = ReverseString -s $decodedString} 'unicode' {$decodedString = UnicodeEscapeDecode -s $decodedString} 'urldecode' {for ($i = 0; $i -lt $number; $i++) {$decodedString = URLDecode -s $decodedString}} 'zlib' {$decodedString = ZlibDecode -s $decodedString} default {usage; return}} # Output. Write-Host -f cyan "`n$source" # File saving if chosen. if (($save) -and (Test-Path $encodedsource) -and -not $outfile) {$baseName = [System.IO.Path]::GetFileNameWithoutExtension($source); $extension = [System.IO.Path]::GetExtension($source); $newFileName = "$baseName - decoded$extension"; Set-Content $newFileName $decodedString; Write-Host -f cyan "Output saved to: $newFileName"} # Custom destination file save. elseif (($save) -and (Test-Path $encodedsource) -and $outfile) {Set-Content $outfile $decodedString; Write-Host -f cyan "Output saved to: $outfile"} # Output to screen. Write-Host -f yellow ("-" * 100); Write-Host "$fileContent"; Write-Host -f yellow ("-" * 100); Write-Host "$decodedString"; Write-Host -f yellow ("-" * 100); ""} Export-ModuleMember -Function decodesource <# ## Overview This started out as a simple URLDecoder tool, in order to allow security personnel to decode strings iteratively if necessary, but I decided to expand it, by adding more decode methodologies. So, I started digging into what could be accomplished natively in PowerShell without extensions and these are the ones I came up with. Yes, the "reverse" method is kind of silly and no I did not include ROT-13, because my Caesar tool already does that and a lot more, but this list covers pretty much everything else: • Base64 • Deflate • GZip • Hex • HTMLEntity • QuotedPrintable • Reverse • Unicode • URLDecode * • ZLib Usage: decodesource "source string/file" decodemethod <number for urldecode iterations> -save <outfile> -help One important note is that I did not use the traditional method of URLDecoding, which is to use the native System Web HttpUtility, because this is readily abused by threat actors and I do not want this utility getting mistaken for anything other than legitimate software. Therefore, I used the safer UnescapeDataString method and applied some Regex logic around the plus sign "+" to " " space character conversions that is so easily handled by the web utility, but not handled as gracefully by this alternate method. While this method may not be 100% accurate in all cases, I believe the trade off with what is essentially human readable text in either case, still allows for a reasonable decoding method that should make this safely detected by even the most aggressive antimalware and antivirus software programs. ## Examples In order to test each of the decoding methods, you can use these samples: decodesource "SGVsbG8gV29ybGQh" base64 decodesource "eJzLSM3JyVcozy/KSVEEAB0JBF4=" deflate decodesource "H4sIAAAAAAAACvNIzcnJ11EIzy/KSVFUCMnILFbILFZIVMjJz0tPLVIoSS0uUchNLS5OTE/VAwB8FnlLLAAAAA==" gzip decodesource "48656c6c6f20576f726c6421" hex decodesource "Hello World!" htmlentity decodesource "Hello=20World=21" quotedprintable decodesource "!dlroW olleH" reverse decodesource "\u0048\u0065\u006c\u006c\u006f\u0020\u0057\u006f\u0072\u006c\u0064\u0021" unicode decodesource "Hello%20World%21" urldecode decodesource "eJzLSM3JyVcozy/KSVEEAB0JBF4=" zlib I wasn't able to get a smaller sample successfully compressed and decompressed using GZip. So, I used a longer sample, but I doubt that this will be a problem in real-world use. In order to test the auto-detection mechanism, use the same samples as above, but replace the decode method with "auto" and let the heuristics work their magic: decodesource "SGVsbG8gV29ybGQh" auto decodesource "eJzLSM3JyVcozy/KSVEEAB0JBF4=" auto decodesource "H4sIAAAAAAAACvNIzcnJ11EIzy/KSVFUCMnILFbILFZIVMjJz0tPLVIoSS0uUchNLS5OTE/VAwB8FnlLLAAAAA==" auto decodesource "48656c6c6f20576f726c6421" auto decodesource "Hello World!" auto decodesource "Hello=20World=21" auto decodesource "!dlroW olleH" auto decodesource "\u0048\u0065\u006c\u006c\u006f\u0020\u0057\u006f\u0072\u006c\u0064\u0021" auto decodesource "Hello%20World%21" auto decodesource "eJzLSM3JyVcozy/KSVEEAB0JBF4=" auto Enjoy and happy threat hunting! ##> |