阶段四: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 副本、从载荷派生的批量产物。
This commit is contained in:
commit
2e7ab7c33c
113 files changed
+25327
No files matched your search
@@ -0,0 +1,90 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user