首页
社区
课程
招聘
[分享]某环逆向top1:从 Oodle 定位到拿到 IoStore AES 密钥
发表于: 2026-9-26 07:51 181

[分享]某环逆向top1:从 Oodle 定位到拿到 IoStore AES 密钥

2026-9-26 07:51
181

一篇文章讲清两件事:这个游戏的资源用了哪种 Oodle 压缩,以及加密密钥是怎么从引擎库里"挖"出来的。
全程本地静态 + 少量模拟,不上真机。


0. 背景:资源是怎么装的

游戏是 UE 5.6.1(Shipping / ASTC),资源走 IoStore 容器:.utoc(目录) + .ucas(数据) + 可选 .pak。

读取管线:

pakchunk 索引  =  ① Oodle 压缩  →  ② AES-256 加密

所以想解包,得同时搞定"Oodle 解压"和"AES 解密"。

资源不在 APK 里,在 OBB(assets/main.obb.png,其实是 ZIP) 内:

HT/Content/Paks/pakchunk0-Android_ASTC.utoc/.ucas
HT/Content/Paks/pakchunk1-Android_ASTC.utoc/.ucas
HT/Content/Paks/global.utoc/.ucas

1. 先定位:这游戏用的哪种 Oodle

1.1 哪个库带 Oodle

扫全部 so 的特征串,只有引擎库 libUnreal.so 带,其余 0。

1.2 里面有两套 Oodle

套 用途 特征串
OodleData 资源压缩(IoStore/pak) OodleLZ_Decompress / OodleLZ_Compress / EDecompressionType::Oodle
OodleNetwork 网络包压缩 OodleNetworkHandlerComponent / EOodleNetResult::*

算法族 Kraken / Mermaid / Selkie / Leviathan;加上源码泄漏串 v:\devel\projects\oodle2\core\newlz*.cpp,可判定为 Oodle 2.x SDK 静态链入(不是外挂 oo2core 动态库)。

1.3 一个反直觉的点:名字只是"报错用的"

对 OodleLZ_Decompress 做精确交叉引用 → 只在两处出现,都不是调用点:

  • 一处走"日志回调指针",
  • 一处是报错串拼接。

真正的解压逻辑是编译进 .text 的代码,名字串基本只在异常时打印。

1.4 为什么静态拿不到"解压器入口"

Oodle 的函数表在一个全局结构里(…+0xe63d590 附近,各槽 blr 过去),而这段落在 BSS——文件里全是 0,运行时才被插件初始化写入。
→ 静态读文件拿不到解压器地址。

1.5 关键结论:Oodle 不是卡点

索引 = Oodle 压缩 + AES 加密,而 Oodle 有公开实现(ooz/oozextract,retoc 内建)——不用自己逆。
真正缺的是 AES 密钥。所以".utoc 打不开"的真因是缺密钥,不是 Oodle 版本对不上。


2. 拿到 AES 密钥的过程

2.1 先踩了一个坑:坏的 oracle

早期我用 SHA1(索引) 当 oracle,去筛"哪个候选常量是密钥"——结果全不对,一度误判"密钥不是静态的"。

真相:IoStore 索引是 compress(Oodle) + encrypt,明文 SHA1 根本对不上 → 这个 oracle 会把正确的 key 筛掉。
换 oracle:用 retoc(内建 Oodle + 真正解密)来判定。

2.2 密钥本体

在 libUnreal.so 里找到 32 字节常量(文件偏移 0xcdbc40):

c09b98c5b96110ffe43ecb8d38422facd7ca967655412eed0e530304690f2f1c

2.3 关键:它不是"孤立常量",而是包在回调里

静态一眼看不出,因为它藏在 key-provider 回调里:

key-provider 回调 0x9a93d08:
   adrp x8, #0xcdb000 ; add x8, x8, #0xc40    ; x8 = 0xcdbc40
   ldp  q1, q0, [x8]                          ; 读 32B
   stp  q1, q0, [x0]                          ; 写回输出 = 密钥
  • 注册 0x26a26e8:构造 key-provider 对象,存进全局 keystore(0xe295cd8)。
  • 触发 0x9aae9e0:在模块启动函数里 bl 0x26a26e8。
    ⚠️ 注意:不在 .init_array → 只跑构造器的模拟(如纯 Qiling)到不了这一步。

解密调用链:

sub_3AC7CA8(buf,size)                 ; pak/IoStore 索引解密包装
  ├ 有注册 crypto → crypto_arr[0]->vtable+0x60 解密
  └ 无注册 → sub_3AC7E14(out32,guid)  ; 从 keystore 取 32B
              → sub_264DA0C(key32,buf,size) = br [0xe1ec250]  ; OpenSSL AES

2.4 验证(决定性)

retoc -a c09b98c5b96110ffe43ecb8d38422facd7ca967655412eed0e530304690f2f1c \
      info pakchunk0-Android_ASTC.utoc
→ rc=0  container_id=c9842237aafca4e  chunks=7915  packages=4854
        flags=Compressed|Encrypted|Indexed  compression=[Oodle]

密钥正确 → 读出 7915 chunks / 4854 packages。

2.5 解包

retoc -a <key> unpack pakchunk0-Android_ASTC.utoc  assets/
retoc -a <key> unpack pakchunk1-Android_ASTC.utoc  assets/
→ 31,880 个文件(27,300 .uasset + 4,500 .ubulk + 71 .umap + …)

3. 复盘:三条经验

  1. 别用错的 oracle 否定静态密钥 —— 压缩+加密的容器,SHA1/明文校验会误杀正确 key;要用真解包器当 oracle。
  2. 密钥常藏在"结构/回调"里 —— 包成 key-provider(靠 adrp+add 才引用常量),要顺"谁调用 → 存到哪 → 谁触发"。
  3. 触发点可能不在 .init_array —— 本例在模块启动函数里注册,纯跑构造器会漏。

4. 一句话总结

资源 = UE5 IoStore(Oodle 压缩 + AES-256 加密)。Oodle 有公开实现,不用逆;AES 密钥静态藏在 libUnreal.so 偏移 0xcdbc40,由 key-provider 回调 0x9a93d08 在模块启动时注册。用 retoc 当 oracle 验证后,一把解出 31,880 个资源文件。


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

上传的附件:
收藏
点赞 ・1
打赏
分享
最新回复 (1)
雪    币: 413
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2 楼
这类分析如果能补充目标版本、运行环境和关键日志,复现与讨论会更准确。
5天前
0
游客
登录 | 注册 方可回帖
返回