阶段四: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,214 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user