#!/usr/bin/env node
'use strict'
/**
* Keet-Core-Hyperdb Deobfuscator
*
* Handles obfuscator.io pattern:
* 1. String array function -> function _0xNAME() { return [...strings...]; }
* 2. Rotation IIFE -> (function(arr, checksum) { while(!![]) { push/shift } })(arrayFunc, N)
* 3. Base decoder -> function _0xNAME(a, b) { arr = arrayFunc(); a = a - OFFSET; return arr[a]; }
* 4. Helper chains -> function _0xNAME(a,b,c,d) { return _0xBASE(expr, expr); }
* 5. Complex helpers -> function _0xNAME(a,b,c,d) { var C = {...}; return _0xBASE(expr); }
* 6. Anti-debug wrappers -> self-defending + console protection IIFEs
*
* Usage: node deobfuscate.cjs <input.js> [output.js]
* node deobfuscate.cjs --batch <dir> <outdir> (process all .js files)
*/
const fs = require('fs')
const vm = require('vm')
const path = require('path')
// -- CLI ----------------------------------------------------------------------
const args = process.argv.slice(2)
if (args[0] === '--batch') {
batchMode(args[1], args[2])
} else if (args[0] === '--help' || args.length === 0) {
console.error('Usage: node deobfuscate.cjs <input.js> [output.js]')
console.error(' node deobfuscate.cjs --batch <input-dir> <output-dir>')
process.exit(args.length === 0 ? 1 : 0)
} else {
singleFile(args[0], args[1])
}
function singleFile (inputFile, outputFile) {
const code = fs.readFileSync(inputFile, 'utf8')
const result = deobfuscate(code, path.basename(inputFile))
if (outputFile) {
fs.mkdirSync(path.dirname(outputFile), { recursive: true })
fs.writeFileSync(outputFile, result)
console.error(` -> ${outputFile}`)
} else {
process.stdout.write(result)
}
}
function batchMode (inputDir, outputDir) {
if (!inputDir || !outputDir) {
console.error('Usage: node deobfuscate.cjs --batch <input-dir> <output-dir>')
process.exit(1)
}
const files = findJSFiles(inputDir)
console.error(`Found ${files.length} JS files in ${inputDir}`)
let success = 0
let skipped = 0
let failed = 0
for (const file of files) {
const rel = path.relative(inputDir, file)
const outFile = path.join(outputDir, rel)
try {
const code = fs.readFileSync(file, 'utf8')
// Skip non-obfuscated files (no _0x pattern)
if (!code.includes('_0x') || !code.includes('while(!![])')) {
fs.mkdirSync(path.dirname(outFile), { recursive: true })
fs.writeFileSync(outFile, code)
skipped++
continue
}
console.error(`\nDeobfuscating: ${rel}`)
const result = deobfuscate(code, rel)
fs.mkdirSync(path.dirname(outFile), { recursive: true })
fs.writeFileSync(outFile, result)
success++
} catch (e) {
console.error(` FAILED: ${rel} - ${e.message}`)
// Write original on failure
try {
fs.mkdirSync(path.dirname(outFile), { recursive: true })
fs.copyFileSync(file, outFile)
} catch (_) {}
failed++
}
}
console.error(`\nDone: ${success} deobfuscated, ${skipped} clean, ${failed} failed`)
}
function findJSFiles (dir) {
const results = []
const entries = fs.readdirSync(dir, { withFileTypes: true })
for (const entry of entries) {
const full = path.join(dir, entry.name)
if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.git') {
results.push(...findJSFiles(full))
} else if (entry.isFile() && entry.name.endsWith('.js')) {
results.push(full)
}
}
return results
}
// -- Main Pipeline ------------------------------------------------------------
function deobfuscate (code, filename) {
// Step 1: Extract string array function
const arrayFunc = extractStringArrayFunc(code)
if (!arrayFunc) {
console.error(' SKIP: No string array function found (not obfuscated?)')
return formatCode(code)
}
console.error(` Array func: ${arrayFunc.name}`)
// Step 2: Extract base decoder function
const baseDecoder = extractBaseDecoder(code, arrayFunc.name)
if (!baseDecoder) {
console.error(' SKIP: No base decoder found')
return formatCode(code)
}
console.error(` Base decoder: ${baseDecoder.name}`)
// Step 3: Extract rotation IIFE
const rotationIIFE = extractRotationIIFE(code)
// Step 4: Bootstrap the VM
const ctx = bootstrapVM(arrayFunc, baseDecoder, rotationIIFE)
// Step 5: Extract ALL constants from the file
const constants = extractAllConstants(code)
registerConstants(ctx, constants)
// Step 6: Extract ALL helper functions (simple + complex)
const helpers = extractAllHelpers(code, baseDecoder.name)
const registered = registerHelpers(ctx, helpers)
console.error(` Helpers: ${registered}/${helpers.size} registered`)
// Step 7: Remove infrastructure FIRST, then replace calls in clean code
let output = code
// Remove rotation IIFE (dead code after bootstrap)
if (rotationIIFE) {
output = safeRemove(output, rotationIIFE.body)
}
// Remove anti-debug wrappers
output = removeAntiDebug(output)
// Step 8: Replace all obfuscated string calls
const stats = { replacements: 0, failures: 0 }
output = replaceAllCalls(output, ctx, helpers, baseDecoder.name, stats)
console.error(` Replaced: ${stats.replacements} strings (${stats.failures} unresolved)`)
// Step 9: Remove obfuscation infrastructure definitions
output = safeRemove(output, arrayFunc.body)
output = safeRemove(output, baseDecoder.body)
for (const [, helper] of helpers) {
if (helper.registered) {
output = safeRemove(output, helper.body)
}
}
// Step 10: Clean orphaned constants (only if name no longer appears in code)
output = removeOrphanedConstants(output)
// Step 11: Restore property access, merge strings, and format
output = mergeStringConcat(output)
output = restorePropertyAccess(output)
output = cleanupPunctuation(output)
output = formatCode(output)
return output
}
// -- Step 1: String Array Function -------------------------------------------
function extractStringArrayFunc (code) {
// Find: function _0xNAME() { (const|var|let) _0xLOCAL = [...]; _0xNAME = function() { return _0xLOCAL; }; return _0xNAME(); }
// The array can be very large and contain ] chars, so we match the function name first then find the body
// Find the function name
const nameMatch = code.match(
/function (_0x[0-9a-f]+)\(\)\{(?:const|var|let) (_0x[0-9a-f]+)=\[/
)
if (!nameMatch) return null
const name = nameMatch[1]
const localVar = nameMatch[2]
// Find the complete function body by looking for the closing pattern
const closingPattern = name + '=function(){return ' + localVar + ';};return ' + name + '();}'
const closingIdx = code.indexOf(closingPattern)
if (closingIdx === -1) return null
// Walk backwards from nameMatch to find 'function' keyword
const funcStart = code.lastIndexOf('function ' + name, nameMatch.index + 10)
if (funcStart === -1) return null
const body = code.substring(funcStart, closingIdx + closingPattern.length)
return { name, body }
}
// -- Step 2: Base Decoder ----------------------------------------------------
function extractBaseDecoder (code, arrayFuncName) {
const esc = escapeRegex(arrayFuncName)
// Try exact pattern first
const exact = code.match(new RegExp(
'(function (_0x[0-9a-f]+)\\((_0x[0-9a-f]+),(_0x[0-9a-f]+)\\)\\{' +
'(?:const|var|let) (_0x[0-9a-f]+)=' + esc + '\\(\\);' +
'return \\2=function\\((_0x[0-9a-f]+),(_0x[0-9a-f]+)\\)\\{' +
'\\6=\\6-\\(?[^;]+\\)?;' +
'(?:let|var|const) (_0x[0-9a-f]+)=\\5\\[\\6\\];' +
'return \\8;\\}' +
',\\2\\(\\3,\\4\\);\\})'
))
if (exact) return { name: exact[2], body: exact[1] }
// Fallback: find function that references arrayFunc and self-reassigns
const fallback = code.match(new RegExp(
'(function (_0x[0-9a-f]+)\\((_0x[0-9a-f]+),(_0x[0-9a-f]+)\\)\\{' +
'(?:const|var|let) [^=]+=' + esc + '\\(\\);' +
'return \\2=function[^}]+\\}' +
',\\2\\(\\3,\\4\\);\\})'
))
if (fallback) return { name: fallback[2], body: fallback[1] }
return null
}
// -- Step 3: Rotation IIFE --------------------------------------------------
function extractRotationIIFE (code) {
// Find the (function(a,b){ ... while(!![]) { try { ... push(shift) ... } } })(arrayFunc, checksum)
// This is always near the start of the file
const marker = 'while(!![])'
const whileIdx = code.indexOf(marker)
if (whileIdx === -1) return null
// Walk backwards to find the opening (function(
let searchStart = whileIdx
let iifeFuncStart = -1
for (let i = searchStart; i >= 0; i--) {
if (code.substring(i, i + 10) === '(function(') {
iifeFuncStart = i
break
}
}
if (iifeFuncStart === -1) return null
// Find the matching ) for the outer ( at iifeFuncStart
const outerEnd = findMatchingParen(code, iifeFuncStart)
if (outerEnd === -1) return null
// Include the trailing semicolon if present
let endIdx = outerEnd + 1
if (code[endIdx] === ';') endIdx++
const body = code.substring(iifeFuncStart, endIdx)
// Verify it has push/shift
if (!body.includes("'push'") && !body.includes("['push']")) {
// Some use direct property access
if (!body.includes('.push(') && !body.includes('.shift()')) return null
}
return { body, startIdx: iifeFuncStart, endIdx }
}
// -- Step 4: Bootstrap VM ----------------------------------------------------
function bootstrapVM (arrayFunc, baseDecoder, rotationIIFE) {
const ctx = vm.createContext({
parseInt,
parseFloat,
isNaN,
isFinite,
String,
Number,
Boolean,
Array,
Object,
RegExp,
Math,
Date,
JSON,
console: { log () {}, error () {}, warn () {}, info () {} },
setTimeout: () => {},
setInterval: () => {},
clearTimeout: () => {},
clearInterval: () => {}
})
// Execute string array function
vm.runInContext(arrayFunc.body, ctx)
// Execute base decoder
vm.runInContext(baseDecoder.body, ctx)
// Execute rotation IIFE
if (rotationIIFE) {
try {
vm.runInContext(rotationIIFE.body, ctx, { timeout: 5000 })
} catch (e) {
console.error(` WARNING: Rotation IIFE failed: ${e.message}`)
}
}
// Quick sanity test
let tested = false
for (let idx = 0x40; idx < 0x200 && !tested; idx++) {
try {
const r = vm.runInContext(`${baseDecoder.name}(${idx})`, ctx)
if (typeof r === 'string') {
console.error(` VM OK: ${baseDecoder.name}(0x${idx.toString(16)}) = "${r.substring(0, 40)}"`)
tested = true
}
} catch (_) {}
}
return ctx
}
// -- Step 5: Constants -------------------------------------------------------
function extractAllConstants (code) {
const constants = new Map()
// Match constant objects with hex-only values
// These appear in two forms:
// 1. (const|var|let) NAME = { _0xKEY: 0xVAL, ... }
// 2. ,NAME = { _0xKEY: 0xVAL, ... } (comma-separated in same declaration)
// Also match inside function bodies (scoped constants)
const re = /(?:(?:const|var|let|,)\s*)(_0x[0-9a-f]+)=\{((?:_0x[0-9a-f]+:0x[0-9a-f]+,?)+)\}/g
let m
while ((m = re.exec(code)) !== null) {
const obj = {}
const kvRe = /(_0x[0-9a-f]+):(0x[0-9a-f]+)/g
let kv
while ((kv = kvRe.exec(m[2])) !== null) {
obj[kv[1]] = parseInt(kv[2], 16)
}
constants.set(m[1], { name: m[1], obj, decl: m[0] })
}
return constants
}
function registerConstants (ctx, constants) {
for (const [name, c] of constants) {
try {
const entries = Object.entries(c.obj).map(([k, v]) => `'${k}':${v}`).join(',')
vm.runInContext(`var ${name} = {${entries}};`, ctx)
} catch (_) {}
}
}
// -- Step 6: Helpers ---------------------------------------------------------
function extractAllHelpers (code, baseDecoderName) {
const helpers = new Map()
// Simple helpers: function NAME(params) { return TARGET(args); }
const simpleRe = /function (_0x[0-9a-f]+)\(([^)]+)\)\{return (_0x[0-9a-f]+)\(([^)]+)\);\}/g
let m
while ((m = simpleRe.exec(code)) !== null) {
helpers.set(m[1], { name: m[1], body: m[0], registered: false })
}
// Complex helpers: function NAME(params) { var/const C = {...}; return TARGET(args); }
const complexRe = /function (_0x[0-9a-f]+)\(([^)]+)\)\{(?:var|const|let) (_0x[0-9a-f]+)=\{((?:_0x[0-9a-f]+:0x[0-9a-f]+,?)+)\};return (_0x[0-9a-f]+)\(([^)]+)\);\}/g
while ((m = complexRe.exec(code)) !== null) {
if (!helpers.has(m[1])) {
helpers.set(m[1], { name: m[1], body: m[0], registered: false })
}
}
return helpers
}
function registerHelpers (ctx, helpers) {
let registered = 0
let lastCount = -1
// Multi-pass: helpers may depend on other helpers
for (let pass = 0; pass < 15 && registered !== lastCount; pass++) {
lastCount = registered
for (const [, h] of helpers) {
if (h.registered) continue
try {
vm.runInContext(h.body, ctx, { timeout: 1000 })
h.registered = true
registered++
} catch (_) {}
}
}
return registered
}
// -- Step 7: Anti-Debug Removal ----------------------------------------------
function removeAntiDebug (code) {
let output = code
// Pattern: Self-defending + console protection wrapper
// var FACTORY=(function(){var a=!![];return function(b,c){...apply...};}()),
// RESULT=FACTORY(this,function(){...toString()...constructor...search...});
// RESULT();
//
// The FACTORY uses !![] (true) and the RESULT contains toString/constructor patterns.
// We detect FACTORY, then find RESULT, then find RESULT() activation.
// Find factory: (function(){var X=!![];return function(
const factoryRe = /(?:const|var|let) (_0x[0-9a-f]+)=\(function\(\)\{(?:let|var) _0x[0-9a-f]+=!!\[\];return function\(/g
let m
while ((m = factoryRe.exec(output)) !== null) {
const factoryName = m[1]
const declStart = m.index
// Find: ,RESULT=FACTORY(this,function(){...}); or similar
// The RESULT variable is declared in the same var statement
const afterFactory = output.substring(declStart)
const resultRe = new RegExp(',(_0x[0-9a-f]+)=' + escapeRegex(factoryName) + '\\(this,function\\(\\)\\{')
const resultMatch = resultRe.exec(afterFactory)
if (resultMatch) {
const resultName = resultMatch[1]
// Find the activation: RESULT();
const activationRe = new RegExp(escapeRegex(resultName) + '\\(\\);?')
const activationMatch = activationRe.exec(afterFactory)
if (activationMatch) {
const removeEnd = declStart + activationMatch.index + activationMatch[0].length
output = output.substring(0, declStart) + output.substring(removeEnd)
factoryRe.lastIndex = declStart
continue
}
}
// Fallback: just try to remove from declStart to the first standalone _0x();
// after the factory pattern
const fallbackRe = /(_0x[0-9a-f]+)\(\);/g
fallbackRe.lastIndex = declStart + m[0].length
const fbMatch = fallbackRe.exec(output)
if (fbMatch && fbMatch.index < declStart + 5000) {
const removeEnd = fbMatch.index + fbMatch[0].length
output = output.substring(0, declStart) + output.substring(removeEnd)
factoryRe.lastIndex = declStart
}
}
return output
}
// -- Step 8: Replace Calls ---------------------------------------------------
function replaceAllCalls (code, ctx, helpers, baseDecoderName, stats) {
const resolvable = new Set([baseDecoderName])
for (const [name, h] of helpers) {
if (h.registered) resolvable.add(name)
}
// Match calls with numeric/hex literal args or constant obj references
// Each arg can be: -?0x[hex], -?[dec], -?_0xNAME._0xKEY
const ARG = '(?:[-]?0x[0-9a-f]+|[-]?\\d+|[-]?_0x[0-9a-f]+\\._0x[0-9a-f]+)'
const callRe = new RegExp('(_0x[0-9a-f]+)\\((' + ARG + '(?:,' + ARG + ')*)\\)', 'g')
return code.replace(callRe, (full, funcName, argsStr) => {
if (!resolvable.has(funcName)) return full
try {
const result = vm.runInContext(`${funcName}(${argsStr})`, ctx, { timeout: 500 })
if (typeof result === 'string') {
stats.replacements++
return JSON.stringify(result)
}
return full
} catch (_) {
stats.failures++
return full
}
})
}
// -- Step 9-11: Cleanup ------------------------------------------------------
function safeRemove (code, fragment) {
const idx = code.indexOf(fragment)
if (idx === -1) return code
return code.substring(0, idx) + code.substring(idx + fragment.length)
}
function removeOrphanedConstants (code) {
// Remove constant object declarations whose name doesn't appear elsewhere
return code.replace(
/(?:const|var|let) (_0x[0-9a-f]+)=\{(?:_0x[0-9a-f]+:0x[0-9a-f]+,?)+\};?/g,
(match, name) => {
// Count occurrences of the name in the code EXCLUDING this declaration
const without = code.replace(match, '')
return without.includes(name) ? match : ''
}
)
}
function restorePropertyAccess (code) {
// obj["property"] or obj['property'] -> obj.property (for valid JS identifiers)
return code.replace(/\[["']([a-zA-Z_$][a-zA-Z0-9_$]*)["']\]/g, '.$1')
}
function mergeStringConcat (code) {
// Merge adjacent string literals: "abc" + "def" -> "abcdef"
// Also handles: "abc" + 'def', 'abc' + "def", 'abc' + 'def'
let prev = ''
let result = code
// Keep merging until stable (handles chains of 3+ concatenations)
while (result !== prev) {
prev = result
result = result.replace(/(["'])((?:[^"'\\]|\\.)*)(\1)\s*\+\s*(["'])((?:[^"'\\]|\\.)*)(\4)/g,
(_, q1, s1, _q1, q2, s2, _q2) => {
return '"' + s1 + s2 + '"'
}
)
}
return result
}
function cleanupPunctuation (code) {
let output = code
.replace(/,{2,}/g, ',')
.replace(/;{2,}/g, ';')
.replace(/\n{3,}/g, '\n\n')
// Remove leading commas after { or ; or newline (left behind from removed constants)
output = output.replace(/(\{|;|\n)\s*,\s*/g, '$1')
// Remove leading commas at start of file
output = output.replace(/^[,;\s]+/, '')
// Clean up leftover hex arithmetic in simple cases: 0xA+0xB-0xC -> evaluate
output = output.replace(/\b(-?0x[0-9a-f]+(?:\s*[+\-*]\s*-?0x[0-9a-f]+)+)\b/g, (match) => {
try {
const result = Function('return ' + match)()
if (typeof result === 'number' && Number.isInteger(result)) {
return result >= 0 ? '0x' + result.toString(16) : '-0x' + (-result).toString(16)
}
} catch (_) {}
return match
})
return output
}
function formatCode (code) {
let output = addNewlines(code)
output = indent(output)
return output
}
function addNewlines (code) {
const result = []
let inStr = false
let strCh = null
let esc = false
let pDepth = 0
let bDepth = 0
let sqDepth = 0
for (let i = 0; i < code.length; i++) {
const ch = code[i]
if (esc) { result.push(ch); esc = false; continue }
if (ch === '\\' && inStr) { result.push(ch); esc = true; continue }
if (inStr) { result.push(ch); if (ch === strCh) inStr = false; continue }
if (ch === '"' || ch === "'" || ch === '`') { inStr = true; strCh = ch; result.push(ch); continue }
if (ch === '(') pDepth++
if (ch === ')') pDepth--
if (ch === '{') bDepth++
if (ch === '}') bDepth--
if (ch === '[') sqDepth++
if (ch === ']') sqDepth--
result.push(ch)
// Newline at statement boundaries (not inside parens/brackets)
if (pDepth === 0 && sqDepth === 0) {
if (ch === ';') result.push('\n')
if (ch === '{') result.push('\n')
if (ch === '}') {
const after = code.substring(i + 1, i + 20).match(/^\s*(else|catch|finally|[),;])/)?.[1]
if (!after || after === ';') result.push('\n')
}
}
}
return result.join('')
}
function indent (code) {
const lines = code.split('\n')
const out = []
let level = 0
for (const raw of lines) {
const line = raw.trim()
if (!line) continue
const opens = countOutsideStrings(line, '{')
const closes = countOutsideStrings(line, '}')
if (line[0] === '}' || line[0] === ']') level = Math.max(0, level - 1)
out.push(' '.repeat(level) + line)
level += opens - closes
if (level < 0) level = 0
}
return out.join('\n') + '\n'
}
function countOutsideStrings (line, char) {
let count = 0
let inStr = false
let strCh = null
let esc = false
for (const ch of line) {
if (esc) { esc = false; continue }
if (ch === '\\' && inStr) { esc = true; continue }
if (inStr) { if (ch === strCh) inStr = false; continue }
if (ch === '"' || ch === "'" || ch === '`') { inStr = true; strCh = ch; continue }
if (ch === char) count++
}
return count
}
// -- Utilities ---------------------------------------------------------------
function findMatchingParen (code, openIdx) {
let depth = 0
let inStr = false
let strCh = null
let esc = false
for (let i = openIdx; i < code.length; i++) {
const ch = code[i]
if (esc) { esc = false; continue }
if (ch === '\\' && inStr) { esc = true; continue }
if (inStr) { if (ch === strCh) inStr = false; continue }
if (ch === '"' || ch === "'" || ch === '`') { inStr = true; strCh = ch; continue }
if (ch === '(') depth++
if (ch === ')') {
depth--
if (depth === 0) return i
}
}
return -1
}
function escapeRegex (s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}