Files
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

139 lines
4.2 KiB
Python
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.
#!/usr/bin/env python3
"""把 3.8.99 载荷里所有方法的访问标志加上 public。
为什么敢改访问标志:解密函数算 key 用的是 **(调用方类)** 的常量池大小
key = (cpSize + 调用类名 + 调用方法名 + cpSize).hashCode()
其中 cpSize 每次都取 st[2](调用解密函数的那个类),跟解密函数所在类无关。
而 access_flags 是方法头里的两个字节,改它不会动常量池,密文一个字节都不受影响。
改 public 的目的:我们可以造一个合成类,用**原始调用方的类名和方法名**去调解密函数,
从而让 key 自然对上,不需要自己实现 AES —— 载荷里有好几个内联副本,全开 public 最省事。
"""
import sys
import zipfile
ACC_PUBLIC = 0x0001
ACC_PRIVATE = 0x0002
ACC_PROTECTED = 0x0004
class Reader:
def __init__(self, data):
self.d = data
self.p = 0
def u1(self):
v = self.d[self.p]
self.p += 1
return v
def u2(self):
v = (self.d[self.p] << 8) | self.d[self.p + 1]
self.p += 2
return v
def u4(self):
v = int.from_bytes(self.d[self.p:self.p + 4], "big")
self.p += 4
return v
def skip(self, n):
self.p += n
def parse_constant_pool(r):
"""返回 (utf8 列表, 索引->绝对偏移) 以及常量池结束位置。"""
count = r.u2()
utf8 = {}
offsets = {}
i = 1
while i < count:
offsets[i] = r.p
tag = r.u1()
if tag == 1:
length = r.u2()
raw = r.d[r.p:r.p + length]
r.skip(length)
utf8[i] = raw
elif tag in (3, 4):
r.skip(4)
elif tag in (5, 6):
r.skip(8)
i += 1
elif tag in (7, 8, 16, 19, 20):
r.skip(2)
elif tag in (9, 10, 11, 12, 17, 18):
r.skip(4)
elif tag == 15:
r.skip(3)
else:
raise ValueError("未知常量池 tag %d @%d" % (tag, r.p - 1))
i += 1
return utf8, offsets, count
def skip_attributes(r):
n = r.u2()
for _ in range(n):
r.skip(2)
r.skip(r.u4())
def patch(data):
"""把类里所有方法的访问标志加上 ACC_PUBLIC,就地修改 data(bytearray)。"""
r = Reader(data)
if r.u4() != 0xCAFEBABE:
raise ValueError("不是 class 文件")
r.u2()
r.u2()
utf8, offsets, cp_count = parse_constant_pool(r)
r.skip(2) # access_flags(类的)
r.skip(2) # this_class
r.skip(2) # super_class
r.skip(2 * r.u2()) # interfaces
for _ in range(r.u2()): # fields
r.skip(6)
skip_attributes(r)
changed = 0
for _ in range(r.u2()): # methods
access_pos = r.p
access = r.u2()
r.skip(4) # name_index / descriptor_index
skip_attributes(r)
# 不能直接 OR:private static final 是 0x1A,OR 上 0x01 会变成 0x1B(public+private,非法)
new_access = (access & ~(ACC_PRIVATE | ACC_PROTECTED)) | ACC_PUBLIC
if new_access != access:
data[access_pos] = (new_access >> 8) & 0xFF
data[access_pos + 1] = new_access & 0xFF
changed += 1
return changed, cp_count
def main():
source, target = sys.argv[1], sys.argv[2]
patched_classes = 0
patched_methods = 0
with zipfile.ZipFile(source) as zin:
with zipfile.ZipFile(target, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
raw = zin.read(item.filename)
if item.filename.endswith(".class"):
raw = bytearray(raw)
try:
changed, cp_count = patch(raw)
patched_classes += 1
patched_methods += changed
except Exception as error:
# 载荷里有几个不以 CAFEBABE 开头的 .class(比如 wvv.class 是签名数据),跳过
print("跳过 %s : %s" % (item.filename, error))
zout.writestr(item, raw)
print("已把 %d 个类里的 %d 个方法改成 public" % (patched_classes, patched_methods))
print("已写出 " + target)
if __name__ == "__main__":
main()