Skip to content

Commit

Permalink
refactor!: implement NodeCounter & Deobfuscator class
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken committed Feb 7, 2024
1 parent 59c5b51 commit 9dab74b
Show file tree
Hide file tree
Showing 36 changed files with 813 additions and 718 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,11 @@
"@nodesecure/estree-ast-utils": "^1.3.1",
"@nodesecure/sec-literal": "^1.2.0",
"estree-walker": "^3.0.1",
"frequency-set": "^1.0.2",
"is-minified-code": "^2.0.0",
"meriyah": "^4.3.3",
"safe-regex": "^2.1.1"
"safe-regex": "^2.1.1",
"ts-pattern": "^5.0.6"
},
"devDependencies": {
"@nodesecure/eslint-config": "^1.6.0",
Expand Down
192 changes: 192 additions & 0 deletions src/Deobfuscator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
// Import Third-party Dependencies
import { getVariableDeclarationIdentifiers } from "@nodesecure/estree-ast-utils";
import { Utils, Patterns } from "@nodesecure/sec-literal";
import { match } from "ts-pattern";

// Import Internal Dependencies
import { NodeCounter } from "./NodeCounter.js";
import { extractNode } from "./utils/index.js";

import * as jjencode from "./obfuscators/jjencode.js";
import * as jsfuck from "./obfuscators/jsfuck.js";
import * as freejsobfuscator from "./obfuscators/freejsobfuscator.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;

/** @type {Set<string>} */
morseLiterals = new Set();

/** @type {number[]} */
literalScores = [];

/** @type {({ name: string; type: string; })[]} */
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)
})
];

/**
* @param {!NodeCounter} nc
* @param {*} node
*/
#extractCounterIdentifiers(nc, node) {
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":
this.identifiers.push({ name: node.name, type });
break;
}
}

analyzeString(str) {
const score = Utils.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 (Utils.isMorse(str)) {
this.morseLiterals.add(str);
}
}

walk(node) {
const { type } = node;

const isFunctionParams = node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
const nodesToExtract = match(type)
.with("ClassDeclaration", () => [node.id, node.superClass])
.with("FunctionDeclaration", () => node.params)
.with("FunctionExpression", () => node.params)
.with("MethodDefinition", () => [node.key])
.otherwise(() => []);

kIdentifierNodeExtractor(
({ name }) => this.identifiers.push({ name, type: isFunctionParams ? "FunctionParams" : type }),
nodesToExtract
);

this.#counters.forEach((counter) => counter.walk(node));
}

aggregateCounters() {
const defaultValue = {
Identifiers: this.identifiers.length
};

return this.#counters.reduce((result, counter) => {
result[counter.name] = counter.lookup ?
counter.properties :
counter.count;

return result;
}, defaultValue);
}

#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;

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 } = Patterns.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;
}
}
76 changes: 76 additions & 0 deletions src/NodeCounter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Import Third-party Dependencies
import FrequencySet from "frequency-set";

// Import Internal Dependencies
import { isNode } from "./utils/index.js";

// eslint-disable-next-line func-style
const noop = (node) => true;

export class NodeCounter {
lookup = null;

#count = 0;
#properties = null;
#filterFn = noop;
#matchFn = noop;

/**
* @param {!string} type
* @param {Object} [options]
* @param {string} [options.name]
* @param {(node: any) => boolean} [options.filter]
* @param {(node: any, nc: NodeCounter) => void} [options.match]
*
* @example
* new NodeCounter("FunctionDeclaration");
* new NodeCounter("VariableDeclaration[kind]");
*/
constructor(type, options = {}) {
if (typeof type !== "string") {
throw new TypeError("type must be a string");
}

const typeResult = /([A-Za-z]+)(\[[a-zA-Z]+\])?/g.exec(type);
if (typeResult === null) {
throw new Error("invalid type argument syntax");
}
this.type = typeResult[1];
this.lookup = typeResult[2]?.slice(1, -1) ?? null;
this.name = options?.name ?? this.type;
if (this.lookup) {
this.#properties = new FrequencySet();
}

this.#filterFn = options.filter ?? noop;
this.#matchFn = options.match ?? noop;
}

get count() {
return this.#count;
}

get properties() {
return Object.fromEntries(
this.#properties?.entries() ?? []
);
}

walk(node) {
if (!isNode(node) || node.type !== this.type) {
return;
}
if (!this.#filterFn(node)) {
return;
}

this.#count++;
if (this.lookup === null) {
this.#matchFn(node, this);
}
else if (this.lookup in node) {
this.#properties.add(node[this.lookup]);
this.#matchFn(node, this);
}
}
}
22 changes: 4 additions & 18 deletions src/ProbeRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,11 @@ import isUnsafeCallee from "./probes/isUnsafeCallee.js";
import isLiteral from "./probes/isLiteral.js";
import isLiteralRegex from "./probes/isLiteralRegex.js";
import isRegexObject from "./probes/isRegexObject.js";
import isVariableDeclaration from "./probes/isVariableDeclaration.js";
import isRequire from "./probes/isRequire/isRequire.js";
import isImportDeclaration from "./probes/isImportDeclaration.js";
import isMemberExpression from "./probes/isMemberExpression.js";
import isArrayExpression from "./probes/isArrayExpression.js";
import isFunction from "./probes/isFunction.js";
import isAssignmentExpression from "./probes/isAssignmentExpression.js";
import isObjectExpression from "./probes/isObjectExpression.js";
import isUnaryExpression from "./probes/isUnaryExpression.js";
import isWeakCrypto from "./probes/isWeakCrypto.js";
import isClassDeclaration from "./probes/isClassDeclaration.js";
import isMethodDefinition from "./probes/isMethodDefinition.js";
import isBinaryExpression from "./probes/isBinaryExpression.js";
import isArrayExpression from "./probes/isArrayExpression.js";

// Import Internal Dependencies
import { SourceFile } from "./SourceFile.js";
Expand Down Expand Up @@ -50,17 +43,10 @@ export class ProbeRunner {
isLiteral,
isLiteralRegex,
isRegexObject,
isVariableDeclaration,
isImportDeclaration,
isMemberExpression,
isAssignmentExpression,
isObjectExpression,
isArrayExpression,
isFunction,
isUnaryExpression,
isWeakCrypto,
isClassDeclaration,
isMethodDefinition
isBinaryExpression,
isArrayExpression
];

/**
Expand Down
Loading

0 comments on commit 9dab74b

Please sign in to comment.