Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-ears-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nodesecure/js-x-ray": minor
---

Reduce HEAP allocation and GC pressure to optimize execution
43 changes: 30 additions & 13 deletions workspaces/js-x-ray/src/AstAnalyser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
isOneLineExpressionExport
} from "./utils/index.ts";
import { walkEnter } from "./walker/index.ts";
import { getCallExpressionIdentifier } from "./estree/index.ts";
import { getCallExpressionIdentifier, isLiteral } from "./estree/index.ts";
import {
generateWarning,
type OptionalWarningName,
Expand Down Expand Up @@ -269,24 +269,32 @@ export class AstAnalyser extends EventEmitter<AstAnalyserEvents> {
body: ESTree.Statement[],
probeRunner: ProbeRunner
) {
const recur = this.#walkEnter.bind(this);
const recursiveWalkEnter = this.#walkEnter.bind(this);
walkEnter(body, function walk(node) {
if (Array.isArray(node)) {
return;
}

for (const probeNode of probeRunner.sourceFile.walk(node)) {
const action = probeRunner.walk(probeNode);
if (action === "skip") {
this.skip();
probeRunner.sourceFile.walk(
node,
(probeNode) => {
const action = probeRunner.walk(probeNode);
if (action === "skip") {
this.skip();
}

if (
isEvalCallExpr(probeNode) &&
isLiteral(probeNode.arguments[0])
) {
const evalBody = AstAnalyser.DefaultParser.parse(
probeNode.arguments[0].value,
void 0
);
recursiveWalkEnter(evalBody, probeRunner);
}
}
if (probeNode.type === "CallExpression" && getCallExpressionIdentifier(probeNode, {
resolveCallExpression: true
}) === "eval" && probeNode.arguments[0].type === "Literal" && typeof probeNode.arguments[0].value === "string") {
const evalBody = AstAnalyser.DefaultParser.parse(probeNode.arguments[0].value, void 0);
recur(evalBody, probeRunner);
}
}
);
});
}

Expand Down Expand Up @@ -477,3 +485,12 @@ export class AstAnalyser extends EventEmitter<AstAnalyserEvents> {
return this.#collectableSetRegistry?.get(type);
}
}

function isEvalCallExpr(
node: ESTree.Node
): node is ESTree.CallExpression {
return (
node.type === "CallExpression" &&
getCallExpressionIdentifier(node, { resolveCallExpression: true }) === "eval"
);
}
48 changes: 22 additions & 26 deletions workspaces/js-x-ray/src/Deobfuscator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,37 +142,31 @@ export class Deobfuscator {
walk(
node: ESTree.Node
): void {
let nodesToExtract: (ESTree.Node | null | undefined)[];
switch (node.type) {
case "ClassDeclaration":
nodesToExtract = [node.id, node.superClass];
kIdentifierNodeExtractor(
({ name }) => this.identifiers.push({ name, type: node.type }),
[node.id, node.superClass].filter((node) => node !== null) as ESTree.Node[]
);
break;
case "FunctionDeclaration":
nodesToExtract = node.params;
break;
case "FunctionExpression":
nodesToExtract = node.params;
kIdentifierNodeExtractor(
({ name }) => this.identifiers.push({ name, type: "FunctionParams" }),
node.params
);
break;
case "MethodDefinition":
nodesToExtract = [node.key];
kIdentifierNodeExtractor(
({ name }) => this.identifiers.push({ name, type: node.type }),
[node.key]
);
break;
default:
nodesToExtract = [];
}

const isFunctionParams =
node.type === "FunctionDeclaration" ||
node.type === "FunctionExpression";

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

this.#counters.forEach((counter) => counter.walk(node));
for (const counter of this.#counters) {
counter.walk(node);
}
}

aggregateCounters(): ObfuscatedCounters {
Expand Down Expand Up @@ -220,11 +214,13 @@ export class Deobfuscator {
return "morse";
}

const { prefix } = commonHexadecimalPrefix(
this.identifiers.flatMap(
({ name }) => (typeof name === "string" ? [name] : [])
)
);
const names: string[] = [];
for (const { name } of this.identifiers) {
if (typeof name === "string") {
names.push(name);
}
}
const { prefix } = commonHexadecimalPrefix(names);
const uPrefixNames = new Set(Object.keys(prefix));

if (this.identifiers.length > kMinimumIdsCount && uPrefixNames.size > 0) {
Expand Down
11 changes: 7 additions & 4 deletions workspaces/js-x-ray/src/ProbeRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import type { TracedIdentifierReport } from "./VariableTracer.ts";
import type { SourceFile } from "./SourceFile.ts";
import type { OptionalWarningName } from "./warnings.ts";
import {
getCallExpressionIdentifier
getCallExpressionIdentifier,
type GetCallExpressionIdentifierOptions
} from "./estree/index.ts";
import { CALL_EXPRESSION_DATA, CALL_EXPRESSION_IDENTIFIER } from "./contants.ts";

Expand Down Expand Up @@ -89,6 +90,7 @@ export class ProbeRunner {
#probeMainCtx = new Map<Probe, ProbeMainContext>();
#nodeTypeIndex = new Map<string, Probe[]>();
#catchAllProbes: Probe[] = [];
#callExprIdentifierOptions: GetCallExpressionIdentifierOptions;

static Signals = Object.freeze({
Break: Symbol.for("breakWalk"),
Expand Down Expand Up @@ -132,6 +134,9 @@ export class ProbeRunner {
probes: Probe[] = ProbeRunner.Defaults
) {
this.sourceFile = sourceFile;
this.#callExprIdentifierOptions = {
externalIdentifierLookup: (name) => sourceFile.tracer.literalIdentifiers.get(name)?.value ?? null
};

for (const probe of probes) {
assert(
Expand Down Expand Up @@ -283,9 +288,7 @@ export class ProbeRunner {
let tracedIdentifier: string | null | undefined;

if (node.type === "CallExpression") {
const id = getCallExpressionIdentifier(node, {
externalIdentifierLookup: (name) => this.sourceFile.tracer.literalIdentifiers.get(name)?.value ?? null
});
const id = getCallExpressionIdentifier(node, this.#callExprIdentifierOptions);
if (id !== null) {
tracedIdentifierReport = this.sourceFile.tracer.getDataFromIdentifier(id);
tracedIdentifier = id;
Expand Down
36 changes: 20 additions & 16 deletions workspaces/js-x-ray/src/SourceFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,18 @@ export class SourceFile {
);
}

const identifiersLengthArr = this.deobfuscator.identifiers
.filter((value) => value.type !== "Property" && typeof value.name === "string")
.map((value) => value.name.length);

const [idsLengthAvg, stringScore] = [
sum(identifiersLengthArr),
sum(this.deobfuscator.literalScores)
];
if (!isMinified && identifiersLengthArr.length > 5 && idsLengthAvg <= 1.5) {
let filteredLen = 0;
let filteredSum = 0;
for (const value of this.deobfuscator.identifiers) {
if (value.type !== "Property" && typeof value.name === "string") {
filteredLen++;
filteredSum += value.name.length;
}
}
const idsLengthAvg = filteredLen === 0 ? 0 : filteredSum / filteredLen;
const stringScore = sum(this.deobfuscator.literalScores);

if (!isMinified && filteredLen > 5 && idsLengthAvg <= 1.5) {
this.warnings.push(
generateWarning("short-identifiers", { value: String(idsLengthAvg) })
);
Expand All @@ -199,19 +202,20 @@ export class SourceFile {
}

walk(
node: ESTree.Node
): ESTree.Node[] {
node: ESTree.Node,
callback: (node: ESTree.Node) => void
): void {
const split = InlinedRequire.split(node);
if (split !== null) {
this.tracer.walk(split.virtualDeclaration);
if (split.rebuildExpression) {
this.tracer.walk(split.rebuildExpression);
}

return [
split.virtualDeclaration,
...(split.rebuildExpression ? [split.rebuildExpression] : [])
];
callback(split.virtualDeclaration);
if (split.rebuildExpression) {
callback(split.rebuildExpression);
}
}

this.tracer.walk(node);
Expand All @@ -225,7 +229,7 @@ export class SourceFile {
this.inTryStatement = false;
}

return [node];
callback(node);
}
}

Expand Down
17 changes: 8 additions & 9 deletions workspaces/js-x-ray/src/VariableTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,14 @@ export class VariableTracer extends EventEmitter {
});

if (identifierOrMemberExpr.includes(".")) {
const exprs = [...getSubMemberExpressionSegments(identifierOrMemberExpr)]
.filter((expr) => !this.#traced.has(expr));

for (const expr of exprs) {
this.trace(expr, {
followConsecutiveAssignment: true,
name,
moduleName
});
for (const expr of getSubMemberExpressionSegments(identifierOrMemberExpr)) {
if (!this.#traced.has(expr)) {
this.trace(expr, {
followConsecutiveAssignment: true,
name,
moduleName
});
}
}
}

Expand Down
25 changes: 15 additions & 10 deletions workspaces/js-x-ray/src/estree/functions/arrayExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ export function joinArrayExpression(
return null;
}

const id = Array.from(
getMemberExpressionIdentifier(node.callee)
).join(".");
let id = "";
for (const part of getMemberExpressionIdentifier(node.callee)) {
id = id === "" ? part : `${id}.${part}`;
}
if (
id !== "join" ||
!isLiteral(node.arguments[0])
Expand All @@ -112,13 +113,17 @@ export function joinArrayExpression(

const separator = node.arguments[0].value;

const iter = arrayExpressionToString(
node.callee.object,
{
...options,
resolveCharCode: false
let result = "";
let first = true;
for (const part of arrayExpressionToString(node.callee.object, { ...options, resolveCharCode: false })) {
if (first) {
result = part;
first = false;
}
else {
result += separator + part;
}
);
}

return [...iter].join(separator);
return result;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ export function getCallExpressionIdentifier(
}
if (node.callee.type === "MemberExpression") {
const memberObject = node.callee.object;
const lastId = [
...getMemberExpressionIdentifier(node.callee, { externalIdentifierLookup })
].join(".");
let lastId = "";
for (const part of getMemberExpressionIdentifier(node.callee, { externalIdentifierLookup })) {
lastId = lastId === "" ? part : `${lastId}.${part}`;
}

return resolveCallExpression && memberObject.type === "CallExpression" ?
getCallExpressionIdentifier(memberObject) + `.${lastId}` :
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ export function* getMemberExpressionIdentifier(

// foo.bar["k" + "e" + "y"]
case "BinaryExpression": {
const literal = [...concatBinaryExpression(node.property, options)].join("");
let literal = "";
for (const part of concatBinaryExpression(node.property, options)) {
literal += part;
}
if (literal.trim() !== "") {
yield literal;
}
Expand Down
4 changes: 2 additions & 2 deletions workspaces/js-x-ray/src/obfuscators/freejsobfuscator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function verify(
prefix: Record<string, number>
) {
const pValue = Object.keys(prefix).pop()!;
const regexStr = `^${RegExp.escape(pValue)}[a-zA-Z]{1,2}[0-9]{0,2}$`;
const regex = new RegExp(`^${RegExp.escape(pValue)}[a-zA-Z]{1,2}[0-9]{0,2}$`);

return identifiers.every(({ name }) => new RegExp(regexStr).test(name));
return identifiers.every(({ name }) => regex.test(name));
}
8 changes: 6 additions & 2 deletions workspaces/js-x-ray/src/obfuscators/jjencode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ export function verify(
if (!notNullOrUndefined(name)) {
return false;
}
const charsCode = [...new Set([...name])];
for (const char of name) {
if (!kJJRegularSymbols.has(char)) {
return false;
}
}

return charsCode.every((char) => kJJRegularSymbols.has(char));
return true;
}).length;
const pourcent = ((matchCount / identifiers.length) * 100);

Expand Down
Loading
Loading