备份: 完整开发状态(含反混淆脚本和临时文件)
This commit is contained in:
213
tools/auto_deobf.js
Normal file
213
tools/auto_deobf.js
Normal file
@@ -0,0 +1,213 @@
|
||||
'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();
|
||||
Reference in New Issue
Block a user