Files
Spine_lab/tools/agent/ForceAgent.java
T
tianmo 2e7ab7c33c 阶段四:Spine 3.8.99 编辑器里挂上自建视图面板
目标:在 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 副本、从载荷派生的批量产物。
2026-10-16 13:34:18 +08:00

91 lines
4.0 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ListIterator;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
/**
* 3.8.99 载荷跑在自备 JDK8 上时,Editor.main 里有一道
* "请使用 Spine 自带的 Java 运行时" 的校验,不通过就 Runtime.halt(1)。
*
* 字节码里的形状是:
* iload 2
* ifne <成功分支>
* goto <报错 + halt>
*
* 这个 agent 把那段 iload 前面塞上 iconst_1 / istore 2,
* 让分支永远走成功那一条。只作用于内存中的类,不动磁盘文件。
*/
public final class ForceAgent {
private static final String TARGET = "com/esotericsoftware/spine/editor/Editor";
private static boolean patched;
private static boolean skipLicencePatch;
public static void premain(String arguments, Instrumentation instrumentation) {
skipLicencePatch = arguments != null && arguments.contains("wyridonly");
System.err.println("[force-agent] premain 已加载" + (skipLicencePatch ? "(只改 WYRiDdid)" : ""));
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> beingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (!TARGET.equals(className)) return null;
try {
return patch(classfileBuffer);
} catch (Throwable error) {
System.err.println("[force-agent] 打补丁失败: " + error);
return null;
}
}
});
}
private static byte[] patch(byte[] data) {
ClassNode node = new ClassNode();
new ClassReader(data).accept(node, 0);
for (MethodNode method : node.methods) {
if (!"main".equals(method.name) || !"([Ljava/lang/String;)V".equals(method.desc)) continue;
InsnList list = method.instructions;
for (ListIterator<AbstractInsnNode> it = list.iterator(); it.hasNext();) {
AbstractInsnNode insn = it.next();
// 第二道门:invokedynamic WYRiDdid()Z 为 false 就直接 return。
// 直接把这次调用的结果换成常量 true —— 一对一替换,栈效果完全一样,
// 所以栈帧表和 max_stack 都不用动(改成跳转就会破坏栈帧)。
if (insn instanceof InvokeDynamicInsnNode
&& "WYRiDdid".equals(((InvokeDynamicInsnNode) insn).name)) {
list.set(insn, new InsnNode(Opcodes.ICONST_1));
System.err.println("[force-agent] 已把 WYRiDdid() 守卫改成永远通过");
}
if (insn.getOpcode() != Opcodes.ILOAD || ((VarInsnNode) insn).var != 2) continue;
if (skipLicencePatch) continue;
AbstractInsnNode next = insn.getNext();
if (next == null || next.getOpcode() != Opcodes.IFNE) continue;
AbstractInsnNode after = next.getNext();
if (after == null || after.getOpcode() != Opcodes.GOTO) continue;
InsnList force = new InsnList();
force.add(new InsnNode(Opcodes.ICONST_1));
force.add(new VarInsnNode(Opcodes.ISTORE, 2));
list.insertBefore(insn, force);
patched = true;
System.err.println("[force-agent] 已把 Editor.main 的运行时校验改成永远通过");
}
}
if (!patched) return null;
// 只插两条不入栈的指令,帧信息不变,用 COMPUTE_MAXS 就够了。
// 用 COMPUTE_FRAMES 的话 ASM 会在定义类的过程中反射加载同一个类,
// 触发 "attempted duplicate class definition"。
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
node.accept(writer);
return writer.toByteArray();
}
}