diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 5fcba297..75615f8a 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -493,6 +493,11 @@ "PHP Deserialize", "PHP Serialize", "Microsoft Script Decoder", + "PowerShell Format String Deobfuscate", + "PowerShell Decode EncodedCommand", + "PowerShell Backtick Remove", + "PowerShell Concatenation Join", + "PowerShell Char Decode", "Strip HTML tags", "Diff", "To Snake case", diff --git a/src/core/operations/PowerShellBacktickRemove.mjs b/src/core/operations/PowerShellBacktickRemove.mjs new file mode 100644 index 00000000..e95a17ad --- /dev/null +++ b/src/core/operations/PowerShellBacktickRemove.mjs @@ -0,0 +1,65 @@ +/** + * @author vigneshrajan94 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * PowerShell Backtick Remove operation + */ +class PowerShellBacktickRemove extends Operation { + + constructor() { + super(); + + this.name = "PowerShell Backtick Remove"; + this.module = "Default"; + this.description = "Removes obfuscating backtick characters from PowerShell code. Attackers insert backticks before regular letters to break up recognisable keywords without changing execution (In`voke-Ex`pressionInvoke-Expression).

By default all backticks are stripped, which maximally deobfuscates the script. Enable Preserve escape sequences to retain legitimate PowerShell escape sequences (`n, `t, `r, `0, `a, `b, `f, `v, `e, `", `', ``, `$, `u)."; + this.infoURL = "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_special_characters"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + name: "Preserve escape sequences", + type: "boolean", + value: false + } + ]; + this.checks = [ + { + pattern: "`[a-zA-Z0-9]", + flags: "", + args: [] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const preserveEscapes = args[0]; + + if (!preserveEscapes) { + // Strip all backticks — maximally deobfuscating. + return input.replace(/`/g, ""); + } + + // Preserve recognised PowerShell escape sequences by consuming each backtick + // together with its following character in one match. This ensures `` (two backticks) + // is treated as a single escape unit rather than two independent backticks. + // `n `t `r `0 `a `b `f `v `e → control characters + // `" `' `` `$ → literal punctuation + // `u → Unicode escape (PS 6+) + return input.replace(/`([\s\S]?)/g, (match, next) => { + return /[ntrabfve0"'`$u]/.test(next) ? match : next; + }); + } + +} + +export default PowerShellBacktickRemove; diff --git a/src/core/operations/PowerShellCharDecode.mjs b/src/core/operations/PowerShellCharDecode.mjs new file mode 100644 index 00000000..b6958a98 --- /dev/null +++ b/src/core/operations/PowerShellCharDecode.mjs @@ -0,0 +1,78 @@ +/** + * @author vigneshrajan94 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * PowerShell Char Decode operation + */ +class PowerShellCharDecode extends Operation { + + constructor() { + super(); + + this.name = "PowerShell Char Decode"; + this.module = "Default"; + this.description = "Resolves PowerShell [char] cast expressions to their character values. Handles decimal and hexadecimal operands, single casts, and array forms.

Supported patterns
[char]65A
[char]0x41A
[System.Char]65A
[char[]](73,69,88)\"IEX\"
-join [char[]](73,69,88)\"IEX\"

Invalid or out-of-range values are left unchanged."; + this.infoURL = "https://learn.microsoft.com/en-us/dotnet/api/system.char"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + this.checks = [ + { + pattern: "\\[(?:System\\.)?[Cc]har\\]", + flags: "", + args: [] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let result = input; + + // Resolve array form first: [char[]](65,66,67) or -join [char[]](65,66,67) + result = result.replace( + /(?:-join\s*)?\[(?:System\.)?[Cc]har\[\]\]\s*\(([^)]+)\)/g, + (match, nums) => { + try { + const chars = nums.split(",").map(n => { + const t = n.trim(); + const code = /^0x/i.test(t) ? parseInt(t, 16) : parseInt(t, 10); + if (isNaN(code) || code < 0 || code > 0xFFFF) throw new RangeError(); + return String.fromCharCode(code); + }); + return `"${chars.join("")}"`; + } catch (e) { + return match; + } + } + ); + + // Resolve single [char] or [System.Char] cast (decimal or hex) + result = result.replace( + /\[(?:System\.)?[Cc]har\]\s*(0x[0-9a-fA-F]+|\d+)/g, + (match, code) => { + try { + const num = /^0x/i.test(code) ? parseInt(code, 16) : parseInt(code, 10); + if (isNaN(num) || num < 0 || num > 0xFFFF) return match; + return String.fromCharCode(num); + } catch (e) { + return match; + } + } + ); + + return result; + } + +} + +export default PowerShellCharDecode; diff --git a/src/core/operations/PowerShellConcatenationJoin.mjs b/src/core/operations/PowerShellConcatenationJoin.mjs new file mode 100644 index 00000000..ca22fc43 --- /dev/null +++ b/src/core/operations/PowerShellConcatenationJoin.mjs @@ -0,0 +1,61 @@ +/** + * @author vigneshrajan94 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * PowerShell Concatenation Join operation + */ +class PowerShellConcatenationJoin extends Operation { + + constructor() { + super(); + + this.name = "PowerShell Concatenation Join"; + this.module = "Default"; + this.description = "Resolves PowerShell string concatenation obfuscation by joining adjacent static string literals separated by +. Only resolves runs where every operand is a quoted literal — expressions involving variables or sub-expressions are left untouched.

Example
Input: 'In'+'vo'+'ke'+'-'+'Item'
Output: 'Invoke-Item'

Handles single-quoted, double-quoted, and mixed-quote chains. Runs iteratively until no further reductions are possible."; + this.infoURL = "https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-07"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + this.checks = [ + { + pattern: "(?:\"[^\"]*\"|'[^']*')\\s*\\+\\s*(?:\"[^\"]*\"|'[^']*')", + flags: "", + args: [] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + let result = input; + let prev; + + // Iteratively collapse adjacent quoted-string pairs until stable. + // Four passes per iteration to cover all quote-type combinations. + do { + prev = result; + // "a" + "b" → "ab" + result = result.replace(/"([^"]*)"\s*\+\s*"([^"]*)"/g, (_, a, b) => `"${a}${b}"`); + // 'a' + 'b' → 'ab' + result = result.replace(/'([^']*)'\s*\+\s*'([^']*)'/g, (_, a, b) => `'${a}${b}'`); + // "a" + 'b' → "ab" (normalise to leading quote style) + result = result.replace(/"([^"]*)"\s*\+\s*'([^']*)'/g, (_, a, b) => `"${a}${b}"`); + // 'a' + "b" → 'ab' + result = result.replace(/'([^']*)'\s*\+\s*"([^"]*)"/g, (_, a, b) => `'${a}${b}'`); + } while (result !== prev); + + return result; + } + +} + +export default PowerShellConcatenationJoin; diff --git a/src/core/operations/PowerShellDecodeEncodedCommand.mjs b/src/core/operations/PowerShellDecodeEncodedCommand.mjs new file mode 100644 index 00000000..faa549a2 --- /dev/null +++ b/src/core/operations/PowerShellDecodeEncodedCommand.mjs @@ -0,0 +1,66 @@ +/** + * @author vigneshrajan94 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import {fromBase64} from "../lib/Base64.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * PowerShell Decode EncodedCommand operation + */ +class PowerShellDecodeEncodedCommand extends Operation { + + constructor() { + super(); + + this.name = "PowerShell Decode EncodedCommand"; + this.module = "Default"; + this.description = "Decodes a PowerShell -EncodedCommand payload. The encoded command is a Base64-encoded, UTF-16LE string. Accepts the raw Base64 value, a flag-prefixed form (-enc <b64>), or a full invocation (powershell.exe -w hidden -enc <b64>).

Example
Input: -enc SQBFAFgA
Output: IEX"; + this.infoURL = "https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + this.checks = [ + { + pattern: "-e(?:n(?:c(?:o(?:d(?:e(?:d(?:c(?:o(?:m(?:m(?:and?)?)?)?)?)?)?)?)?)?)?)?\\s+[A-Za-z0-9+/]+=*\\s*$", + flags: "i", + args: [] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // The Base64 payload is always the last whitespace-separated token. + // This cleanly handles all input forms: + // "SQBFAFgA" + // "-enc SQBFAFgA" + // "powershell.exe -w 1 -nop -ep bypass -enc SQBFAFgA" + const b64 = input.trim().split(/\s+/).pop().replace(/^["']|["']$/g, ""); + + if (!b64) throw new OperationError("No input provided."); + + const bytes = fromBase64(b64, null, "byteArray"); + + if (bytes.length < 2) throw new OperationError("Decoded data too short to be a valid UTF-16LE string."); + if (bytes.length % 2 !== 0) throw new OperationError("Decoded byte length is odd — not valid UTF-16LE."); + + // Decode UTF-16LE: each character is two bytes, little-endian + let result = ""; + for (let i = 0; i + 1 < bytes.length; i += 2) { + result += String.fromCharCode(bytes[i] | (bytes[i + 1] << 8)); + } + + return result; + } + +} + +export default PowerShellDecodeEncodedCommand; diff --git a/src/core/operations/PowerShellFormatStringDeobfuscate.mjs b/src/core/operations/PowerShellFormatStringDeobfuscate.mjs new file mode 100644 index 00000000..d41fbae3 --- /dev/null +++ b/src/core/operations/PowerShellFormatStringDeobfuscate.mjs @@ -0,0 +1,71 @@ +/** + * @author vigneshrajan94 + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * PowerShell Format String Deobfuscate operation + */ +class PowerShellFormatStringDeobfuscate extends Operation { + + constructor() { + super(); + + this.name = "PowerShell Format String Deobfuscate"; + this.module = "Default"; + this.description = "Deobfuscates PowerShell strings that use the -f (format) operator with reordered index placeholders. Malware authors use this technique to scramble string fragments, hiding readable commands from static analysis.

Example
Input: (\"{0}{3}{4}{1}{2}\" -f \"S\",\"Mo\",\"de\",\"et-Stri\",\"ct\")
Output: Set-StrictMode

All matching expressions in the input are resolved in-place. Unresolvable placeholders (e.g. out-of-range indices) are left unchanged."; + this.infoURL = "https://github.com/bobby-tablez/Format-String-Deobfuscator"; + this.inputType = "string"; + this.outputType = "string"; + this.args = []; + this.checks = [ + { + pattern: "\\(\\s*[\"'][^\"']*\\{\\d+\\}[^\"']*[\"']\\s*-f\\s*[\"']", + flags: "i", + args: [] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + // Match: ("