package com.spineview.dec; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodNode; /** * 把每个 invokedynamic 站点的**加密串**按 BSM 自己的算法解出真实目标: * (类名, 方法名, 描述符) —— 静态还是虚调用看首字符 0xB8=static、0xB6=virtual。 * * 为什么要它:`rewrite_source.py` 只按"混淆名 → 类.方法"改名,**丢描述符**, * 于是 `SelectBox.getSelected()` 和 `SelectBox.setSelected(String)` 都被写成 `KP.B`, * 读源码时就会以为是同一个方法(这是之前把 KP.B 误当"建对象 API"的根因)。 * * 用法: * java -cp "asm.jar:本工具:已全 public 化的载荷.zip" com.spineview.dec.DumpSites <载荷> [类名...] */ public final class DumpSites { private static final String BSM_DESC = "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"; public static void main(String[] args) throws Exception { Path payload = Paths.get(args[0]); Path out = Paths.get(args[1]); List filters = new ArrayList(); for (int i = 2; i < args.length; i++) filters.add(args[i]); List rows = new ArrayList(); try (ZipFile zip = new ZipFile(payload.toFile())) { Enumeration entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (!entry.getName().endsWith(".class")) continue; ClassNode node; try { node = parse(readAll(zip.getInputStream(entry))); } catch (Throwable error) { continue; } if (!filters.isEmpty() && !filters.contains(node.name)) continue; for (MethodNode method : node.methods) { for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (!(insn instanceof InvokeDynamicInsnNode)) continue; InvokeDynamicInsnNode indy = (InvokeDynamicInsnNode) insn; if (!BSM_DESC.equals(indy.bsm.getDesc())) continue; if (indy.bsmArgs.length == 0 || !(indy.bsmArgs[0] instanceof String)) continue; String decoded = decode(indy.name, (String) indy.bsmArgs[0]); String caller = node.name + "." + method.name + method.desc; // 顺带把"最近压栈的那个 ldc 常量"当成描述文本,方便在源码里定位。 String hint = nearestLdc(method, insn); rows.add(new String[] { caller, indy.name, indy.desc, decoded, hint }); } } } } try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { writer.write("caller\tindyName\tsiteDesc\tdecoded\tnearbyLdc\n"); for (String[] row : rows) { for (int i = 0; i < row.length; i++) { if (i > 0) writer.write('\t'); writer.write(escape(row[i])); } writer.write('\n'); } } System.out.println("站点 " + rows.size() + " -> " + out); } private static String nearestLdc(MethodNode method, AbstractInsnNode site) { AbstractInsnNode node = site; for (int i = 0; i < 12 && node != null; i++) { node = node.getPrevious(); if (node instanceof org.objectweb.asm.tree.LdcInsnNode) { Object value = ((org.objectweb.asm.tree.LdcInsnNode) node).cst; if (value instanceof String) return printable((String) value); } } return ""; } private static String printable(String text) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c >= 0x20 && c < 0x7F) builder.append(c); else builder.append('.'); } return builder.toString(); } /** 完全按 BSM 字节码里的算法复刻;先用已知站点自校验过。 */ static String decode(String indyName, String cipher) { char[] name = indyName.toCharArray(); int[] seed = new int[4]; seed[0] = (name[1] << 16) | name[0]; seed[1] = (name[3] << 16) | name[2]; seed[2] = (name[5] << 16) | name[4]; seed[3] = (name[7] << 16) | name[6]; char[] out = cipher.toCharArray(); int n4 = 0, n5 = 0, n6 = 0, n7 = 0; for (int i = 0; i < out.length; i++) { if (i % 8 == 0) { for (int k = 0; k < 48; k++) { int s = seed[k % 4] + k; n5 += n4 + ((n4 << 6) ^ (n4 >> 8)) + s; n4 += n5 + ((n5 << 6) ^ (n5 >> 8)) + s; n7 += n6 + ((n6 << 6) ^ (n6 >> 8)) + s; n6 += n7 + ((n7 << 6) ^ (n7 >> 8)) + s; } } int key; switch (i % 8) { case 0: case 4: key = n4 >> 16; break; case 1: case 5: key = n5; break; case 2: case 6: key = n6 >> 16; break; default: key = n7; break; } out[i] = (char) (key ^ out[i]); } // [0]=调用种类(0xB8 静态 / 0xB6 虚) [1]=类名长度 类名 [n]=方法名长度 方法名 描述符 boolean isStatic = out[0] == '\u00b8'; int classLength = out[1]; String className = new String(out, 2, classLength); int nameLength = out[classLength + 2]; String methodName = new String(out, classLength + 3, nameLength); String descriptor = new String(out, classLength + nameLength + 3, out.length - classLength - nameLength - 3); return (isStatic ? "static " : "virtual ") + className + "." + methodName + descriptor; } private static String escape(String text) { StringBuilder builder = new StringBuilder(text.length() + 8); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c == '\t') builder.append("\\t"); else if (c >= 0x20 && c <= 0x7E) builder.append(c); else builder.append(String.format("\\u%04x", Integer.valueOf(c))); } return builder.toString(); } private static ClassNode parse(byte[] raw) { ClassNode node = new ClassNode(); new ClassReader(raw).accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); return node; } private static byte[] readAll(InputStream in) throws java.io.IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] chunk = new byte[65536]; int n; while ((n = in.read(chunk)) > 0) buffer.write(chunk, 0, n); return buffer.toByteArray(); } }