231 lines
7.2 KiB
JavaScript
231 lines
7.2 KiB
JavaScript
/**
|
||
* 反混淆并美化原版 extension/out 下的核心文件,输出到 codexfanbianyi/extension/out。
|
||
* 策略:动态执行字符串数组 + 解码函数,解出所有形如 _0x****(0x123,'key') 的调用并替换为明文,再用 js-beautify 排版。
|
||
*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const vm = require('vm');
|
||
const beautify = require('js-beautify').js;
|
||
|
||
const baseDir = __dirname;
|
||
const inputBase = path.join(baseDir, '原版本', 'extension', 'out');
|
||
const outputBase = path.join(baseDir, 'codexfanbianyi', 'extension', 'out');
|
||
|
||
const targets = [
|
||
'webview/provider.js',
|
||
'extension.js',
|
||
'utils/account.js',
|
||
'utils/sqlite.js',
|
||
'api/client.js'
|
||
];
|
||
|
||
const mapPaths = {
|
||
'webview/provider.js': path.join(baseDir, 'provider_decoded_map.json'),
|
||
'extension.js': path.join(baseDir, 'extension_decoded_map.json'),
|
||
'utils/account.js': path.join(baseDir, 'account_decoded_map.json'),
|
||
'utils/sqlite.js': path.join(baseDir, 'sqlite_decoded_map.json'),
|
||
'api/client.js': path.join(baseDir, 'client_decoded_map.json')
|
||
};
|
||
|
||
function ensureDir(dir) {
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
}
|
||
|
||
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,
|
||
setTimeout,
|
||
clearTimeout,
|
||
setInterval,
|
||
clearInterval,
|
||
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 {
|
||
// 1) 先只注入字符串数组函数 + 解码函数(避免执行到 exports/require 等顶层逻辑)
|
||
vm.runInContext(`${arrayFunc.code}\n${decoderFunc.code}`, sandbox, { timeout: 4000 });
|
||
|
||
// 2) 执行“数组旋转/IIFE”等启动代码(通常位于文件开头,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') {
|
||
console.warn(` 解码器 ${decoderFunc.name} 不是函数`);
|
||
return null;
|
||
}
|
||
return decoder.bind(sandbox);
|
||
} catch (e) {
|
||
console.warn(` 解码器初始化失败: ${e.message}`);
|
||
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 replaceCalls(code, decoder) {
|
||
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 replacedCount = 0;
|
||
for (const [call, literal] of replacements) {
|
||
const newCode = code.split(call).join(literal);
|
||
if (newCode !== code) {
|
||
replacedCount++;
|
||
code = newCode;
|
||
}
|
||
}
|
||
return { code, replacedCount, found: replacements.size };
|
||
}
|
||
|
||
function replaceWithMap(code, relPath) {
|
||
const mapPath = mapPaths[relPath];
|
||
if (!mapPath || !fs.existsSync(mapPath)) {
|
||
return { code, replacedCount: 0, entries: 0 };
|
||
}
|
||
const decodeMap = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
|
||
let replacedCount = 0;
|
||
for (const [pattern, decoded] of Object.entries(decodeMap)) {
|
||
const literal = literalFor(decoded);
|
||
const newCode = code.split(pattern).join(literal);
|
||
if (newCode !== code) {
|
||
replacedCount++;
|
||
code = newCode;
|
||
}
|
||
}
|
||
return { code, replacedCount, entries: Object.keys(decodeMap).length };
|
||
}
|
||
|
||
function cleanDecoderBlocks(code) {
|
||
// 保留原始函数,避免因匹配不准破坏结构
|
||
return code;
|
||
}
|
||
|
||
function processFile(relPath) {
|
||
const inputPath = path.join(inputBase, relPath);
|
||
const outputPath = path.join(outputBase, relPath);
|
||
|
||
if (!fs.existsSync(inputPath)) {
|
||
console.warn(`跳过缺失文件: ${relPath}`);
|
||
return;
|
||
}
|
||
|
||
let code = fs.readFileSync(inputPath, 'utf8');
|
||
console.log(`\n==== 处理 ${relPath} ====`);
|
||
|
||
const mapResult = replaceWithMap(code, relPath);
|
||
if (mapResult.entries > 0) {
|
||
console.log(` 预置映射 ${mapResult.entries} 条,替换 ${mapResult.replacedCount} 处`);
|
||
code = mapResult.code;
|
||
}
|
||
|
||
const decoder = buildDecoder(code);
|
||
if (!decoder) {
|
||
console.warn(' 未找到可用的解码器,直接美化原始代码');
|
||
} else {
|
||
const result = replaceCalls(code, decoder);
|
||
console.log(` 找到 ${result.found} 个唯一调用,成功替换 ${result.replacedCount} 处`);
|
||
code = result.code;
|
||
}
|
||
|
||
code = cleanDecoderBlocks(code);
|
||
code = beautify(code, { indent_size: 4, max_preserve_newlines: 2, end_with_newline: true });
|
||
|
||
ensureDir(path.dirname(outputPath));
|
||
fs.writeFileSync(outputPath, code, 'utf8');
|
||
console.log(` 输出: ${outputPath}`);
|
||
}
|
||
|
||
function main() {
|
||
targets.forEach(processFile);
|
||
console.log('\n全部处理完成。');
|
||
}
|
||
|
||
main();
|