Skip to content

Commit e272a2b

Browse files
committed
refactor!: implement NodeCounter & Deobfuscator class
1 parent 59c5b51 commit e272a2b

36 files changed

+756
-717
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@
4747
"@nodesecure/estree-ast-utils": "^1.3.1",
4848
"@nodesecure/sec-literal": "^1.2.0",
4949
"estree-walker": "^3.0.1",
50+
"frequency-set": "^1.0.2",
5051
"is-minified-code": "^2.0.0",
5152
"meriyah": "^4.3.3",
52-
"safe-regex": "^2.1.1"
53+
"safe-regex": "^2.1.1",
54+
"ts-pattern": "^5.0.6"
5355
},
5456
"devDependencies": {
5557
"@nodesecure/eslint-config": "^1.6.0",

src/Deobfuscator.js

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
/** @type {NodeCounter[]} */
40+
#counters = [];
41+
42+
constructor() {
43+
this.#counters = [
44+
new NodeCounter("VariableDeclaration[kind]"),
45+
new NodeCounter("AssignmentExpression", {
46+
match: (node, nc) => this.#extractCounterIdentifiers(nc, node.left)
47+
}),
48+
new NodeCounter("FunctionDeclaration", {
49+
match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id)
50+
}),
51+
new NodeCounter("MemberExpression[computed]"),
52+
new NodeCounter("Property", {
53+
filter: (node) => node.key.type === "Identifier",
54+
match: (node, nc) => this.#extractCounterIdentifiers(nc, node.key)
55+
}),
56+
// TODO: implement name option?
57+
new NodeCounter("UnaryExpression", {
58+
filter: ({ argument }) => argument.type === "UnaryExpression" && argument.argument.type === "ArrayExpression"
59+
}),
60+
new NodeCounter("VariableDeclarator", {
61+
match: (node, nc) => this.#extractCounterIdentifiers(nc, node.id)
62+
})
63+
];
64+
}
65+
66+
/**
67+
* @param {!NodeCounter} nc
68+
* @param {*} node
69+
*/
70+
#extractCounterIdentifiers(nc, node) {
71+
const { type } = nc;
72+
73+
switch (type) {
74+
case "VariableDeclarator":
75+
case "AssignmentExpression": {
76+
for (const { name } of getVariableDeclarationIdentifiers(node)) {
77+
this.identifiers.push({ name, type });
78+
}
79+
break;
80+
}
81+
case "Property":
82+
case "FunctionDeclaration":
83+
this.identifiers.push({ name: node.name, type });
84+
break;
85+
}
86+
}
87+
88+
analyzeString(str) {
89+
const score = Utils.stringSuspicionScore(str);
90+
if (score !== 0) {
91+
this.literalScores.push(score);
92+
}
93+
94+
if (!this.hasDictionaryString) {
95+
const isDictionaryStr = kDictionaryStrParts.every((word) => str.includes(word));
96+
if (isDictionaryStr) {
97+
this.hasDictionaryString = true;
98+
}
99+
}
100+
101+
// Searching for morse string like "--.- --.--."
102+
if (Utils.isMorse(str)) {
103+
this.morseLiterals.add(str);
104+
}
105+
}
106+
107+
walk(node) {
108+
const { type } = node;
109+
110+
const isFunctionParams = node.type === "FunctionDeclaration" || node.type === "FunctionExpression";
111+
const nodesToExtract = match(type)
112+
.with("ClassDeclaration", () => [node.id, node.superClass])
113+
.with("FunctionDeclaration", () => [node.params])
114+
.with("FunctionExpression", () => [node.params])
115+
.with("MethodDefinition", () => [node.key])
116+
.otherwise(() => []);
117+
118+
kIdentifierNodeExtractor(
119+
({ name }) => this.identifiers.push({ name, type: isFunctionParams ? "Params" : type }),
120+
nodesToExtract.flat()
121+
);
122+
123+
this.#counters.forEach((counter) => counter.walk(node));
124+
}
125+
126+
aggregateCounters() {
127+
const defaultValue = {
128+
Identifiers: this.identifiers.length
129+
};
130+
131+
return this.#counters.reduce((result, counter) => {
132+
result[counter.type] = counter.lookup ?
133+
counter.properties :
134+
counter.count;
135+
136+
return result;
137+
}, defaultValue);
138+
}
139+
140+
assertObfuscation() {
141+
const counters = this.aggregateCounters();
142+
const identifierLength = this.identifiers.length;
143+
144+
if (jsfuck.verify(counters)) {
145+
return "jsfuck";
146+
}
147+
if (jjencode.verify(this.identifiers, counters)) {
148+
return "jjencode";
149+
}
150+
if (this.morseLiterals.size >= 36) {
151+
return "morse";
152+
}
153+
154+
const identifiers = this.identifiers
155+
.map((value) => value?.name ?? null)
156+
.filter((name) => typeof name === "string");
157+
158+
const { prefix } = Patterns.commonHexadecimalPrefix(
159+
identifiers
160+
);
161+
const uPrefixNames = new Set(Object.keys(prefix));
162+
163+
if (identifierLength > kMinimumIdsCount && uPrefixNames.size > 0) {
164+
this.hasPrefixedIdentifiers = calcAvgPrefixedIdentifiers(counters, prefix) > 80;
165+
}
166+
167+
if (uPrefixNames.size === 1 && freejsobfuscator.verify(this.identifiers, prefix)) {
168+
return "freejsobfuscator";
169+
}
170+
if (obfuscatorio.verify(this, counters)) {
171+
return "obfuscator.io";
172+
}
173+
// if ((identifierLength > (kMinimumIdsCount * 3) && this.hasPrefixedIdentifiers)
174+
// && (oneTimeOccurence <= 3 || this.encodedArrayValue > 0)) {
175+
// return "unknown";
176+
// }
177+
178+
return null;
179+
}
180+
}
181+
182+
function calcAvgPrefixedIdentifiers(
183+
counters,
184+
prefix
185+
) {
186+
const valuesArr = Object.values(prefix).slice().sort((left, right) => left - right);
187+
if (valuesArr.length === 0) {
188+
return 0;
189+
}
190+
const nbOfPrefixedIds = valuesArr.length === 1 ? valuesArr.pop() : (valuesArr.pop() + valuesArr.pop());
191+
const maxIds = counters.Identifiers - counters.Property;
192+
193+
return ((nbOfPrefixedIds / maxIds) * 100);
194+
}

