HXV4 XP3 解密到底是咋回事?
写给第一次接触 Kirikiri 封包解密的朋友。
Deepseek-V4-Flash编写,略有不足请见谅
项目推荐:
文档分析及相关工具:
https://github.com/hktkqj/cxdec-hxv4-static-analysis/
自动化扫描字符串与拼接字符串:
当你满怀希望,第一次,打开Xmoe大佬的KrKrExtract把XP3拖入进去的时候
发现他为什么会提示一个倀,然后提示内存不足解包失败
因为这与之前的加密不一样,柚子社的软件外包公司对加密进行了进一步的升级
新手问题:XP3 是什么?
XP3 就像是一个 压缩包,里面装了游戏的各种资源:
bgimage.xp3
├── bgm_001.opus ← 背景音乐
├── ev_001.png ← 事件 CG
├── ......
└── ex_xxx.png ← 事件 CG N
与之前不同的是:文件内容被加密了,不能直接解压出来用。
那软件作者加了什么锁?
游戏的作者给 XP3 加了两层锁:
第一层锁:索引加密
XP3 里面有个叫 HXV4 的段,记录了每个文件的加密信息
这个段是加密的,需要钥匙才能打开
第二层锁:内容加密
即使打开了 HXV4,拿到了文件信息
每个文件本身的数据也被 XOR 过了
还需要另一把钥匙才能还原
这个工具包就是用来开这两把锁的。
钥匙在哪里?
这就很有意思了。钥匙不在明面上,而是藏在游戏的 EXE 文件里:
游戏.exe (4.5MB 的主程序)
│
├─ 内嵌了 3 个加密资源
├─ 内嵌了一段 0x2000 字节的 "盐"
└─ 内嵌了一条路径密钥
你可以把游戏的 EXE 想象成一个 俄罗斯套娃:
第 1 层:EXE 本身(CafeStella.exe)
└─ 第 2 层:bres 加密资源(STARTUP.TJS、BOOTSTRAP)
└─ 第 3 层:解密后的启动脚本(TJS2 字节码)
└─ 第 3 层:解密后的插件 DLL(763KB)
└─ 第 4 层:FilterManager 派生程序
└─ 第 5 层:HXV4 密钥 + nonce
└─ 第 6 层:XP3 文件里的 PNG、OPUS...
每打开一层,就离最终的解密密钥更近一步。故此臭名昭著的加密
解密流程(13 步走)
Step 1: 找到 EXE
你需要找到游戏的 .exe 文件
Step 2: 提取加密资源
EXE 里面藏了三个重要东西:
| 资源 | 大小 | 用途 |
|---|---|---|
| RCDATA/STARTUP.TJS | ~7KB | 加密的启动脚本 |
| RCDATA/BOOTSTRAP | ~328KB | 加密的插件 DLL |
| TEXT/127 | ~50B | 一条路径密钥文本 |
工具包用 --exe 参数就把它们读出来了:
# 解析 PE 结构,找到 resource directory
pe = pefile.PE("CafeStella.exe")
startup = pe.get_resource("RCDATA/STARTUP.TJS") # 6932 bytes
bootstrap = pe.get_resource("RCDATA/BOOTSTRAP") # 327659 bytes
salt = pe.read_at(0x2e4a00, 0x2000) # 8192 bytes
Step 3: 解析 TEXT/127 → 拿到 startup_path_key
TEXT/127 的内容是:
bres://./xfgp9i53ygpktxjfjyzcjf5hg2/
去掉前缀和后缀,得到路径密钥:
startup_key = "xfgp9i53ygpktxjfjyzcjf5hg2"
Step 4: 解密 STARTUP.TJS
解密公式很简单:
密钥材料 = startup_key 转 UTF-16LE + salt (0x2000 字节)
摘要 = SHA3-384(密钥材料) → 48 字节
密文 = ChaCha8(摘要, STARTUP.TJS) → 明文
解密成功后,文件头应该是 TJS2100(这是 TJS2 字节码的魔法签名)。
Step 5: 反编译 STARTUP.TJS → 拿到 bootStrap prefix
STARTUP.TJS 是游戏的启动脚本,里面有一行关键代码:
System.bootStrap(
"Cafe Stella and the Reapers Butterflies (C)YUZUSOFT/JUNOS INC. All Rights Reserved.",
...);
双引号里的那段话就是 bootstrap prefix。它后面会参与 DLL 的初始化。
Step 6: 从 STARTUP.TJS 找 BOOTSTRAP URL
脚本里还藏了一个字符串:
bres://./daagz6fftpcf5ayewqa7246z6w/bootstrap
从 URL 上取出 bootstrap_key:
bootstrap_key = "daagz6fftpcf5ayewqa7246z6w"
Step 7: 解密 BOOTSTRAP → 得到插件 DLL
和 Step 4 同样的算法,但用 bootstrap_key:
密钥材料 = bootstrap_key 转 UTF-16LE + salt (0x2000 字节)
摘要 = SHA3-384(密钥材料) → 48 字节
密文 = ChaCha8(摘要, BOOTSTRAP) → 明文(跳过前 8 字节)
明文 = zlib 解压 → 763KB 的 DLL
这个 DLL 就是游戏实际加载的加密插件。
Step 8: 从 DLL 读出配置表
DLL 里有一个配置表(默认在偏移 0x80E38 处),记录了:
UNIQUE: {Kanna*Natsume*Nozomi*Mei*Suzune}
WARNING: (一些后缀文本)
PARAMS: (参数表)
PUBKEY: (公钥)
其中的 UNIQUE 是归档级唯一标识,参与后续派生。
Step 9: 生成 Drip Program
这时候工具包调用一个叫 FilterManagerDerive 的程序:
┌─────────────────────────────────────────────┐
│ FilterManagerDerive
│
│ 输入:
│ ├─ bootstrap.dll (刚解出来的插件 DLL)
│ ├─ bootstrap_prefix (Step 5 拿到的)
│ └─ archive_unique_key (Step 8 拿到的)
│
│ 过程:
│ 1. 加载 DLL
│ 2. 模拟调用 System_bootStrap 初始化
│ 3. 执行内部的 DripValue VM
│ 4. 跟踪 128 条 lane 的状态
│ 5. 派生所有 filter state
│
│ 输出: drip_program.json
│ ├─ hxv4_key (32 字节)
│ ├─ hxv4_nonce0 (24 字节)
│ ├─ context_u32[] (DripValue 全局表)
│ └─ lanes[128] (filter 状态机)
└─────────────────────────────────────────────┘
drip_program.json 是整个游戏的解密的"万能钥匙"。有了它,就可以解密任何
被同款加密保护(同一游戏版本)的 XP3 文件。
Step 10: 打开 XP3 → 找到 HXV4 段
XP3 文件的结构是:
[XP3 头] [文件目录 (加密的)] [文件数据 (加密的)]
│
└─ HXV4 段 ← 这就是我们要找的
├─ payload_offset
├─ payload_size
└─ flags
HXV4 段记录了:
-
每个文件对应哪个 entry key
-
每个文件的 open flag
-
每个文件的 filter state
Step 11: 解密 HXV4 段
用 Step 9 拿到的密钥:
密钥 = hxv4_key (32 字节)
nonce = 根据 flags & 1 选择 hxv4_nonce0 或 hxv4_nonce1
密文 = XP3 文件中 HXV4 payload 的数据
解密 = XChaCha20-Poly1305(密钥, nonce, 密文)
→ 明文(前 4 字节 = 解压后大小)
→ zlib 解压
→ TJS Variant 格式的 entry 列表
解密后得到类似这样的记录:
[
{xp3_index: 0, key: 0x187d559bd9ac65f2, ...},
{xp3_index: 1, key: 0xa1774fe9aed9a9f3, ...},
...
]
每个 key 就是对应那个文件的 entry key。
Step 12: 生成 filter state
有了 entry key 还不够,还需要用它来生成 filter state(过滤器状态)。
这个步骤是 DripValue VM 的工作:
entry_key (uint64)
+ open_flag (0/1)
+ drip_program.json 里的 context_u32[] + lanes[]
= filter_seed_state (48 字节)
filter_seed_state 包含:
├─ boundary_seed_0 (16 字节) ← 边界 XOR 种子
├─ boundary_seed_1 (16 字节)
├─ split_offset (8 字节) ← 分割位置
└─ bulk_key (16 字节) ← 批量 XOR 密钥
Step 13: 解密文件
拿到 filter state 后,解密的操作其实很简单——就是 XOR:
文件数据 (从 XP3 读出)
第 1 层: Bulk XOR
只对文件的前 16 字节做 XOR:
data[i] ^= bulk_key[i] (i = 0..15)
第 2 层: Boundary XOR
对分割位置附近的两个区域做 XOR:
data[offset + i] ^= boundary_seed_0[i]
data[offset + i] ^= boundary_seed_1[i]
// 具体位置和范围由 split_offset 决定
做完这些 XOR,文件就还原成原始的 PNG/OPUS 了!
全景图
CafeStella.exe
│
├─ 读 TEXT/127 → startup_key = "xfgp9i53..."
├─ 读 salt (0x2000 字节)
├─ 读 STARTUP.TJS (bres 加密)
│
▼
SHA3-384(startup_key + salt) → ChaCha8 → 解密 STARTUP.TJS
│
├─ 反编译 → bootStrap prefix
└─ 找 bootstrap URL → bootstrap_key = "daagz6ff..."
│
▼
SHA3-384(bootstrap_key + salt) → ChaCha8 → 解密 BOOTSTRAP
│
▼
zlib 解压 → bootstrap.dll
│
▼
FilterManagerDerive(bootstrap.dll, prefix, UNIQUE)
│
▼
drip_program.json (hxv4_key, nonce, lanes, context...)
│
▼
XChaCha20-Poly1305(hxv4_key, nonce) → 解密 HXV4 索引
│
▼
entry keys + DripValue VM → filter state for each file
│
▼
XOR(文件数据, bulk_key, boundary_seed) → 明文 PNG/OPUS
一句话总结就是
解密 XP3 = 先从 EXE 的俄罗斯套娃里拿出这个游戏的万能钥匙 (drip_program.json),
再用万能钥匙开 HXV4 索引锁拿到 entry key,
最后用 entry key 派生 filter state 把文件数据 XOR 还原。
常见问题
Q: 这个对所有 Kirikiri 游戏都有效吗?
A: 对使用同款 Cxdec/HXV4 加密的才有效。大部分柚子社游戏都支持。
Q: drip_program.json 可以跨游戏用吗?
A: 绝对不行。 每个游戏、每个版本的 drip_program.json 都不一样,必须现场生成,然后记录保存之后应用在你自己的其他项目中即可。



5 条回复