Skip to content

Commit 3f51fed

Browse files
authored
refactor!: implement NodeCounter & Deobfuscator class (#239)
1 parent 59c5b51 commit 3f51fed

36 files changed

+813
-718
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: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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+
}

src/NodeCounter.js

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

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)