src/NodeCounter.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Import Third-party Dependencies
2+
import FrequencySet from "frequency-set";
3+
4+
// Import Internal Dependencies
5+
import { isNode } from "./utils/index.js";
6+
7+
// eslint-disable-next-line func-style
8+
const noop = (node) => true;
9+
10+
export class NodeCounter {
11+
lookup = null;
12+
13+
#matchingCount = 0;
14+
#properties = null;
15+
#filterFn = noop;
16+
#matchFn = noop;
17+
18+
/**
19+
* @param {!string} type
20+
* @param {Object} [options]
21+
* @param {(node: any) => boolean} [options.filter]
22+
* @param {(node: any, nc: NodeCounter) => void} [options.match]
23+
*
24+
* @example
25+
* new NodeCounter("FunctionDeclaration");
26+
* new NodeCounter("VariableDeclaration[kind]");
27+
*/
28+
constructor(type, options = {}) {
29+
if (typeof type !== "string") {
30+
throw new TypeError("type must be a string");
31+
}
32+
33+
const typeResult = /([A-Za-z]+)(\[[a-zA-Z]+\])?/g.exec(type);
34+
if (typeResult === null) {
35+
throw new Error("invalid type argument syntax");
36+
}
37+
this.type = typeResult[1];
38+
this.lookup = typeResult[2]?.slice(1, -1) ?? null;
39+
if (this.lookup) {
40+
this.#properties = new FrequencySet();
41+
}
42+
43+
this.#filterFn = options.filter ?? noop;
44+
this.#matchFn = options.match ?? noop;
45+
}
46+
47+
get count() {
48+
return this.#matchingCount;
49+
}
50+
51+
get properties() {
52+
return Object.fromEntries(
53+
this.#properties?.entries() ?? []
54+
);
55+
}
56+
57+
walk(node) {
58+
if (!isNode(node) || node.type !== this.type) {
59+
return;
60+
}
61+
if (!this.#filterFn(node)) {
62+
return;
63+
}
64+
65+
this.#matchingCount++;
66+
if (this.lookup === null) {
67+
this.#matchFn(node, this);
68+
}
69+
else if (this.lookup in node) {
70+
this.#properties.add(node[this.lookup]);
71+
this.#matchFn(node, this);
72+
}
73+
}
74+
}

src/ProbeRunner.js

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,11 @@ import isUnsafeCallee from "./probes/isUnsafeCallee.js";
66
import isLiteral from "./probes/isLiteral.js";
77
import isLiteralRegex from "./probes/isLiteralRegex.js";
88
import isRegexObject from "./probes/isRegexObject.js";
9-
import isVariableDeclaration from "./probes/isVariableDeclaration.js";
109
import isRequire from "./probes/isRequire/isRequire.js";
1110
import isImportDeclaration from "./probes/isImportDeclaration.js";
12-
import isMemberExpression from "./probes/isMemberExpression.js";
13-
import isArrayExpression from "./probes/isArrayExpression.js";
14-
import isFunction from "./probes/isFunction.js";
15-
import isAssignmentExpression from "./probes/isAssignmentExpression.js";
16-
import isObjectExpression from "./probes/isObjectExpression.js";
17-
import isUnaryExpression from "./probes/isUnaryExpression.js";
1811
import isWeakCrypto from "./probes/isWeakCrypto.js";
19-
import isClassDeclaration from "./probes/isClassDeclaration.js";
20-
import isMethodDefinition from "./probes/isMethodDefinition.js";
12+
import isBinaryExpression from "./probes/isBinaryExpression.js";
13+
import isArrayExpression from "./probes/isArrayExpression.js";
2114

2215
// Import Internal Dependencies
2316
import { SourceFile } from "./SourceFile.js";
@@ -50,17 +43,10 @@ export class ProbeRunner {
5043
isLiteral,
5144
isLiteralRegex,
5245
isRegexObject,
53-
isVariableDeclaration,
5446
isImportDeclaration,
55-
isMemberExpression,
56-
isAssignmentExpression,
57-
isObjectExpression,
58-
isArrayExpression,
59-
isFunction,
60-
isUnaryExpression,
6147
isWeakCrypto,
62-
isClassDeclaration,
63-
isMethodDefinition
48+
isBinaryExpression,
49+
isArrayExpression
6450
];
6551

6652
/**

0 commit comments

Comments
 (0)