/** * 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}`);