某 GAL 汉化补丁 BGI.chs.exe 逆向分析记录
分析某GAL汉化补丁BGI.chs.exe,发现其使用自定义壳和LZMA压缩,通过内存Dump和调试分析得到其加密机制,成功提取中文资源与脚本。

起因
有一天同学 Call 我,大概意思是某游戏出了一个重制版,他想把旧版的某个作者的人工汉化搬到新版上,但是作者没有发布明文译文,他自己尝试了一下用 AI 逆向,但是因为汉化补丁加了壳,最终没有成功。
虽然我也没有逆向经验,是第一次接触逆向,但同学一说突然就有点兴趣,就拿这个老补丁当靶子学一回。
一、静态分析与内存探查
如图,补丁由三个文件组成,直接让 LLM 从二进制方向检查一下 exe 与两个容器的内容。
经过一番分析:
- 壳不是任何公开且有流传的壳,没有检测到相关的指纹,这个壳要不然是内部流通、要不然是小众打包工具的结果。
- exe 二进制内搜不到常见的反调试 API,这可能是好事,也就是说壳没做什么反调试手段,也可能是反调试 API 调用本身也被混淆了。
- 两个 arc 文件虽然后缀和格式和游戏本体的资源是一样的,但是内部数据也加密了。
exe 结构静态分析
| 节 | 磁盘 RawSize | 映像声明 |
|---|---|---|
.text |
0 | RVA 0x1000,VSize 0xCF908(851,208 B),可执行 |
.rdata |
0 | RVA 0xD1000,VSize 0x1EE1B(126,491 B),只读 |
.data |
0 | RVA 0xF0000,VSize 0x63528(406,824 B),可读写 |
'0' |
0 | RVA 0x154000,VSize 0xA411B96(172,039,062 B),可读可执行 |
'1' |
149,805,568 | RVA 0xA566000,VSize 0x8EDD840,RawPtr 0x400 |
.rsrc |
100,352 | RVA 0x13444000,VSize 0x18780 |
.text、.rdata、.data:文件里没有这个节的内容,按 PE 规则加载后是 0 填充。0:文件里没有内容,但是映像里声明了 164 MiB 的地址空间。'1':熵为 7.9999,可能是强加密,这是整个文件自身从 0x400 到 .rsrc 前的原样拷贝。.rsrc:标准 PE 资源目录,7 项 = 4 图标(82,304 / 9,640 / 4,264 / 1,128)+ 组图标(62)+ 版本信息(1,236,含 Ethornell - BURIKO General Interpreter 等串)+ manifest(1,140,XML 文本)
exe 数据结构:
0x400 (头) + 0x8EDDA00 (节'1') + 0x18800 (.rsrc) = 0x8EF6600 = 149,906,944没有 overlay,也没有空洞。接下来运行一次,进到游戏 Dump 看看内存里面有什么。
任务管理器 -> 选中进程 -> 创建内存转储运行时内存分析
| 功能层 | 地址范围 | 大小 | 内容 | 判定依据 | 置信度 |
|---|---|---|---|---|---|
| 原版引擎·代码 | 0x401000–0x4D0908 |
850,184 B | BGI/Ethornell 1.614.1 机器码 | 3,823 个函数序言;E8/E9 83% 目标在节内;42,527 个 CC |
高 |
| 原版引擎·只读数据 | 0x4D1000–0x4EFE1B |
126,491 B | 字符串 / RTTI / 导入名 | src\DSSpeakerModel.cpp、BURIKO ARC20、system.arc |
高 |
| 原版引擎·可写数据 | 0x4F0000–0x553528 |
406,824 B | vtable / RTTI / 全局变量 | 945 个指向 .text 的指针;CBurikoWaveBox* RTTI |
高 |
| 原版引擎·资源 | 0x13844000–0x1385C780 |
100,224 B | 图标 / 版本信息 / manifest | 版本信息 "Ethornell - BURIKO General Interpreter" | 高 |
| 壳·加载器 stub | 0x13600000–0x13843840 |
2.3 MB | 混淆代码 + PE 元数据 | 入口点 0x1372462B 落在其中;熵 7.0–7.6 |
高 |
| 壳·加密载荷 | 0x0A966000–0x13600000 |
147.4 MB | 密文/压缩数据 | 熵 7.9999;4 MB 采样 16 B 块零重复 | 高 |
| 壳·激活/反篡改 | 0x554000–0x580000 |
180 KB | 激活客户端 + HOOK 检测 | Error at hooking API "%S"、ActivationRequest 等 |
高 |
| 壳·未知 | 0x57F000–0x0A966000 |
171.9 MB | 高熵填充 | 熵 7.99;~2% 的 16 B 块重复(非理想密文) | 中高 |
| 系统 / 第三方模块 | 0x58690000–0x7C44F000 等 |
虚拟 505 MB / 提交约 198 MB | 82 个 32 位 DLL | 模块列表 | 高 |
| 运行时数据 | 0x1385D000–0x30D9F000 |
150.7 MB(私有) | 脚本 / 音频 / 纹理 / 堆 / 栈 | —— | 高 |
| 保留 / 空闲 | 其余 | 2382 MB / 1025 MB | — | MemoryInfo 状态位 | — |
把屏幕上显示的台词直接在内存里面搜索,这次直接搜明文搜出来了:
0x2CA2F16B 映入眼帘的尽是飞散的樱花。 // 搜索的台词
0x2CA2F188 那副风景中,既没有悲伤,也没有痛苦。
0x2CA2F1AF 所以我才没有哭吗?
0x2CA2F1C4 随风舞动的那抹淡红确实很美丽,而我感受到的也仅此而已。
0x2CA2F3C3 在只有关系者才能参与的这个小小的葬礼结束的同时,父亲的人生也终于划上了句号。
0x2CA2F412 现在正伫立在这座樱之森的我,眼睛里丝毫没有泪水。
0x2CA2F445 而且也丝毫没有悲伤之情。
0x2CA2F460 这里的风景一点也感觉不到悲伤的气氛。
0x2CA2F487 周围的人又是怎样呢?
0x2CA2F49F 果然会悲伤吗?
0x2CA2F4B1 会心痛吗?
0x2CA2F4BE 只是……看着落樱下的尸体,有些事情无论如何都会想起。
0x2CA2F4F5 内心一瞬间有种撕裂的感觉。
......再往前一点还能看到脚本引用的资源名:00_op_01.bss、Yuzu_2G.h、bgm004、sp0065a、map023… 说明这块 4.2 MB 里同时装着该场景的指令、资源引用和文本。从这里也可以知道,壳是使用虚拟文件交付引擎这种常见的手法,这些资源在磁盘上压根不存在。
好消息是有大片中文明文直接躺在内存里,坏消息是 Dump 出来中文只有 KB 级别,要完整获取估计得通关游戏,这个效率是不可接受的。
二、x32dbg 的调试分析
一开始尝试让 LLM 挂进程分析,结果游戏直接崩了,退出码:0xDEADC0DE,看来这个壳其实是有反调试的。最后发现直接上 ScyllaHide 就可以随便调了。
破解初始化过程
.text 是 PAGE_EXECUTE_READ,壳要写就必须先改保护,再加上已知明文的地址,所以:
bp VirtualProtect
bph 0x401000, w跟了几轮,最后bph 0x401000, w断点进了壳区
成功抓到了
136CDD04 | 881439 | mov byte ptr ds:[ecx+edi],dl正在一个一个字节初始化空节,设置追踪跑一圈,看看算法是什么。

