Hxv4静态解包解决方案与分析流程讲解

该话题被推 逆向工程 AI 算法Hxv4静态解密柚子社CXDEC
140 编辑于

 HXV4 XP3 解密到底是咋回事?

写给第一次接触 Kirikiri 封包解密的朋友。


Deepseek-V4-Flash编写,略有不足请见谅


项目推荐:

文档分析及相关工具:

https://github.com/hktkqj/cxdec-hxv4-static-analysis/


自动化扫描字符串与拼接字符串:

https://github.com/1F1E33-float32/Cxdec_Tools


当你满怀希望,第一次,打开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 参数就把它们读出来了:

python
# 解析 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 是游戏的启动脚本,里面有一行关键代码:

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 都不一样,必须现场生成,然后记录保存之后应用在你自己的其他项目中即可。

本文版权遵循 CC BY-NC 协议 本站版权政策

5 条回复

听到过万水千山的感觉
发布于
kinotern
发布于
kinotern
发布于
bfloat16
发布于

@kinotern #2那个只能用disasm,decompile有问题

kinotern
发布于

(。>︿<。) 已经一滴回复都不剩了哦~