UNPKG

@nodesecure/js-x-ray

Version:
155 lines 6.14 kB
import { match } from "ts-pattern"; // Import Internal Dependencies import { getVariableDeclarationIdentifiers } from "./estree/index.js"; import { NodeCounter } from "./NodeCounter.js"; import { extractNode, stringSuspicionScore, commonHexadecimalPrefix } from "./utils/index.js"; import * as freejsobfuscator from "./obfuscators/freejsobfuscator.js"; import * as jjencode from "./obfuscators/jjencode.js"; import * as jsfuck from "./obfuscators/jsfuck.js"; import * as obfuscatorio from "./obfuscators/obfuscator-io.js"; // CONSTANTS const kIdentifierNodeExtractor = extractNode("Identifier"); const kDictionaryStrParts = [ "abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789" ]; const kMinimumIdsCount = 5; export class Deobfuscator { deepBinaryExpression = 0; encodedArrayValue = 0; hasDictionaryString = false; hasPrefixedIdentifiers = false; morseLiterals = new Set(); literalScores = []; identifiers = []; #counters = [ new NodeCounter("VariableDeclaration[kind]"), new NodeCounter("AssignmentExpression", { match: (node, nc) => this.#extractCounterIdentifiers(nc, node.left) }), new NodeCounter("FunctionDeclaration", { match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) }), new NodeCounter("MemberExpression[computed]"), new NodeCounter("Property", { filter: (node) => node.key.type === "Identifier", match: (node, nc) => this.#extractCounterIdentifiers(nc, node.key) }), new NodeCounter("UnaryExpression", { name: "DoubleUnaryExpression", filter: ({ argument }) => argument.type === "UnaryExpression" && argument.argument.type === "ArrayExpression" }), new NodeCounter("VariableDeclarator", { match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id) }) ]; #extractCounterIdentifiers(nc, node) { if (node === null) { return; } const { type } = nc; switch (type) { case "VariableDeclarator": case "AssignmentExpression": { for (const { name } of getVariableDeclarationIdentifiers(node)) { this.identifiers.push({ name, type }); } break; } case "Property": case "FunctionDeclaration": if (node.type === "Identifier") { this.identifiers.push({ name: node.name, type }); } break; } } #isMorse(str) { return /^[.-]{1,5}(?:[\s\t]+[.-]{1,5})*(?:[\s\t]+[.-]{1,5}(?:[\s\t]+[.-]{1,5})*)*$/g.test(str); } analyzeString(str) { const score = stringSuspicionScore(str); if (score !== 0) { this.literalScores.push(score); } if (!this.hasDictionaryString) { const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word)); if (isDictionaryStr) { this.hasDictionaryString = true; } } // Searching for morse string like "--.- --.--" if (this.#isMorse(str)) { this.morseLiterals.add(str); } } walk(node) { const nodesToExtract = match(node) .with({ type: "ClassDeclaration" }, (node) => [node.id, node.superClass]) .with({ type: "FunctionDeclaration" }, (node) => node.params) .with({ type: "FunctionExpression" }, (node) => node.params) .with({ type: "MethodDefinition" }, (node) => [node.key]) .otherwise(() => []); const isFunctionParams = node.type === "FunctionDeclaration" || node.type === "FunctionExpression"; kIdentifierNodeExtractor(({ name }) => this.identifiers.push({ name, type: isFunctionParams ? "FunctionParams" : node.type }), nodesToExtract); this.#counters.forEach((counter) => counter.walk(node)); } aggregateCounters() { return this.#counters.reduce((result, counter) => { result[counter.name] = counter.lookup ? counter.properties : counter.count; return result; }, { Identifiers: this.identifiers.length }); } #calcAvgPrefixedIdentifiers(counters, prefix) { const valuesArr = Object .values(prefix) .slice() .sort((left, right) => left - right); if (valuesArr.length === 0) { return 0; } const nbOfPrefixedIds = valuesArr.length === 1 ? valuesArr.pop() : (valuesArr.pop() + valuesArr.pop()); const maxIds = counters.Identifiers - (counters.Property ?? 0); return ((nbOfPrefixedIds / maxIds) * 100); } assertObfuscation() { const counters = this.aggregateCounters(); if (jsfuck.verify(counters)) { return "jsfuck"; } if (jjencode.verify(this.identifiers, counters)) { return "jjencode"; } if (this.morseLiterals.size >= 36) { return "morse"; } const { prefix } = commonHexadecimalPrefix(this.identifiers.flatMap(({ name }) => (typeof name === "string" ? [name] : []))); const uPrefixNames = new Set(Object.keys(prefix)); if (this.identifiers.length > kMinimumIdsCount && uPrefixNames.size > 0) { this.hasPrefixedIdentifiers = this.#calcAvgPrefixedIdentifiers(counters, prefix) > 80; } if (uPrefixNames.size === 1 && freejsobfuscator.verify(this.identifiers, prefix)) { return "freejsobfuscator"; } if (obfuscatorio.verify(this, counters)) { return "obfuscator.io"; } // if ((identifierLength > (kMinimumIdsCount * 3) && this.hasPrefixedIdentifiers) // && (oneTimeOccurence <= 3 || this.encodedArrayValue > 0)) { // return "unknown"; // } return null; } } //# sourceMappingURL=Deobfuscator.js.map