const fs = require('fs'); const t = require('@babel/types'); const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generate = require('@babel/generator').default; const vm = require('vm') const { readFileSync, writeFile, writeFileSync, } = require("fs"); const { exit } = require('process'); var output = "" let beautify_opts = { comments: true, minified: false, concise: false, } const [_,__,inputPath,outputPath] = process.argv; const script = readFileSync(inputPath, "utf-8"); const AST = parser.parse(script, {}) var decryptFuncCtx = vm.createContext(); var decryptCode = "" var decryptFuncName = "" const constantReplacer = { VariableDeclarator(path) { const {node} = path; if(!node.id || !node.init || (node.init.type != "StringLiteral" && node.init.type != "NumericLiteral") || node.id.type != "Identifier") { return } if((node.init.type == "NumericLiteral" && node.init.value == 0) || (node.init.type == "StringLiteral" && node.init.value == "")) { return } let binding = path.scope.getBinding(node.id.name) if(!binding) { return } for(var i = 0; i < binding.referencePaths.length; i++) { binding.referencePaths[i].replaceWith(node.init) } path.remove() } } const replaceObjSimple = { VariableDeclarator(path) { const {node} = path; if(!node.id || !node.init || node.init.type != "ObjectExpression" || node.id.type != "Identifier" || node.init.properties.length < 1) { return } var valid = true var map = {} for(var i = 0; i < node.init.properties.length; i++) { var prop = node.init.properties[i] if(!prop.key || !prop.value || prop.key.type != "Identifier" || (prop.value.type != "NumericLiteral" && prop.value.type != "StringLiteral")) { valid = false break } map[prop.key.name] = prop.value } if(!valid) { return } path.scope.crawl() let binding = path.scope.getBinding(node.id.name) if(!binding) { return } for(var i = 0; i < binding.referencePaths.length; i++) { let refPath = binding.referencePaths[i].parentPath if(refPath.node.type != "MemberExpression" || !refPath.node.property) { continue } let key; if(refPath.node.property.type == "Identifier") { key = refPath.node.property.name } else{ key = refPath.node.property.value } refPath.replaceWith(map[key]) } path.remove() } } const replaceExprStmts = { MemberExpression(path) { const {node} = path; if(!node.property || node.property.type != "SequenceExpression" || !node.property.expressions || node.property.expressions.length < 3) { return } var callExprIndex = node.property.expressions.length-1 if(node.property.expressions[callExprIndex].type != "CallExpression") { return } var values = [] var order = [] for(var i = 0; i < node.property.expressions.length; i++) { var expr = node.property.expressions[i] if(expr.type != "AssignmentExpression" || !expr.right || !expr.left) { continue } values.push(generate(expr.right).code) order.push(expr.left.name) } let newArgs = [] for(var i = 0; i < node.property.expressions[callExprIndex].arguments.length; i++) { let arg = node.property.expressions[callExprIndex].arguments[i] let str = generate(arg).code if(str.match(/[A-z]/g) == null) { newArgs.push(arg) continue } let key = str.match(/[A-z]/g)[0] let index = order.indexOf(key) str = str.replace(key, values[index]) if(str.match(/[0-9]/g) != null && str.match(/[0-9]/g).length > 1 && !str.match(/[A-z"]/g)) { newArgs.push(t.numericLiteral(eval(str))) continue } str = str.slice(1) str = str.slice(0, -1) newArgs.push(t.stringLiteral(str)) } path.replaceWith(t.memberExpression(node.object, t.callExpression(node.property.expressions[callExprIndex].callee, newArgs), true)) }, // ! same thing except ExpressionStatement, SequenceExpression // ! example: (a = "7d]D", k = -497, m = -404, C = -368, uo(k - -1644, a - 298, a, m - 199, C - 208)) SequenceExpression(path) { const {node} = path; if(!node.expressions || node.expressions.length < 3) { return } var callExprIndex = node.expressions.length-1 if(node.expressions[callExprIndex].type != "CallExpression") { return } var values = [] var order = [] for(var i = 0; i < node.expressions.length; i++) { var expr = node.expressions[i] if(expr.type != "AssignmentExpression" || !expr.right || !expr.left) { continue } values.push(generate(expr.right).code) order.push(expr.left.name) } let newArgs = [] for(var i = 0; i < node.expressions[callExprIndex].arguments.length; i++) { let arg = node.expressions[callExprIndex].arguments[i] let str = generate(arg).code if(str.match(/[A-z]/g) == null) { newArgs.push(arg) continue } let key = str.match(/[A-z]/g)[0] let index = order.indexOf(key) str = str.replace(key, values[index]) if(str.match(/[0-9]/g) != null && str.match(/[0-9]/g).length > 1 && !str.match(/[A-z"]/g)) { newArgs.push(t.numericLiteral(eval(str))) continue } str = str.slice(1) str = str.slice(0, -1) newArgs.push(t.stringLiteral(str)) } path.replaceWith(t.callExpression(node.expressions[callExprIndex].callee, newArgs)) } } const replaceWeirdProxyCall = { MemberExpression(path) { const {node} = path; if(!node.object || node.object.type != "Identifier" || !node.property || node.property.type != "CallExpression") { return } if(!node.property.callee || node.property.callee.type != "FunctionExpression") { return } let values = [generate(node.property.arguments[0]).code, generate(node.property.arguments[1]).code, generate(node.property.arguments[2]).code, generate(node.property.arguments[3]).code, generate(node.property.arguments[4]).code] let order = [node.property.callee.params[0].name, node.property.callee.params[1].name, node.property.callee.params[2].name, node.property.callee.params[3].name, node.property.callee.params[4].name] let newArgs = [] for(var i = 0; i < node.property.callee.body.body[0].argument.arguments.length; i++) { let arg = node.property.callee.body.body[0].argument.arguments[i] let str = generate(arg).code if(str.match(/[A-z]/g) == null) { newArgs.push(arg) continue } let key = str.match(/[A-z]/g)[0] let index = order.indexOf(key) str = str.replace(key, values[index]) if(str.match(/[0-9]/g) != null && str.match(/[0-9]/g).length > 1&& !str.match(/[A-z"]/g)) { newArgs.push(t.numericLiteral(eval(str))) continue } str = str.slice(1) str = str.slice(0, -1) newArgs.push(t.stringLiteral(str)) } path.replaceWith(t.memberExpression(node.object, t.callExpression(node.property.callee.body.body[0].argument.callee, newArgs), true)) } } const getStringDeobfFuncs = { ExpressionStatement(path) { const {node} = path; if(!node.expression || node.expression.operator != "!" || !node.expression.prefix || !node.expression.argument || node.expression.argument.type != "CallExpression") { return } // ! get array func let binding = path.scope.getBinding(node.expression.argument.arguments[0].name) if(!binding) { return } decryptCode += generate(binding.path.node).code + "\n" // ! get decrypt func var bodyIndex = 0 for(var i = 0; i < node.expression.argument.callee.body.body.length; i++) { if(node.expression.argument.callee.body.body[i].type == "FunctionDeclaration") { bodyIndex = i break } } decryptFuncName = node.expression.argument.callee.body.body[bodyIndex].body.body[0].argument.callee.name path.scope.crawl() let binding1 = path.scope.getBinding(decryptFuncName) if(!binding1){ return } decryptCode += generate(binding1.path.node).code + "\n" decryptCode += generate(node).code + "\n" binding1.path.remove() binding.path.remove() path.remove() path.stop() } } function makeid(length) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const charactersLength = characters.length; let counter = 0; while (counter < length) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); counter += 1; } return result; } const replaceInterceptingFuncNames = { FunctionDeclaration(path) { const {node} = path; if(!node.id || node.id.type != "Identifier" || node.id.name != decryptFuncName || !node.body || !node.body.body || node.body.body.length != 1) { return } path.scope.crawl() let binding = path.parentPath.scope.getBinding(node.id.name) if(!binding) { return } var ID = t.identifier(makeid(10)) for(var i = 0; i < binding.referencePaths.length; i++) { binding.referencePaths[i].replaceWith(ID) } node.id = ID } } const deobfStrings = { CallExpression(path) { const {node} = path; if(!node.callee || node.callee.type != "Identifier" || !node.arguments || node.arguments.length < 2) { return } var valid = true for(var i = 0; i < node.arguments.length; i++) { var arg = node.arguments[i] let str = generate(arg).code if(arg.type == "StringLiteral" || str == "NaN") { continue } if(arg.type != "UnaryExpression" && arg.type != "BinaryExpression" && arg.type != "NumericLiteral") { valid = false break } if(str.match(/[A-z]/g) != null) { valid = false break } } if(!valid) { return } // ! the logic here is we want to get the function this is calling // ! then we want to keep getting the nested function calls until we get to the final function, aka the decryptFuncName let code = "" path.scope.crawl() let binding = path.scope.getBinding(node.callee.name) if(!binding) { // ! hopefully no binding will always mean that the function in question is `r`??? path.replaceWith(t.valueToNode(vm.runInContext(generate(node).code, decryptFuncCtx))) return } // ! loop until we get to a place where we can't get a binding (aka hopefully the root function) while(true){ if(!binding){ let a = generate(node).code if(a[0] == decryptFuncName) { a[0] = "asd" } code += a break } code += generate(binding.path.node).code + "\n" path.scope.crawl() binding = binding.path.scope.getBinding(binding.path.node.body.body[0].argument.callee.name) } // ! now we should have all the code we need path.replaceWith(t.valueToNode(vm.runInContext(code, decryptFuncCtx))) } } const deobfuscateStringConcatVisitor = { BinaryExpression(path) { let { confident, value } = path.evaluate(); if (!confident) return; if (typeof value == "string") { path.replaceWith(t.stringLiteral(value)); } }, } const getObfioObjs = { VariableDeclarator(path) { const {node} = path; if(!node.id || node.id.type != "Identifier" || !node.init || node.init.type != "ObjectExpression" || !node.init.properties || node.init.properties.length < 1) { return } // ! further validation, just incase let map = {} let valid = true for (var i = 0; i < node.init.properties.length; i++) { var prop = node.init.properties[i] if (!prop.key || !prop.value || prop.key.type != "Identifier") { valid = false break; } if (prop.value.type != "FunctionExpression" && prop.value.type != "StringLiteral" && prop.value.type != "MemberExpression") { valid = false break; } if (prop.key.name.length != 5) { valid = false break; } if (prop.value.type == "FunctionExpression" && prop.value.body.body[0].type != "ReturnStatement") { valid = false break; } map[prop.key.name] = prop.value } if (!valid) { return } path.scope.crawl() let binding = path.scope.getBinding(node.id.name) if(!binding) { return } var ID = t.identifier(makeid(20)) for(var i = 0; i < binding.referencePaths.length; i++) { binding.referencePaths[i].replaceWith(ID) } obfioObjMap[ID.name] = map path.remove() } } function getArgs(arguments, cutFirst) { var out = [] for (var i = cutFirst ? 1 : 0; i < arguments.length; i++) { out.push(arguments[i]) } return out } const objDeobfMemberExpr = { MemberExpression(path) { const { node } = path; if (!node.object || !node.property || node.object.type != "Identifier" || !obfioObjMap[node.object.name]) { return } let map = obfioObjMap[node.object.name] let key; if (node.property.type == "Identifier") { key = node.property.name } else { key = node.property.value } let value = map[key] if (value.type == "StringLiteral") { path.replaceWith(value) return } if (value.type == "MemberExpression") { map = obfioObjMap[value.object.name] if (value.property.type == "Identifier") { key = value.property.name } else { key = value.property.value } value = map[key] path.replaceWith(value) return } output += `FAILED (1): ${generate(node).code}\n\n` }, CallExpression(path) { const { node } = path; if (!node.callee || node.callee.type != "MemberExpression" || !node.callee.object || !node.callee.property || node.callee.object.type != "Identifier" || !obfioObjMap[node.callee.object.name]) { return } let map = obfioObjMap[node.callee.object.name] let key; if (node.callee.property.type == "Identifier") { key = node.callee.property.name } else { key = node.callee.property.value } let value = map[key] // ! replace functions let retNode = value.body.body[0].argument // ! call expression if (retNode.type == "CallExpression") { var callExprID; // ! check if it's a reference to another object if (retNode.callee.type == "MemberExpression") { callExprID = retNode.callee } else { callExprID = node.arguments[0] } var args = [] if (node.arguments.length > 1 || retNode.callee.type == "MemberExpression") { args = getArgs(node.arguments, retNode.callee.type != "MemberExpression") } path.replaceWith(t.callExpression(callExprID, args)) return } // ! BinaryExpression if (retNode.type == "BinaryExpression") { path.replaceWith(t.binaryExpression(retNode.operator, node.arguments[0], node.arguments[1])) return } output += `FAILED (2): ${generate(node).code}\n\n` } } function evalValue(left, right, op) { switch (op) { case "===": return left == right case "!==": return left != right } } const cleanupDeadCode = { FunctionDeclaration(path) { const {node} = path; if(!node.id || node.id.type != "Identifier" || !node.body || !node.body.body || !node.params || node.params.length < 2 || node.body.body.length != 1 || node.body.body[0].type != "ReturnStatement") { return } path.remove() }, "IfStatement|ConditionalExpression"(path) { const { node } = path; if (!node.test || !node.consequent || node.test.type != "BinaryExpression" || !node.test.left || !node.test.right || node.test.left.type != "StringLiteral" || node.test.right.type != "StringLiteral") { // ! handle if(!("x" !== "x")) { } else { } here if (!node.test || !node.consequent || node.test.type != "UnaryExpression" || !node.test.argument || node.test.argument.type != "BinaryExpression" || !node.test.argument.left || !node.test.argument.right || node.test.argument.left.type != "StringLiteral" || node.test.argument.right.type != "StringLiteral") { return } if (!evalValue(node.test.argument.left.value, node.test.argument.right.value, node.test.argument.operator)) { path.replaceWithMultiple(node.consequent) return } if(!node.alternate){ path.remove() return } path.replaceWithMultiple(node.alternate) return } if (evalValue(node.test.left.value, node.test.right.value, node.test.operator)) { path.replaceWithMultiple(node.consequent) return } path.replaceWithMultiple(node.alternate) } } // ! replace the `x = 123` and `y = "asd"` things traverse(AST, constantReplacer) // ! replace `const n = {T: 702}` traverse(AST, replaceObjSimple) // ! replace mr[(t = "7d]D", o = 896, r(o - 493, t)), mr[(t = "hyP7", r = 661, o = 735, $(t - 252, o - 1061, t, r - 204, o - 6))] traverse(AST, replaceExprStmts) /* replace the stuff that looks like this: iu[function (n, t, W, r, u) { return On(n - 247, W, r - 606, r - 38, u - 235); }(1095, 0, "e9so", 1006, 1053)] */ // ! if this code breaks, I'd assume it's likely because of the static way I'm doing the variable replacement traverse(AST, replaceWeirdProxyCall) // ! get the string deobf code and func name, the entry point is the array shifter exprstmt traverse(AST, getStringDeobfFuncs) // ! this is a really hacky way to fix this "problem" since I'm doing the string deobf in a bad way // ! some func names are the same as the main decrypt func name so it'll error when you try to deobf the strings traverse(AST, replaceInterceptingFuncNames) writeFileSync("output.js", decryptCode, "utf-8") vm.runInContext(decryptCode, decryptFuncCtx); // ! finally we can decrypt/deobf our strings traverse(AST, deobfStrings) // ! now we need to concat strings so we can properly deobf the object obfuscation // (stolen from pianoman) traverse(AST, deobfuscateStringConcatVisitor) let obfioObjMap = {} // ! first we need to rename all objects and all uses of those objects // ! some objects have conflicting names which can mess up this solution, easiest fix is just renaming them // ! then we will populate the obfioObjMap traverse(AST, getObfioObjs) // ! now we can deobf the object obfuscation traverse(AST, objDeobfMemberExpr) // ! clean dead code, like the proxy functions we never removed traverse(AST, cleanupDeadCode) writeFileSync("output.log", output, 'utf-8') const final_code = generate(AST, beautify_opts).code; fs.writeFileSync(outputPath, final_code);