distinctiomagnam
Version:
JavaScript Obfuscation Tool.
355 lines (318 loc) • 11.5 kB
text/typescript
import Transform from "../transform";
import { walk } from "../../traverse";
import {
Node,
Location,
Identifier,
VariableDeclaration,
VariableDeclarator,
} from "../../util/gen";
import {
clone,
deleteDeclaration,
getVarContext,
isVarContext,
prepend,
} from "../../util/insert";
import { ObfuscateOrder } from "../../order";
import { getIdentifierInfo } from "../../util/identifiers";
import { isValidIdentifier } from "../../util/compare";
import { ComputeProbabilityMap } from "../../probability";
import { ok } from "assert";
/**
* Extracts keys out of an object if possible.
* ```js
* // Input
* var utils = {
* isString: x=>typeof x === "string",
* isBoolean: x=>typeof x === "boolean"
* }
* if ( utils.isString("Hello") ) {
* ...
* }
*
* // Output
* var utils_isString = x=>typeof x === "string";
* var utils_isBoolean = x=>typeof x === "boolean"
*
* if ( utils_isString("Hello") ) {
* ...
* }
* ```
*/
export default class ObjectExtraction extends Transform {
constructor(o) {
super(o, ObfuscateOrder.ObjectExtraction);
}
match(object: Node, parents: Node[]) {
return isVarContext(object);
}
transform(context: Node, contextParents: Node[]) {
// ObjectExpression Extractor
return () => {
// First pass through to find the maps
var objectDefs: { [name: string]: Location } = Object.create(null);
var objectDefiningIdentifiers: { [name: string]: Location } =
Object.create(null);
var illegal = new Set<string>();
walk(context, contextParents, (object: Node, parents: Node[]) => {
if (object.type == "ObjectExpression") {
// this.log(object, parents);
if (
parents[0].type == "VariableDeclarator" &&
parents[0].init == object &&
parents[0].id.type == "Identifier"
) {
var name = parents[0].id.name;
if (name) {
if (getVarContext(object, parents) != context) {
illegal.add(name);
return;
}
if (!object.properties.length) {
illegal.add(name);
return;
}
// duplicate name
if (objectDefiningIdentifiers[name]) {
illegal.add(name);
return;
}
// check for computed properties
object.properties.forEach((prop) => {
if (prop.computed && prop.key.type == "Literal") {
prop.computed = false;
}
});
var computed = object.properties.find((x) => x.computed);
if (computed) {
this.log(
name + " has computed property: " + computed.key.name ||
computed.key.value
);
illegal.add(name);
return;
} else {
var illegalName = object.properties
.map((x) =>
x.computed ? x.key.value : x.key.name || x.key.value
)
.find((x) => !x || !isValidIdentifier(x));
if (illegalName) {
this.log(
name + " has an illegal property '" + illegalName + "'"
);
illegal.add(name);
return;
} else {
var isIllegal = false;
walk(object, parents, (o, p) => {
if (o.type == "ThisExpression" || o.type == "Super") {
isIllegal = true;
return "EXIT";
}
});
if (isIllegal) {
illegal.add(name);
return;
}
objectDefs[name] = [object, parents];
objectDefiningIdentifiers[name] = [
parents[0].id,
[...parents],
];
}
}
}
}
}
});
illegal.forEach((name) => {
delete objectDefs[name];
delete objectDefiningIdentifiers[name];
});
// this.log("object defs", objectDefs);
// huge map of changes
var objectDefChanges: {
[name: string]: { key: string; object: Node; parents: Node[] }[];
} = {};
if (Object.keys(objectDefs).length) {
// A second pass through is only required when extracting object keys
// Second pass through the exclude the dynamic map (counting keys, re-assigning)
walk(context, contextParents, (object: any, parents: Node[]) => {
if (object.type == "Identifier") {
var info = getIdentifierInfo(object, parents);
if (!info.spec.isReferenced) {
return;
}
var def = objectDefs[object.name];
if (def) {
var isIllegal = false;
if (info.spec.isDefined) {
if (objectDefiningIdentifiers[object.name][0] !== object) {
this.log(object.name, "you can't redefine the object");
isIllegal = true;
}
} else {
var isMemberExpression =
parents[0].type == "MemberExpression" &&
parents[0].object == object;
if (
(parents.find((x) => x.type == "AssignmentExpression") &&
!isMemberExpression) ||
parents.find(
(x) => x.type == "UnaryExpression" && x.operator == "delete"
)
) {
this.log(object.name, "you can't re-assign the object");
isIllegal = true;
} else if (isMemberExpression) {
var key =
parents[0].property.value || parents[0].property.name;
if (
parents[0].computed &&
parents[0].property.type !== "Literal"
) {
this.log(
object.name,
"object[expr] detected, only object['key'] is allowed"
);
isIllegal = true;
} else if (
!parents[0].computed &&
parents[0].property.type !== "Identifier"
) {
this.log(
object.name,
"object.<expr> detected, only object.key is allowed"
);
isIllegal = true;
} else if (
!key ||
!def[0].properties.some(
(x) => (x.key.value || x.key.name) == key
)
) {
// check if initialized property
// not in initialized object.
this.log(
object.name,
"not in initialized object.",
def[0].properties,
key
);
isIllegal = true;
}
if (!isIllegal && key) {
// allowed.
// start the array if first time
if (!objectDefChanges[object.name]) {
objectDefChanges[object.name] = [];
}
// add to array
objectDefChanges[object.name].push({
key: key,
object: object,
parents: parents,
});
}
} else {
this.log(
object.name,
"you must access a property on the when referring to the identifier (accessors must be hard-coded literals), parent is " +
parents[0].type
);
isIllegal = true;
}
}
if (isIllegal) {
// this is illegal, delete it from being moved and delete accessor changes from happening
this.log(object.name + " is illegal");
delete objectDefs[object.name];
delete objectDefChanges[object.name];
}
}
}
});
Object.keys(objectDefs).forEach((name) => {
if (
!ComputeProbabilityMap(
this.options.objectExtraction,
(x) => x,
name
)
) {
//continue;
return;
}
var [object, parents] = objectDefs[name];
var declarator = parents[0];
var declaration = parents[2];
ok(declarator.type === "VariableDeclarator");
ok(declaration.type === "VariableDeclaration");
var properties = object.properties;
// change the prop names while extracting
var newPropNames: { [key: string]: string } = {};
var variableDeclarators = [];
properties.forEach((property: Node) => {
var keyName = property.key.name || property.key.value;
var nn = name + "_" + keyName;
newPropNames[keyName] = nn;
var v = property.value;
variableDeclarators.push(
VariableDeclarator(nn, this.addComment(v, `${name}.${keyName}`))
);
});
declaration.declarations.splice(
declaration.declarations.indexOf(declarator),
1,
...variableDeclarators
);
// update all identifiers that pointed to the old object
objectDefChanges[name] &&
objectDefChanges[name].forEach((change) => {
if (!change.key) {
this.error(new Error("key is undefined"));
}
if (newPropNames[change.key]) {
var memberExpression = change.parents[0];
if (memberExpression.type == "MemberExpression") {
this.replace(
memberExpression,
this.addComment(
Identifier(newPropNames[change.key]),
`Original Accessor: ${name}.${change.key}`
)
);
} else {
// Provide error with more information:
console.log(memberExpression);
this.error(
new Error(
`should be MemberExpression, found type=${memberExpression.type}`
)
);
}
} else {
console.log(objectDefChanges[name], newPropNames);
this.error(
new Error(
`"${change.key}" not found in [${Object.keys(
newPropNames
).join(", ")}] while flattening ${name}.`
)
);
}
});
this.log(
`Extracted ${
Object.keys(newPropNames).length
} properties from ${name}, affecting ${
Object.keys(objectDefChanges[name] || {}).length
} line(s) of code.`
);
});
}
};
}
}