Files
tianmo 7fbb042aa6 编辑器交互/数据 API 调研完成:解出 KP=SelectBox、选中链路、增删改真实落点与撤销栈
- 新工具 tools/decrypt/DumpSites.java:逐 indy 站点解出『类.方法 + 描述符』(带描述符,
  解决 rewrite_source.py 丢描述符导致的重载混淆);ResolveSites.java 走运行时 CallSite 交叉验证。
- 结论改正:KP = libGDX SelectBox,KP.B(String)V = setSelected;XW/acW/acK/Yr 是
  『属性面板控件』而不是场景数据;场景对象是树节点上的 GP/Gp/GU/GO。
- 补齐:当前选择怎么设(VZ.D(Object))、谁刷新界面(VZ.D(GP,ZZ) / rW.B / fC.C)、
  撤销栈(载荷类 un 的 D()/B() 成对包一层)、删除边界(root 的删除按钮本来就是 disabled)、
  查/选/改名/删除/导出的真实实现与出处。
- 新增运行时探针 -Dspine.probe=objects(SceneObjects.java),实测打印整棵场景对象树。
- 文档:编辑器交互与数据API.md 重写;混淆名对照表.md 增补类/方法对照与大小写同名类坑。
2026-10-16 14:38:25 +08:00

171 lines
6.7 KiB
Java
Raw Permalink 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.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.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.MethodNode;
/**
* 把每个 invokedynamic 站点的**加密串**按 BSM 自己的算法解出真实目标:
* (类名, 方法名, 描述符) —— 静态还是虚调用看首字符 0xB8=static、0xB6=virtual。
*
* 为什么要它:`rewrite_source.py` 只按"混淆名 → 类.方法"改名,**丢描述符**,
* 于是 `SelectBox.getSelected()` 和 `SelectBox.setSelected(String)` 都被写成 `KP.B`,
* 读源码时就会以为是同一个方法(这是之前把 KP.B 误当"建对象 API"的根因)。
*
* 用法:
* java -cp "asm.jar:本工具:已全 public 化的载荷.zip" com.spineview.dec.DumpSites <载荷> <out.tsv> [类名...]
*/
public final class DumpSites {
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]);
List<String> filters = new ArrayList<String>();
for (int i = 2; i < args.length; i++) filters.add(args[i]);
List<String[]> rows = new ArrayList<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;
}
if (!filters.isEmpty() && !filters.contains(node.name)) 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;
if (!BSM_DESC.equals(indy.bsm.getDesc())) continue;
if (indy.bsmArgs.length == 0 || !(indy.bsmArgs[0] instanceof String)) continue;
String decoded = decode(indy.name, (String) indy.bsmArgs[0]);
String caller = node.name + "." + method.name + method.desc;
// 顺带把"最近压栈的那个 ldc 常量"当成描述文本,方便在源码里定位。
String hint = nearestLdc(method, insn);
rows.add(new String[] { caller, indy.name, indy.desc, decoded, hint });
}
}
}
}
try (BufferedWriter writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) {
writer.write("caller\tindyName\tsiteDesc\tdecoded\tnearbyLdc\n");
for (String[] row : rows) {
for (int i = 0; i < row.length; i++) {
if (i > 0) writer.write('\t');
writer.write(escape(row[i]));
}
writer.write('\n');
}
}
System.out.println("站点 " + rows.size() + " -> " + out);
}
private static String nearestLdc(MethodNode method, AbstractInsnNode site) {
AbstractInsnNode node = site;
for (int i = 0; i < 12 && node != null; i++) {
node = node.getPrevious();
if (node instanceof org.objectweb.asm.tree.LdcInsnNode) {
Object value = ((org.objectweb.asm.tree.LdcInsnNode) node).cst;
if (value instanceof String) return printable((String) value);
}
}
return "";
}
private static String printable(String text) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c >= 0x20 && c < 0x7F) builder.append(c);
else builder.append('.');
}
return builder.toString();
}
/** 完全按 BSM 字节码里的算法复刻;先用已知站点自校验过。 */
static String decode(String indyName, String cipher) {
char[] name = indyName.toCharArray();
int[] seed = new int[4];
seed[0] = (name[1] << 16) | name[0];
seed[1] = (name[3] << 16) | name[2];
seed[2] = (name[5] << 16) | name[4];
seed[3] = (name[7] << 16) | name[6];
char[] out = cipher.toCharArray();
int n4 = 0, n5 = 0, n6 = 0, n7 = 0;
for (int i = 0; i < out.length; i++) {
if (i % 8 == 0) {
for (int k = 0; k < 48; k++) {
int s = seed[k % 4] + k;
n5 += n4 + ((n4 << 6) ^ (n4 >> 8)) + s;
n4 += n5 + ((n5 << 6) ^ (n5 >> 8)) + s;
n7 += n6 + ((n6 << 6) ^ (n6 >> 8)) + s;
n6 += n7 + ((n7 << 6) ^ (n7 >> 8)) + s;
}
}
int key;
switch (i % 8) {
case 0: case 4: key = n4 >> 16; break;
case 1: case 5: key = n5; break;
case 2: case 6: key = n6 >> 16; break;
default: key = n7; break;
}
out[i] = (char) (key ^ out[i]);
}
// [0]=调用种类(0xB8 静态 / 0xB6 虚) [1]=类名长度 类名 [n]=方法名长度 方法名 描述符
boolean isStatic = out[0] == '\u00b8';
int classLength = out[1];
String className = new String(out, 2, classLength);
int nameLength = out[classLength + 2];
String methodName = new String(out, classLength + 3, nameLength);
String descriptor = new String(out, classLength + nameLength + 3,
out.length - classLength - nameLength - 3);
return (isStatic ? "static " : "virtual ") + className + "." + methodName + descriptor;
}
private static String escape(String text) {
StringBuilder builder = new StringBuilder(text.length() + 8);
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c == '\t') builder.append("\\t");
else if (c >= 0x20 && c <= 0x7E) builder.append(c);
else builder.append(String.format("\\u%04x", Integer.valueOf(c)));
}
return builder.toString();
}
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();
}
}