;位流读取
135FE2B7 movzx ebx, byte ptr ds:[ebx] ; 从 0x13662045 取一个字节
135FE2BB shl esi, 8 ; 位缓冲左移
135FE2CF or esi, ebx ; 塞进低 8 位
135FE2D4 inc dword ptr ss:[ebp-8] ; 流指针 +1
;概率模型自适应
13712C29 shr ecx, 0xB ; >> 11 ← 概率总位数 kNumBitModelTotalBits
13825E70 imul ecx, ebx ; bound = (range>>11) * prob
13825E74 cmp esi, ecx ; code ? bound
137EFCB7 jae ...
;...
137EFCC8 mov ecx, 0x800 ; 2048 ← kBitModelTotal
137EFCD1 sub ecx, ebx ; 2048 - prob
137EFCD3 sar ecx, 5 ; >> 5 ← kNumMoveBits
;输出是顺序字节
136E943B mov edi, dword ptr ss:[ebp+10] ; edi = 0x401000(.text 基址,每轮重载)
136CDD04 mov byte ptr ds:[ecx+edi], dl ; [0x401000 + ecx] = dl伪代码:
ecx++; // 输出下标
if (ebx < 4) {
bitbuf = (bitbuf << 8) | *stream++; // ESI 喂位流
save = dl; // 0x137B3585: [ebp-1] = dl
...
// 用 EBX 里的概率查表:bound = (range>>11) * prob
if (code < bound) { range = bound; prob += (2048-prob)>>5; }
else { range -= bound; code -= bound; prob -= prob>>5; }
// 表驱动解码出一个字节 -> DL
...
}
dl = decode_literal();
*((u8*)0x401000 + ecx) = dl; // 写进 .text好家伙,这是一台 LZMA 解压器,'1'节的高熵内容其实是压缩包。
bph 0x13825E74trace 里 [ebp-8] 就是流指针(当时是 0x13662045,每读一字节 inc)。第一次命中时它是"已消费 5 字节之后"的位置,所以:
流起点 = [ebp-8] - 5 // 结果为 0x13662038拿 0x13662038 起的数据暴力试 LZMA 参数,一次命中:
lc=3, lp=0, pb=2 (即标准 props 0x5D)
dict_size ≥ 1 MB (64 KB 会在解到 72,571 字节时崩,说明有超过 64KB 的回溯引用)
流范围 0x13662038 – 0x136BCE8C,共 372,308 字节
解压输出 850,432 字节 = .text(850,184) + 248 字节零填充试解
import lzma
d = open(r'BGI.chs.DMP', 'rb').read()
stream = d[0x133E9EBA:0x13444D0E] # 372,308 字节
filt = [{'id': lzma.FILTER_LZMA1,
'dict_size': 1 << 20, # 1 MB 即可,实际值不影响结果
'lc': 3, 'lp': 0, 'pb': 2}]
out = lzma.decompress(stream, format=lzma.FORMAT_RAW, filters=filt)
open('text.bin', 'wb').write(out[:0xCF908]) # 850,184 字节,就是 .text
print(len(out), out[:8].hex()) # 850432 558bec83e4f88b4d输出前 8 字节 55 8B EC 83 E4 F8 8B 4D 和引擎入口一模一样,马上把整个条带解压出来
发现解压到0节的时候会报错,经过一番测试,解压参数改成 'dict_size': 1 << 26 就可以完整解压
四条流按文件偏移排列([起点, 压缩长度]),参数完全一致:lc=3, lp=0, pb=2,dict_size ≥ 1 MB(实测用 64 MB)。
| 节 | 流起点(文件) | 压缩长度 | 解压长度 |
|---|---|---|---|
'0' |
0x4C62B |
147,055,757 B | 172,039,168 B |
.data |
0x8CBAAA4 |
11,827 B | 54,784 B |
.text |
0x8CFC438 |
372,308 B | 850,432 B |
.rdata |
0x8E314E1 |
47,100 B | 126,976 B |
完整得出初始化代码
import lzma
FILT = [{"id": lzma.FILTER_LZMA1, "dict_size": 1 << 26,
"lc": 3, "lp": 0, "pb": 2}]
with open("BGI.chs.exe", "rb") as f:
img = f.read()
def un(off, n):
return lzma.decompress(img[off:off + n], format=lzma.FORMAT_RAW, filters=FILT)
text = un(0x8CFC438, 372308)[:0xCF908] # 850,184 引擎代码
rdata = un(0x8E314E1, 47100)[:0x1EE1B] # 126,491 常量/字符串
data = un(0x8CBAAA4, 11827)[:0x63528] # 406,824 其余补零
sec0 = un(0x4C62B, 147055757) # 172,039,168 '0' 节
print(len(text), text[:8].hex()) # 850184 558bec83e4f88b4d
print(len(sec0), sec0[:8].hex()) # 172039168 568bf133c9b80100产物与内存对照
运行时 '0' 映射在 0x554000,'1' 映射在 0xA966000(运行时节表被改写)。把解压结果按节 VSize 截断,与进程转储逐字节比:
| 节 | 差异字节 | 说明 |
|---|---|---|
.text |
0 | 完全一致 |
.rdata |
52 | +0xDC 起是指针,运行期导入绑定改写(如 1E E9 0E 00 → 30 DC DE 76) |
.data |
411 | +0x724 起同上 |
'0' |
2,355(514 处) | 运行期写入/随机化,分散在全段 |
'0' 解压结果比 VSize 多 106 字节零尾(0xA411C00,0x400 对齐)。开头 0x2C000(180 KB)是激活/反篡改区:UTF-16 Error at hooking API 位于 +0x22034,ASCII ActivationRequest 位于 +0x283E4。
尝试通过明文写者追踪解密器
尝试从明文写入者追踪解密器
从 Dump 分析台词块(其实大部分是空的)
块范围 0x2CA02000 + 0x407000(4.0 MB)
零字节 97.8% → 真正有内容的只有约 120 KB| 偏移 | VA |
|---|---|
| +0x20 | 0x2CA02020 |
| +0x190 | 0x2CA02190 |
| +0x1B690 | 0x2CA1D690 |
| +0x1B910 | 0x2CA1D910 |
成对出现,相隔 0x170 / 0x280 ,说明装的是两个脚本,每个带一份头,第一个脚本约 112 KB。
直接搜索屏幕上的中文,得到脚本池地址,然后
bph 0x2A02B020, w抓到了

