package com.spineview.dec; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Constructor; import java.lang.reflect.Method; 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.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Handle; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.MethodNode; /** * 把 3.8.99 的"混淆化动态派发"整层解出来。 * * 编辑器里几乎每个方法调用都被编译成 invokedynamic,每个类都有自己的一份引导方法 * (Editor.Ob / AB.Nb / AD.HW ...,共 1421 个),签名统一是 * (Lookup, String 名字, MethodType, 密文描述) -> Object * 按 JVM 的方式把它调一遍拿回 CallSite,再顺着绑定句柄里的 MemberName, * 就能问出这次调用真正指向哪个方法。 * * 用法: * java -cp "asm.jar:本工具:已全 public 化的载荷.zip" com.spineview.dec.ObMap <载荷> <映射.tsv> */ public final class ObMap { 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]); // 1) 收集所有 invokedynamic 站点 List sites = new ArrayList(); Set distinctNames = new HashSet(); 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; } 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; Handle bsm = indy.bsm; if (!BSM_DESC.equals(bsm.getDesc())) continue; sites.add(new Site(node.name, method.name, bsm.getOwner(), bsm.getName(), indy.name, indy.desc, indy.bsmArgs.length > 0 ? indy.bsmArgs[0] : null)); distinctNames.add(indy.name); } } } } System.err.println("invokedynamic 站点 " + sites.size() + ",不同名字 " + distinctNames.size()); // 2) 每个 (引导类,引导方法,名字) 只需解析一次 Map resolved = new LinkedHashMap(); Map bootstrapCache = new HashMap(); MethodHandles.Lookup[] lookupCache = new MethodHandles.Lookup[1]; int done = 0, failed = 0; for (Site site : sites) { String triple = site.bsmOwner + "." + site.bsmName + "#" + site.indyName; if (resolved.containsKey(triple)) continue; String target; try { target = resolve(site, bootstrapCache, lookupCache); } catch (Throwable error) { target = "<失败> " + error; failed++; } resolved.put(triple, target); if (++done % 2000 == 0) System.err.println(" 已解析 " + done); } System.err.println("解析组合 " + resolved.size() + " 个,失败 " + failed); try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) { for (Site site : sites) { String triple = site.bsmOwner + "." + site.bsmName + "#" + site.indyName; writer.write(site.bsmOwner); writer.write('\t'); writer.write(site.bsmName); writer.write('\t'); writer.write(site.indyName); writer.write('\t'); writer.write(resolved.get(triple)); writer.write('\n'); } } System.err.println("映射已写出 -> " + out); } private static String resolve(Site site, Map bootstrapCache, MethodHandles.Lookup[] lookupCache) throws Throwable { Class owner = Class.forName(site.bsmOwner.replace('/', '.')); String key = site.bsmOwner + "." + site.bsmName; Method bootstrap = bootstrapCache.get(key); if (bootstrap == null) { for (Method candidate : owner.getDeclaredMethods()) { if (candidate.getName().equals(site.bsmName) && candidate.getParameterTypes().length == 4) { bootstrap = candidate; break; } } if (bootstrap == null) throw new NoSuchMethodException(site.bsmName); bootstrap.setAccessible(true); bootstrapCache.put(key, bootstrap); } if (lookupCache[0] == null) lookupCache[0] = trustedLookup(); MethodType type = MethodType.fromMethodDescriptorString(site.desc, ObMap.class.getClassLoader()); Object callSite = bootstrap.invoke(null, lookupCache[0], site.indyName, type, site.arg); if (!(callSite instanceof CallSite)) return String.valueOf(callSite); return describe(((CallSite) callSite).getTarget()); } /** CallSite 的目标是"真实方法 + 若干个已绑定的参数",往里挖一层就能拿到方法本体。 */ private static String describe(MethodHandle target) { String direct = memberOf(target); if (direct != null) return direct; for (Class type = target.getClass(); type != null; type = type.getSuperclass()) { for (java.lang.reflect.Field field : type.getDeclaredFields()) { if (!field.getName().startsWith("argL") && !field.getName().startsWith("argJ") && !field.getName().startsWith("argI")) continue; try { field.setAccessible(true); Object value = field.get(target); if (!(value instanceof MethodHandle)) continue; String nested = memberOf((MethodHandle) value); if (nested != null) return nested; } catch (Throwable ignored) { // 继续找 } } } return "?" + target.type(); } /** 直接句柄能拿到 MemberName,也就能知道 类名.方法名 以及是不是静态方法。 */ private static String memberOf(MethodHandle handle) { try { Method internal = MethodHandle.class.getDeclaredMethod("internalMemberName"); internal.setAccessible(true); Object member = internal.invoke(handle); if (member == null) return null; Class memberClass = member.getClass(); Method getDeclaring = memberClass.getMethod("getDeclaringClass"); Method getName = memberClass.getMethod("getName"); Method isStatic = memberClass.getMethod("isStatic"); getDeclaring.setAccessible(true); getName.setAccessible(true); isStatic.setAccessible(true); Object declaring = getDeclaring.invoke(member); String owner = String.valueOf(declaring).replace("class ", "").replace("interface ", ""); return owner + "." + getName.invoke(member) + (Boolean.TRUE.equals(isStatic.invoke(member)) ? " (static)" : ""); } catch (Throwable error) { return null; } } /** Java 8 上的经典手法:造一个权限全开的 Lookup。 */ private static MethodHandles.Lookup trustedLookup() throws Exception { Constructor constructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class); constructor.setAccessible(true); return constructor.newInstance(Object.class, Integer.valueOf(-1)); } private static final class Site { final String owner; final String method; final String bsmOwner; final String bsmName; final String indyName; final String desc; final Object arg; Site(String owner, String method, String bsmOwner, String bsmName, String indyName, String desc, Object arg) { this.owner = owner; this.method = method; this.bsmOwner = bsmOwner; this.bsmName = bsmName; this.indyName = indyName; this.desc = desc; this.arg = arg; } } 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(); } }