目标:在 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 副本、从载荷派生的批量产物。
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""把反编译源码里的混淆动态派发还原成真实方法名。
|
|
|
|
编辑器里几乎每个调用都被编译成 invokedynamic,反编译出来长这样:
|
|
yf_2.OB("lKRXTTGf", "<密文>", "Find")
|
|
其中 "lKRXTTGf" 是混淆名,Ob/OB/Nb/HW… 是每个类各自的引导方法。
|
|
obmap.tsv 给出了每个混淆名真正指向的方法,于是:
|
|
yf_2.OB("lKRXTTGf", "<密文>", "Find") -> java.lang.String.contains("Find")
|
|
|
|
实例方法统一写成 类名.方法(接收者, 参数...) 的静态形式 —— 不做参数搬移,
|
|
这样永远不会解析错参数边界,读起来照样一目了然。
|
|
|
|
用法:rewrite_source.py <反编译目录> <obmap.tsv> <输出目录>
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
# X.bsm("混淆名", "<密文>", <- 只吃掉这一段
|
|
CALL = re.compile(
|
|
r'([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\(\s*"([A-Za-z0-9_$]+)"\s*,\s*"(?:[^"\\]|\\.)*"\s*(?:,\s*)?'
|
|
)
|
|
|
|
|
|
def load_map(path):
|
|
mapping = {}
|
|
with open(path, encoding="utf-8") as handle:
|
|
for line in handle:
|
|
parts = line.rstrip("\n").split("\t")
|
|
if len(parts) < 4:
|
|
continue
|
|
name, target = parts[2], parts[3]
|
|
if target.startswith("<失败>") or target.startswith("?"):
|
|
continue
|
|
target = target.replace(" (static)", "")
|
|
mapping.setdefault(name, target)
|
|
return mapping
|
|
|
|
|
|
def main():
|
|
source_dir, map_path, out_dir = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
mapping = load_map(map_path)
|
|
print("映射条目 %d" % len(mapping))
|
|
|
|
files = replaced_total = files_touched = 0
|
|
missed = set()
|
|
for root, _, names in os.walk(source_dir):
|
|
for name in names:
|
|
if not name.endswith(".java"):
|
|
continue
|
|
files += 1
|
|
path = os.path.join(root, name)
|
|
with open(path, encoding="utf-8", errors="replace") as handle:
|
|
text = handle.read()
|
|
|
|
count = [0]
|
|
|
|
def swap(match):
|
|
target = mapping.get(match.group(3))
|
|
if target is None:
|
|
missed.add(match.group(3))
|
|
return match.group(0)
|
|
count[0] += 1
|
|
return target + "("
|
|
|
|
new_text = CALL.sub(swap, text)
|
|
if count[0]:
|
|
files_touched += 1
|
|
replaced_total += count[0]
|
|
relative = os.path.relpath(path, source_dir)
|
|
destination = os.path.join(out_dir, relative)
|
|
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
|
with open(destination, "w", encoding="utf-8") as handle:
|
|
handle.write(new_text)
|
|
|
|
print("扫描 %d 个文件,改写 %d 个文件,替换 %d 处调用" % (files, files_touched, replaced_total))
|
|
print("没映射上的混淆名 %d 个" % len(missed))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|