Files
Spine_lab/tools/decrypt/ObMap.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

232 lines
9.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.
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();
}
}