|
| 1 | +// Import Third-party Dependencies |
| 2 | +import { getVariableDeclarationIdentifiers } from "@nodesecure/estree-ast-utils"; |
| 3 | +import { Utils, Patterns } from "@nodesecure/sec-literal"; |
| 4 | +import { match } from "ts-pattern"; |
| 5 | + |
| 6 | +// Import Internal Dependencies |
| 7 | +import { NodeCounter } from "./NodeCounter.js"; |
| 8 | +import { extractNode } from "./utils/index.js"; |
| 9 | + |
| 10 | +import * as jjencode from "./obfuscators/jjencode.js"; |
| 11 | +import * as jsfuck from "./obfuscators/jsfuck.js"; |
| 12 | +import * as freejsobfuscator from "./obfuscators/freejsobfuscator.js"; |
| 13 | +import * as obfuscatorio from "./obfuscators/obfuscator-io.js"; |
| 14 | + |
| 15 | +// CONSTANTS |
| 16 | +const kIdentifierNodeExtractor = extractNode("Identifier"); |
| 17 | +const kDictionaryStrParts = [ |
| 18 | + "abcdefghijklmnopqrstuvwxyz", |
| 19 | + "ABCDEFGHIJKLMNOPQRSTUVWXYZ", |
| 20 | + "0123456789" |
| 21 | +]; |
| 22 | +const kMinimumIdsCount = 5; |
| 23 | + |
| 24 | +export class Deobfuscator { |
| 25 | + deepBinaryExpression = 0; |
| 26 | + encodedArrayValue = 0; |
| 27 | + hasDictionaryString = false; |
| 28 | + hasPrefixedIdentifiers = false; |
| 29 | + |
| 30 | + /** @type {Set<string>} */ |
| 31 | + morseLiterals = new Set(); |
| 32 | + |
| 33 | + /** @type {number[]} */ |
| 34 | + literalScores = []; |
| 35 | + |
| 36 | + /** @type {({ name: string; type: string; })[]} */ |
| 37 | + identifiers = []; |
| 38 | + |
| 39 | + #counters = [ |
| 40 | + new NodeCounter("VariableDeclaration[kind]"), |
| 41 | + new NodeCounter("AssignmentExpression", { |
| 42 | + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.left) |
| 43 | + }), |
| 44 | + new NodeCounter("FunctionDeclaration", { |
| 45 | + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) |
| 46 | + }), |
| 47 | + new NodeCounter("MemberExpression[computed]"), |
| 48 | + new NodeCounter("Property", { |
| 49 | + filter: (node) => node.key.type === "Identifier", |
| 50 | + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.key) |
| 51 | + }), |
| 52 | + new NodeCounter("UnaryExpression", { |
| 53 | + name: "DoubleUnaryExpression", |
| 54 | + filter: ({ argument }) => argument.type === "UnaryExpression" && argument.argument.type === "ArrayExpression" |
| 55 | + }), |
| 56 | + new NodeCounter("VariableDeclarator", { |
| 57 | + match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) |
| 58 | + }) |
| 59 | + ]; |
| 60 | + |
| 61 | + /** |
| 62 | + * @param {!NodeCounter} nc |
| 63 | + * @param {*} node |
| 64 | + */ |
| 65 | + #extractCounterIdentifiers(nc, node) { |
| 66 | + const { type } = nc; |
| 67 | + |
| 68 | + switch (type) { |
| 69 | + case "VariableDeclarator": |
| 70 | + case "AssignmentExpression": { |
| 71 | + for (const { name } of getVariableDeclarationIdentifiers(node)) { |
| 72 | + this.identifiers.push({ name, type }); |
| 73 | + } |
| 74 | + break; |
| 75 | + } |
| 76 | + case "Property": |
| 77 | + case "FunctionDeclaration": |
| 78 | + this.identifiers.push({ name: node.name, type }); |
| 79 | + break; |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + analyzeString(str) { |
| 84 | + const score = Utils.stringSuspicionScore(str); |
| 85 | + if (score !== 0) { |
| 86 | + this.literalScores.push(score); |
| 87 | + } |
| 88 | + |
| 89 | + if (!this.hasDictionaryString) { |
| 90 | + const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word)); |
| 91 | + if (isDictionaryStr) { |
| 92 | + this.hasDictionaryString = true; |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + // Searching for morse string like "--.- --.--" |
| 97 | + if (Utils.isMorse(str)) { |
| 98 | + this.morseLiterals.add(str); |
| 99 | + } |
| 100 | + } |
| 101 | + |
| 102 | + walk(node) { |
| 103 | + const { type } = node; |
| 104 | + |
| 105 | + const isFunctionParams = node.type === "FunctionDeclaration" || node.type === "FunctionExpression"; |
| 106 | + const nodesToExtract = match(type) |
| 107 | + .with("ClassDeclaration", () => [node.id, node.superClass]) |
| 108 | + .with("FunctionDeclaration", () => node.params) |
| 109 | + .with("FunctionExpression", () => node.params) |
| 110 | + .with("MethodDefinition", () => [node.key]) |
| 111 | + .otherwise(() => []); |
| 112 | + |
| 113 | + kIdentifierNodeExtractor( |
| 114 | + ({ name }) => this.identifiers.push({ name, type: isFunctionParams ? "FunctionParams" : type }), |
| 115 | + nodesToExtract |
| 116 | + ); |
| 117 | + |
| 118 | + this.#counters.forEach((counter) => counter.walk(node)); |
| 119 | + } |
| 120 | + |
| 121 | + aggregateCounters() { |
| 122 | + const defaultValue = { |
| 123 | + Identifiers: this.identifiers.length |
| 124 | + }; |
| 125 | + |
| 126 | + return this.#counters.reduce((result, counter) => { |
| 127 | + result[counter.name] = counter.lookup ? |
| 128 | + counter.properties : |
| 129 | + counter.count; |
| 130 | + |
| 131 | + return result; |
| 132 | + }, defaultValue); |
| 133 | + } |
| 134 | + |
| 135 | + #calcAvgPrefixedIdentifiers( |
| 136 | + counters, |
| 137 | + prefix |
| 138 | + ) { |
| 139 | + const valuesArr = Object |
| 140 | + .values(prefix) |
| 141 | + .slice() |
| 142 | + .sort((left, right) => left - right); |
| 143 | + if (valuesArr.length === 0) { |
| 144 | + return 0; |
| 145 | + } |
| 146 | + |
| 147 | + const nbOfPrefixedIds = valuesArr.length === 1 ? |
| 148 | + valuesArr.pop() : |
| 149 | + (valuesArr.pop() + valuesArr.pop()); |
| 150 | + const maxIds = counters.Identifiers - counters.Property; |
| 151 | + |
| 152 | + return ((nbOfPrefixedIds / maxIds) * 100); |
| 153 | + } |
| 154 | + |
| 155 | + assertObfuscation() { |
| 156 | + const counters = this.aggregateCounters(); |
| 157 | + |
| 158 | + if (jsfuck.verify(counters)) { |
| 159 | + return "jsfuck"; |
| 160 | + } |
| 161 | + if (jjencode.verify(this.identifiers, counters)) { |
| 162 | + return "jjencode"; |
| 163 | + } |
| 164 | + if (this.morseLiterals.size >= 36) { |
| 165 | + return "morse"; |
| 166 | + } |
| 167 | + |
| 168 | + const { prefix } = Patterns.commonHexadecimalPrefix( |
| 169 | + this.identifiers.flatMap( |
| 170 | + ({ name }) => (typeof name === "string" ? [name] : []) |
| 171 | + ) |
| 172 | + ); |
| 173 | + const uPrefixNames = new Set(Object.keys(prefix)); |
| 174 | + |
| 175 | + if (this.identifiers.length > kMinimumIdsCount && uPrefixNames.size > 0) { |
| 176 | + this.hasPrefixedIdentifiers = this.#calcAvgPrefixedIdentifiers(counters, prefix) > 80; |
| 177 | + } |
| 178 | + |
| 179 | + if (uPrefixNames.size === 1 && freejsobfuscator.verify(this.identifiers, prefix)) { |
| 180 | + return "freejsobfuscator"; |
| 181 | + } |
| 182 | + if (obfuscatorio.verify(this, counters)) { |
| 183 | + return "obfuscator.io"; |
| 184 | + } |
| 185 | + // if ((identifierLength > (kMinimumIdsCount * 3) && this.hasPrefixedIdentifiers) |
| 186 | + // && (oneTimeOccurence <= 3 || this.encodedArrayValue > 0)) { |
| 187 | + // return "unknown"; |
| 188 | + // } |
| 189 | + |
| 190 | + return null; |
| 191 | + } |
| 192 | +} |
0 commit comments