编辑器交互/数据 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 增补类/方法对照与大小写同名类坑。
This commit is contained in:
1 parent
2b23ce59db
commit
7fbb042aa6
6 files changed
+990
-126
No files matched your search
@@ -0,0 +1,170 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.spineview.dec;
|
||||
|
||||
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.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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;
|
||||
|
||||
/** 把 (类,方法) 里的所有 invokedynamic 站点解出来,打印 站点描述符 + 目标方法签名。 */
|
||||
public final class ResolveSites {
|
||||
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 {
|
||||
String payload = args[0];
|
||||
List<String> filters = new ArrayList<String>();
|
||||
for (int i = 1; i < args.length; i++) filters.add(args[i]);
|
||||
List<Site> sites = new ArrayList<Site>();
|
||||
try (ZipFile zip = new ZipFile(payload)) {
|
||||
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 t) { continue; }
|
||||
if (!matches(node.name, filters)) continue;
|
||||
System.out.println("== class " + node.name + " extends " + node.superName);
|
||||
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, method.desc, bsm.getOwner(), bsm.getName(),
|
||||
indy.name, indy.desc, indy.bsmArgs.length > 0 ? indy.bsmArgs[0] : null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String, Method> bootstrapCache = new HashMap<String, Method>();
|
||||
MethodHandles.Lookup[] lookupCache = new MethodHandles.Lookup[1];
|
||||
Map<String, String> resolved = new LinkedHashMap<String, String>();
|
||||
for (Site site : sites) {
|
||||
String triple = site.bsmOwner + "." + site.bsmName + "#" + site.indyName;
|
||||
String target = resolved.get(triple);
|
||||
if (target == null) {
|
||||
try { target = resolve(site, bootstrapCache, lookupCache); }
|
||||
catch (Throwable error) { target = "<失败> " + error; }
|
||||
resolved.put(triple, target);
|
||||
}
|
||||
System.out.println(" " + site.owner + "." + site.method + site.methodDesc
|
||||
+ "\n indy " + site.bsmOwner + "." + site.bsmName + " name=" + site.indyName
|
||||
+ " siteType=" + site.desc + "\n -> " + target);
|
||||
}
|
||||
System.out.println("站点 " + sites.size());
|
||||
}
|
||||
|
||||
private static boolean matches(String name, List<String> filters) {
|
||||
if (filters.isEmpty()) return true;
|
||||
for (String f : filters) if (name.equals(f)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
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,
|
||||
ResolveSites.class.getClassLoader());
|
||||
Object callSite = bootstrap.invoke(null, lookupCache[0], site.indyName, type, site.arg);
|
||||
if (!(callSite instanceof CallSite)) return String.valueOf(callSite);
|
||||
MethodHandle target = ((CallSite) callSite).getTarget();
|
||||
return describe(target) + " [type=" + target.type() + "]";
|
||||
}
|
||||
|
||||
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 + " (via " + target.getClass().getSimpleName() + ")";
|
||||
} catch (Throwable ignored) { }
|
||||
}
|
||||
}
|
||||
return "?" + target.type();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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, method, methodDesc, bsmOwner, bsmName, indyName, desc;
|
||||
final Object arg;
|
||||
Site(String owner, String method, String methodDesc, String bsmOwner, String bsmName,
|
||||
String indyName, String desc, Object arg) {
|
||||
this.owner = owner; this.method = method; this.methodDesc = methodDesc;
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user