Files
hcapReverse/asset/deobfuscate.js
2026-03-11 14:21:26 +08:00

470 lines
16 KiB
JavaScript

/**
* hsw.js AST 解混淆脚本 v4
*
* 双解码器支持:
* - PW/QQ: base=311, 596条, rotation=275
* - Qg: base=438, 133条, 无旋转
*
* AST 遍历 passes:
* 1. 别名收集 (两个解码器各自的别名链)
* 2. 常量传播 + 解码替换
* 3. 属性访问简化: obj["prop"] → obj.prop
* 4. 字符串拼接折叠: "a".concat("b") → "ab"
* 5. 死代码清理
*/
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const generate = require("@babel/generator").default;
const t = require("@babel/types");
const SRC = path.join(__dirname, "hsw.js");
const OUT = path.join(__dirname, "hsw.deobfuscated.js");
const src = fs.readFileSync(SRC, "utf-8");
// ═══════════════════════════════════════════
// Phase 1: 提取两个字符串数组 + 解码
// ═══════════════════════════════════════════
console.log("[*] Phase 1: 提取字符串数组...");
function decode(encoded) {
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=";
let raw = "";
let bc = 0;
let bs = 0;
let ch;
let idx = 0;
while ((ch = encoded.charAt(idx++))) {
ch = chars.indexOf(ch);
if (~ch) {
bs = bc % 4 ? 64 * bs + ch : ch;
if (bc++ % 4)
raw += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)));
}
}
let uri = "";
for (let i = 0; i < raw.length; i++)
uri += "%" + ("00" + raw.charCodeAt(i).toString(16)).slice(-2);
return decodeURIComponent(uri);
}
// ── 解码器 1: tH / QQ / PW (base=311, 有旋转) ──
const arr1Match = src.match(
/tH\s*=\s*function\s*\(\)\s*\{\s*var\s+Ig\s*=\s*\[([\s\S]*?)\];\s*return\s*\(\s*tH/
);
if (!arr1Match) { console.error("[-] 无法提取 tH 数组"); process.exit(1); }
const raw1 = eval(`[${arr1Match[1]}]`);
const decoded1 = raw1.map((s) => { try { return decode(s); } catch { return null; } });
console.log(`[+] 解码器1 (PW/QQ): ${raw1.length} 条, base=311`);
// 暴力搜索旋转
const BASE1 = 311;
const LEN1 = decoded1.length;
const lookup1 = (rot, idx) => decoded1[((((idx - BASE1 + rot) % LEN1) + LEN1) % LEN1)];
const constraints1 = [
[814, "length"], [507, "concat"], [606, "createElement"],
[675, "canvas"], [440, "prototype"], [719, "toString"],
[874, "call"], [532, "create"], [716, "test"],
[897, "width"], [811, "height"], [873, "toDataURL"],
];
let rot1 = -1;
for (let r = 0; r < LEN1; r++) {
if (constraints1.every(([i, e]) => lookup1(r, i) === e)) { rot1 = r; break; }
}
if (rot1 === -1) { console.error("[-] PW/QQ 旋转搜索失败"); process.exit(1); }
console.log(`[+] PW/QQ 旋转偏移: ${rot1}`);
const map1 = new Map();
for (let i = BASE1; i < BASE1 + LEN1; i++) {
const v = lookup1(rot1, i);
if (v != null) map1.set(i, v);
}
// ── 解码器 2: xf / Qg (base=438, 无旋转) ──
const arr2Match = src.match(
/xf\s*=\s*function\s*\(\)\s*\{\s*var\s+Ig\s*=\s*\[([\s\S]*?)\];\s*return\s*\(\s*xf/
);
if (!arr2Match) { console.error("[-] 无法提取 xf 数组"); process.exit(1); }
const raw2 = eval(`[${arr2Match[1]}]`);
const decoded2 = raw2.map((s) => { try { return decode(s); } catch { return null; } });
console.log(`[+] 解码器2 (Qg): ${raw2.length} 条, base=438`);
const BASE2 = 438;
const map2 = new Map();
for (let i = 0; i < decoded2.length; i++) {
if (decoded2[i] != null) map2.set(i + BASE2, decoded2[i]);
}
// 验证 Qg 的一些已知值
const constraints2 = [
[441, "length"], // Qg(441) 在 wasm 段出现大量
[439, "buffer"], // Qg(439)
[453, "name"], // Qg(453)
[456, "isArray"], // Array[Qg(456)]
[457, "exec"], // regex.exec
[447, "string"], // typeof === Qg(447)
[449, "constructor"],// Ig[Qg(449)]
[462, "message"], // Ig[Qg(462)]
[463, "stack"], // Ig[Qg(463)]
[476, "set"], // cP().set()
[467, "subarray"], // Ig[Qg(467)]
];
let c2pass = 0;
for (const [idx, exp] of constraints2) {
const act = map2.get(idx);
if (act === exp) c2pass++;
else console.log(` [!] Qg(${idx}): 期望 "${exp}", 实际 "${act}"`);
}
if (c2pass < constraints2.length) {
// 需要旋转
console.log(`[!] Qg 直接映射验证 ${c2pass}/${constraints2.length}, 尝试旋转搜索...`);
const LEN2 = decoded2.length;
let rot2 = -1;
for (let r = 0; r < LEN2; r++) {
const lk = (idx) => decoded2[((((idx - BASE2 + r) % LEN2) + LEN2) % LEN2)];
if (constraints2.every(([i, e]) => lk(i) === e)) { rot2 = r; break; }
}
if (rot2 !== -1 && rot2 !== 0) {
console.log(`[+] Qg 旋转偏移: ${rot2}`);
map2.clear();
for (let i = BASE2; i < BASE2 + LEN2; i++) {
const v = decoded2[((((i - BASE2 + rot2) % LEN2) + LEN2) % LEN2)];
if (v != null) map2.set(i, v);
}
}
}
console.log(`[+] 映射表: PW/QQ=${map1.size}条, Qg=${map2.size}条, 合计=${map1.size + map2.size}`);
// ── 合并映射: 根据解码器名来分派 ──
// decoderGroup1: PW, QQ, 以及它们的别名 → 查 map1
// decoderGroup2: Qg, 以及它的别名 → 查 map2
const group1Names = new Set(["PW", "QQ"]);
const group2Names = new Set(["Qg"]);
function getMapForDecoder(name) {
if (group1Names.has(name)) return map1;
if (group2Names.has(name)) return map2;
return null;
}
// ═══════════════════════════════════════════
// Phase 2: 解析 AST
// ═══════════════════════════════════════════
console.log("[*] Phase 2: 解析 AST...");
const ast = parser.parse(src, {
sourceType: "script",
plugins: [],
errorRecovery: true,
});
console.log("[+] AST 解析完成");
// ═══════════════════════════════════════════
// Phase 3: Pass 1 — 别名收集 (两个解码器)
// ═══════════════════════════════════════════
console.log("[*] Phase 3: 收集解码器别名...");
// 追踪: var X = PW → X 属于 group1
// var Y = Qg → Y 属于 group2
// var Z = X → Z 属于 X 的 group
const allDecoders = new Set([...group1Names, ...group2Names]);
traverse(ast, {
VariableDeclarator(path) {
if (
t.isIdentifier(path.node.id) &&
t.isIdentifier(path.node.init) &&
allDecoders.has(path.node.init.name)
) {
const src = path.node.init.name;
const dst = path.node.id.name;
if (group1Names.has(src)) group1Names.add(dst);
else if (group2Names.has(src)) group2Names.add(dst);
allDecoders.add(dst);
}
},
AssignmentExpression(path) {
if (
t.isIdentifier(path.node.left) &&
t.isIdentifier(path.node.right) &&
allDecoders.has(path.node.right.name)
) {
const src = path.node.right.name;
const dst = path.node.left.name;
if (group1Names.has(src)) group1Names.add(dst);
else if (group2Names.has(src)) group2Names.add(dst);
allDecoders.add(dst);
}
},
});
// 多轮别名链传播
let prevSize;
do {
prevSize = allDecoders.size;
traverse(ast, {
noScope: true,
VariableDeclarator(path) {
if (
t.isIdentifier(path.node.id) &&
t.isIdentifier(path.node.init) &&
allDecoders.has(path.node.init.name) &&
!allDecoders.has(path.node.id.name)
) {
const s = path.node.init.name;
const d = path.node.id.name;
if (group1Names.has(s)) group1Names.add(d);
else if (group2Names.has(s)) group2Names.add(d);
allDecoders.add(d);
}
},
AssignmentExpression(path) {
if (
t.isIdentifier(path.node.left) &&
t.isIdentifier(path.node.right) &&
allDecoders.has(path.node.right.name) &&
!allDecoders.has(path.node.left.name)
) {
const s = path.node.right.name;
const d = path.node.left.name;
if (group1Names.has(s)) group1Names.add(d);
else if (group2Names.has(s)) group2Names.add(d);
allDecoders.add(d);
}
},
});
} while (allDecoders.size > prevSize);
console.log(`[+] 解码器1别名 (${group1Names.size}): ${[...group1Names].join(", ")}`);
console.log(`[+] 解码器2别名 (${group2Names.size}): ${[...group2Names].join(", ")}`);
// ═══════════════════════════════════════════
// Phase 4: Pass 2 — 常量传播 + 解码替换
// ═══════════════════════════════════════════
console.log("[*] Phase 4: 常量传播 + 字符串解码...");
let resolvedCount = 0;
let constPropCount = 0;
function resolveNumericValue(node, scopePath) {
if (t.isNumericLiteral(node)) return node.value;
if (t.isUnaryExpression(node) && node.operator === "-" && t.isNumericLiteral(node.argument)) {
return -node.argument.value;
}
if (t.isIdentifier(node)) {
const binding = scopePath.scope.getBinding(node.name);
if (binding && binding.constant && binding.path.isVariableDeclarator()) {
const init = binding.path.node.init;
if (t.isNumericLiteral(init)) return init.value;
}
// 非 constant binding 但 init 是数字且只赋值一次
if (binding && binding.path.isVariableDeclarator()) {
const init = binding.path.node.init;
if (t.isNumericLiteral(init) && !binding.constantViolations.length) {
return init.value;
}
}
}
return null;
}
traverse(ast, {
CallExpression(path) {
const callee = path.node.callee;
if (
t.isIdentifier(callee) &&
allDecoders.has(callee.name) &&
path.node.arguments.length >= 1
) {
const arg = path.node.arguments[0];
const numVal = resolveNumericValue(arg, path);
if (numVal === null) return;
const map = getMapForDecoder(callee.name);
if (!map) return;
if (map.has(numVal)) {
path.replaceWith(t.stringLiteral(map.get(numVal)));
resolvedCount++;
if (!t.isNumericLiteral(arg)) constPropCount++;
}
}
},
});
console.log(`[+] 解码替换: ${resolvedCount} (常量传播: ${constPropCount})`);
// ═══════════════════════════════════════════
// Phase 5: Pass 3 — 属性简化
// ═══════════════════════════════════════════
console.log("[*] Phase 5: 属性访问简化...");
let memberCount = 0;
const RESERVED = new Set([
"break","case","catch","continue","debugger","default","delete","do",
"else","finally","for","function","if","in","instanceof","new","return",
"switch","this","throw","try","typeof","var","void","while","with",
"class","const","enum","export","extends","import","super","implements",
"interface","let","package","private","protected","public","static","yield",
]);
traverse(ast, {
MemberExpression(path) {
if (
path.node.computed &&
t.isStringLiteral(path.node.property) &&
/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(path.node.property.value) &&
!RESERVED.has(path.node.property.value)
) {
path.node.computed = false;
path.node.property = t.identifier(path.node.property.value);
memberCount++;
}
},
});
console.log(`[+] 属性简化: ${memberCount}`);
// ═══════════════════════════════════════════
// Phase 6: Pass 4 — 字符串拼接折叠
// ═══════════════════════════════════════════
console.log("[*] Phase 6: 字符串拼接折叠...");
let concatCount = 0;
let round;
do {
round = 0;
traverse(ast, {
CallExpression(path) {
if (
t.isMemberExpression(path.node.callee) &&
!path.node.callee.computed &&
t.isIdentifier(path.node.callee.property, { name: "concat" }) &&
t.isStringLiteral(path.node.callee.object) &&
path.node.arguments.length === 1 &&
t.isStringLiteral(path.node.arguments[0])
) {
path.replaceWith(
t.stringLiteral(
path.node.callee.object.value + path.node.arguments[0].value
)
);
round++;
}
},
});
concatCount += round;
} while (round > 0);
console.log(`[+] 拼接折叠: ${concatCount}`);
// ═══════════════════════════════════════════
// Phase 7: Pass 5 — 常量折叠 (typeof, 简单比较)
// ═══════════════════════════════════════════
console.log("[*] Phase 7: 常量表达式折叠...");
let constFoldCount = 0;
traverse(ast, {
// "string" == typeof x → 保留 (运行时), 但 typeof "xxx" → "string"
UnaryExpression(path) {
if (path.node.operator === "typeof" && t.isStringLiteral(path.node.argument)) {
path.replaceWith(t.stringLiteral("string"));
constFoldCount++;
}
},
// void 0 → undefined (可选, 增加可读性)
// 暂时不做, 可能影响语义
});
console.log(`[+] 常量折叠: ${constFoldCount}`);
// ═══════════════════════════════════════════
// Phase 8: Pass 6 — 清理无用别名声明
// ═══════════════════════════════════════════
console.log("[*] Phase 8: 清理无用别名...");
let deadCount = 0;
traverse(ast, {
VariableDeclarator(path) {
if (
t.isIdentifier(path.node.id) &&
t.isIdentifier(path.node.init) &&
allDecoders.has(path.node.init.name) &&
allDecoders.has(path.node.id.name)
) {
const binding = path.scope.getBinding(path.node.id.name);
if (binding && binding.references === 0) {
path.remove();
deadCount++;
}
}
},
});
console.log(`[+] 清理: ${deadCount}`);
// ═══════════════════════════════════════════
// Phase 9: 生成输出
// ═══════════════════════════════════════════
console.log("[*] Phase 9: 生成代码...");
const output = generate(ast, {
comments: true,
compact: false,
retainLines: false,
concise: false,
}).code;
fs.writeFileSync(OUT, output, "utf-8");
const origKB = (fs.statSync(SRC).size / 1024).toFixed(1);
const newKB = (Buffer.byteLength(output) / 1024).toFixed(1);
console.log(`\n${"═".repeat(50)}`);
console.log(`[=] 解混淆完成`);
console.log(` 原始: ${origKB} KB`);
console.log(` 输出: ${newKB} KB`);
console.log(` 解码替换: ${resolvedCount} (常量传播 ${constPropCount})`);
console.log(` 属性简化: ${memberCount}`);
console.log(` 拼接折叠: ${concatCount}`);
console.log(` 常量折叠: ${constFoldCount}`);
console.log(` 死代码清理: ${deadCount}`);
console.log(` PW/QQ 别名: ${group1Names.size} | Qg 别名: ${group2Names.size}`);
console.log(` 输出: ${OUT}`);
console.log(`${"═".repeat(50)}`);
// 导出映射表
const mapOut = path.join(__dirname, "hsw.string-map.json");
const mapObj = { "decoder1_PW_QQ": {}, "decoder2_Qg": {} };
for (const [k, v] of map1) mapObj.decoder1_PW_QQ[k] = v;
for (const [k, v] of map2) mapObj.decoder2_Qg[k] = v;
fs.writeFileSync(mapOut, JSON.stringify(mapObj, null, 2), "utf-8");
console.log(` 映射: ${mapOut}`);