004A7D78 8B C6 mov eax,esi
004A7D7A 83 E0 0F and eax,0xF ; 检查源对齐
004A7D85 8B D1 mov edx,ecx
004A7D87 83 E1 7F and ecx,0x7F ; ecx = 长度 % 128
004A7D8A C1 EA 07 shr edx,7 ; edx = 长度 / 128
;...
004A7D92 66 0F 6F 06 movdqa xmm0,[esi] ;┐
004A7D96 66 0F 6F 4E 10 movdqa xmm1,[esi+0x10] ;│ 一次搬 128 字节
004A7D9B 66 0F 6F 56 20 movdqa xmm2,[esi+0x20] ;│ 源 = ESI
004A7D9F 66 0F 6F 5E 30 movdqa xmm3,[esi+0x30] ;│ 目的 = EDI
004A7DA3 66 0F 7F 07 movdqa [edi],xmm0 ;│
004A7DA7 66 0F 7F 4F 10 movdqa [edi+0x10],xmm1 ;│
004A7DAB 66 0F 7F 57 20 movdqa [edi+0x20],xmm2 ;│
;...
004A7DE0 8D B6 80 00 00 00 lea esi,[esi+0x80]
004A7DE6 8D BF 80 00 00 00 lea edi,[edi+0x80]
004A7DEC 4A dec edx
004A7DED 75 A3 jnz 0x4A7D92 ;┘引擎正在把一块 95 KB 的编译脚本从 0x2F7BA130 搬进池子的 0x2A02B020。
接着往上游追
bph 0x2F7BA130, w
得到
0x460B00 装载器
├─ size = [this+0x14] ; 解压后长度,上限 64MB
├─ call 0x4A1B00 ; new(size) → dst
└─ call 0x45F690 ; DSC 解码(dst=EDI, record=ESI)
├─ 0x45F340 建 Huffman 表
│ eax = [record+0x10] ← LCG key
│ 0x48BF70 ← init: [0x4F8E18] = key
│ 0x48BF80 ← step: 返回密钥流字节
│ cl = record[0x20+i] - al ← 解密,512 次
└─ 0x45F570 逐符号输出
0x45F640 LZ 回拷(count=(sym&0xFF)+2,距离 12 位)
0x45F678 字面量
↓
0x4A2CB0 memmove → 0x4A7D71 快路径 → 4MB arena是引擎侧的脚本解压解密器,追错了...... 从这里倒是可以得到日语的解密器
意外发现的引擎侧 VM 分配器

