目标:在 Spine 编辑器窗口内部加一个自己的视图面板(按钮 + 文字显示), 点击读取场景对象,并像原生面板一样出现在 Views 菜单、可停靠可折叠。 本轮打通: 1. 编辑器在我们自己的进程里完整启动 - 类加载器必须是 URLClassLoader 本身(自检要求) - Editor.main 必须在 AWT EDT 上调用 - jvm.cfg 第 21 行要以两个空格结尾 + -DSpineJava - Editor.D:I 版本号、fonts/、/tzax 资源、Java8 版 launcher.Launcher 替身 2. 3.8.99 全量解密 → 可读源码 - 字符串 17251/17461(伪造同名调用帧对齐 key) - 混淆动态派发还原 53871 处(调引导方法取 MemberName) 3. 自建视图 - agent 在内存里给 us 枚举加 spineView 常量(不改磁盘,避开完整性自检) - 面板 Actor 由外部反射填进 us.spineView.D - 复用已有图标做别名(不新建图片资源) 最终 "Spine View" 出现在 Views 菜单,可与 Tree 并排停靠、折叠展开 另外: - ViewPatcher/ViewAgent:视图枚举与面板挂载的字节码插桩 - DecAll/ObMap/rewrite_source:字符串解密、派发还原、可读源码生成 - 阶段四-视图面板挂载.md:完整记录做法与踩过的坑 不纳入版本管理:rea/(独立仓库)、tools 下的 JDK/Ghidra 工具链、 Spine.app 与 spine-lab 副本、从载荷派生的批量产物。
232 lines
9.0 KiB
Java
232 lines
9.0 KiB
Java
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<Site> sites = new ArrayList<Site>();
|
||
Set<String> distinctNames = new HashSet<String>();
|
||
try (ZipFile zip = new ZipFile(payload.toFile())) {
|
||
Enumeration<? extends ZipEntry> 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<String, String> resolved = new LinkedHashMap<String, String>();
|
||
Map<String, Method> bootstrapCache = new HashMap<String, Method>();
|
||
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<String, Method> 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<MethodHandles.Lookup> 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();
|
||
}
|
||
}
|