'use strict'; /** * 通用解混淆脚本:对指定目录下的 JS 文件批量解码字符串、清理控制流并重命名 _0x 前缀标识符。 * 使用: * node tools/auto_deobf.js * 可配置 inputDir/outputDir/mapDir/rename 见下方常量。 */ const fs = require('fs'); const path = require('path'); const vm = require('vm'); const beautify = require('js-beautify').js; const parser = require('@babel/parser'); const traverse = require('@babel/traverse').default; const generate = require('@babel/generator').default; // 配置:如有需要可修改 const inputDir = path.join(__dirname, '..', '原版本', 'extension', 'out'); const outputDir = path.join(__dirname, '..', 'codexfanbianyi', 'extension', 'out'); const mapDir = path.join(__dirname, '..'); // *_decoded_map.json 所在目录 const enableRename = true; // 是否批量重命名 _0x**** -> refN function ensureDir(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } function listJsFiles(dir) { let files = []; for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, entry.name); if (entry.isDirectory()) { files = files.concat(listJsFiles(full)); } else if (entry.isFile() && entry.name.endsWith('.js')) { files.push(full); } } return files; } function extractFunctionWithBody(code, nameRegex) { const match = nameRegex.exec(code); if (!match) return null; const start = match.index; const braceStart = code.indexOf('{', start); if (braceStart === -1) return null; let depth = 0; for (let i = braceStart; i < code.length; i++) { const ch = code[i]; if (ch === '{') depth++; else if (ch === '}') { depth--; if (depth === 0) { return { name: match[1], code: code.slice(start, i + 1), end: i + 1 }; } } } return null; } function buildSandbox() { const sandbox = { console: { log: () => {}, warn: () => {}, error: () => {} }, vip: 'cursor', Buffer, atob: (str) => Buffer.from(str, 'base64').toString('binary'), btoa: (str) => Buffer.from(str, 'binary').toString('base64'), decodeURIComponent, encodeURIComponent, parseInt, String, exports: {}, module: { exports: {} }, require: () => ({}) }; vm.createContext(sandbox); return sandbox; } function buildDecoder(code) { const arrayFunc = extractFunctionWithBody(code, /function\s+(_0x[a-f0-9]+)\s*\(\s*\)/i); const decoderFunc = extractFunctionWithBody(code, /function\s+(_0x[a-f0-9]+)\s*\(\s*_0x[a-f0-9]+\s*,\s*_0x[a-f0-9]+\s*\)/i); if (!arrayFunc || !decoderFunc) return null; const sandbox = buildSandbox(); try { // 仅执行字符串数组和解码函数,避免触发业务逻辑 vm.runInContext(`${arrayFunc.code}\n${decoderFunc.code}`, sandbox, { timeout: 4000 }); // 启动段:取 exports 之前的代码,通常包含数组旋转 const stopMarkers = ['var __createBinding', 'Object.defineProperty(exports', 'exports.', 'module.exports']; let bootstrapEnd = code.length; for (const marker of stopMarkers) { const idx = code.indexOf(marker); if (idx !== -1 && idx < bootstrapEnd) bootstrapEnd = idx; } const bootstrapCode = code.slice(0, bootstrapEnd); vm.runInContext(bootstrapCode, sandbox, { timeout: 4000 }); const decoder = sandbox[decoderFunc.name]; if (typeof decoder !== 'function') return null; return decoder.bind(sandbox); } catch { return null; } } function literalFor(decoded) { if (decoded == null) return 'undefined'; if (typeof decoded !== 'string') return JSON.stringify(decoded); if (decoded.includes('\n')) return '`' + decoded.replace(/`/g, '\\`') + '`'; return JSON.stringify(decoded); } function replaceWithMap(code, filePath) { const mapName = path.basename(filePath).replace('.js', '_decoded_map.json'); const mapPath = path.join(mapDir, mapName); if (!fs.existsSync(mapPath)) return { code, replaced: 0, entries: 0 }; const decodeMap = JSON.parse(fs.readFileSync(mapPath, 'utf8')); let replaced = 0; for (const [pattern, decoded] of Object.entries(decodeMap)) { const literal = literalFor(decoded); const newCode = code.split(pattern).join(literal); if (newCode !== code) { replaced++; code = newCode; } } return { code, replaced, entries: Object.keys(decodeMap).length }; } function replaceWithDecoder(code, decoder) { if (!decoder) return { code, replaced: 0, found: 0 }; const callPattern = /_0x[a-f0-9]+\s*\(\s*(0x[a-f0-9]+|\d+)\s*,\s*['"]([^'"]+)['"]\s*\)/gi; const replacements = new Map(); let match; while ((match = callPattern.exec(code)) !== null) { const full = match[0]; if (replacements.has(full)) continue; const index = match[1].startsWith('0x') ? parseInt(match[1], 16) : parseInt(match[1], 10); const key = match[2]; try { const decoded = decoder(index, key); if (typeof decoded === 'string') { replacements.set(full, literalFor(decoded)); } } catch { /* ignore individual decode errors */ } } let replaced = 0; for (const [call, lit] of replacements) { const newCode = code.split(call).join(lit); if (newCode !== code) { replaced++; code = newCode; } } return { code, replaced, found: replacements.size }; } function renameObfuscatedIdentifiers(code) { if (!enableRename) return code; const ast = parser.parse(code, { sourceType: 'unambiguous', plugins: ['jsx'] }); let counter = 1; const renameMap = new Map(); traverse(ast, { Identifier(path) { const name = path.node.name; if (!/^_0x[a-f0-9]+$/i.test(name)) return; const binding = path.scope.getBinding(name); if (!binding) return; if (renameMap.has(name)) return; const newName = `ref${counter++}`; binding.scope.rename(name, newName); renameMap.set(name, newName); } }); return generate(ast, { compact: false }).code; } function processFile(inFile) { const rel = path.relative(inputDir, inFile); const outFile = path.join(outputDir, rel); console.log(`\n[process] ${rel}`); let code = fs.readFileSync(inFile, 'utf8'); const mapResult = replaceWithMap(code, inFile); if (mapResult.entries > 0) console.log(` map: ${mapResult.replaced}/${mapResult.entries}`); code = mapResult.code; const decoder = buildDecoder(code); if (!decoder) { console.log(' decoder: none'); } else { const decRes = replaceWithDecoder(code, decoder); console.log(` decoder replace: ${decRes.replaced}/${decRes.found}`); code = decRes.code; } code = renameObfuscatedIdentifiers(code); code = beautify(code, { indent_size: 2, max_preserve_newlines: 2, end_with_newline: true }); ensureDir(path.dirname(outFile)); fs.writeFileSync(outFile, code, 'utf8'); console.log(` output: ${outFile}`); } function main() { const files = listJsFiles(inputDir); console.log(`Found ${files.length} JS files under ${inputDir}`); files.forEach(processFile); console.log('\nAll done.'); } main();