经过了一番折腾才发现完全走错了,这一大块是引擎的临时缓冲池,用完就释放,写入时明文已经生成,所以明文写入的第一现场不在这里。顺着写入链条虽然追到了一块原始带中文的内存,但是和壳代码没什么关系。
尝试通过挂文件读取 API 定位解密器
结果非常让人惊讶,补丁的两个 arc 资源文件完全没有被读取过,尝试把两个 arc 文件直接替换为 0 字节文件,汉化仍然正常加载。这条路直接失败了,没想到两个 arc 文件是障眼法。
三、TTD 回放法
抓取明文写入目前证明十分困难,壳侧解密时间通过连续内存 Dump 目测只有亚秒级,解密完通过虚拟文件交付引擎立即释放,每次内存地址都不同。
目前接下来就两条路:
1、直接猛攻壳代码:壳的条带根本无法观察出来,而且有花指令、数据与代码混放、填垃圾字节等等混淆操作,壳都是通过各种跳板、失败路径回退与桩调用代码块,API 调用、常量等全部是运行时拼凑出来,直接猛攻代价极大,如果有逆向大佬倒是可以尝试,对我来说不可行。
2、抓读取密文的人:就是这一条路。
找到四个解压节的读取者
通过抓内存可知:读档前内存内无明文,但是读档后明文就躺在内存里面,直接使用 win32dbg 录制进程从读档前到读档后的过程,得到了 trace 文件 BGI.chs01.run(9.66 GB)与索引 BGI.chs01.idx(4.06 GB)
我盲猜密文就在壳初始化的时候解压的四个节里面,马上统计读取了四个节的 ip:
!tt 100
dx @$cursession.TTD.Memory(0x4d1000, 0x4d1100, "r").Count()
dx @$cursession.TTD.Memory(0x4d1000, 0x4d1100, "r").Select(r => r.IP).Take(10)按 4 KB 窗口在全轨迹上采样:
| 节 / 窗口 | 读次数 | 读指令 IP | 说明 |
|---|---|---|---|
.rdata @0x4D1000 |
6,183 | 0x45CC4F、0x499531 |
引擎读 IAT/全局 |
.rdata @0x4EE000 |
297 | 0x558CA7 |
'0' 内代码读 .rdata |
.data @0x500000 |
1,062,583 | 0x40AE9F、0x40AE7A |
引擎热全局 |
.text @0x401000 |
12 | — | 几乎无数据读 |
'0' @0x554000 |
828 | 0x8C31363 |
字节读+查表 |
'0' @0x560000 |
3,245 | 0xA8CBCAF |
字节读循环头 |
'0' @0x580000 |
1,107 | 0x5552FB |
CRC 步进 |
'0' @0xA00000 |
4,096 | 0x7F7A555 |
1 读/字节 |
'0' @0x7000000 |
5,776 | 0x69E254 |
CRC(表 0xA8C68D0) |
'0' @0x8000000 |
4,938 | 0xA895CEF、0x5CD26E |
dword 读+写回 |
'0' @0xA000000 |
4,096 | 0x7F9E088 |
CRC(表 0x7F73C48) |
'0' @0xA800000 |
4,096 | 0x7FC0800 |
XOR 字节读+写回 |
'1' 全部 13 个窗口 |
0 | — | 整段未被数据读 |
把每个读点让 LLM 分析:
0x7F9E088、0x5552FB、0x7F7A555、0x69E254、0x8C31363、0xA8AB8DC、0xA8CBCAF 都是同一族校验循环,去花后如下:
movzx esi, byte [ptr]
xor esi, state
and esi, 0FFh
mov esi, [table + esi*4]
shr state, 8
xor state, esi不同点只是各用一张 256 项表(0x7F73C48、0xA8A7A58、0x6BCDFC、0xA8C68D0、0x8C270BC),属于分布式的完整性校验,这是壳的反修改,不是解密器。
继续检查,读了内存又写了内存的只有三处:0x7FC0800(xor bl,[eax+edi] 后 mov [esi+eax-1],bl)、0xA895CEF、0x5CD26E。
先看看0x7FC0800在干什么
bph 0x7FC0800
该块被大量花指令包裹,去花后有效指令如下:
7FC07AA 33 C9 xor ecx,ecx
7FC07B1 8B C6 mov eax,esi
7FC07BC 03 45 0C add eax,dword ptr [ebp+0Ch]
7FC07CE 46 inc esi
7FC07E0 8B 4D 08 mov ecx,dword ptr [ebp+8]
7FC07EA 8B 59 04 mov ebx,dword ptr [ecx+4]
7FC07F0 8A C8 mov cl,al
7FC07F4 D3 C3 rol ebx,cl
7FC07F7 02 D8 add bl,al
7FC0800 32 1C 38 xor bl,byte ptr [eax+edi]
7FC0807 8B 45 14 mov eax,dword ptr [ebp+14h]
7FC080B 88 5C 06 FF mov byte ptr [esi+eax-1],bl
7FC0811 3B F2 cmp esi,edx
7FC0813 0F 82 91 FF FF FF jb 7FC07AA伪代码:
// esi=起始, edx=结束, edi=源基址
// [ebp+8]=ctx, [ebp+0Ch]=源偏移C, [ebp+14h]=目的基址A
for (i = esi; i < edx; i++) {
u32 seed = *(u32 *)(ctx + 4);
u8 x = (u8)(i + C);
u32 k = ROL32(seed, x & 31);
u8 ks = (u8)k + x;
dst[A + i] = src[edi + C + i] ^ ks;
}算法:
x = (i + C) & 0xFF
ks = (ROL32(seed, x & 31) & 0xFF) + x
out = in ^ ks其中:seed = 0xFF462BF0、ctx = *(u32*)([ebp+8])、C = [ebp+0Ch]、A = [ebp+14h]、edi = 源基址、esi/edx = 起止。本轮 L = edx - esi。
在断点处读到:seed=0xFF462BF0, edi=0x60D03C, C=0x27780, A=0x2A6A9DB8, esi=0x0D, edx=0x5FE0。输出开头为 00 00 00 10 00 14 20 11 04 0C 00 01 14 04 08 02 09 02 01 ...。与预期一致。
马上继续运行,观察写出内存,此代码块每运行一轮,就往内存写入一个字节明文。
!!!这就是解密器!!!
继续分析 seed 规律。
x32dbg 里在 0x7FC07A4 下静默日志断点,记录:
SEED={d:[[ebp+8]+4]} C={d:[ebp+C]} A={d:[ebp+14]} EDI={edi} ESI={esi} EDX={edx}发现 seed 完全不变。
尝试解密完整条带
seed 既然不变,每个字节的密钥流就只取决于 (偏移 & 0xFF)。通过已知明文对 256 个相位分别构造密文,直接在内存 Dump 上搜:
def rol32(v, n):
n &= 31
return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF if n else v
table = [((rol32(SEED, x) & 0xFF) + x) & 0xFF for x in range(256)]
def cipher_pattern(magic, p):
return bytes(magic[j] ^ table[(p + j) & 0xFF] for j in range(len(magic)))
def scan(img, magic):
hits = {}
for p in range(256):
pat = cipher_pattern(magic, p)
i = img.find(pat)
while i >= 0:
ok = bytes(img[i + j] ^ table[(p + j) & 0xFF]
for j in range(len(magic))) == magic
if ok:
hits[i] = p
i = img.find(pat, i + 1)
return hits结果:
BurikoCompiledScriptVer1.00:133 处(131 处在脚本区,0x7BEC000–0x86B4000);{0x10, len, 0, 0}记录头:58 处(0x5D2AD1–0x66F13C);BURIKO ARC20:5 处;CompressedBG___:114 处;OggS:15 处。
!!!解密成功!!!
四、收尾
解密器、初始化器已经完整破解,接下来就只要分析中文容器,写离线脚本就行了。
ARC20 结构
struct Arc {
char magic[12]; // "BURIKO ARC20"
u32 count;
Entry entries[count]; // 每条 0x80 字节,从 +0x10 开始
// 数据区起始 = 0x10 + count*0x80
};
struct Entry { // 0x80 字节
char name[16]; // +0x00
...
u32 offset; // +0x60,相对数据区
u32 size; // +0x64
};解密规则:plain[rel] = cipher[rel] ^ table[rel & 0xFF],rel 为归档偏移(归档首字节相位 0)。
| 归档 | 基址 | 条目 | 数据区 | 导出体积 | 类型 |
|---|---|---|---|---|---|
ipl._bp |
0x5D2941 |
3 | 0x190 |
24,845 | bin ×3 |
anmmngrm._bp |
0x60D03C |
57 | 0x1C90 |
397,600 | bin ×57 |
Att03 |
0x6E2D73 |
36 | 0x1210 |
9,563,229 | CompressedBG ×35 |
00_op_01 |
0x813C420 |
131 | 0x4190 |
11,303,885 | BurikoCompiledScript ×131 |
SGCG000000 |
0x8C67CEA |
156 | 0x4E10 |
29,473,584 | CompressedBG ×79 + bin ×77 |
这些也正好是壳交付的虚拟文件名
合计 383 个条目,实际导出 382 个(Att03 有 1 条 size 异常被跳过),约 50.7 MB。
导出代码
import lzma
import os
import struct
SEED = 0xFF462BF0
FILT = [{"id": lzma.FILTER_LZMA1, "dict_size": 1 << 26, "lc": 3, "lp": 0, "pb": 2}]
MAGIC = b"BURIKO ARC20"
def rol32(v, n):
n &= 31
return ((v << n) | (v >> (32 - n))) & 0xFFFFFFFF if n else v
TABLE = [((rol32(SEED, x) & 0xFF) + x) & 0xFF for x in range(256)]
def lzma_unpack(exe, off, n):
return lzma.decompress(exe[off:off + n], format=lzma.FORMAT_RAW, filters=FILT)
def dec_arc(sec0, base, rel, n):
return bytes(sec0[base + rel + j] ^ TABLE[(rel + j) & 0xFF] for j in range(n))
def find_arcs(sec0):
pat = bytes(c ^ TABLE[i] for i, c in enumerate(MAGIC))
arcs, i = [], sec0.find(pat)
while i >= 0:
hdr = dec_arc(sec0, i, 0, 0x10)
if hdr[:12] == MAGIC:
count = struct.unpack_from("<I", hdr, 12)[0]
name = dec_arc(sec0, i, 0x10, 0x18).split(b"\x00")[0].decode("latin1")
arcs.append((i, count, name))
i = sec0.find(pat, i + 1)
return arcs
def main(exe_path, out_dir):
exe = open(exe_path, "rb").read() # 读文件
text = lzma_unpack(exe, 0x8CFC438, 372308)[:0xCF908] # 解 exe 四条流
rdata = lzma_unpack(exe, 0x8E314E1, 47100)[:0x1EE1B]
data = lzma_unpack(exe, 0x8CBAAA4, 11827)[:0x63528]
sec0 = lzma_unpack(exe, 0x4C62B, 147055757)
for base, count, arc_name in find_arcs(sec0):
tab = dec_arc(sec0, base, 0, 0x10 + count * 0x80)
data_start = 0x10 + count * 0x80
arc_dir = os.path.join(out_dir, arc_name)
os.makedirs(arc_dir, exist_ok=True)
for k in range(count):
e = 0x10 + k * 0x80
name = tab[e:e + 16].split(b"\x00")[0].decode("latin1")
off = struct.unpack_from("<I", tab, e + 0x60)[0]
size = struct.unpack_from("<I", tab, e + 0x64)[0]
if not size or size > 0x400000:
continue
blob = dec_arc(sec0, base, data_start + off, size)
with open(os.path.join(arc_dir, f"{k:03d}_{name}.bin"), "wb") as f:
f.write(blob) # 明文写出验证:
00_op_01 的第一个条目是完整的编译脚本(BurikoCompiledScriptVer1.00,脚本名 Yuzu_2G,入口 main),内文可直接读出:
Yuzu_2G / main
... bss.h Function.h Yuzu_2G.h ic0004a
暑假结束后,我成了英雄。
bg1003sa bgm028
女生A「原来如此,说的也是。直到暑假前都要一直上学呢。要是发生点什么有趣的事情就好了」Att03、SGCG000000 的条目是 CompressedBG___ 图像块,anmmngrm._bp 是 {0x10,len} 记录。
遗留问题
- 脚本区 131 个
BurikoCompiledScript头全部是Yuzu_2G / main,表项+0x34处为 0,该区更像同一脚本的多版本/多块或目录项。 Att03有 1 个条目的 size 字段异常,原因未知。- CRC 校验的具体参数与 patch 方法未研究(
'1'节整段未被读,'0'、.rdata有读)。 '1'节在观察的运行期 0 次数据读,其作用未知。
本文授权:CC BY-NC 4.0 · 署名—非商业性使用