@nodesecure/js-x-ray
Version:
JavaScript AST XRay analysis
155 lines • 6.14 kB
JavaScript
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