终局
This commit is contained in:
469
asset/deobfuscate.js
Normal file
469
asset/deobfuscate.js
Normal file
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* 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}`);
|
||||
Reference in New Issue
Block a user