├── .gitignore ├── deobfuscator ├── deobfuscator.js ├── flow.js ├── misc.js ├── numbers.js ├── strings.js ├── switch.js └── utils.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | package-lock.json -------------------------------------------------------------------------------- /deobfuscator/deobfuscator.js: -------------------------------------------------------------------------------- 1 | const parser = require("@babel/parser"); 2 | const generate = require("@babel/generator").default; 3 | 4 | const { 5 | unescapeStrings, 6 | resolveStringCharCodes, 7 | mergeStrings, 8 | deobfuscateBase64EncodedStrings, 9 | deobfuscateEncryptedStringsAndNumbers, 10 | } = require("./strings"); 11 | const { 12 | resolveMathFunctions, 13 | solveStaticMathOperations, 14 | resolveStaticMath, 15 | } = require("./numbers"); 16 | const { 17 | resolveWindowCalls, 18 | resolveMemberExprCalls, 19 | cleanWildNumbers, 20 | } = require("./misc"); 21 | const { resolveOpaquePredicates, deobfuscateSwitchCases } = require("./switch"); 22 | const { 23 | removeTrashFlow, 24 | fixLogicalExpressions, 25 | fixTrashIfs, 26 | } = require("./flow"); 27 | 28 | function deobfuscate(script) { 29 | const parsedProgram = parser.parse(script, {}); 30 | const start = performance.now(); 31 | 32 | unescapeStrings(parsedProgram); 33 | resolveMathFunctions(script, parsedProgram); 34 | resolveStringCharCodes(parsedProgram); 35 | mergeStrings(parsedProgram); 36 | deobfuscateBase64EncodedStrings(parsedProgram); 37 | deobfuscateEncryptedStringsAndNumbers(parsedProgram); 38 | resolveMathFunctions(script, parsedProgram); 39 | resolveWindowCalls(parsedProgram); 40 | resolveMemberExprCalls(parsedProgram); 41 | resolveStaticMath(parsedProgram); 42 | resolveOpaquePredicates(parsedProgram); 43 | solveStaticMathOperations(parsedProgram); 44 | removeTrashFlow(parsedProgram); 45 | removeTrashFlow(parsedProgram); 46 | deobfuscateSwitchCases(parsedProgram); 47 | cleanWildNumbers(parsedProgram); 48 | fixLogicalExpressions(parsedProgram); 49 | fixTrashIfs(parsedProgram); 50 | 51 | console.log("took " + (performance.now() - start) + "ms to run transformers"); 52 | const result = generate(parsedProgram, { 53 | comments: true, 54 | minified: false, 55 | concise: false, 56 | }).code; 57 | 58 | return result; 59 | } 60 | 61 | module.exports = {deobfuscate}; 62 | -------------------------------------------------------------------------------- /deobfuscator/flow.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | const traverse = require("@babel/traverse").default; 3 | const { getNumberFromNode, isNumberNode, getExpressions, revertTest } = require("./utils"); 4 | 5 | function removeTrashFlow(program) { 6 | let removed = 0; 7 | 8 | // returns expressions or undefined 9 | const fixStaticConditional = function (node) { 10 | while (t.isConditionalExpression(node.test)) { 11 | const temp = fixStaticConditional(node.test); 12 | if (temp === undefined) return undefined; 13 | node.test = temp; 14 | } 15 | 16 | while (t.isBinaryExpression(node.test) && node.test.operator == "==") { 17 | if (!isNumberNode(node.test.left) || !isNumberNode(node.test.right)) { 18 | return undefined; 19 | } 20 | 21 | const left = getNumberFromNode(node.test.left); 22 | const right = getNumberFromNode(node.test.right); 23 | if (left == right) { 24 | return getExpressions(node.consequent); 25 | } else { 26 | return getExpressions(node.alternate); 27 | } 28 | } 29 | 30 | while (t.isLogicalExpression(node.test)) { 31 | if (t.isConditionalExpression(node.test.left)) { 32 | const temp = fixStaticConditional(node.test.left); 33 | if (temp === undefined) return undefined; 34 | node.test.left = temp; 35 | } 36 | 37 | if (t.isConditionalExpression(node.test.right)) { 38 | const temp = fixStaticConditional(node.test.right); 39 | if (temp === undefined) return undefined; 40 | node.test.right = temp; 41 | } 42 | 43 | if (!isNumberNode(node.test.left) || !isNumberNode(node.test.right)) { 44 | return undefined; 45 | } 46 | 47 | const left = getNumberFromNode(node.test.left); 48 | const right = getNumberFromNode(node.test.right); 49 | 50 | if (node.test.operator === "&&") { 51 | if (left && right) { 52 | return getExpressions(node.consequent); 53 | } else { 54 | return getExpressions(node.alternate); 55 | } 56 | } else if (node.test.operator === "||") { 57 | if (left || right) { 58 | return getExpressions(node.consequent); 59 | } else { 60 | return getExpressions(node.alternate); 61 | } 62 | } else { 63 | throw new Error("unhandled logical op " + node.test.operator); 64 | } 65 | } 66 | 67 | if (isNumberNode(node.test)) { 68 | if (getNumberFromNode(node.test)) { 69 | return getExpressions(node.consequent); 70 | } else { 71 | return getExpressions(node.alternate); 72 | } 73 | } 74 | 75 | return undefined; 76 | }; 77 | 78 | traverse(program, { 79 | ConditionalExpression(path) { 80 | const res = fixStaticConditional(path.node); 81 | if (res !== undefined) { 82 | path.replaceWithMultiple(res); 83 | removed++; 84 | } else if (res === null) { 85 | path.remove(); 86 | } 87 | }, 88 | IfStatement(path) { 89 | const res = fixStaticConditional(path.node); 90 | if (res !== undefined) { 91 | path.replaceWithMultiple(res); 92 | removed++; 93 | } else if (res === null) { 94 | path.remove(); 95 | } 96 | }, 97 | }); 98 | 99 | console.log("removed " + removed + " trash cf"); 100 | } 101 | 102 | function fixTrashIfs(program) { 103 | traverse(program, { 104 | IfStatement(path) { 105 | const node = path.node; 106 | if ( 107 | t.isBlockStatement(node.alternate) && 108 | node.alternate.body.length === 0 109 | ) { 110 | node.alternate = null; 111 | } 112 | 113 | // duplicated code because I don't know how to let babel traverse the conditional while being the traversed path 114 | // we could traverse the if path but then it would traverse as well actual body 115 | a: { 116 | const node = path.node.test; 117 | if (!t.isConditionalExpression(node)) { 118 | break a; 119 | } 120 | if ( 121 | !t.isNumericLiteral(node.consequent) || 122 | !t.isNumericLiteral(node.alternate) 123 | ) { 124 | break a; 125 | } 126 | 127 | const consequent = node.consequent.value; 128 | const alternate = node.alternate.value; 129 | 130 | if ( 131 | (consequent !== 0 && consequent !== 1) || 132 | (alternate !== 0 && alternate !== 1) 133 | ) { 134 | break a; 135 | } 136 | 137 | if (consequent === 0 && alternate === 1) { 138 | const temp = revertTest(node.test); 139 | if (temp !== undefined) { 140 | path.node.test = temp; 141 | } 142 | } else if (consequent == 1 && alternate == 0) { 143 | path.node.test = node.test; 144 | } 145 | } 146 | 147 | traverse( 148 | path.node.test, 149 | { 150 | ConditionalExpression(p) { 151 | if ( 152 | !t.isNumericLiteral(p.node.consequent) || 153 | !t.isNumericLiteral(p.node.alternate) 154 | ) { 155 | return; 156 | } 157 | 158 | const consequent = p.node.consequent.value; 159 | const alternate = p.node.alternate.value; 160 | 161 | if ( 162 | (consequent !== 0 && consequent !== 1) || 163 | (alternate !== 0 && alternate !== 1) 164 | ) { 165 | return; 166 | } 167 | 168 | if (consequent === 0 && alternate === 1) { 169 | const temp = revertTest(p.node.test); 170 | if (temp !== undefined) { 171 | if ( 172 | t.isUnaryExpression(temp) && 173 | t.isUnaryExpression(p.parentPath.node) 174 | ) { 175 | p.parentPath.replaceWith(temp.argument); 176 | } else { 177 | p.replaceWith(temp); 178 | } 179 | } 180 | } else if (consequent == 1 && alternate == 0) { 181 | p.replaceWith(p.node.test); 182 | } 183 | }, 184 | }, 185 | path.scope 186 | ); 187 | 188 | if ( 189 | t.isBlockStatement(node.consequent) && 190 | (node.consequent.body === null || node.consequent.body.length === 0) && 191 | node.alternate !== null 192 | ) { 193 | const revert = revertTest(node.test); 194 | if (revert !== undefined) { 195 | node.test = revert; 196 | node.consequent = node.alternate; 197 | node.alternate = null; 198 | } 199 | } 200 | }, 201 | }); 202 | } 203 | 204 | function fixLogicalExpressions(program) { 205 | let simplified = 0; 206 | traverse(program, { 207 | LogicalExpression(path) { 208 | const left = path.node.left; 209 | const right = path.node.right; 210 | 211 | if (!isNumberNode(left) && !isNumberNode(right)) { 212 | return; 213 | } 214 | 215 | if (isNumberNode(left) && !isNumberNode(right)) { 216 | const leftNumber = getNumberFromNode(left); 217 | if (path.node.operator === "||" && !leftNumber) { 218 | path.replaceWith(right); 219 | simplified++; 220 | } else if (path.node.operator === "&&" && leftNumber) { 221 | path.replaceWith(right); 222 | simplified++; 223 | } 224 | } else if (isNumberNode(right) && !isNumberNode(left)) { 225 | const rightNumber = getNumberFromNode(right); 226 | if (path.node.operator === "||" && !rightNumber) { 227 | path.replaceWith(left); 228 | simplified++; 229 | } else if (path.node.operator === "&&" && rightNumber) { 230 | path.replaceWith(left); 231 | simplified++; 232 | } 233 | } 234 | }, 235 | }); 236 | 237 | console.log("simplified " + simplified + " logical expressions"); 238 | } 239 | 240 | module.exports = { removeTrashFlow, fixTrashIfs, fixLogicalExpressions }; 241 | -------------------------------------------------------------------------------- /deobfuscator/misc.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | const generate = require("@babel/generator").default; 3 | const traverse = require("@babel/traverse").default; 4 | 5 | function resolveWindowCalls(program) { 6 | let resolved = 0; 7 | traverse(program, { 8 | VariableDeclarator(path) { 9 | const node = path.node; 10 | const init = node.init; 11 | 12 | if (t.isIdentifier(init) && init.name === "window") { 13 | let binding = path.scope.getBinding(path.node.id.name); 14 | 15 | for (const reference of binding.referencePaths) { 16 | const parentPath = reference.parentPath; 17 | const parent = parentPath.node; 18 | 19 | if ( 20 | !t.isMemberExpression(parent) || 21 | !t.isIdentifier(parent.object) || 22 | !t.isStringLiteral(parent.property) 23 | ) { 24 | // just replace value with window 25 | reference.replaceWith(t.stringLiteral("window")); 26 | resolved++; 27 | continue; 28 | } 29 | 30 | if (parent.object.name === node.id.name) { 31 | parentPath.replaceWith(t.identifier(parent.property.value)); 32 | resolved++; 33 | } 34 | } 35 | 36 | // there's currently one window var so we don't need to look at every vars 37 | path.stop(); 38 | } 39 | }, 40 | }); 41 | 42 | console.log("resolved " + resolved + " window calls"); 43 | } 44 | 45 | function resolveMemberExprCalls(program) { 46 | let resolved = 0; 47 | traverse(program, { 48 | MemberExpression(path) { 49 | if ( 50 | t.isStringLiteral(path.node.property) && 51 | !path.node.property.value.includes("@") 52 | ) { 53 | path.node.property = t.identifier(path.node.property.value); 54 | path.node.computed = false; 55 | resolved++; 56 | } 57 | 58 | if (t.isArrayExpression(path.node.property)) { 59 | const arr = path.node.property.elements; 60 | if ( 61 | arr.length === 1 && 62 | t.isStringLiteral(arr[0]) && 63 | !arr[0].value.includes("@") 64 | ) { 65 | path.node.property = t.identifier(arr[0].value); 66 | path.node.computed = false; 67 | resolved++; 68 | } 69 | } 70 | }, 71 | }); 72 | 73 | console.log("resolved " + resolved + " member expr string lit calls"); 74 | } 75 | 76 | function cleanWildNumbers(program) { 77 | let cleaned = 0; 78 | traverse(program, { 79 | SequenceExpression(path) { 80 | const prevSize = path.node.expressions.length; 81 | path.node.expressions = path.node.expressions.filter( 82 | (expr) => !t.isNumericLiteral(expr) 83 | ); 84 | 85 | if (prevSize !== path.node.expressions.length) { 86 | cleaned++; 87 | } 88 | 89 | if (path.node.expressions.length === 0) { 90 | path.parentPath.remove(); 91 | } 92 | }, 93 | }); 94 | 95 | console.log("cleaned " + cleaned + " wild numbers"); 96 | } 97 | 98 | module.exports = { 99 | resolveWindowCalls, 100 | resolveMemberExprCalls, 101 | cleanWildNumbers, 102 | }; 103 | -------------------------------------------------------------------------------- /deobfuscator/numbers.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | const traverse = require("@babel/traverse").default; 3 | const { getNumberFromNode, isNumberNode, numberToNode } = require("./utils"); 4 | const vm = require("node:vm"); 5 | 6 | function resolveMathFunctions(script, program) { 7 | let replaced = 0; 8 | traverse(program, { 9 | FunctionDeclaration(path) { 10 | if (path.node.params.length < 2) { 11 | return; 12 | } 13 | 14 | // let debug = path.node.id.name === "n" && path.node.params.length === 6; 15 | 16 | let bodyStatement = path.node.body; 17 | if ( 18 | !t.isBlockStatement(bodyStatement) || 19 | bodyStatement.body.length > 5 || 20 | !t.isReturnStatement(bodyStatement.body.at(-1)) 21 | ) { 22 | return; 23 | } 24 | 25 | let isInvalid = false; 26 | traverse( 27 | path.node.body, 28 | { 29 | enter(bodyPath) { 30 | if ( 31 | !t.isBinaryExpression(bodyPath.node) && 32 | !t.isUnaryExpression(bodyPath.node) && 33 | !t.isIdentifier(bodyPath.node) && 34 | !t.isConditionalExpression(bodyPath.node) && 35 | !t.isNumericLiteral(bodyPath.node) && 36 | !t.isMemberExpression(bodyPath.node) && 37 | !t.isReturnStatement(bodyPath.node) && 38 | !t.isVariableDeclaration(bodyPath.node) && 39 | !t.isVariableDeclarator(bodyPath.node) && 40 | !t.isAssignmentExpression(bodyPath.node) 41 | ) { 42 | // console.log("failed " + path.node.id.name + " because of " + bodyPath.node.type); 43 | isInvalid = true; 44 | bodyPath.stop(); 45 | } 46 | }, 47 | }, 48 | path.scope 49 | ); 50 | 51 | if (isInvalid) { 52 | return; 53 | } 54 | 55 | const functionCode = script.slice( 56 | path.node.loc.start.index, 57 | path.node.loc.end.index 58 | ); 59 | 60 | const references = path.parentPath.scope.getBinding( 61 | path.node.id.name 62 | ).referencePaths; 63 | 64 | const filtered = references.filter( 65 | (refPath) => 66 | t.isCallExpression(refPath.parentPath) && 67 | refPath.parentPath.node.arguments.length === 2 && 68 | isNumberNode(refPath.parentPath.node.arguments[0]) && 69 | isNumberNode(refPath.parentPath.node.arguments[1]) 70 | ); 71 | 72 | if (filtered.length === 0) { 73 | return; 74 | } 75 | 76 | let canRemovePath = 77 | filtered.length === 78 | references.filter((ref) => t.isCallExpression(ref.parentPath.node)) 79 | .length; 80 | 81 | let vmCode = "let array = [];" + functionCode + "; output=["; 82 | 83 | for (const refPath of filtered) { 84 | const callPath = refPath.parentPath; 85 | const callNode = callPath.node; 86 | vmCode += 87 | path.node.id.name + 88 | "(" + 89 | getNumberFromNode(callNode.arguments[0]) + 90 | "," + 91 | getNumberFromNode(callNode.arguments[1]) + 92 | "),"; 93 | } 94 | 95 | vmCode = vmCode.slice(0, -1); 96 | vmCode += "];"; 97 | 98 | const ctx = {}; 99 | vm.createContext(ctx); 100 | vm.runInContext(vmCode, ctx); 101 | 102 | // let output = eval(vmCode); 103 | // console.log(vmCode); 104 | let output = ctx.output; 105 | for (let i = 0; i < filtered.length; i++) { 106 | const refPath = filtered[i]; 107 | const callPath = refPath.parentPath; 108 | 109 | callPath.replaceWith(t.numericLiteral(output[i])); 110 | replaced++; 111 | } 112 | 113 | path.scope.crawl(); 114 | if (canRemovePath) { 115 | path.remove(); 116 | } 117 | }, 118 | }); 119 | 120 | console.log("solved " + replaced + " int obfuscation calls"); 121 | } 122 | 123 | function resolveStaticMath(program) { 124 | let resolved = 0; 125 | let inlined = 0; 126 | 127 | const inlineReferences = function (path) { 128 | if (!t.isVariableDeclarator(path.parentPath.node)) { 129 | return; 130 | } 131 | 132 | const varName = path.parentPath.node.id.name; 133 | const binding = path.parentPath.scope.getBinding(varName); 134 | 135 | if (binding !== undefined) { 136 | let privInlined = 0; 137 | for (const refPath of binding.referencePaths) { 138 | refPath.replaceWith(path.node); 139 | inlined++; 140 | privInlined++; 141 | } 142 | 143 | if (privInlined == binding.referencePaths.length) { 144 | if ( 145 | t.isVariableDeclaration(path.parentPath.parentPath.node) && 146 | path.parentPath.parentPath.node.declarations.length === 1 147 | ) { 148 | path.parentPath.parentPath.remove(); 149 | } else if (t.isVariableDeclarator(path.parentPath.node)) { 150 | path.parentPath.remove(); 151 | } 152 | } 153 | } 154 | }; 155 | 156 | traverse(program, { 157 | CallExpression(path) { 158 | const node = path.node; 159 | 160 | if ( 161 | t.isIdentifier(node.callee) && 162 | node.callee.name === "parseInt" && 163 | node.arguments.length === 1 164 | ) { 165 | const num = getNumberFromNode(node.arguments[0]); 166 | if (num === undefined) return; 167 | path.replaceWith(numberToNode(parseInt(num))); 168 | resolved++; 169 | inlineReferences(path); 170 | return; 171 | } 172 | 173 | if ( 174 | t.isIdentifier(node.callee) && 175 | node.callee.name === "Number" && 176 | node.arguments.length === 1 177 | ) { 178 | const num = getNumberFromNode(node.arguments[0]); 179 | if (num === undefined) return; 180 | path.replaceWith(numberToNode(num)); 181 | resolved++; 182 | inlineReferences(path); 183 | return; 184 | } 185 | 186 | if ( 187 | !t.isMemberExpression(node.callee) || 188 | !t.isIdentifier(node.callee.object) || 189 | !t.isIdentifier(node.callee.property) 190 | ) { 191 | return; 192 | } 193 | 194 | if (node.callee.object.name !== "Math") { 195 | return; 196 | } 197 | 198 | switch (node.callee.property.name) { 199 | case "ceil": 200 | const ceilResult = getNumberFromNode(node.arguments[0]); 201 | if (ceilResult === undefined) return; 202 | path.replaceWith(numberToNode(Math.ceil(ceilResult))); 203 | break; 204 | case "floor": 205 | const floorResult = getNumberFromNode(node.arguments[0]); 206 | if (floorResult === undefined) return; 207 | path.replaceWith(numberToNode(Math.floor(floorResult))); 208 | break; 209 | default: 210 | return; 211 | } 212 | 213 | inlineReferences(path); 214 | resolved++; 215 | }, 216 | }); 217 | 218 | console.log("resolved " + resolved + " static math functions"); 219 | console.log("inlined " + inlined + " numbers"); 220 | } 221 | 222 | function solveBin(bin) { 223 | let left = bin.left; 224 | let right = bin.right; 225 | 226 | while (!isNumberNode(left)) { 227 | if (t.isUnaryExpression(left) && t.isBinaryExpression(left.argument)) { 228 | const temp = solveBin(left.argument); 229 | if (temp === undefined) return undefined; 230 | left.argument = numberToNode(temp); 231 | } else if (t.isBinaryExpression(left)) { 232 | left = solveBin(left); 233 | if (left === undefined) return undefined; 234 | left = numberToNode(left); 235 | } else { 236 | return undefined; 237 | } 238 | } 239 | 240 | while (!isNumberNode(right)) { 241 | if (t.isUnaryExpression(right) && t.isBinaryExpression(right.argument)) { 242 | const temp = solveBin(right.argument); 243 | if (temp === undefined) return undefined; 244 | right.argument = numberToNode(temp); 245 | } else if (t.isBinaryExpression(right)) { 246 | right = solveBin(right); 247 | if (right === undefined) return undefined; 248 | right = numberToNode(right); 249 | } else { 250 | return undefined; 251 | } 252 | } 253 | 254 | const leftValue = getNumberFromNode(left); 255 | const rightValue = getNumberFromNode(right); 256 | if (leftValue === undefined || rightValue === undefined) { 257 | return undefined; 258 | } 259 | 260 | switch (bin.operator) { 261 | case "+": 262 | return leftValue + rightValue; 263 | case "-": 264 | return leftValue - rightValue; 265 | case "*": 266 | return leftValue * rightValue; 267 | case "/": 268 | return leftValue / rightValue; 269 | case ">>": 270 | return leftValue >> rightValue; 271 | case ">>>": 272 | return leftValue >>> rightValue; 273 | case "<<": 274 | return leftValue << rightValue; 275 | case "&": 276 | return leftValue & rightValue; 277 | case "|": 278 | return leftValue | rightValue; 279 | case "^": 280 | return leftValue ^ rightValue; 281 | case ">": 282 | return leftValue > rightValue ? 1 : 0; 283 | case "<": 284 | return leftValue < rightValue ? 1 : 0; 285 | case "!=": 286 | return leftValue != rightValue ? 1 : 0; 287 | case "!==": 288 | return leftValue !== rightValue ? 1 : 0; 289 | case "===": 290 | return leftValue === rightValue ? 1 : 0; 291 | case "==": 292 | return leftValue == rightValue ? 1 : 0; 293 | case ">=": 294 | return leftValue >= rightValue ? 1 : 0; 295 | case "<=": 296 | return leftValue <= rightValue ? 1 : 0; 297 | case "%": 298 | return leftValue % rightValue ? 1 : 0; 299 | default: 300 | throw new Error( 301 | "unhandled operator while solving math operations: " + bin.operator 302 | ); 303 | } 304 | } 305 | 306 | function solveStaticMathOperations(program) { 307 | let solved = 0; 308 | 309 | traverse(program, { 310 | BinaryExpression(path) { 311 | const res = solveBin(path.node); 312 | if (res !== undefined) { 313 | path.replaceWith(numberToNode(res)); 314 | solved++; 315 | } 316 | }, 317 | }); 318 | console.log("solved " + solved + " static math operations"); 319 | } 320 | 321 | module.exports = {resolveMathFunctions, resolveStaticMath, solveBin, solveStaticMathOperations}; -------------------------------------------------------------------------------- /deobfuscator/strings.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | const generate = require("@babel/generator").default; 3 | const traverse = require("@babel/traverse").default; 4 | 5 | function unescapeStrings(program) { 6 | traverse(program, { 7 | StringLiteral(path) { 8 | let escaped = JSON.stringify(path.node.value).slice(1, -1); 9 | path.node.extra.rawValue = `"` + escaped + `"`; 10 | }, 11 | }); 12 | } 13 | 14 | function mergeStrings(program) { 15 | let solve = function (binary) { 16 | let left = binary.left; 17 | let right = binary.right; 18 | 19 | if (t.isBinaryExpression(left)) { 20 | let res = solve(left); 21 | if (res === undefined) return undefined; 22 | 23 | left = res; 24 | } 25 | 26 | if (t.isBinaryExpression(right)) { 27 | let res = solve(right); 28 | if (res === undefined) return undefined; 29 | 30 | right = res; 31 | } 32 | 33 | if (!t.isStringLiteral(left) || !t.isStringLiteral(right)) { 34 | return undefined; 35 | } 36 | 37 | return t.stringLiteral(left.value + right.value); 38 | }; 39 | 40 | let merged = 0; 41 | traverse(program, { 42 | BinaryExpression(path) { 43 | const node = path.node; 44 | if (node.operator !== "+") { 45 | return; 46 | } 47 | 48 | let result = solve(node); 49 | if (result !== undefined) { 50 | path.replaceWith(result); 51 | merged++; 52 | } 53 | }, 54 | }); 55 | 56 | console.log("merged " + merged + " strings"); 57 | } 58 | 59 | function resolveStringCharCodes(program) { 60 | let resolved = 0; 61 | traverse(program, { 62 | VariableDeclarator(path) { 63 | if (!t.isMemberExpression(path.node.init)) { 64 | return; 65 | } 66 | 67 | const init = path.node.init; 68 | if ( 69 | !t.isIdentifier(init.object) || 70 | !t.isIdentifier(init.property) || 71 | init.object.name !== "String" || 72 | init.property.name !== "fromCharCode" 73 | ) { 74 | if ( 75 | init.object.name !== "String" || 76 | !t.isArrayExpression(init.property) 77 | ) { 78 | return; 79 | } 80 | 81 | let propertyElements = init.property.elements; 82 | if ( 83 | propertyElements.length !== 1 || 84 | !t.isStringLiteral(propertyElements[0]) || 85 | propertyElements[0].value !== "fromCharCode" 86 | ) { 87 | return; 88 | } 89 | } 90 | 91 | const binding = path.scope.getBinding(path.node.id.name); 92 | for (const refPath of binding.referencePaths) { 93 | const parent = refPath.parentPath; 94 | if (!t.isCallExpression(parent)) { 95 | console.log("not a call expr wtf"); 96 | return; 97 | } 98 | const callArgs = parent.node.arguments; 99 | if (callArgs.length !== 1) { 100 | console.log("args len is not equal to 1"); 101 | continue; 102 | } 103 | 104 | if (!t.isNumericLiteral(callArgs[0])) { 105 | console.log("call arg 0 is not a numeric literal"); 106 | continue; 107 | } 108 | 109 | parent.replaceWith( 110 | t.stringLiteral(String.fromCharCode(callArgs[0].value)) 111 | ); 112 | resolved++; 113 | } 114 | }, 115 | }); 116 | 117 | console.log("resolved " + resolved + " String.fromCharCode calls"); 118 | } 119 | 120 | function deobfuscateBase64EncodedStrings(program) { 121 | let deobfuscated = 0; 122 | let decodeFunctionPath = null; 123 | let arrayPath = null; 124 | let arrayElements = null; 125 | 126 | traverse(program, { 127 | FunctionDeclaration(path) { 128 | const node = path.node; 129 | if (node.body.body.length > 5) { 130 | return; 131 | } 132 | 133 | let hasAtobCall = false; 134 | let arrayMemberPath = undefined; 135 | path.traverse({ 136 | CallExpression(p) { 137 | if (!hasAtobCall) { 138 | // not our func 139 | p.stop(); 140 | } 141 | if ( 142 | t.isIdentifier(p.node.callee) && 143 | p.node.callee.name === "atob" && 144 | p.node.arguments.length === 1 145 | ) { 146 | hasAtobCall = true; 147 | } 148 | }, 149 | MemberExpression(p) { 150 | if ( 151 | !p.node.computed || 152 | !t.isIdentifier(p.node.object) || 153 | !t.isIdentifier(p.node.property) 154 | ) { 155 | return; 156 | } 157 | 158 | if (arrayMemberPath !== undefined) { 159 | p.stop(); 160 | hasAtobCall = false; 161 | arrayMemberPath = undefined; 162 | return; 163 | } 164 | 165 | arrayMemberPath = p; 166 | }, 167 | }); 168 | 169 | if (!hasAtobCall || arrayMemberPath === undefined) { 170 | return; 171 | } 172 | 173 | let binding = path.scope.getBinding(arrayMemberPath.node.object.name); 174 | if (binding === undefined || binding.path === undefined) { 175 | console.log("could not bind b64 array strings"); 176 | return; 177 | } 178 | 179 | decodeFunctionPath = path; 180 | arrayPath = binding.path; 181 | arrayElements = binding.path.node.init.elements; 182 | path.stop(); 183 | }, 184 | }); 185 | 186 | if (decodeFunctionPath !== null && arrayElements !== null) { 187 | let references = decodeFunctionPath.scope.getBinding( 188 | decodeFunctionPath.node.id.name 189 | ).referencePaths; 190 | for (const refPath of references) { 191 | const parentRefPath = refPath.parentPath; 192 | if (!t.isCallExpression(parentRefPath.node)) { 193 | console.log( 194 | "found id to base 64 decode func but parent was not a call expr" 195 | ); 196 | continue; 197 | } 198 | 199 | if (parentRefPath.node.arguments.length !== 1) { 200 | console.log( 201 | "found base 64 decode func call expr but with unexpected args len" 202 | ); 203 | continue; 204 | } 205 | 206 | if (!t.isNumericLiteral(parentRefPath.node.arguments[0])) { 207 | console.log( 208 | "found base 64 decode func call expr with correct args len but not a number" 209 | ); 210 | continue; 211 | } 212 | 213 | const idx = parentRefPath.node.arguments[0].value; 214 | const elem = arrayElements[idx]; 215 | 216 | if (!t.isStringLiteral(elem)) { 217 | console.log("base 64 array elem is not a string lit: "); 218 | console.log(elem); 219 | continue; 220 | } 221 | 222 | parentRefPath.replaceWith(t.stringLiteral(atob(elem.value))); 223 | deobfuscated++; 224 | } 225 | 226 | arrayPath.remove(); 227 | } 228 | 229 | console.log("deobfuscated " + deobfuscated + " base 64 encoded strings"); 230 | } 231 | 232 | function deobfuscateEncryptedStringsAndNumbers(program) { 233 | let decryptFunctionPath = null; 234 | let arrayName = null; 235 | let arrayPath = null; 236 | let arrayElements = null; 237 | let deobfuscated = 0; 238 | 239 | traverse(program, { 240 | FunctionDeclaration(path) { 241 | const node = path.node; 242 | const debug = path.node.id.name === "r" && path.node.params.length === 2; 243 | 244 | if (node.body.body.length > 3) { 245 | return; 246 | } 247 | 248 | const collectedStrings = []; 249 | path.traverse({ 250 | StringLiteral(p) { 251 | collectedStrings.push(p.node.value); 252 | }, 253 | Identifier(p) { 254 | collectedStrings.push(p.node.name); 255 | } 256 | }); 257 | 258 | if ( 259 | !collectedStrings.includes("fromCharCode") || 260 | !collectedStrings.includes("charAt") || 261 | !collectedStrings.includes("string") || 262 | !collectedStrings.includes("replace") 263 | ) { 264 | return; 265 | } 266 | 267 | let arrayMemberPath = undefined; 268 | path.traverse({ 269 | MemberExpression(p) { 270 | if ( 271 | !p.node.computed || 272 | !t.isIdentifier(p.node.object) || 273 | !t.isIdentifier(p.node.property) || 274 | p.node.property.name !== path.node.params[0].name 275 | ) { 276 | return; 277 | } 278 | 279 | arrayMemberPath = p; 280 | p.stop(); 281 | }, 282 | }); 283 | 284 | if (arrayMemberPath === undefined) { 285 | return; 286 | } 287 | 288 | // maybe add more checks 289 | // too lazy for now 290 | const arrayBinding = path.scope.getBinding( 291 | arrayMemberPath.node.object.name 292 | ); 293 | if (arrayBinding === undefined) { 294 | console.log("could not find encrypted strings/numbers arr"); 295 | return; 296 | } 297 | 298 | arrayName = arrayMemberPath.node.object.name; 299 | arrayPath = arrayBinding.path; 300 | arrayElements = arrayBinding.path.node.init.elements; 301 | decryptFunctionPath = path; 302 | path.stop(); 303 | }, 304 | }); 305 | 306 | if (decryptFunctionPath === null || arrayElements === null) { 307 | console.log("could not find string decrypt func"); 308 | return; 309 | } 310 | 311 | let vmCode = 312 | "let array = [];" + 313 | generate( 314 | t.variableDeclaration("let", [ 315 | t.variableDeclarator( 316 | t.identifier(arrayName), 317 | t.arrayExpression(arrayElements) 318 | ), 319 | ]) 320 | ).code + 321 | ";" + 322 | generate(decryptFunctionPath.node).code + 323 | ";"; 324 | 325 | const encryptedStringIdxToPaths = []; 326 | 327 | let references = decryptFunctionPath.scope.getBinding( 328 | decryptFunctionPath.node.id.name 329 | ).referencePaths; 330 | for (const refPath of references) { 331 | const parentRefPath = refPath.parentPath; 332 | if (!t.isCallExpression(parentRefPath.node)) { 333 | console.log("found id to decrypt func but parent was not a call expr"); 334 | continue; 335 | } 336 | 337 | if (parentRefPath.node.arguments.length !== 1) { 338 | console.log("found decrypt func call expr but with unexpected args len"); 339 | continue; 340 | } 341 | 342 | if (!t.isNumericLiteral(parentRefPath.node.arguments[0])) { 343 | console.log( 344 | "found decrypt func call expr with correct args len but not a number" 345 | ); 346 | continue; 347 | } 348 | 349 | const idx = parentRefPath.node.arguments[0].value; 350 | const elem = arrayElements[idx]; 351 | 352 | if (!t.isStringLiteral(elem)) { 353 | if (t.isNumericLiteral(elem)) { 354 | parentRefPath.replaceWith(elem); 355 | deobfuscated++; 356 | continue; 357 | } 358 | 359 | if (t.isUnaryExpression(elem)) { 360 | const unaryArg = elem.argument; 361 | if ( 362 | elem.operator === "-" && 363 | t.isNumericLiteral(unaryArg) && 364 | elem.prefix 365 | ) { 366 | parentRefPath.replaceWith(t.unaryExpression("-", unaryArg, true)); 367 | deobfuscated++; 368 | continue; 369 | } 370 | } 371 | 372 | if (t.isNullLiteral(elem)) { 373 | parentRefPath.replaceWith(elem); 374 | deobfuscated++; 375 | continue; 376 | } 377 | 378 | if ( 379 | t.isUnaryExpression(elem) && 380 | elem.prefix && 381 | elem.operator === "!" && 382 | t.isNumericLiteral(elem.argument) 383 | ) { 384 | parentRefPath.replaceWith(t.booleanLiteral(elem.argument.value === 0)); 385 | deobfuscated++; 386 | continue; 387 | } 388 | 389 | console.log("unexpected decrypt func arr val type: "); 390 | console.log(elem); 391 | continue; 392 | } 393 | 394 | let idxArray = encryptedStringIdxToPaths[idx]; 395 | if (idxArray === undefined) { 396 | encryptedStringIdxToPaths[idx] = []; 397 | idxArray = encryptedStringIdxToPaths[idx]; 398 | vmCode += 399 | "array[" + 400 | idx + 401 | "]=" + 402 | decryptFunctionPath.node.id.name + 403 | "(" + 404 | idx + 405 | ");"; 406 | } 407 | 408 | idxArray.push(parentRefPath); 409 | } 410 | 411 | vmCode += "array"; 412 | let decryptedArray = eval(vmCode); 413 | 414 | for (let i = 0; i < decryptedArray.length; ++i) { 415 | const result = decryptedArray[i]; 416 | if (result === undefined) { 417 | continue; 418 | } 419 | 420 | let paths = encryptedStringIdxToPaths[i]; 421 | for (const path of paths) { 422 | path.replaceWith(t.stringLiteral(result)); 423 | } 424 | deobfuscated++; 425 | } 426 | 427 | decryptFunctionPath.remove(); 428 | arrayPath.remove(); 429 | 430 | console.log( 431 | "deobfuscated " + deobfuscated + " encrypted strings/pooled numbers" 432 | ); 433 | } 434 | 435 | module.exports = { 436 | unescapeStrings, 437 | mergeStrings, 438 | resolveStringCharCodes, 439 | deobfuscateBase64EncodedStrings, 440 | deobfuscateEncryptedStringsAndNumbers, 441 | }; 442 | -------------------------------------------------------------------------------- /deobfuscator/switch.js: -------------------------------------------------------------------------------- 1 | const t = require("@babel/types"); 2 | const generate = require("@babel/generator").default; 3 | const traverse = require("@babel/traverse").default; 4 | const { getExpressions, isNumberNode, getNumberFromNode } = require("./utils"); 5 | 6 | function buildEquivalences(arr) { 7 | const equivalences = new Map(); 8 | let groupId = 1; 9 | 10 | for (let i = 0; i < 128; i++) { 11 | for (let j = 0; j < 512; j++) { 12 | const ref = arr[i][j]; 13 | let found = false; 14 | 15 | for (const [id, arrays] of equivalences.entries()) { 16 | if (arrays.includes(ref)) { 17 | found = true; 18 | break; 19 | } 20 | } 21 | 22 | if (!found) { 23 | equivalences.set(groupId++, [ref]); 24 | } 25 | } 26 | } 27 | 28 | const pairToGroupMap = new Map(); 29 | for (let i = 0; i < 256; i++) { 30 | for (let j = 0; j < 999; j++) { 31 | const ref = arr[i][j]; 32 | 33 | for (const [id, arrays] of equivalences.entries()) { 34 | if (arrays.includes(ref)) { 35 | pairToGroupMap.set(`${i},${j}`, id); 36 | break; 37 | } 38 | } 39 | } 40 | } 41 | 42 | return pairToGroupMap; 43 | } 44 | 45 | function transformIndices(arr, i, j) { 46 | const mod_i = i % 128; 47 | if (!transformIndices.equivalences) { 48 | transformIndices.equivalences = buildEquivalences(arr); 49 | } 50 | 51 | return transformIndices.equivalences.get(`${mod_i},${j}`); 52 | } 53 | 54 | function resolveOpaquePredicates(program) { 55 | // let genIndex = function (e, t, a, n, c, i, r, o) { 56 | // return (((t * a) ^ (r * i) ^ (e * n)) >>> 0) & (c - 1); 57 | // }; 58 | 59 | let deobfuscated = 0; 60 | let getIdxFuncPath = null; 61 | let arrayVarDeclaratorPath = null; 62 | 63 | traverse(program, { 64 | FunctionExpression(path) { 65 | if (path.node.body.body.length < 1) { 66 | return; 67 | } 68 | 69 | const lastExpr = path.node.body.body.at(-1); 70 | if ( 71 | !t.isReturnStatement(lastExpr) || 72 | !t.isMemberExpression(lastExpr.argument) 73 | ) { 74 | return; 75 | } 76 | 77 | const id = lastExpr.argument.property; 78 | if (!t.isNumericLiteral(id)) { 79 | return; 80 | } 81 | 82 | let arrayStartIndex = id.value; // dw 83 | let tempGetIdxFunc = null; 84 | 85 | traverse( 86 | path.node.body, 87 | { 88 | CallExpression(callPath) { 89 | if (!t.isIdentifier(callPath.node.callee)) { 90 | return; 91 | } 92 | 93 | if (callPath.node.arguments.length < 7) { 94 | return; 95 | } 96 | 97 | const identifierNum = callPath.node.arguments.filter((arg) => 98 | t.isIdentifier(arg) 99 | ).length; 100 | const numNum = callPath.node.arguments.filter((arg) => 101 | t.isNumericLiteral(arg) 102 | ).length; 103 | 104 | if (identifierNum !== 2 || numNum < 5) { 105 | return; 106 | } 107 | 108 | tempGetIdxFunc = callPath.scope.getBinding( 109 | callPath.node.callee.name 110 | ).path; 111 | callPath.stop(); 112 | }, 113 | }, 114 | path.scope 115 | ); 116 | 117 | if (arrayStartIndex === -1 || id.value != arrayStartIndex) { 118 | return; 119 | } 120 | 121 | while (!t.isVariableDeclarator(path)) { 122 | path = path.parentPath; 123 | } 124 | 125 | getIdxFuncPath = tempGetIdxFunc; 126 | arrayVarDeclaratorPath = path; 127 | }, 128 | }); 129 | 130 | if (getIdxFuncPath == null) { 131 | console.log("could not find opaque predicates gen func"); 132 | return; 133 | } 134 | 135 | const vmCode = 136 | generate(getIdxFuncPath.node).code + 137 | ";" + 138 | generate(t.variableDeclaration("let", [arrayVarDeclaratorPath.node])).code + 139 | ";" + 140 | arrayVarDeclaratorPath.node.id.name; 141 | const arr = eval(vmCode); 142 | 143 | const references = arrayVarDeclaratorPath.scope.getBinding( 144 | arrayVarDeclaratorPath.node.id.name 145 | ).referencePaths; 146 | for (let refPath of references) { 147 | while (t.isMemberExpression(refPath.parentPath)) { 148 | refPath = refPath.parentPath; 149 | } 150 | 151 | if ( 152 | !t.isMemberExpression(refPath.node.object) || 153 | !t.isIdentifier(refPath.node.object.object) 154 | ) { 155 | continue; 156 | } 157 | 158 | if (!t.isNumericLiteral(refPath.node.property)) { 159 | continue; 160 | } 161 | 162 | const firstIdx = refPath.node.object.property.value; 163 | const secondIdx = refPath.node.property.value; 164 | 165 | refPath.replaceWith( 166 | t.numericLiteral(transformIndices(arr, firstIdx, secondIdx)) 167 | ); 168 | deobfuscated++; 169 | } 170 | 171 | arrayVarDeclaratorPath.remove(); 172 | console.log("deobfuscated " + deobfuscated + " opaque predicates"); 173 | } 174 | 175 | function getLastNumber(binding, locEnd) { 176 | let value = undefined; 177 | let closestLoc = 0; 178 | 179 | const reassignements = binding.constantViolations; 180 | if (binding.path !== undefined) { 181 | reassignements.push(binding.path); 182 | } 183 | 184 | for (const refPath of reassignements) { 185 | if ( 186 | t.isAssignmentExpression(refPath.node) && 187 | isNumberNode(refPath.node.right) && 188 | closestLoc <= refPath.node.loc.start.index && 189 | refPath.node.loc.start.index <= locEnd 190 | ) { 191 | value = getNumberFromNode(refPath.node.right); 192 | closestLoc = refPath.node.loc.start.index; 193 | } 194 | 195 | if ( 196 | t.isVariableDeclarator(refPath.node) && 197 | isNumberNode(refPath.node.init) && 198 | closestLoc <= refPath.node.loc.start.index && 199 | refPath.node.loc.start.index <= locEnd 200 | ) { 201 | value = getNumberFromNode(refPath.node.init); 202 | closestLoc = refPath.node.loc.start.index; 203 | } 204 | } 205 | 206 | return value; 207 | } 208 | 209 | function deobfuscateSwitchCases(program) { 210 | const parseDiscriminant = function (scope, node) { 211 | // should never be reached 212 | if (isNumberNode(node)) { 213 | return getNumberFromNode(node); 214 | } 215 | 216 | if (t.isIdentifier(node)) { 217 | const binding = scope.getBinding(node.name); 218 | const value = getLastNumber(binding, node.loc.end.index); 219 | return value; 220 | } 221 | 222 | return undefined; 223 | }; 224 | 225 | const getCaseByNum = function (switchExpr, num) { 226 | return switchExpr.cases.filter((c) => getNumberFromNode(c.test) === num)[0]; 227 | }; 228 | 229 | let deobfuscated = 0; 230 | let deobfuscatedCases = 0; 231 | 232 | traverse(program, { 233 | SwitchStatement(path) { 234 | let isForLoopObfuscation = false; 235 | const hasNonStaticCases = path.node.cases.some( 236 | (c) => !isNumberNode(c.test) 237 | ); 238 | 239 | if (hasNonStaticCases) { 240 | // not every cases are numbers so we just skip it as we can't recreate it 241 | return; 242 | } 243 | 244 | path.traverse({ 245 | ContinueStatement(path) { 246 | isForLoopObfuscation = true; 247 | path.stop(); 248 | }, 249 | }); 250 | 251 | if (!isForLoopObfuscation) { 252 | return; 253 | } 254 | 255 | const startingDiscriminant = parseDiscriminant( 256 | path.scope, 257 | path.node.discriminant 258 | ); 259 | let plainCodeExpressions = []; 260 | let caseToInspect = getCaseByNum(path.node, startingDiscriminant); 261 | 262 | while (caseToInspect !== undefined) { 263 | let nextCaseToInspect = -1; 264 | let continued = false; 265 | let changed = false; 266 | let switchEnd = false; 267 | let hasReturned = false; 268 | 269 | // find next case 270 | traverse( 271 | caseToInspect, 272 | { 273 | IfStatement(cPath) { 274 | cPath.skip(); 275 | }, 276 | ForStatement(cPath) { 277 | cPath.skip(); 278 | }, 279 | ConditionalExpression(cPath) { 280 | cPath.skip(); 281 | }, 282 | AssignmentExpression(cPath) { 283 | if ( 284 | t.isIdentifier(cPath.node.left) && 285 | cPath.node.left.name === path.node.discriminant.name 286 | ) { 287 | const next = getNumberFromNode(cPath.node.right); 288 | if (next === undefined) { 289 | throw new Error( 290 | "wasn't able to retrieve next case because assignement wasn't a num" + 291 | cPath.node.type 292 | ); 293 | } 294 | // caseToInspect = getCaseByNum(path.node, next); 295 | nextCaseToInspect = next; 296 | changed = true; 297 | cPath.remove(); 298 | } 299 | }, 300 | ContinueStatement(cPath) { 301 | continued = true; 302 | cPath.remove(); 303 | cPath.stop(); 304 | }, 305 | BreakStatement(cPath) { 306 | switchEnd = true; 307 | cPath.remove(); 308 | cPath.stop(); 309 | }, 310 | ReturnStatement(cPath) { 311 | switchEnd = true; 312 | hasReturned = true; 313 | cPath.skip(); 314 | }, 315 | enter(cPath) { 316 | if (hasReturned) cPath.remove(); 317 | }, 318 | }, 319 | path.scope 320 | ); 321 | 322 | if (continued && !changed) { 323 | // throw new Error( 324 | // "Switch case discrimant hasn't changed but was reiterated." 325 | // ); 326 | console.log("squid game " + caseToInspect.test.value); 327 | return; 328 | } 329 | 330 | plainCodeExpressions.push(...getExpressions(caseToInspect.consequent)); 331 | if (switchEnd) { 332 | break; 333 | } 334 | 335 | if (changed) { 336 | caseToInspect = getCaseByNum(path.node, nextCaseToInspect); 337 | } else { 338 | // console.log("no assigment found going to next case"); 339 | caseToInspect = path.node.cases.at( 340 | path.node.cases.indexOf(caseToInspect) + 1 341 | ); 342 | continue; 343 | } 344 | } 345 | 346 | deobfuscatedCases += path.node.cases.length; 347 | while (path !== null && !t.isForStatement(path)) { 348 | path = path.parentPath; 349 | } 350 | if (path === null) { 351 | return; 352 | } 353 | 354 | if (path.node.init !== undefined && path.node.init !== null) { 355 | plainCodeExpressions.unshift(path.node.init); 356 | } 357 | path.replaceWithMultiple(plainCodeExpressions); 358 | deobfuscated++; 359 | }, 360 | }); 361 | 362 | console.log( 363 | "deobfuscated " + 364 | deobfuscated + 365 | " switch cases with a total of " + 366 | deobfuscatedCases + 367 | " cases" 368 | ); 369 | } 370 | 371 | module.exports = { resolveOpaquePredicates, deobfuscateSwitchCases }; 372 | -------------------------------------------------------------------------------- /deobfuscator/utils.js: -------------------------------------------------------------------------------- 1 | const { moduleExpression } = require("@babel/types"); 2 | const t = require("@babel/types"); 3 | const generate = require("@babel/generator").default; 4 | const traverse = require("@babel/traverse").default; 5 | 6 | function isNumberNode(node) { 7 | if (t.isSequenceExpression(node)) { 8 | for (const expr of node.expressions) { 9 | if (isNumberNode(expr)) { 10 | return true; 11 | } 12 | } 13 | } 14 | 15 | while (!t.isNumericLiteral(node)) { 16 | if ( 17 | t.isUnaryExpression(node) && 18 | (node.operator === "+" || 19 | node.operator === "-" || 20 | node.operator === "~" || 21 | node.operator === "!") && 22 | isNumberNode(node.argument) 23 | ) { 24 | node = node.argument; 25 | } else { 26 | return false; 27 | } 28 | } 29 | 30 | return true; 31 | } 32 | 33 | function numberToNode(num) { 34 | if (num < 0) { 35 | return t.unaryExpression("-", t.numericLiteral(-num), true); 36 | } 37 | 38 | return t.numericLiteral(num); 39 | } 40 | 41 | function isSupportedOperator(op) { 42 | return ( 43 | op === "+" || 44 | op === "-" || 45 | op === "~" || 46 | op === "!" 47 | ); 48 | } 49 | 50 | function getNumberFromNode(node) { 51 | if (t.isSequenceExpression(node)) { 52 | for (let i = node.expressions.length - 1; i >= 0; i--) { 53 | if (isNumberNode(node.expressions[i])) { 54 | return getNumberFromNode(node.expressions[i]); 55 | } 56 | } 57 | } 58 | 59 | if (t.isNumericLiteral(node)) { 60 | return node.value; 61 | } 62 | 63 | if (t.isUnaryExpression(node)) { 64 | const changes = []; 65 | while (!t.isNumericLiteral(node)) { 66 | if (!t.isUnaryExpression(node) || !isSupportedOperator(node.operator)) { 67 | return undefined; 68 | } 69 | 70 | changes.push(node.operator); 71 | node = node.argument; 72 | } 73 | 74 | let value = node.value; 75 | for (const ch of changes.reverse()) { 76 | switch (ch) { 77 | case "+": 78 | value = +value; 79 | break; 80 | case "-": 81 | value = -value; 82 | break; 83 | case "~": 84 | value = ~value; 85 | break; 86 | case "!": 87 | value = !value; 88 | break; 89 | } 90 | } 91 | 92 | return value; 93 | 94 | // let value; 95 | // if (t.isNumericLiteral(node.argument)) { 96 | // value = node.argument.value; 97 | // } else if (t.isUnaryExpression(node.argument)) { 98 | // value = getNumberFromNode(node.argument); 99 | // } else { 100 | // return undefined; 101 | // } 102 | 103 | // switch (node.operator) { 104 | // case "+": 105 | // value = +value; 106 | // break; 107 | // case "-": 108 | // value = -value; 109 | // break; 110 | // case "~": 111 | // value = ~value; 112 | // break; 113 | // case "!": 114 | // value = !value; 115 | // break; 116 | // } 117 | 118 | // return value; 119 | // } 120 | 121 | // // throw new Error("unknown num node"); 122 | // return undefined; 123 | } 124 | } 125 | 126 | function revertTest(node) { 127 | if (t.isUnaryExpression(node) && node.operator === "!") { 128 | return node.argument; 129 | } 130 | 131 | if ( 132 | t.isConditionalExpression(node) && 133 | t.isNumericLiteral(node.consequent) && 134 | t.isNumericLiteral(node.alternate) 135 | ) { 136 | const leftNumber = node.consequent.value; 137 | const rightNumber = node.alternate.value; 138 | 139 | if (leftNumber === 1 && rightNumber === 0) { 140 | return revertTest(node.test); 141 | } else if (leftNumber === 0 && rightNumber === 1) { 142 | return node.test; 143 | } 144 | } 145 | 146 | if (t.isSequenceExpression(node)) { 147 | for (let i = node.expressions.length - 1; i > 0; i--) { 148 | const expr = node.expressions[i]; 149 | if (t.isAssignmentExpression(expr)) break; 150 | 151 | const temp = revertTest(expr); 152 | if (temp === undefined) continue; 153 | node.expressions[i] = temp; 154 | } 155 | 156 | return node; 157 | } 158 | 159 | if ( 160 | t.isIdentifier(node) || 161 | t.isCallExpression(node) || 162 | t.isMemberExpression(node) 163 | ) { 164 | return t.unaryExpression("!", node, true); 165 | } 166 | 167 | if (t.isLogicalExpression(node)) { 168 | const tempLeft = revertTest(node.left); 169 | const tempRight = revertTest(node.right); 170 | if (tempLeft === undefined || tempRight === undefined) { 171 | return undefined; 172 | } 173 | 174 | if (node.operator === "&&") { 175 | node.operator = "||"; 176 | } else if (node.operator === "||") { 177 | node.operator = "&&"; 178 | } 179 | 180 | node.left = tempLeft; 181 | node.right = tempRight; 182 | return node; 183 | } 184 | 185 | if (t.isBinaryExpression(node)) { 186 | switch (node.operator) { 187 | case "!==": 188 | node.operator = "==="; 189 | break; 190 | case "===": 191 | node.operator = "!=="; 192 | break; 193 | case "==": 194 | node.operator = "!="; 195 | break; 196 | case "!=": 197 | node.operator = "=="; 198 | break; 199 | case ">": 200 | node.operator = "<"; 201 | break; 202 | case "<": 203 | node.operator = ">"; 204 | break; 205 | case ">=": 206 | node.operator = "<="; 207 | break; 208 | case "<=": 209 | node.operator = ">="; 210 | break; 211 | default: 212 | return undefined; 213 | } 214 | 215 | return node; 216 | } 217 | 218 | return undefined; 219 | } 220 | 221 | function getExpressions(node) { 222 | if (t.isExpressionStatement(node)) { 223 | return node.expressions; 224 | } 225 | 226 | return node; 227 | } 228 | 229 | module.exports = { 230 | revertTest, 231 | isNumberNode, 232 | getNumberFromNode, 233 | getExpressions, 234 | numberToNode, 235 | }; 236 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { deobfuscate } = require("./deobfuscator/deobfuscator"); 2 | const fs = require("node:fs"); 3 | 4 | const input = fs.readFileSync("input.js"); 5 | const output = deobfuscate(input.toString()); 6 | fs.writeFileSync("output.js", output.toString()); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datadome-deobfuscator", 3 | "version": "1.0.0", 4 | "description": "Deobfuscates datadome PUZZLE script", 5 | "keywords": [ 6 | "deobfuscator" 7 | ], 8 | "homepage": "https://github.com/thisnun/datadome-deobfuscator#readme", 9 | "bugs": { 10 | "url": "https://github.com/thisnun/datadome-deobfuscator/issues" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/thisnun/datadome-deobfuscator.git" 15 | }, 16 | "license": "ISC", 17 | "author": "thisnun", 18 | "type": "commonjs", 19 | "main": "index.js", 20 | "scripts": { 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "dependencies": { 24 | "@babel/generator": "^7.27.0", 25 | "@babel/parser": "^7.27.0", 26 | "@babel/traverse": "^7.27.0", 27 | "@babel/types": "^7.27.0" 28 | } 29 | } 30 | --------------------------------------------------------------------------------