├── .gitignore ├── .gitattributes ├── img ├── hsv-output.png ├── hsv-original.png ├── workers-output.png └── workers-original.png ├── package.json ├── transformers ├── extractWorkers.js ├── removeDeadCode.js ├── extractWasm.js ├── replaceDicts.js ├── cleanUp.js ├── replaceIdentifiers.js ├── decryptVars.js └── deobfuscateWorker.js ├── main.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.wasm 3 | *.txt 4 | *.out.* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /img/hsv-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glizzykingdreko/hCaptcha-hsv-Deobfuscator/HEAD/img/hsv-output.png -------------------------------------------------------------------------------- /img/hsv-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glizzykingdreko/hCaptcha-hsv-Deobfuscator/HEAD/img/hsv-original.png -------------------------------------------------------------------------------- /img/workers-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glizzykingdreko/hCaptcha-hsv-Deobfuscator/HEAD/img/workers-output.png -------------------------------------------------------------------------------- /img/workers-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glizzykingdreko/hCaptcha-hsv-Deobfuscator/HEAD/img/workers-original.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@babel/core": "^7.23.6", 4 | "@babel/generator": "^7.23.6", 5 | "@babel/traverse": "^7.23.6", 6 | "@babel/types": "^7.23.6" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /transformers/extractWorkers.js: -------------------------------------------------------------------------------- 1 | const traverse = require('@babel/traverse').default; 2 | const fs = require('fs'); 3 | 4 | let extractWorkers = (ast) => { 5 | let workers = new Map(); 6 | traverse(ast, { 7 | VariableDeclarator(path) { 8 | if ( 9 | path.node.init && 10 | path.node.init.type === 'CallExpression' && 11 | path.node.init.callee.type === 'Identifier' && 12 | path.node.init.arguments.length === 3 && 13 | path.node.init.arguments[0].type === 'StringLiteral' 14 | ) { 15 | workerCode = atob(path.node.init.arguments[0].value); 16 | workers.set(path.node.id.name, workerCode); 17 | } 18 | } 19 | }) 20 | return workers; 21 | } 22 | 23 | module.exports = extractWorkers; -------------------------------------------------------------------------------- /transformers/removeDeadCode.js: -------------------------------------------------------------------------------- 1 | const traverse = require('@babel/traverse').default; 2 | 3 | let removeDeadCode = (ast) => { 4 | let declaredVariables = new Set(); 5 | let referencedIdentifiers = new Set(); 6 | 7 | traverse(ast, { 8 | VariableDeclarator(path) { 9 | if ( 10 | path.node.id && 11 | path.node.id.name && 12 | // the name can't be hsw 13 | path.node.id.name !== 'hsw' 14 | ) { 15 | declaredVariables.add(path.node.id.name); 16 | } 17 | }, 18 | Identifier(path) { 19 | if (!path.parentPath.isFunctionDeclaration() && !path.parentPath.isVariableDeclarator()) { 20 | referencedIdentifiers.add(path.node.name); 21 | } 22 | } 23 | }); 24 | 25 | let unusedVariables = [...declaredVariables].filter(varName => !referencedIdentifiers.has(varName)); 26 | 27 | traverse(ast, { 28 | VariableDeclarator(path) { 29 | if (unusedVariables.includes(path.node.id.name)) { 30 | path.remove(); 31 | } 32 | } 33 | }); 34 | } 35 | 36 | module.exports = removeDeadCode; -------------------------------------------------------------------------------- /transformers/extractWasm.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const types = require('@babel/types'); 3 | const { default: traverse } = require('@babel/traverse'); 4 | 5 | const extractWasm = (ast) => { 6 | traverse(ast, { 7 | CallExpression(path) { 8 | if ( 9 | path.node.callee.type === 'FunctionExpression' && 10 | path.node.arguments.length === 4 && 11 | path.node.arguments[2].type === 'StringLiteral' 12 | ) { 13 | let encryptedWasm = path.node.arguments[2].value; 14 | // save the encrypted wasm file 15 | fs.writeFileSync('./wasm.txt', encryptedWasm); 16 | // replace the third argument with the following code: 17 | // require("fs").readFileSync("./hsv.wasm") 18 | path.node.arguments[2] = types.callExpression( 19 | types.memberExpression( 20 | types.callExpression( 21 | types.identifier('require'), 22 | [types.stringLiteral('fs')] 23 | ), 24 | types.identifier('readFileSync') 25 | ), 26 | [types.stringLiteral('./wasm.txt')] 27 | ); 28 | } 29 | } 30 | }); 31 | } 32 | 33 | module.exports = extractWasm; -------------------------------------------------------------------------------- /transformers/replaceDicts.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'); 2 | const { default: generate } = require('@babel/generator'); 3 | const types = require('@babel/types'); 4 | const { default: traverse } = require('@babel/traverse'); 5 | 6 | let replaceDicts = (ast) => { 7 | const context = vm.createContext({}); 8 | let matches = 0; 9 | traverse(ast, { 10 | VariableDeclarator(path) { 11 | if ( 12 | path.node.init && 13 | path.node.init.type === 'ObjectExpression' && 14 | // check if each property is a number 15 | path.node.init.properties.every(prop => types.isNumericLiteral(prop.value)) 16 | ) { 17 | vm.runInContext(generate(path.node).code, context); 18 | path.remove(); 19 | matches++; 20 | } 21 | } 22 | }); 23 | 24 | traverse(ast, { 25 | MemberExpression(path) { 26 | if ( 27 | !path.node.computed && 28 | path.node.object.name in context 29 | ) { 30 | let oldCode = generate(path.node).code; 31 | let output = vm.runInContext(oldCode, context); 32 | path.replaceWith( 33 | types.numericLiteral(output) 34 | ); 35 | matches++; 36 | } 37 | } 38 | }); 39 | 40 | return matches; 41 | } 42 | 43 | module.exports = replaceDicts; -------------------------------------------------------------------------------- /transformers/cleanUp.js: -------------------------------------------------------------------------------- 1 | const types = require('@babel/types'); 2 | const traverse = require('@babel/traverse').default; 3 | 4 | let cleanUp = (ast, rename=false) => { 5 | traverse(ast, { 6 | MemberExpression(path) { 7 | // Check if the property is accessed using bracket notation and is a string literal 8 | if (path.node.computed && types.isStringLiteral(path.node.property)) { 9 | const property = path.node.property.value; 10 | 11 | // Check if the property is a valid JavaScript identifier 12 | if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(property)) { 13 | // Transform to dot notation 14 | path.node.property = types.identifier(property); 15 | path.node.computed = false; 16 | } 17 | } 18 | }, 19 | NumericLiteral(path) { 20 | if (path.node.extra && /^0x/i.test(path.node.extra.raw)) { 21 | // Replace the hexadecimal literal with its decimal equivalent 22 | path.replaceWith(types.numericLiteral(path.node.value)); 23 | } 24 | } 25 | }); 26 | 27 | if (rename) { 28 | let varCounter = 1; 29 | let funcCounter = 1; 30 | const renamed = new Map(); 31 | traverse(ast, { 32 | Identifier(path) { 33 | // Skip key names in object properties 34 | if (path.parent.type === 'ObjectProperty' && path.parent.key === path.node) return; 35 | 36 | // Skip function names if they are part of declarations 37 | if (path.parent.type === 'FunctionDeclaration' && path.parent.id === path.node) return; 38 | 39 | // Skip updating identifiers that have already been renamed 40 | if (renamed.has(path.node.name)) { 41 | path.node.name = renamed.get(path.node.name); 42 | return; 43 | } 44 | 45 | const name = path.scope.hasBinding(path.node.name) ? `var${varCounter++}` : `func${funcCounter++}`; 46 | renamed.set(path.node.name, name); 47 | path.node.name = name; 48 | } 49 | }) 50 | } 51 | }; 52 | 53 | module.exports = cleanUp; -------------------------------------------------------------------------------- /transformers/replaceIdentifiers.js: -------------------------------------------------------------------------------- 1 | const traverse = require("@babel/traverse").default; 2 | const types = require("@babel/types"); 3 | 4 | let replaceIdentifiers = (ast) => { 5 | let matches = 0; 6 | 7 | traverse(ast, { 8 | CallExpression(path) { 9 | // Check if the call expression has only one argument 10 | if (path.node.arguments.length === 1) { 11 | const arg = path.node.arguments[0]; 12 | 13 | if (types.isIdentifier(arg) && path.scope.hasBinding(arg.name)) { 14 | const binding = path.scope.getBinding(arg.name); 15 | 16 | // Check if the binding is a variable initialized with a numeric literal 17 | // and the numeric value is neither 1 nor 0 18 | if ( 19 | binding && 20 | types.isVariableDeclarator(binding.path.node) && 21 | types.isNumericLiteral(binding.path.node.init) && 22 | binding.path.node.init.value !== 1 && 23 | binding.path.node.init.value !== 0 24 | ) { 25 | path.node.arguments[0] = types.numericLiteral(binding.path.node.init.value); 26 | matches++; 27 | } 28 | } 29 | } 30 | } 31 | }); 32 | 33 | traverse(ast, { 34 | MemberExpression(path) { 35 | if (types.isCallExpression(path.node.property) && 36 | path.node.property.arguments.length === 1 && 37 | types.isMemberExpression(path.node.property.arguments[0]) && 38 | types.isIdentifier(path.node.property.arguments[0].object) && 39 | types.isIdentifier(path.node.property.arguments[0].property)) { 40 | 41 | const objectName = path.node.property.arguments[0].object.name; 42 | const propertyName = path.node.property.arguments[0].property.name; 43 | 44 | // Find the variable declarator that corresponds to the object 45 | const binding = path.scope.getBinding(objectName); 46 | if (binding && types.isVariableDeclarator(binding.path.node) && 47 | types.isObjectExpression(binding.path.node.init)) { 48 | // Look for the property in the object 49 | for (const prop of binding.path.node.init.properties) { 50 | if (types.isIdentifier(prop.key) && prop.key.name === propertyName) { 51 | if (types.isNumericLiteral(prop.value)) { 52 | path.node.property = types.numericLiteral(prop.value.value); 53 | matches++; 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | }) 61 | 62 | return matches; 63 | } 64 | 65 | module.exports = replaceIdentifiers; -------------------------------------------------------------------------------- /transformers/decryptVars.js: -------------------------------------------------------------------------------- 1 | const vm = require('vm'); 2 | const { default: generate } = require('@babel/generator'); 3 | const types = require('@babel/types'); 4 | const { default: traverse } = require('@babel/traverse'); 5 | 6 | let findDecryptionVarFunction = (ast, context) => { 7 | 8 | let decoders = new Set(); 9 | traverse(ast, { 10 | CallExpression(path) { 11 | if ( 12 | path.node.callee.type === 'Identifier' && 13 | path.node.arguments.length === 1 && 14 | path.node.arguments[0].type === 'NumericLiteral' 15 | ) { 16 | decoders.add(path.node.callee.name); 17 | } 18 | } 19 | }) 20 | 21 | let associations = []; 22 | 23 | traverse(ast, { 24 | VariableDeclarator(path) { 25 | if ( 26 | path.node.init && 27 | path.node.init.type === 'Identifier' && 28 | decoders.has(path.node.id.name) 29 | ) { 30 | associations.push( 31 | path.node.init.name 32 | ) 33 | } 34 | } 35 | }) 36 | 37 | // find most common associations 38 | let counts = {}; 39 | for (let association of associations) { 40 | if (counts[association] === undefined) { 41 | counts[association] = 0; 42 | } 43 | counts[association]++; 44 | } 45 | let max = 0; 46 | let maxAssociation = null; 47 | for (let association in counts) { 48 | if (counts[association] > max) { 49 | max = counts[association]; 50 | maxAssociation = association; 51 | } 52 | } 53 | let listName; 54 | // find the function declaration 55 | traverse(ast, { 56 | FunctionDeclaration(path) { 57 | if (path.node.id.name === maxAssociation) { 58 | vm.runInContext(generate(path.node).code, context); 59 | listName = path.node.body.body[0].declarations[0].init.callee.name; 60 | path.remove(); 61 | } 62 | } 63 | }) 64 | 65 | traverse(ast, { 66 | FunctionDeclaration(path) { 67 | if (path.node.id.name === listName) { 68 | vm.runInContext(generate(path.node).code, context); 69 | path.remove(); 70 | } 71 | } 72 | }) 73 | 74 | traverse(ast, { 75 | UnaryExpression(path) { 76 | if ( 77 | path.node.argument.type === 'CallExpression' && 78 | path.node.argument.arguments.length === 1 && 79 | path.node.argument.arguments[0].type === 'Identifier' && 80 | path.node.argument.arguments[0].name === listName 81 | ) { 82 | vm.runInContext(generate(path.node).code, context); 83 | path.remove(); 84 | } 85 | } 86 | }) 87 | 88 | return maxAssociation; 89 | } 90 | 91 | let decryptCalls = (ast, context, decoderFunction) => { 92 | let matches = 0; 93 | traverse(ast, { 94 | CallExpression(path) { 95 | if ( 96 | path.node.callee.type === 'Identifier' && 97 | path.node.arguments.length === 1 && 98 | path.node.arguments[0].type === 'NumericLiteral' 99 | ) { 100 | let argument = path.node.arguments[0].value; 101 | try { 102 | let decoded = vm.runInContext( 103 | decoderFunction + '(' + argument + ')', 104 | context 105 | ); 106 | path.replaceWith(types.stringLiteral(decoded)); 107 | matches++; 108 | } catch (e) { 109 | } 110 | } 111 | } 112 | }) 113 | return matches; 114 | } 115 | 116 | let decryptVars = (ast) => { 117 | let context = {}; 118 | vm.createContext(context); 119 | let decodeFunction = findDecryptionVarFunction(ast, context); 120 | return decryptCalls(ast, context, decodeFunction); 121 | }; 122 | 123 | module.exports = decryptVars; -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const babel = require('@babel/core'); 3 | const { default: generate } = require('@babel/generator'); 4 | 5 | const replaceIdentifiers = require('./transformers/replaceIdentifiers'); 6 | const extractWasm = require('./transformers/extractWasm'); 7 | const decryptVars = require('./transformers/decryptVars'); 8 | const cleanUp = require('./transformers/cleanUp'); 9 | const removeDeadCode = require('./transformers/removeDeadCode'); 10 | const extractWorkers = require('./transformers/extractWorkers'); 11 | const replaceDicts = require('./transformers/replaceDicts'); 12 | const {deobfuscateWorker, deobfuscateWorkerTwo } = require('./transformers/deobfuscateWorker'); 13 | 14 | const processArgs = () => { 15 | const args = process.argv.slice(2); 16 | let options = { 17 | inputFile: null, 18 | outputFile: null, 19 | extractWasm: false, 20 | cleanWorkers: false, 21 | saveWorkers: false, 22 | renameWorkers: false, 23 | wasmFileName: null 24 | }; 25 | 26 | for (let i = 0; i < args.length; i++) { 27 | const arg = args[i]; 28 | 29 | switch (arg) { 30 | case '--extract-wasm': 31 | options.extractWasm = true; 32 | break; 33 | case '--clean-workers': 34 | options.cleanWorkers = true; 35 | break; 36 | case '--rename-workers': 37 | options.renameWorkers = true; 38 | break; 39 | case '--save-workers': 40 | options.saveWorkers = true; 41 | break; 42 | default: 43 | if (arg.startsWith('--wasm-file=')) { 44 | options.wasmFileName = arg.split('=')[1]; 45 | } else if (!options.inputFile) { 46 | options.inputFile = arg; 47 | } else if (!options.outputFile) { 48 | options.outputFile = arg; 49 | } 50 | } 51 | } 52 | 53 | return options; 54 | }; 55 | 56 | const run = () => { 57 | const options = processArgs(); 58 | let inputFile = options.inputFile || './hsv.js'; 59 | let outputFile = options.outputFile || './hsv.out.js'; 60 | console.log(" | ================= | hCaptcha hsv Deobfuscator | ================= |") 61 | console.log(` | ---> Inputfile: ${inputFile}`); 62 | let code = fs.readFileSync(inputFile, 'utf-8'); 63 | let ast = babel.parse(code, {}); 64 | 65 | if (options.extractWasm) { 66 | console.log(" | ---> Extracting Wasm"); 67 | extractWasm(ast, options.wasmFileName || `${inputFile.replace('.js', '-wasm')}.txt`); 68 | } 69 | 70 | console.log(` | ---> Replaced ${replaceIdentifiers(ast)} identifiers`); 71 | console.log(` | ---> Decrypted ${decryptVars(ast)} variables`); 72 | cleanUp(ast); 73 | console.log(` | ---> Cleaned and removed dead code`); 74 | 75 | if (options.cleanWorkers) { 76 | let workers = extractWorkers(ast); 77 | console.log(` | ---> Extracted ${workers.size} workers`); 78 | let dashCount = 5; // Starting number of dashes 79 | for (let [workerName, workerCode] of workers) { 80 | let workerAst = babel.parse(workerCode, {}); 81 | let dashPrefix = ` | ${'-'.repeat(dashCount)}> `; 82 | console.log(`${dashPrefix} Processing worker "${workerName}"`); 83 | 84 | if (options.saveWorkers) { 85 | let { code: cde } = generate(workerAst); 86 | let outputWorker = `${outputFile.replace('.out.js', '-og-worker')}-${workerName}.js`; 87 | fs.writeFileSync(outputWorker, cde); 88 | console.log(`${dashPrefix} Saved to ${outputWorker}`); 89 | } 90 | 91 | let replaces = replaceDicts(workerAst) + replaceIdentifiers(workerAst); 92 | console.log(`${dashPrefix} Replaced ${replaces} vars`); 93 | 94 | replaces = deobfuscateWorker(workerAst) + deobfuscateWorkerTwo(workerAst); 95 | console.log(`${dashPrefix} Deobfuscated ${replaces} variables`); 96 | 97 | cleanUp(workerAst, options.renameWorkers); 98 | console.log(`${dashPrefix} Cleaned and removed dead code`); 99 | 100 | let { code: cde } = generate(workerAst); 101 | let outputWorker = `${outputFile.replace('.out.js', '-worker')}-${workerName}.out.js`; 102 | fs.writeFileSync(outputWorker, cde); 103 | console.log(`${dashPrefix} Saved to ${outputWorker}`); 104 | dashCount++; // Increase the number of dashes for the next iteration 105 | } 106 | console.log(` | ---> Cleaned workers`); 107 | } 108 | 109 | let { code: newCode } = generate(ast); 110 | fs.writeFileSync(outputFile, newCode); 111 | console.log(` | ---> Saved to ${outputFile}`); 112 | console.log(" | ================= | hCaptcha hsv Deobfuscator | ================= |") 113 | }; 114 | 115 | run(); 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hCaptcha hsv Deobfuscator 2 | 3 | Welcome to the `hCaptcha hsv Deobfuscator` project! This is a Node.js project utilizing Babel AST to fully deobfuscate the hsv hCaptcha file. 4 | 5 | Remember, this is a deobfuscator, not a solver. This project is intended to help you understand how the hCaptcha script works, not to solve captchas for you, have fun studying the code and learning how it works. 6 | 7 | This is a quick project I've done in couple of hours, so it's not perfect, have fun improving it. 8 | Star the repo if you like it, it helps me a lot. 9 | 10 | 11 | ## Table of Contents 12 | - [hCaptcha hsv Deobfuscator](#hcaptcha-hsv-deobfuscator) 13 | - [Table of Contents](#table-of-contents) 14 | - [How the hsv Script Works](#how-the-hsv-script-works) 15 | - [Obfuscation Method](#obfuscation-method) 16 | - [The list](#the-list) 17 | - [The shuffle](#the-shuffle) 18 | - [The decrypt function](#the-decrypt-function) 19 | - [Outputs](#outputs) 20 | - [hsv file](#hsv-file) 21 | - [Original](#original) 22 | - [Deobfuscated](#deobfuscated) 23 | - [Workers](#workers) 24 | - [Original](#original-1) 25 | - [Deobfuscated](#deobfuscated-1) 26 | - [How to Run](#how-to-run) 27 | - [Breakdown of Commands](#breakdown-of-commands) 28 | - [Contributing](#contributing) 29 | - [License](#license) 30 | - [My Links](#my-links) 31 | 32 | ## How the hsv Script Works 33 | In brief, the hsv script performs calculations using WebAssembly (WASM) if supported by the browser, or native JavaScript otherwise. 34 | 35 | These calculations are vital for computing the solution based on the captcha's input and various other variables. 36 | 37 | Additionally, the script retrieves browser details and loads three different workers. 38 | 39 | These workers are embedded within the `hsv.js` file as base64 strings, each obfuscated differently from the hsv file itself and using hexadecimal strings. 40 | 41 | ## Obfuscation Method 42 | The hCaptcha files uses an obfuscation technique referred to as `Encoded Closure Obfuscation`. 43 | 44 | This method combines closure scopes with encoded strings and array manipulations, creating a complex layer of obfuscation that conceals the underlying logic and functionality of the code. 45 | 46 | Is easier to clean instead of explaining :D 47 | 48 | ### The list 49 | ```js 50 | 51 | function eI() { 52 | var A = ["Dg9W", "mZy5"]; 53 | return (eI = function() { 54 | return A 55 | } 56 | )() 57 | } 58 | ``` 59 | 60 | ### The shuffle 61 | ```js 62 | 63 | !function(A, I) { 64 | for (var g = 716, B = 704, Q = 496, C = r, E = A(); ; ) 65 | try { 66 | if (514060 === -parseInt(C(g)) / 1 + parseInt(C(593)) / 2 + -parseInt(C(628)) / 3 + -parseInt(C(B)) / 4 + parseInt(C(607)) / 5 + parseInt(C(426)) / 6 * (parseInt(C(614)) / 7) + parseInt(C(Q)) / 8) 67 | break; 68 | E.push(E.shift()) 69 | } catch (A) { 70 | E.push(E.shift()) 71 | } 72 | }(eI); 73 | ``` 74 | 75 | ### The decrypt function 76 | ```js 77 | 78 | function r(A, I) { 79 | var g = eI(); 80 | return r = function(I, B) { 81 | var Q = g[I -= 102]; 82 | if (void 0 === r.KcMpYy) { 83 | r.cLeSYk = function(A) { 84 | for (var I, g, B = "", Q = "", C = 0, E = 0; g = A.charAt(E++); ~g && (I = C % 4 ? 64 * I + g : g, 85 | C++ % 4) ? B += String.fromCharCode(255 & I >> (-2 * C & 6)) : 0) 86 | g = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=".indexOf(g); 87 | for (var D = 0, i = B.length; D < i; D++) 88 | Q += "%" + ("00" + B.charCodeAt(D).toString(16)).slice(-2); 89 | return decodeURIComponent(Q) 90 | } 91 | , 92 | A = arguments, 93 | r.KcMpYy = !0 94 | } 95 | var C = I + g[0] 96 | , E = A[C]; 97 | return E ? Q = E : (Q = r.cLeSYk(Q), 98 | A[C] = Q), 99 | Q 100 | } 101 | , 102 | r(A, I) 103 | } 104 | ``` 105 | 106 | ## Outputs 107 | Here are the outputs of the deobfuscation process for the hsv file and one of the three workers. 108 | 109 | ### hsv file 110 | #### Original 111 | ![Original hsv file](./img/hsv-original.png) 112 | #### Deobfuscated 113 | ![Deobfuscated hsv file](./img/hsv-output.png) 114 | 115 | ### Workers 116 | The Workers files uses two layers of this obfuscation, while the hsv file uses just one. 117 | #### Original 118 | ![Original workers file](./img/workers-original.png) 119 | #### Deobfuscated 120 | ![Deobfuscated workers file](./img/workers-output.png) 121 | 122 | 123 | 124 | ## How to Run 125 | Before running the script, ensure all the necessary packages are installed by running the following command: 126 | ```bash 127 | npm install 128 | ``` 129 | 130 | **Run Command:** 131 | ```bash 132 | node main.js [inputFile.js] [outputFile.txt] [--extract-wasm][--wasm-file=mywasmfile.wasm] [--clean-workers] [--rename-workers] [--save-workers] 133 | ``` 134 | - Default input file is `hsv.js` 135 | - Default output file is `hsv.out.js` 136 | 137 | ### Breakdown of Commands 138 | - `--extract-wasm`: Extracts the base64 string containing the WASM script into a separate file to prevent IDE overloading. 139 | - `--wasm-file`: Optional, specifies the name for the WASM file. Default is `wasm.txt`. 140 | - `--clean-workers`: Saves the three workers into separate files, deobfuscated, with their respective variable names. 141 | - `--rename-workers`: Renames all functions and variables (e.g., `var1`, `var2`, `func1`, `func2`) for readability, replacing the original hexadecimal names. 142 | - `--save-workers`: Saves the original (obfuscated) workers scripts. 143 | 144 | ## Contributing 145 | Contributions are welcome! For significant changes, please open an issue first to discuss what you would like to change. 146 | 147 | ## License 148 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 149 | 150 | ## My Links 151 | - [Website](https://glizzykingdreko.github.io) 152 | - [GitHub](https://github.com/glizzykingdreko) 153 | - [Twitter](https://mobile.twitter.com/glizzykingdreko) 154 | - [Medium](https://medium.com/@glizzykingdreko) 155 | - [Email](mailto:glizzykingdreko@protonmail.com) -------------------------------------------------------------------------------- /transformers/deobfuscateWorker.js: -------------------------------------------------------------------------------- 1 | const traverse = require("@babel/traverse").default; 2 | const { default: generate } = require("@babel/generator"); 3 | const types = require("@babel/types"); 4 | const vm = require("vm"); 5 | 6 | let deobfuscateWorker = (ast) => { 7 | let context = vm.createContext({}); 8 | let matches = 0; 9 | 10 | // extract list func 11 | let encodedList; 12 | traverse(ast, { 13 | FunctionDeclaration(path) { 14 | if ( 15 | path.node.body.body.length === 3 && 16 | types.isVariableDeclaration(path.node.body.body[0]) && 17 | types.isExpressionStatement(path.node.body.body[1]) && 18 | types.isReturnStatement(path.node.body.body[2]) 19 | ) { 20 | encodedList = path.node.id.name; 21 | vm.runInContext(generate(path.node).code, context); 22 | path.remove(); 23 | path.stop(); 24 | } 25 | } 26 | }); 27 | 28 | let decryptFunction; 29 | // extract decrypt func 30 | traverse(ast, { 31 | FunctionDeclaration(path) { 32 | if ( 33 | path.node.body.body.length === 2 && 34 | types.isVariableDeclaration(path.node.body.body[0]) && 35 | types.isReturnStatement(path.node.body.body[1]) && 36 | path.node.body.body[0].declarations[0].init.callee.name === encodedList 37 | ) { 38 | decryptFunction = path.node.id.name; 39 | vm.runInContext(generate(path.node).code, context); 40 | path.remove(); 41 | path.stop(); 42 | } 43 | } 44 | }) 45 | 46 | // shuffle shit 47 | traverse(ast, { 48 | CallExpression(path) { 49 | if ( 50 | path.node.arguments.length === 2 && 51 | types.isIdentifier(path.node.arguments[0]) && 52 | types.isNumericLiteral(path.node.arguments[1]) && 53 | path.node.arguments[0].name === encodedList 54 | ) { 55 | let shuffler = generate(path.node.callee).code; 56 | vm.runInContext(`let shuffler = ${shuffler}; shuffler(${encodedList}, ${path.node.arguments[1].value})`, context); 57 | path.remove(); 58 | path.stop(); 59 | } 60 | } 61 | }); 62 | 63 | traverse(ast, { 64 | CallExpression(path) { 65 | if ( 66 | path.node.callee.type === "Identifier" && 67 | path.node.arguments.length === 1 && 68 | types.isNumericLiteral(path.node.arguments[0]) && 69 | path.node.arguments[0].value > 3 70 | ) { 71 | try { 72 | let output = vm.runInContext(`${decryptFunction}(${path.node.arguments[0].value})`, context); 73 | path.replaceWith( 74 | types.stringLiteral(output) 75 | ) 76 | matches++; 77 | } catch (e) { 78 | } 79 | } 80 | } 81 | }) 82 | 83 | return matches; 84 | } 85 | 86 | 87 | let deobfuscateWorkerTwo = (ast) => { 88 | let context = vm.createContext({}); 89 | let matches = 0; 90 | 91 | // extract list func 92 | let encodedList; 93 | traverse(ast, { 94 | FunctionDeclaration(path) { 95 | if ( 96 | path.node.body.body.length === 2 && 97 | types.isVariableDeclaration(path.node.body.body[0]) && 98 | path.node.body.body[0].declarations.length === 2 && 99 | types.isReturnStatement(path.node.body.body[1]) 100 | ) { 101 | encodedList = path.node.id.name; 102 | vm.runInContext(generate(path.node).code, context); 103 | path.remove(); 104 | path.stop(); 105 | } 106 | } 107 | }); 108 | 109 | let decryptFunction; 110 | // extract decrypt func 111 | traverse(ast, { 112 | FunctionDeclaration(path) { 113 | if ( 114 | path.node.body.body.length === 2 && 115 | types.isVariableDeclaration(path.node.body.body[0]) && 116 | types.isReturnStatement(path.node.body.body[1]) && 117 | path.node.body.body[0].declarations[0].init && 118 | types.isCallExpression(path.node.body.body[0].declarations[0].init) && 119 | path.node.body.body[0].declarations[0].init.callee.name === encodedList 120 | ) { 121 | decryptFunction = path.node.id.name; 122 | vm.runInContext(generate(path.node).code, context); 123 | path.remove(); 124 | path.stop(); 125 | } 126 | } 127 | }) 128 | 129 | // shuffle shit 130 | traverse(ast, { 131 | UnaryExpression(path) { 132 | if ( 133 | types.isCallExpression(path.node.argument) && 134 | path.node.argument.arguments.length === 1 && 135 | types.isIdentifier(path.node.argument.arguments[0]) && 136 | path.node.argument.arguments[0].name === encodedList 137 | ) { 138 | let shuffler = generate(path.node.callee).code; 139 | vm.runInContext(shuffler, context); 140 | path.remove(); 141 | path.stop(); 142 | } 143 | } 144 | }); 145 | 146 | traverse(ast, { 147 | CallExpression(path) { 148 | if ( 149 | path.node.callee.type === "Identifier" && 150 | path.node.arguments.length === 1 && 151 | types.isNumericLiteral(path.node.arguments[0]) && 152 | path.node.arguments[0].value > 3 153 | ) { 154 | try { 155 | let output = vm.runInContext(`${decryptFunction}(${path.node.arguments[0].value})`, context); 156 | path.replaceWith( 157 | types.stringLiteral(output) ? types.stringLiteral(output) : types.numericLiteral(output) 158 | ) 159 | matches++; 160 | } catch (e) { 161 | } 162 | } 163 | } 164 | }) 165 | 166 | return matches; 167 | } 168 | 169 | module.exports = {deobfuscateWorker, deobfuscateWorkerTwo}; --------------------------------------------------------------------------------