目标:在 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 副本、从载荷派生的批量产物。
139 lines
4.2 KiB
Python
139 lines
4.2 KiB
Python
#!/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()
|