ACTGS 引擎归档容器格式规范
—— 以 WHITE CLARITY(ACTGS ver 2.33)为样本的参考文档
| 项 | 值 |
|---|---|
| 文档版本 | 1.0 |
| 编制日期 | 2026-10-06 |
| 适用引擎 | ACTGS ver 2.33((c) 2003-2005 ACTRESS),Delphi/VCL,Win32 |
| 验证样本 | WHITE CLARITY PC版,Windows 11 x64,系统 ANSI 代码页 936 |
| 证据来源 | 静态反编译(Ghidra 12.1.4,4351 函数全量反编译,失败 0)+ 运行时内存取证 + 逐字节回环实验 |
| 用途 | 本地化、格式研究、数据迁移 |
证据等级标注(本文所有结论均带标记):
- [M] Measured — 由反编译代码或运行时实测直接确认
- [I] Inferred — 由 [M] 事实推导,尚未独立验证
- [U] Unverified — 观察性结论,样本外可能不成立
1. 术语与记号
| 记号 | 含义 |
|---|---|
KEY |
5 字节重复密钥 11 05 22 33 41 [M] |
N |
归档条目数,位于文件头 uint32 |
rec[i] |
第 i 条 32 字节索引记录(已解密形态) |
off[i], len[i] |
记录中的绝对偏移与原始长度 |
blob[i] |
第 i 个数据块在归档中的密文字节 |
plain[i] |
数据块解密后的字节 |
pad[i] |
第 i 块末尾到下一块起点之间的原始字节(对齐填充,内容不保证为 00) |
^ |
按位异或 |
≡ |
逐字节相等 |
2. 样本基准值
| 归档 | 大小 | N |
记录长度 | 混淆 | 备注 |
|---|---|---|---|---|---|
arc.scr |
2,409,936 | 164 | 32 B | 索引 + 数据块 | 纯文本脚本 |
arc.cg |
942,221,792 | 1003 | 32 B | 索引 + 数据块 | 图像/PSD |
arc.wav |
345,471,840 | 5779 | 24 B | 无 | 明文容器,可作对照样本 [M] |
arc.scr 派生量 [M] :
1 | |
arc.wav未混淆且记录长度为 24 字节([off][len][name[16]]),
说明 ACTGS 家族存在多种记录布局。不得假设 §3 的 32 字节布局全局通用 [M] 。
3. 容器布局规范
1 | |
3.1 索引记录(32 字节,解密后)
| 偏移 | 长度 | 类型 | 语义 | 证据 |
|---|---|---|---|---|
+0x00 |
4 | uint32 LE | 该条目数据块的绝对文件偏移 | [M] |
+0x04 |
4 | uint32 LE | 该条目数据块的原始长度(不含对齐填充) | [M] |
+0x08 |
24 | char[] | 文件名,NUL 结尾,其余补 0;字符集 cp932 | [M] |
示例(解密后):00-001.scr 00-002.scr 00-end.scr 01-001.scr … k06-502a.scr open.scr
3.2 数据块排布规则
1 | |
实测间隙序列(arc.scr 前若干块):14, 1, 3, 0, 7 … 字节,与 16 字节对齐推导一致 [M] 。
回写注意:
pad[i]的实际字节内容不保证为0x00。
若需”未修改即可逐字节还原”,必须原样保留pad[i](见 §8.2)。
4. 混淆算法规范
4.1 索引区
作用于文件偏移 0x10 起的 N×32 字节,相位以缓冲区首为 0 [M] :
1 | |
反变换与正变换相同(异或自反)。
4.2 数据块
逐块独立,相位在块内重置 [M] :
1 | |
规范性提示(高频错误点):下标为 (i - 1) % 5 而非 i % 5。
引擎源码写作 u = u % klen + 1 的滚动形式,其下标序列为 0,1,2,3,4,0,…,
配合循环 i 从 1 起始,等价于 (i-1) % 5。
误用 i % 5 将导致整块相位偏移 1,症状为”块首若干字节正常、其后全部错误”,
极易被误判为”存在第二层加密” [M] 。
4.3 密钥实体
KEY 以 NUL 结尾字符串形式存在于 .data,长度由 strlen 运行时求得。
样本内共 4 份副本,内容相同 [M] :
| VA | 引用方 | 作用对象 |
|---|---|---|
0x4C5460 |
FUN_00429568 |
归档索引 |
0x4BFEDF |
FUN_004124d0 |
脚本数据块 |
0x4C2A28 |
FUN_0042629c |
PSD 资源 |
0x4C5857 |
FUN_0042a32c |
DIBitmap 资源 |
1 | |
该密钥为混淆而非加密:无完整性校验、无密钥派生、明文可预测(大量 0 填充)。
不具备任何抗分析能力 [M] 。
5. 脚本正文语法约定(.scr 解密后)
5.1 行分隔与记录
- 行分隔符:
CRLF(0x0D 0x0A) [M] - 块首字节解密后被置为
0x4E('N'),实测为版本/类型标记 [M] - 注释行以
0x3B(;)起始 [M]
5.2 行分类规则(规范性)
引擎按行首字节决定该行语义 [M] :
| 行首 | 语义 | 可否本地化 |
|---|---|---|
A-Z / a-z |
命令行(命令名 + 参数) | 禁止翻译;参数中的资源标识符亦禁止 |
; |
注释 | 可译可不动 |
| 双字节前导 | 显示正文 | 待翻译目标 |
0x00–0x20 等 |
控制/填充 | 禁止改动 |
验证依据:将一行正文替换为 ASCII 文本后,引擎立即报
SCRIPT ERROR / 未定義のコマンド <该行内容>,位置标注为<文件名> (行号)。
即”字母开头 → 命令名查表” [M] 。
5.3 已观测命令表(arc.scr)
1 | |
命令名以明文 ASCII 存于脚本中,不在 exe 的字符串表内(已 grep 确认)[M] 。
因此命令集合的权威来源是脚本自身,而非 exe。
5.4 资源标识符警告
形如 sp1_cf "月"、bgm1 "bgm01"、bg1_fi 天井 的行,
其参数含日文但属于标识符,与 arc.cg 内素材名一一对应 [I] 。
本地化时若需替换,必须同步改名素材条目;否则运行期取不到资源。
5.5 语料规模(样本实测)
1 | |
6. 字符编码与运行时前导判定
6.1 渲染路径
| 项 | 值 | 证据 |
|---|---|---|
| 文本绘制 | ExtTextOutA(ANSI) |
[M] |
| 导入跳板 | 0x4B6850 |
[M] |
| 调用点 | 0x46305D、0x463115(全程序仅 2 处,均为 VCL TCanvas.TextOut/TextRect 包装) |
[M] |
| 文本测量 | GetTextExtentPoint32A,跳板 0x4B68C8,调用点 1 处 |
[M] |
| 字体 | CreateFontIndirectA,ACTGS.ini 指定 MS Pゴシック |
[M] |
渲染入口极窄:2 个调用点覆盖全部游戏文字。任何运行时注入/hook 方案只需处理这两处。
6.2 双字节前导判定表
引擎按行扫描时查询一张 32 字节位图 [M] :
1 | |
- 该地址在镜像文件中全为 0x00,即运行时构建 [M]
- 运行时实测内容(
ReadProcessMemory,进程初始化后) [M] :
1 | |
6.3 规范性结论
| 判定 | 内容 | 证据 |
|---|---|---|
| 该表由系统 ANSI 代码页驱动 | 样本机 ACP = 936,实测表内容等于 GBK 前导全集 | [M] |
Shift-JIS 前导全集仅 46 个(0x81-0x9F + 0xE0-0xEA) |
与实测 126 个不符 | [M] |
| 在 ACP=936 环境下运行该日文引擎,无需内码补丁即可渲染 GBK 中文正文 | 前导判定与 GDI 解码两侧同时为 GBK | [M](表内容、调用点)+ [I](端到端渲染) |
| 同一环境下日文原文会被画成乱码 | 同一事实的反面;可在错误弹窗截图中直接观测 | [M] |
适用条件:结论依赖宿主 ACP。若目标机 ACP = 932,该表将退化为 Shift-JIS 前导集,
GBK 中文的0xA1–0xDF、0xF1–0xFE段前导会被误判为单字节,导致逐字错位 [I] 。
跨环境部署前应重新读取该位图确认。
7. 回写规范
7.1 必办事项
- 保持条目顺序与
N不变(改变条目数需同时改写文件头,本文未覆盖) [U] - 每条
rec[i]的+0x00写新偏移、+0x04写新长度;+0x08..原样保留 [M] - 数据块加密:首字节写
0x58,i≥1用KEY[(i-1)%5](若原块未加密则原样写出) [M] - 块尾对齐到 16 字节 [M]
- 保留尾部残留字节 [M]
7.2 填充保真原则(规范性)
当某条目长度未变化时,必须逐字节复用其原始
pad[i],不得以0x00重写。
理由:pad[i] 内容不保证为 0。若统一填 0,则”未修改→重封”无法满足 ≡ 原文件,
从而丧失 §8.1 这条最强的自检手段 [M] 。
7.3 文本替换的安全边界
- 仅替换 §5.2 中”显示正文”类行;其余字节一律从基准二进制原样拷回 [M]
- 译文按 GBK 编码;不可 GBK 表示的字符(日文假名、生僻字、emoji)必须显式报错或降级,
禁止静默替换为?[I] - 译文行数与
seq数不一致时,整份沿用原文并告警,不得部分写入 [M]
8. 验证协议
三条判据,各自适用范围与局限均予明示。
8.1 无损回环(round-trip)
1 | |
样本实测:orig=2409936 rebuilt=2409936 一致=True [M]
8.2 变长回读
1 | |
样本实测:3 条替换,产物 2409872,偏移单调 = True [M]
8.3 幂等
1 | |
样本实测:True [M]
8.4 算法正确性的唯一有效判据
解密产物必须是”零非法字节的合法编码文本”。
对 .scr:以 cp932 解码全明文,U+FFFD 计数应为 0;
“最长连续合法双字节串”应达到自然语言量级(数十至上千字符)。
样本实测:U+FFFD = 0,总明文 2,403,410 字节 [M]
反例警示(本项目真实经历):一个错误的差分模型
D[i] = P[i] ^ P[i-5]在零填充区会碰巧输出正确字节
(P[i-5]==0 ⇒ D[i]==P[i]),因而能同时满足”解出文件名””偏移单调””可逆回环”,
却仍与引擎算法无关。此时明文的最长合法 SJIS 串仅 11 字符——这才是决定性反证 [M] 。
9. 静态定位索引
| VA / 地址 | 作用 | 关键代码特征 |
|---|---|---|
0x400000 |
ImageBase | — |
.text VA 0x1000 / Raw 0x600 / 0xB6000 |
代码段(内嵌字符串,线性反汇编会失步) | — |
.data VA 0xB7000 / Raw 0xB6600 / 0x1B400(VSize 0xE5000,后段 BSS) |
数据段 | — |
FUN_00406148 |
启动流程,引用 "arc.scr" |
FUN_00429568(h, "arc.scr", 1, 0) |
FUN_00429568 |
归档打开 + 索引解密 | buf[i] ^= KEY[u % klen] |
FUN_004124d0 |
脚本读取 + 数据块解密 | if (*buf=='X') { … *buf='N'; } |
FUN_0042629c / FUN_0042a32c |
PSD / DIBitmap 加载 | 同型异或环,密钥副本 0x4C2A28/0x4C5857 |
FUN_0049ede0 |
按行切分脚本 | \r\n 判定 + 前导位图查询 |
FUN_004a02f8 |
全角/半角与 ASCII 大小写折叠 | 0x41-0x5A ↔ 0x61-0x7A 的 ±0x20 |
0x4B6850 |
ExtTextOutA 跳板(jmp [0x59E8E4]) |
调用点 2 处 |
0x4B68C8 |
GetTextExtentPoint32A 跳板 |
调用点 1 处 |
0x4CE218 |
运行时前导位图(32 B) | 镜像内全 0 |
.text 尾 0x4B6EBF |
321 字节全零代码洞(可放桩) | 实测可用 |
地址→文件偏移换算:
fileOff = 0x600 + (VA - 0x401000)(.text)。
本项目曾因混淆”节内索引”与”文件偏移”导致所有手算地址偏低0x600,见附录 B-5。
10. 复现步骤
10.1 最小复现(仅需 Python,验证 §3–§4)
1 | |
预期:164 行,首行 0 00-001.scr 5264 5986 5986,末行 163 open.scr 2396128 13802 13802。
10.2 完整复现(Ghidra headless)
1 | |
1 | |
Ghidra 脚本 API 注意:方法名是 decompileFunction(f, timeoutSecs, monitor),不是 decompile;
Ghidra 12 不再动态编译 .java 脚本 [M] 。
10.3 前导位图取证
1 | |
预期(ACP=936):126 位,0x81–0xFE。
11. 已知限制与未决项
| # | 项 | 状态 |
|---|---|---|
| 1 | 端到端”中文上屏”实机验收 | 未完成。一次 SCRIPT ERROR 已定位为旧实例窗口/未生效包所致,非格式问题 |
| 2 | 改变条目数 N 的回写 |
未覆盖(需同步改文件头与所有偏移) |
| 3 | arc.cg 图像资源回写 |
容器与密钥同族 [M],但 PSD/DIB 内部结构未解析;界面文字为位图,需另立改图流程 |
| 4 | 影像文件 | Ending1-3.dat、WCOp.mpg 为 MPEG-1,字幕已压入画面,文本替换无效 [M] |
| 5 | 13 个无正文文件 | 语义未细读(疑为过场/纯指令) |
| 6 | 跨 ACP 环境 | §6.3 结论依赖宿主 ACP,换机须重新取证 |
| 7 | 版本普适性 | 本文所有结论仅对 ACTGS ver 2.33 + 该样本验证,未在其他 ACTGS 游戏上复现 [U] |
附录 A 一致性测试向量
| 项 | 期望值 |
|---|---|
arc.scr 大小 |
2,409,936 |
arc.scr md5 |
8c884f90613f6f84f747a4856183cb40 |
N |
164 |
| 索引区范围 | [16, 5264) |
rec[0] 解密后前 8 字节 |
0x1490 = 5264,0x1762 = 5986 |
rec[0] 文件名 |
00-001.scr |
rec[163] 文件名 / 偏移 / 长度 |
open.scr / 2396128 / 13802 |
| 明文总字节 | 2,403,410 |
明文 cp932 解码 U+FFFD |
0 |
| 正文行数 | 31,455 |
| 未修改重封 | ≡ 原文件 = True |
| 运行时前导位图 | 126 位,0x81–0xFE |
附录 B 陷阱登记表
| # | 现象 | 误判后果 | 正解 | 证据 |
|---|---|---|---|---|
| B-1 | 密文出现 5 字节周期重复 | 误推为”反馈链加密”,在错误明文上继续分析并虚构”置换表” | 明文零区使密文等于密钥;变换须由反编译确认 | [M] |
| B-2 | 用”解密→再加密一致”验证算法 | 恒真,无鉴别力 | 该判据仅用于验证回写不丢信息 | [M] |
| B-3 | 差分变换在零区”碰巧正确” | 文件名、偏移字段看似全对 | 以”零非法字节的合法文本”为唯一判据 | [M] |
| B-4 | capstone 一次扫整段 .text |
内嵌数据处失步 → 漏指令 → 假阴性 | 字节模式搜索,或先建函数图 | [M] |
| B-5 | VA/文件偏移换算差 0x600 |
函数头找不到、call 站点校验失败 | 以 Ghidra 符号为准 | [M] |
| B-6 | SIB 表达式分词正则漏 [esi+ecx-5] |
得出”无读改写循环”的假阴性 | re.findall(r"[+-]?[A-Za-z0-9_]+", inner) |
[M] |
| B-7 | 改 IAT 使 API 指向自桩 | 无效:磁盘 IAT 槽存 thunk RVA,加载时被系统覆盖 | 改调用点 call rel32,桩尾 jmp [IAT] |
[M] |
| B-8 | 认为 lstrcpynA 返回长度 |
它返回目标指针,用作长度会写飞 | 拷后调 lstrlenA(buf) |
[M] |
| B-9 | 正文替换为 ASCII 文本 | SCRIPT ERROR / 未定義のコマンド |
仅替换双字节开头的正文行 | [M] |
| B-10 | bytearray 切片改长度后沿用旧偏移 |
后续偏移整体错位 | 按行 split/join 重建,或倒序替换 | [M] |
| B-11 | Ghidra 12 写 .java 脚本 |
ClassNotFoundException |
装 Jython 扩展写 .py |
[M] |
| B-12 | Jython 扩展解压到顶层 Extensions/ |
静默不生效 | 解压到 <根>/Ghidra/Extensions/ |
[M] |
| B-13 | .ps1 含中文且无 BOM |
按 GBK 解析,括号引号错乱 | 存 UTF-8 带 BOM,或正文纯 ASCII | [M] |
| B-14 | curl CRYPT_E_NO_REVOCATION_CHECK |
TLS 失败 | --ssl-no-revoke |
[M] |
| B-15 | Git 自带 tar 解 zip |
does not look like a tar archive |
C:\Windows\System32\tar.exe |
[M] |
| B-16 | 中文路径目录下用 py |
当前目录被生成 Python\ |
用绝对解释器路径 | [M] |
| B-17 | 用 8.3 短路径绕中文 | 首层无短名 → CreateProcess err=267 |
直接写全路径 + BOM | [M] |
| B-18 | 在替换前调用 unpack() |
从已被上一轮污染的归档重解,替换”莫名失效” | 每轮从原始备份重新解包 | [M] |