目标:在 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 副本、从载荷派生的批量产物。
215 lines
6.0 KiB
Python
215 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Minimal Java .class parser.
|
|
|
|
Reads every .class under a directory and prints a JSON model with the pieces we
|
|
need to identify obfuscated classes by *structure* rather than by name:
|
|
this class, super class, declared fields (name + type) and declared methods
|
|
(name + parameter/return types).
|
|
|
|
Usage:
|
|
python3 tools/classdump.py <classes-dir> [--out model.json] [--only TextButton]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import struct
|
|
import sys
|
|
|
|
TYPE_CHARS = {
|
|
"B": "byte", "C": "char", "D": "double", "F": "float", "I": "int",
|
|
"J": "long", "S": "short", "Z": "boolean", "V": "void",
|
|
}
|
|
|
|
|
|
def parse_descriptor(desc, at=0):
|
|
"""Turn one JVM type descriptor into (readable name, next index)."""
|
|
arrays = 0
|
|
while at < len(desc) and desc[at] == "[":
|
|
arrays += 1
|
|
at += 1
|
|
if at >= len(desc):
|
|
return "?", at
|
|
ch = desc[at]
|
|
if ch == "L":
|
|
end = desc.find(";", at)
|
|
if end < 0:
|
|
return "?", len(desc)
|
|
name = desc[at + 1:end].replace("/", ".")
|
|
at = end + 1
|
|
else:
|
|
name = TYPE_CHARS.get(ch, ch)
|
|
at += 1
|
|
return name + "[]" * arrays, at
|
|
|
|
|
|
def parse_method_descriptor(desc):
|
|
params = []
|
|
at = 0
|
|
assert desc[at] == "("
|
|
at += 1
|
|
while desc[at] != ")":
|
|
name, at = parse_descriptor(desc, at)
|
|
params.append(name)
|
|
at += 1
|
|
result, _ = parse_descriptor(desc, at)
|
|
return params, result
|
|
|
|
|
|
class Reader:
|
|
def __init__(self, data):
|
|
self.data = data
|
|
self.pos = 0
|
|
|
|
def u1(self):
|
|
value = self.data[self.pos]
|
|
self.pos += 1
|
|
return value
|
|
|
|
def u2(self):
|
|
value = struct.unpack_from(">H", self.data, self.pos)[0]
|
|
self.pos += 2
|
|
return value
|
|
|
|
def u4(self):
|
|
value = struct.unpack_from(">I", self.data, self.pos)[0]
|
|
self.pos += 4
|
|
return value
|
|
|
|
def skip(self, count):
|
|
self.pos += count
|
|
|
|
|
|
def read_constant_pool(reader):
|
|
count = reader.u2()
|
|
pool = [None] * count
|
|
index = 1
|
|
while index < count:
|
|
tag = reader.u1()
|
|
if tag == 1: # Utf8
|
|
length = reader.u2()
|
|
raw = reader.data[reader.pos:reader.pos + length]
|
|
reader.skip(length)
|
|
pool[index] = raw.decode("utf-8", "replace")
|
|
elif tag in (3, 4): # Integer, Float
|
|
reader.skip(4)
|
|
elif tag in (5, 6): # Long, Double
|
|
reader.skip(8)
|
|
index += 1
|
|
elif tag == 7: # Class -> index of the Utf8 holding the internal name
|
|
pool[index] = ("class", reader.u2())
|
|
elif tag in (8, 16, 19, 20): # String, MethodType, Module, Package
|
|
reader.skip(2)
|
|
elif tag in (9, 10, 11, 12, 17, 18): # Field/Method/Interface refs, NameAndType, Dynamic
|
|
reader.skip(4)
|
|
elif tag == 15: # MethodHandle
|
|
reader.skip(3)
|
|
else:
|
|
raise ValueError("unknown constant pool tag %d" % tag)
|
|
index += 1
|
|
return pool
|
|
|
|
|
|
def parse_class(path):
|
|
with open(path, "rb") as handle:
|
|
data = handle.read()
|
|
if len(data) < 10 or data[:4] != b"\xca\xfe\xba\xbe":
|
|
return None
|
|
reader = Reader(data)
|
|
reader.skip(4)
|
|
reader.u2() # minor
|
|
reader.u2() # major
|
|
pool = read_constant_pool(reader)
|
|
|
|
def utf8(index):
|
|
value = pool[index] if 0 < index < len(pool) else None
|
|
return value if isinstance(value, str) else None
|
|
|
|
def class_name(index):
|
|
entry = pool[index] if 0 < index < len(pool) else None
|
|
if isinstance(entry, tuple) and entry[0] == "class":
|
|
entry = utf8(entry[1])
|
|
if isinstance(entry, str):
|
|
return entry.replace("/", ".")
|
|
return None
|
|
|
|
access_flags = reader.u2()
|
|
this_class = class_name(reader.u2())
|
|
super_class = class_name(reader.u2())
|
|
interfaces = [class_name(reader.u2()) for _ in range(reader.u2())]
|
|
|
|
fields = []
|
|
for _ in range(reader.u2()):
|
|
flags = reader.u2()
|
|
name = utf8(reader.u2())
|
|
desc = utf8(reader.u2())
|
|
for _ in range(reader.u2()):
|
|
reader.u2()
|
|
reader.skip(reader.u4())
|
|
fields.append({
|
|
"name": name,
|
|
"type": parse_descriptor(desc)[0] if desc else "?",
|
|
"flags": flags,
|
|
})
|
|
|
|
methods = []
|
|
for _ in range(reader.u2()):
|
|
flags = reader.u2()
|
|
name = utf8(reader.u2())
|
|
desc = utf8(reader.u2())
|
|
for _ in range(reader.u2()):
|
|
reader.u2()
|
|
reader.skip(reader.u4())
|
|
if desc and desc.startswith("("):
|
|
params, result = parse_method_descriptor(desc)
|
|
else:
|
|
params, result = [], "?"
|
|
methods.append({"name": name, "params": params, "returns": result, "flags": flags})
|
|
|
|
return {
|
|
"name": this_class,
|
|
"super": super_class,
|
|
"interfaces": interfaces,
|
|
"flags": access_flags,
|
|
"fields": fields,
|
|
"methods": methods,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("directory")
|
|
parser.add_argument("--out")
|
|
parser.add_argument("--only", help="only keep classes whose name contains this")
|
|
args = parser.parse_args()
|
|
|
|
model = {}
|
|
failed = 0
|
|
for dirpath, _, filenames in os.walk(args.directory):
|
|
for filename in filenames:
|
|
if not filename.endswith(".class"):
|
|
continue
|
|
path = os.path.join(dirpath, filename)
|
|
try:
|
|
entry = parse_class(path)
|
|
except Exception:
|
|
failed += 1
|
|
continue
|
|
if not entry or not entry["name"]:
|
|
failed += 1
|
|
continue
|
|
if args.only and args.only not in entry["name"]:
|
|
continue
|
|
entry["path"] = path
|
|
model[entry["name"]] = entry
|
|
|
|
print("解析成功 %d 个类,失败 %d" % (len(model), failed), file=sys.stderr)
|
|
if args.out:
|
|
with open(args.out, "w") as handle:
|
|
json.dump(model, handle)
|
|
print("已写入 %s" % args.out, file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|