UNPKG

js-confuser

Version:

JavaScript Obfuscation Tool.

1,013 lines (958 loc) 67.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports["default"] = void 0; var _traverse = _interopRequireWildcard(require("@babel/traverse")); var _order = require("../order"); var _astUtils = require("../utils/ast-utils"); var t = _interopRequireWildcard(require("@babel/types")); var _node = require("../utils/node"); var _template = _interopRequireDefault(require("../templates/template")); var _randomUtils = require("../utils/random-utils"); var _IntGen = require("../utils/IntGen"); var _assert = require("assert"); var _NameGen = require("../utils/NameGen"); var _constants = require("../constants"); var _xorStringTemplate = require("../templates/xorStringTemplate"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; } function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); } function _toConsumableArray(r) { return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread(); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArray(r) { if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r); } function _arrayWithoutHoles(r) { if (Array.isArray(r)) return _arrayLikeToArray(r); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = !0, o = !1; try { if (i = (t = t.call(r)).next, 0 === l) { if (Object(t) !== t) return; f = !1; } else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = !0); } catch (r) { o = !0, n = r; } finally { try { if (!f && null != t["return"] && (u = t["return"](), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t["return"] || t["return"](); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _classCallCheck(a, n) { if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function"); } function _defineProperties(e, r) { for (var t = 0; t < r.length; t++) { var o = r[t]; o.enumerable = o.enumerable || !1, o.configurable = !0, "value" in o && (o.writable = !0), Object.defineProperty(e, _toPropertyKey(o.key), o); } } function _createClass(e, r, t) { return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", { writable: !1 }), e; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } // Function deemed unsafe for CFF var CFF_UNSAFE = Symbol("CFF_UNSAFE"); // For skipping literals var CFF_SKIP = Symbol("CFF_SKIP"); var HashFunctionTemplate = new _template["default"]("\nfunction {fnName}(int) {\n var a = (int | 0) ^ 0x9e3779b9, b = 0x243f6a88 | 0, k = 0x6a09e667 | 0;\n for (var i = 0; i < 19; i++) {\n a = (a + ((b << 7) ^ (b >>> 3)) + k) | 0;\n a = (a ^ (a >>> 15)) | 0;\n a = (a + (a << 11)) | 0;\n b = (b ^ ((a << 4) + (a >>> 9) + k)) | 0;\n b = (b + (b << 6)) | 0;\n b = (b ^ (b >>> 13)) | 0;\n k = (k + 0x7f4a7c15) | 0;\n }\n a = (a ^ b) | 0;\n a = (a + (a << 3)) | 0;\n a = (a ^ (a >>> 11)) | 0;\n a = (a + (a << 15)) | 0;\n b = (b ^ (b >>> 13)) | 0;\n b = (b + (b << 7)) | 0;\n b = (b ^ (b >>> 17)) | 0;\n return (a >>> 0) * 0x100000 + (b >>> 12);\n}\n"); function HashFunction(_int) { var a = (_int | 0) ^ 0x9e3779b9, b = 0x243f6a88 | 0, k = 0x6a09e667 | 0; for (var i = 0; i < 19; i++) { a = a + (b << 7 ^ b >>> 3) + k | 0; a = a ^ a >>> 15 | 0; a = a + (a << 11) | 0; b = b ^ (a << 4) + (a >>> 9) + k | 0; b = b + (b << 6) | 0; b = b ^ b >>> 13 | 0; k = k + 0x7f4a7c15 | 0; } a = a ^ b | 0; a = a + (a << 3) | 0; a = a ^ a >>> 11 | 0; a = a + (a << 15) | 0; b = b ^ b >>> 13 | 0; b = b + (b << 7) | 0; b = b ^ b >>> 17 | 0; return (a >>> 0) * 0x100000 + (b >>> 12); } /** * Breaks functions into DAGs (Directed Acyclic Graphs) * * - 1. Break functions into chunks * - 2. Shuffle chunks but remember their original position * - 3. Create a Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition. * * The Switch statement: * * - 1. The state variable controls which case will run next * - 2. At the end of each case, the state variable is updated to the next block of code. * - 3. The while loop continues until the the state variable is the end state. */ var _default = exports["default"] = function _default(_ref) { var Plugin = _ref.Plugin; var me = Plugin(_order.Order.ControlFlowFlattening, { changeData: { functions: 0, blocks: 0, ifStatements: 0, deadCode: 0, variables: 0 } }); // in Debug mode, the output is much easier to read var isDebug = false; var flattenIfStatements = true; // Converts IF-statements into equivalent 'goto style of code' var flattenFunctionDeclarations = true; // Converts Function Declarations into equivalent 'goto style of code' var addRelativeAssignments = true; // state += (NEW_STATE - CURRENT_STATE) var addDeadCode = true; // add fakes chunks of code var addFakeTests = true; // case 100: case 490: case 510: ... var addComplexTests = true; // case s != 49 && s - 10: var addAssertions = true; // use @js-confuser-assert values? var mangleNumericalLiterals = true; // 50 => state + X var mangleBooleanLiterals = true; // true => state == X var mangleStringLiterals = true; // "hi" => xor("fOq", state + X) var booleanLiteralChance = 100; // 100% for booleans var numericLiteralChance = 50; // 50% for numbers var stringLiteralChance = 100; // 100% for strings var sequenceSizeRange = [100, 125]; var stateVarsRange = [75, 100]; var stateValueRange = [-999, 999]; var cffPrefix = me.getPlaceholder(); // Amount of blocks changed by Control Flow Flattening var cffCounter = 0; var functionsModified = []; function flagFunctionToAvoid(path, reason) { var fnOrProgram = (0, _astUtils.getParentFunctionOrProgram)(path); fnOrProgram.node[CFF_UNSAFE] = reason; } var needsSumFunction = false; var sumFnName = me.getPlaceholder() + "_cff_sum"; var xorFnName = me.getPlaceholder() + "_cff_xor"; var sequenceName = me.getPlaceholder() + "_cff_sequence"; var sliceFnName = me.getPlaceholder() + "_cff_slice"; var hashFnName = me.getPlaceholder() + "_cff_hash"; var stringsName = me.getPlaceholder() + "_strings"; var stringsValue = ""; var sequence = new Array((0, _randomUtils.getRandomInteger)(sequenceSizeRange[0], sequenceSizeRange[1])).fill(0).map(function (_) { return (0, _randomUtils.getRandomInteger)(stateValueRange[0], stateValueRange[1]); }); function cffMain(_path) { var programOrFunctionPath = _path; // if (cffOptions.excludeLoops) { // // Exclude loops // if (programOrFunctionPath.find((p) => p.isForStatement() || p.isWhile())) // return; // } // Exclude 'CFF_UNSAFE' functions if (programOrFunctionPath.node[CFF_UNSAFE]) { return; } var programPath = _path.isProgram() ? _path : null; var functionPath = _path.isFunction() ? _path : null; var blockPath; if (programPath) { blockPath = programPath; } else { var fnBlockPath = functionPath.get("body"); if (!fnBlockPath.isBlock()) return; blockPath = fnBlockPath; } // Don't apply to strict mode blocks var strictModeEnforcingBlock = programOrFunctionPath.find(function (path) { return (0, _astUtils.isStrictMode)(path); }); if (strictModeEnforcingBlock) return; // Must be at least 3 statements or more if (blockPath.node.body.length < 3) return; // Check user's threshold setting if (!me.computeProbabilityMap(me.options.controlFlowFlattening)) { return; } if (functionPath) { // Avoid unsafe functions if (functionPath.node[_constants.UNSAFE]) { return; } if (functionPath.node.async || functionPath.node.generator) return; } programOrFunctionPath.scope.crawl(); var hasIllegalNode = false; var bindingNames = new Set(); blockPath.traverse({ Identifier: function Identifier(path) { if (!path.isBindingIdentifier()) return; var binding = path.scope.getBinding(path.node.name); if (!binding) return; var fnParent = path.getFunctionParent(); if (path.key === "id" && path.parentPath.isFunctionDeclaration()) { fnParent = path.parentPath.getFunctionParent(); } if (fnParent !== functionPath) return; if (!(0, _astUtils.isDefiningIdentifier)(path)) { return; } if (bindingNames.has(path.node.name)) { hasIllegalNode = true; path.stop(); return; } bindingNames.add(path.node.name); } }); if (hasIllegalNode) { return; } me.changeData.blocks++; // Limit how many numbers get entangled var mangledLiteralsCreated = 0; var cffIndex = ++cffCounter; // Start from 1 var prefix = cffPrefix + "_" + cffIndex; var identifier = function identifier(suffix) { var name = prefix + "_" + suffix; var id = t.identifier(name); return id; }; var mainFnName = identifier("main"); var scopeVar = identifier("scope"); var statesVar = identifier("states"); var runtimeVar = identifier("runtime"); var stateVars = new Array(isDebug ? 1 : (0, _randomUtils.getRandomInteger)(stateVarsRange[0], stateVarsRange[1])).fill("").map(function (_, i) { return t.memberExpression((0, _node.deepClone)(statesVar), t.numericLiteral(i), true); }); var dynamicIndexes = []; var dynamicIndexesSet = new Set(); var dynamicIndexesAmount = (0, _randomUtils.getRandomInteger)(8, 12); for (var i = 0; i < dynamicIndexesAmount && dynamicIndexesSet.size < stateVars.length; i++) { var proposed = void 0; do { proposed = (0, _randomUtils.getRandomInteger)(0, stateVars.length); } while (dynamicIndexesSet.has(proposed)); dynamicIndexes.push(proposed); dynamicIndexesSet.add(proposed); } var argVar = identifier("_arg"); var usedArgVar = false; var _didReturnVar = identifier("return"); var basicBlocks = new Map(); // Map labels to states var stateIntGen = new _IntGen.IntGen(stateValueRange[0], stateValueRange[1]); var defaultBlockPath = blockPath; var scopeCounter = 0; var scopeNameGen = new _NameGen.NameGen(me.options.identifierGenerator); if (!isDebug) { scopeNameGen = me.obfuscator.nameGen; } var ScopeManager = /*#__PURE__*/function () { function ScopeManager(scope, initializingBasicBlock) { _classCallCheck(this, ScopeManager); _defineProperty(this, "isNotUsed", true); _defineProperty(this, "requiresInitializing", true); _defineProperty(this, "nameMap", new Map()); _defineProperty(this, "nameGen", new _NameGen.NameGen(me.options.identifierGenerator)); this.scope = scope; this.initializingBasicBlock = initializingBasicBlock; this.propertyName = isDebug ? "_" + cffIndex + "_" + scopeCounter++ : scopeNameGen.generate(); } return _createClass(ScopeManager, [{ key: "getNewName", value: function getNewName(name, originalNode) { if (!this.nameMap.has(name)) { var newName = this.nameGen.generate(false); if (isDebug) { newName = "_" + name; } this.nameMap.set(name, newName); me.changeData.variables++; // console.log( `Renaming ${name} to ${newName}: ${this.scope.path.type} (scope ${this.propertyName})` ); return newName; } return this.nameMap.get(name); } }, { key: "getScopeObject", value: function getScopeObject() { return t.memberExpression((0, _node.deepClone)(scopeVar), t.stringLiteral(this.propertyName), true); } }, { key: "getInitializingStatement", value: function getInitializingStatement() { return t.expressionStatement(t.assignmentExpression("=", this.getScopeObject(), this.getInitializingObjectExpression())); } }, { key: "getInitializingObjectExpression", value: function getInitializingObjectExpression() { return isDebug ? new _template["default"]("\n ({\n identity: \"".concat(this.propertyName, "\"\n })\n ")).expression() : new _template["default"]("({})").expression(); } }, { key: "getMemberExpression", value: function getMemberExpression(name) { var object = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.getScopeObject(); var memberExpression = t.memberExpression(object, t.stringLiteral(name), true); return memberExpression; } }, { key: "parent", get: function get() { return scopeToScopeManager.get(this.scope.parent); } }, { key: "getObjectExpression", value: function getObjectExpression(refreshLabel) { var refreshScope = basicBlocks.get(refreshLabel).scopeManager; var propertyMap = {}; var cursor = this.scope; while (cursor) { var parentScopeManager = scopeToScopeManager.get(cursor); if (parentScopeManager) { propertyMap[parentScopeManager.propertyName] = t.memberExpression((0, _node.deepClone)(scopeVar), t.stringLiteral(parentScopeManager.propertyName), true); } cursor = cursor.parent; } propertyMap[refreshScope.propertyName] = refreshScope.getInitializingObjectExpression(); var properties = []; for (var key in propertyMap) { properties.push(t.objectProperty(t.stringLiteral(key), propertyMap[key], true)); } return t.objectExpression(properties); } }, { key: "hasOwnName", value: function hasOwnName(name) { return this.nameMap.has(name); } }]); }(); var getImpossibleBasicBlocks = function getImpossibleBasicBlocks() { return Array.from(basicBlocks.values()).filter(function (block) { return block.options.impossible; }); }; var scopeToScopeManager = new Map(); /** * A Basic Block is a sequence of instructions with no diversion except at the entry and exit points. */ var BasicBlock = /*#__PURE__*/function () { function BasicBlock(label, parentPath) { var _this = this; var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; _classCallCheck(this, BasicBlock); this.label = label; this.parentPath = parentPath; this.options = options; this.createPath(); if (isDebug) { // States in debug mode are just 1, 2, 3, ... this.totalState = basicBlocks.size + 1; } else { this.totalState = stateIntGen.generate(); } // Correct state values // Start with 90% sequence 10% random numbers this.stateValues = stateVars.map(function (_, i) { return dynamicIndexesSet.has(i) ? (0, _randomUtils.getRandomInteger)(stateValueRange[0], stateValueRange[1]) : sequence[i]; }); // Correct one of the values so that the accumulated sum is equal to the state var correctIndex = (0, _randomUtils.choice)(dynamicIndexes); var getCurrentState = function getCurrentState() { return _this.stateValues.reduce(function (a, b) { return a + b; }, 0); }; // Correct the value this.stateValues[correctIndex] = this.totalState - (getCurrentState() - this.stateValues[correctIndex]); (0, _assert.ok)(getCurrentState() === this.totalState); // Store basic block basicBlocks.set(label, this); // Create a new scope manager if it doesn't exist if (!scopeToScopeManager.has(this.scope)) { scopeToScopeManager.set(this.scope, new ScopeManager(this.scope, this)); } this.initializedScope = this.scopeManager; } return _createClass(BasicBlock, [{ key: "createPath", value: // User-provided variables of their own runtime values with guaranteed value function createPath() { var newPath = _traverse.NodePath.get({ hub: this.parentPath.hub, parentPath: this.parentPath, parent: this.parentPath.node, container: this.parentPath.node.body, listKey: "body", // Set the correct list key key: "virtual" // Set the index of the new node }); newPath.scope = this.parentPath.scope; newPath.parentPath = this.parentPath; newPath.node = t.blockStatement([]); this.thisPath = newPath; this.thisNode = newPath.node; } }, { key: "insertAfter", value: function insertAfter(newNode) { this.body.push(newNode); } }, { key: "scope", get: function get() { return this.parentPath.scope; } }, { key: "scopeManager", get: function get() { return scopeToScopeManager.get(this.scope); } }, { key: "body", get: function get() { return this.thisPath.node.body; } }, { key: "createFalsePredicate", value: function createFalsePredicate() { var predicate = this.createPredicate(); if (predicate.value) { // Make predicate false return t.unaryExpression("!", predicate.node); } return predicate.node; } }, { key: "createTruePredicate", value: function createTruePredicate() { var predicate = this.createPredicate(); if (!predicate.value) { // Make predicate true return t.unaryExpression("!", predicate.node); } return predicate.node; } }, { key: "createPredicate", value: function createPredicate() { var stateVarIndex = (0, _randomUtils.getRandomInteger)(0, stateVars.length); var stateValue = this.stateValues[stateVarIndex]; var compareValue = (0, _randomUtils.choice)([stateValue, (0, _randomUtils.getRandomInteger)(stateValueRange[0], stateValueRange[1])]); var operator = (0, _randomUtils.choice)(["==", "!=", "<", ">"]); var compareResult; switch (operator) { case "==": compareResult = stateValue === compareValue; break; case "!=": compareResult = stateValue !== compareValue; break; case "<": compareResult = stateValue < compareValue; break; case ">": compareResult = stateValue > compareValue; break; } return { node: t.binaryExpression(operator, (0, _node.deepClone)(stateVars[stateVarIndex]), (0, _node.numericLiteral)(compareValue)), value: compareResult }; } }, { key: "identifier", value: function identifier(identifierName, scopeManager) { return scopeManager.getMemberExpression(identifierName); } }]); }(); /** * Stage 1: Flatten the code into Basic Blocks * * This involves transforming the Control Flow / Scopes into blocks with 'goto' statements * * - A block is simply a sequence of statements * - A block can have a 'goto' statement to another block * - A block original scope is preserved * * 'goto' & Scopes are transformed in Stage 2 */ var switchLabel = me.getPlaceholder(); var breakStatement = function breakStatement() { return t.breakStatement(t.identifier(switchLabel)); }; var startLabel = me.getPlaceholder(); var endLabel = me.getPlaceholder(); var currentBasicBlock = new BasicBlock(startLabel, blockPath); var gotoFunctionName = "GOTO__" + me.getPlaceholder() + "__IF_YOU_CAN_READ_THIS_THERE_IS_A_BUG"; function GotoControlStatement(label) { return new _template["default"]("\n ".concat(gotoFunctionName, "(\"").concat(label, "\");\n ")).single(); } // Ends the current block and starts a new one function endCurrentBasicBlock() { var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, _ref2$jumpToNext = _ref2.jumpToNext, jumpToNext = _ref2$jumpToNext === void 0 ? true : _ref2$jumpToNext, _ref2$nextLabel = _ref2.nextLabel, nextLabel = _ref2$nextLabel === void 0 ? me.getPlaceholder() : _ref2$nextLabel, _ref2$prevJumpTo = _ref2.prevJumpTo, prevJumpTo = _ref2$prevJumpTo === void 0 ? null : _ref2$prevJumpTo, _ref2$nextBlockPath = _ref2.nextBlockPath, nextBlockPath = _ref2$nextBlockPath === void 0 ? null : _ref2$nextBlockPath; (0, _assert.ok)(nextBlockPath); if (prevJumpTo) { currentBasicBlock.insertAfter(GotoControlStatement(prevJumpTo)); } else if (jumpToNext) { currentBasicBlock.insertAfter(GotoControlStatement(nextLabel)); } currentBasicBlock = new BasicBlock(nextLabel, nextBlockPath); } var prependNodes = []; var functionExpressions = []; function flattenIntoBasicBlocks(bodyIn) { // if (!Array.isArray(bodyIn) && bodyIn.isBlock()) { // currentBasicBlock.parentPath = bodyIn; // } var body = Array.isArray(bodyIn) ? bodyIn : bodyIn.get("body"); var nextBlockPath = Array.isArray(bodyIn) ? currentBasicBlock.parentPath : bodyIn; var _loop = function _loop() { var _statement$node$leadi; var statement = body[index]; // Keep Imports before everything else if (statement.isImportDeclaration()) { prependNodes.push(statement.node); return 0; // continue } if (statement.isFunctionDeclaration()) { var fnName = statement.node.id.name; var isIllegal = false; if (!flattenFunctionDeclarations || statement.node.async || statement.node.generator || statement.node[_constants.UNSAFE] || statement.node[CFF_UNSAFE] || (0, _astUtils.isStrictMode)(statement)) { isIllegal = true; } var oldBasicBlock = currentBasicBlock; var _fnLabel = me.getPlaceholder(); var sm = currentBasicBlock.scopeManager; var rename = sm.getNewName(fnName); var hoistedBasicBlock = Array.from(basicBlocks.values()).find(function (block) { return block.parentPath === currentBasicBlock.parentPath; }); // Added into scope object due to function flattening causes redeclaration (Test #42 and #44) hoistedBasicBlock.scope.bindings[fnName].kind = "var"; if (isIllegal) { // Function must be converted to function expression as hoisted function declaration // gets redeclared (and losing prototype methods) each iteration (Test #43) var asFunctionExpression = statement.node; asFunctionExpression.type = "FunctionExpression"; hoistedBasicBlock.body.unshift(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(fnName), asFunctionExpression)])); return 0; // continue } me.changeData.functions++; var functionExpression = t.functionExpression(null, [], t.blockStatement([])); functionExpressions.push([fnName, _fnLabel, currentBasicBlock, functionExpression]); // Change the function declaration to a variable declaration hoistedBasicBlock.body.unshift(t.variableDeclaration("var", [t.variableDeclarator(t.identifier(fnName), functionExpression)])); var blockStatement = statement.get("body"); endCurrentBasicBlock({ nextLabel: _fnLabel, nextBlockPath: blockStatement, jumpToNext: false }); var fnTopBlock = currentBasicBlock; // Implicit return blockStatement.node.body.push(t.returnStatement(t.identifier("undefined"))); flattenIntoBasicBlocks(blockStatement); scopeToScopeManager.get(statement.scope).requiresInitializing = false; // Debug label if (isDebug) { fnTopBlock.body.unshift(t.expressionStatement(t.stringLiteral("Function " + statement.node.id.name + " -> Renamed to " + rename))); } // Unpack parameters from the parameter 'argVar' if (statement.node.params.length > 0) { usedArgVar = true; fnTopBlock.body.unshift(t.variableDeclaration("var", [t.variableDeclarator(t.arrayPattern(statement.node.params), (0, _node.deepClone)(argVar))])); // Change bindings from 'param' to 'var' statement.get("params").forEach(function (param) { var ids = param.getBindingIdentifierPaths(); // Loop over the record of binding identifiers for (var identifierName in ids) { var identifierPath = ids[identifierName]; if (identifierPath.getFunctionParent() === statement) { var binding = statement.scope.getBinding(identifierName); if (binding) { // console.log(`Remapped param binding ${identifierName} ${binding.kind} ${binding.identifier?.loc?.start} to var`); binding.kind = "var"; } } } }); } currentBasicBlock = oldBasicBlock; return 0; // continue } // Convert IF statements into Basic Blocks if (statement.isIfStatement() && flattenIfStatements) { var test = statement.get("test"); var consequent = statement.get("consequent"); var alternate = statement.get("alternate"); // Both consequent and alternate are blocks if (consequent.isBlockStatement() && (!alternate.node || alternate.isBlockStatement())) { me.changeData.ifStatements++; var consequentLabel = me.getPlaceholder(); var alternateLabel = alternate.node ? me.getPlaceholder() : null; var afterPath = me.getPlaceholder(); currentBasicBlock.insertAfter(t.ifStatement(test.node, GotoControlStatement(consequentLabel), alternateLabel ? GotoControlStatement(alternateLabel) : GotoControlStatement(afterPath))); var _oldBasicBlock = currentBasicBlock; endCurrentBasicBlock({ jumpToNext: false, nextLabel: consequentLabel, nextBlockPath: consequent }); flattenIntoBasicBlocks(consequent); currentBasicBlock.initializedScope = _oldBasicBlock.scopeManager; if (alternate.isBlockStatement()) { endCurrentBasicBlock({ prevJumpTo: afterPath, nextLabel: alternateLabel, nextBlockPath: alternate }); flattenIntoBasicBlocks(alternate); } endCurrentBasicBlock({ prevJumpTo: afterPath, nextLabel: afterPath, nextBlockPath: _oldBasicBlock.parentPath }); return 0; // continue } } if (Number(index) === body.length - 1 && statement.isExpressionStatement() && statement.findParent(function (p) { return p.isBlock(); }) === blockPath && programPath // <- ONLY APPLY TO TOP-LEVEL NEVER FUNCTIONS! ) { // Return the result of the last expression for eval() purposes currentBasicBlock.insertAfter(t.returnStatement(statement.get("expression").node)); return 0; // continue } // 3 or more statements should be split more if (currentBasicBlock.body.length > 1 && (0, _randomUtils.chance)(50 + currentBasicBlock.body.length)) { endCurrentBasicBlock({ nextBlockPath: nextBlockPath }); } if (addAssertions && statement.node.type === "ExpressionStatement" && (_statement$node$leadi = statement.node.leadingComments) !== null && _statement$node$leadi !== void 0 && _statement$node$leadi.some(function (comment) { return comment.value.includes("@js-confuser-assert"); })) { var expr = statement.node.expression; (0, _assert.ok)(expr.type === "BinaryExpression" && ["==", "==="].includes(expr.operator), "Must be == or === for js-confuser-assert"); var rightValue = parseInt(me.obfuscator.generateCode(expr.right).code); (0, _assert.ok)(typeof rightValue === "number" && !Number.isNaN(rightValue), "right value must be number for js-confuser-assert"); var userVar = me.getPlaceholder(); var userExpr = expr.left; var salt = (0, _randomUtils.getRandomInteger)(1000000, 1000000000); var hashedValue = HashFunction(salt + rightValue); currentBasicBlock.body.push(new _template["default"]("var {userVar} = {hashFnName}({salt} + {userExpr})").single({ userVar: userVar, userExpr: userExpr, hashFnName: hashFnName, salt: salt })); currentBasicBlock.scope.push({ id: t.identifier(userVar), init: userExpr, kind: "var" }); endCurrentBasicBlock({ nextBlockPath: nextBlockPath }); currentBasicBlock.assertions = _defineProperty({}, userVar, hashedValue); return 0; // continue } // console.log(currentBasicBlock.thisPath.type); // console.log(currentBasicBlock.body); currentBasicBlock.body.push(statement.node); }, _ret; for (var index in body) { _ret = _loop(); if (_ret === 0) continue; } } // Convert our code into Basic Blocks flattenIntoBasicBlocks(blockPath.get("body")); // Ensure always jumped to the Program end endCurrentBasicBlock({ jumpToNext: true, nextLabel: endLabel, nextBlockPath: defaultBlockPath }); if (!isDebug && addDeadCode) { // DEAD CODE 1/3: Add fake chunks that are never reached var fakeChunkCount = (0, _randomUtils.getRandomInteger)(2, 6); for (var _i = 0; _i < fakeChunkCount; _i++) { // These chunks just jump somewhere random, they are never executed // so it could contain any code var fakeBlock = new BasicBlock(me.getPlaceholder(), blockPath, { impossible: true }); var fakeJump = void 0; do { fakeJump = (0, _randomUtils.choice)(Array.from(basicBlocks.keys())); } while (fakeJump === fakeBlock.label); fakeBlock.insertAfter(GotoControlStatement(fakeJump)); me.changeData.deadCode++; } // DEAD CODE 2/3: Add fake jumps to really mess with deobfuscators // "irreducible control flow" var addFakeJumpsCount = (0, _randomUtils.getRandomInteger)(3, 6); for (var _i2 = 0; _i2 < addFakeJumpsCount; _i2++) { var _basicBlock = (0, _randomUtils.choice)(Array.from(basicBlocks.values())); if (!_basicBlock) continue; var randomLabel = (0, _randomUtils.choice)(Array.from(basicBlocks.keys())); // Smartly find index BEFORE current jumps so that Minify doesn't remove them var insertIndex = _basicBlock.body.findIndex(function (p) { return p.type === "ExpressionStatement" && p.expression.type === "CallExpression" && p.expression.callee.type === "Identifier" && p.expression.callee.name === gotoFunctionName; }); if (insertIndex === -1) { insertIndex = _basicBlock.body.length; } _basicBlock.body.splice(insertIndex, 0, new _template["default"]("\n if({predicate}){\n {goto}\n }\n ").single({ "goto": GotoControlStatement(randomLabel), predicate: _basicBlock.createFalsePredicate() })); me.changeData.deadCode++; } // DEAD CODE 3/3: Clone chunks but these chunks are never ran var cloneChunkCount = (0, _randomUtils.getRandomInteger)(1, 4); var _loop2 = function _loop2() { var randomChunk = (0, _randomUtils.choice)(Array.from(basicBlocks.values())); // Don't double define functions var hasDeclaration = randomChunk.body.find(function (stmt) { return t.isDeclaration(stmt); }); if (!hasDeclaration) { var clonedChunk = new BasicBlock(me.getPlaceholder(), randomChunk.parentPath, { impossible: true }); randomChunk.thisNode.body.map(function (x) { return (0, _node.deepClone)(x); }).forEach(function (node) { if (node.type === "EmptyStatement") return; clonedChunk.insertAfter(node); }); me.changeData.deadCode++; } }; for (var _i3 = 0; _i3 < cloneChunkCount; _i3++) { _loop2(); } } /** * Stage 2: Transform 'goto' statements into valid JavaScript * * - 'goto' is replaced with equivalent state updates and break statements * - Original identifiers are converted into member expressions */ // Remap 'GotoStatement' to actual state assignments and Break statements var _iterator = _createForOfIteratorHelper(basicBlocks.values()), _step; try { var _loop4 = function _loop4() { var basicBlock = _step.value; var currentStateValues = basicBlock.stateValues; // Wrap the statement in a Babel path to allow traversal var outerFn = (0, _astUtils.getParentFunctionOrProgram)(basicBlock.parentPath); function isWithinSameFunction(path) { var fn = (0, _astUtils.getParentFunctionOrProgram)(path); return fn.node === outerFn.node; } var visitor = { SequenceExpression: { enter: function enter(path) { var _path$parentPath; var hasComment = function hasComment(node) { var _node$leadingComments; return node === null || node === void 0 || (_node$leadingComments = node.leadingComments) === null || _node$leadingComments === void 0 ? void 0 : _node$leadingComments.some(function (p) { return p.value.includes("@js-confuser-assert"); }); }; var asserted = hasComment(path.node) || hasComment((_path$parentPath = path.parentPath) === null || _path$parentPath === void 0 ? void 0 : _path$parentPath.node); if (!asserted) { return; } var exprs = path.get("expressions"); if (exprs.length !== 2) { throw new Error("Asserted sequence expressions must have exactly 2 expressions"); } var assertedValue = exprs[1].evaluate(); if (!assertedValue.confident) { throw new Error("Asserted sequence expression must be statically evaluable"); } (0, _assert.ok)(typeof assertedValue.value === "number", "Asserted sequence expression must evaluate to a number"); path.replaceWith(t.assignmentExpression("=", (0, _node.deepClone)(runtimeVar), exprs[0].node)); } }, BooleanLiteral: { exit: function exit(boolPath) { // Don't mangle booleans in debug mode if (isDebug || !mangleBooleanLiterals || boolPath.find(function (p) { return p.node[CFF_SKIP]; })) return; if (!isWithinSameFunction(boolPath)) return; if (!(0, _randomUtils.chance)(booleanLiteralChance)) return; mangledLiteralsCreated++; var index = (0, _randomUtils.getRandomInteger)(0, stateVars.length - 1); var stateVar = stateVars[index]; var stateVarValue = currentStateValues[index]; var compareValue = (0, _randomUtils.choice)([(0, _randomUtils.getRandomInteger)(stateValueRange[0], stateValueRange[1]), stateVarValue]); var compareResult = stateVarValue === compareValue; var newExpression = t.binaryExpression(boolPath.node.value === compareResult ? "==" : "!=", (0, _node.deepClone)(stateVar), (0, _node.numericLiteral)(compareValue)); (0, _astUtils.ensureComputedExpression)(boolPath); boolPath.replaceWith(newExpression); newExpression[CFF_SKIP] = cffIndex; } }, // Mangle numbers with the state values NumericLiteral: { exit: function exit(numPath) { // Don't mangle numbers in debug mode if (isDebug || !mangleNumericalLiterals || numPath.find(function (p) { return p.node[CFF_SKIP]; })) return; var num = numPath.node.value; if (Math.floor(num) !== num || Math.abs(num) > 100000 || !Number.isFinite(num) || Number.isNaN(num)) return; if (!isWithinSameFunction(numPath)) return; if (!(0, _randomUtils.chance)(numericLiteralChance)) return; mangledLiteralsCreated++; var index = (0, _randomUtils.getRandomInteger)(0, stateVars.length - 1); var stateVar = stateVars[index]; // num = 50 // stateVar = 30 // stateVar + 30 var diff = t.binaryExpression("+", (0, _node.deepClone)(stateVar), me.skip((0, _node.numericLiteral)(num - currentStateValues[index]))); (0, _astUtils.ensureComputedExpression)(numPath); numPath.replaceWith(diff); numPath.skip(); diff[CFF_SKIP] = cffIndex; } }, // Xor encrypt string literals StringLiteral: { exit: function exit(strPath) { // Don't mangle strings in debug mode if (isDebug || !mangleStringLiterals || strPath.find(function (p) { return p.node[CFF_SKIP]; })) return; // Don't accidentally break goto call expressions var p = strPath.parentPath; if (p !== null && p !== void 0 && p.isCallExpression() && t.isIdentifier(p.node.callee) && p.node.callee.name === gotoFunctionName) { return; } var str = strPath.node.value; if (str.length > 5000 || str.length < 3) return; if (!isWithinSameFunction(strPath)) return; if (!(0, _randomUtils.chance)(stringLiteralChance)) return; var num = (0, _randomUtils.getRandomInteger)(0, 1000000); // XOR string encrypt key var encryptedStr = (0, _xorStringTemplate.xorEncodeString)(str, num); var decryptedStr = (0, _xorStringTemplate.xorDecodeString)(encryptedStr, num); // If decryption fails for some reason, skip if (decryptedStr !== str) { return; } mangledLiteralsCreated++; var index = (0, _randomUtils.getRandomInteger)(0, stateVars.length - 1); var stateVar = stateVars[index]; // Equals 'num' but entangled with current state var var diff = t.binaryExpression("+", (0, _node.deepClone)(stateVar), me.skip((0, _node.numericLiteral)(num - currentStateValues[index]))); // Random printable-ASCII char used as decoy padding var decoyChar = function decoyChar() { return String.fromCharCode((0, _randomUtils.getRandomInteger)(32, 127)); }; // Leading decoy for (var _p = (0, _randomUtils.getRandomInteger)(0, 5); _p > 0; _p--) { stringsValue += decoyChar(); } var start = stringsValue.length; stringsValue += encryptedStr; // Trailing decoy for (var _p2 = (0, _randomUtils.getRandomInteger)(0, 5); _p2 > 0; _p2--) { stringsValue += decoyChar(); } var newExpr = t.callExpression(t.identifier(xorFnName), [diff, t.numericLiteral(start), t.numericLiteral(encryptedStr.length)]); (0, _astUtils.ensureComputedExpression)(strPath); strPath.replaceWith(newExpr); strPath.skip(); } }, Identifier: { exit: function exit(path) { if (!(0, _astUtils.isVariableIdentifier)(path)) return; if (me.isSkipped(path)) return; if (path.node[_constants.NO_RENAME] === cffIndex) return; // For identifiers using implicit with discriminant, skip if (path.node[_constants.WITH_STATEMENT]) return; var identifierName = path.node.name; if (identifierName === gotoFunctionName) return; var binding = path.scope.getBinding(identifierName); if (!binding) { return; } if (binding.kind === "var" || binding.kind === "let" || binding.kind === "const") {} else { return; } var scopeManager = scopeToScopeManager.get(binding.scope); if (!scopeManager) return; var newName = scopeManager.getNewName(identifierName, path.node); var memberExpression = scopeManager.getMemberExpression(newName); scopeManager.isNotUsed = false; if ((0, _astUtils.isDefiningIdentifier)(path)) { (0, _astUtils.replaceDefiningIdentifierToMemberExpression)(path, memberExpression); return; } if (!path.container) return; me.skip(memberExpression); path.replaceWith(memberExpression); path.skip(); // Preserve proper 'this' context when directly calling functions // X.Y.Z() -> (1, X.Y.Z)() if (path.parentPath.isCallExpression() && path.key === "callee") { path.replaceWith(t.sequenceExpression([t.numericLiteral(1), path.node])); } } }, // Top-level returns set additional flag to indicate that the function has returned ReturnStatement: { exit: function exit(path) { var functionParent = path.getFunctionParent(); if (!functionParent || functionParent.get("body") !== blockPath) return; var returnArgument = path.node.argument || t.identifier("undefined"); path.node.argument = new _template["default"]("\n ({didReturnVar} = true, {returnArgument})\n ").expression({ returnArgument: returnArgument, didReturnVar: (0, _node.deepClone)(_didReturnVar) }); } }, // goto() calls are replaced with state updates and break statements CallExpression: { exit: function exit(path) { if (t.isIdentifier(path.node.callee) && path.node.callee.name === gotoFunctionName) { var _path$node$arguments = _slicedToArray(path.node.arguments, 1), labelNode = _path$node$arguments[0]; (0, _assert.ok)(t.isStringLiteral(labelNode)); var _label = labelNode.value; var jumpBlock = basicBlocks.get(_label); (0, _assert.ok)(jumpBlock, "Label not found: " + _label); var newStateValues = jumpBlock.stateValues, newTotalState = jumpBlock.totalState; var assignments = []; var mutatingStateValues = _toConsumableArray(currentStateValues); var hasAssertions = Object.keys(jumpBlock.assertions || {}).length > 0; var _loop5 = function _loop5(_i8) { var oldValue = currentStateValues[_i8]; var newValue = newStateValues[_i8]; // console.log(oldValue, newValue); if (oldValue === newValue) return 1; // continue // No diff needed if the value doesn't change var assignment = t.assignmentExpression("=", (0, _node.deepClone)(stateVars[_i8]), (0, _node.numericLiteral)(newValue)); if (!isDebug && addRelativeAssignments) { // Use two level of diffs to create confusing code: // stateVar += anotherStateVar - diff // has the effect of: // stateVar = newState var mangledNumericLiteral = function mangledNumericLiteral(value) { var stateVarIndex; do { stateVarIndex = (0, _randomUtils.getRandomInteger)(0, stateVars.length); } while (stateVarIndex === _i8 && stateVars.length > 1); var stateVarId = stateVars[stateVarIndex]; var stateVarValue = mutatingStateValues[stateVarIndex]; var diff = stateVarValue - value; return t.binaryExpression("-", (0, _node.deepClone)(stateVarId), hasAssertions ? mangledAssertion(diff) : (0, _node.numericLiteral)(diff)); }; var mangledAssertion = function mangledAssertion(value) { var userVar = (0, _randomUtils.choice)(Object.keys(jumpBlock.assertions)); if (!hasAssertions) return (0, _node.numericLiteral)(value); var assertedValue = jumpBlock.assertions[userVar];