214 lines
6.9 KiB
JavaScript
214 lines
6.9 KiB
JavaScript
'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();
|