Deep Archive Writeup
题目结论
- 伪 flag:
flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!}
- 真 flag:
flag{Cr@ckme_JigUr0_5ZPoJie_MT8B5_2O26:05:1G_xV9qL7n_L0L}
总览
这题最容易被带偏的地方,是 native 里同时存在两条链:
- 一条会还原出很像最终答案的伪 flag,并且还能继续解出隐藏日志和提示图。
- 另一条才是真正的校验链,最终返回系统最高权限。
关键点在于:伪 flag 不是纯干扰,它是后续解密真正线索的钥匙。
1. 真实校验点在哪里
Java 层会调用 native 的 sub_968096 作为输入总入口,对应:
verify_input_968096
- 地址:
0x2fa8
这条函数的逻辑很清楚:
- 先做反调试 / 反 Frida / 反 Xposed 检查。
- 输入长度必须在
5..256 之间。
- 如果命中
sub_350158,返回 2,进入 hidden-note 分支。
- 否则调用
sub_375940 计算 64 位十六进制 digest。
- digest 命中常量时返回
1,进入真成功分支。
也就是说:
返回 2:说明输入是伪 flag
返回 1:说明输入是真 flag
2. 伪 flag 的来源
native 里还有两个关键函数:
sub_132918:直接返回一条内置字符串
sub_350158:校验用户输入是否等于那条字符串
它们都会做同一件事:
- 拼接一段 base64:
9r28/DC8PBsB+ZaxUQ4O+MLiq6g5gWkbZf/CnU0/Epf3o+n2ao8=
- base64 解码。
- 再和内置 16 字节 XOR key 异或。
- 还原出伪 flag:
flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!}
所以前半段静态分析拿到的 flag,其实只是 hidden 分支的入口钥匙。
3. hidden-note 分支怎么走
当输入伪 flag 时,sub_968096 返回 2,Java 层进入隐藏分支:
- 先调用
sub_460339
- 解出 9 条隐藏日志
- 然后读取
R.raw.rundll32
- 把资源字节和伪 flag 一起传给
sub_979970
- 解出一张 note 图片
这 9 条日志内容如下:
> [ SYSTEM ] 检测到有效记忆碎片...
> [ SYSTEM ] 身份哈希部分匹配
> [ INFO ] 同步率:17%
> [ ERROR ] 同步错误!
> [ WARNING ] 当前凭证已被标记为:[伪造权限]
> [ INFO ] 检测到部分信息痕迹
> [ INFO ] 正在恢复隐藏文件...
> [ INFO ] 正在解密历史日志...
> [ RUN ] vim /deep-archive/recovery/notes/note_21470327.log
这几句已经明确说明:
- 当前凭证是伪造的
- 但它能恢复隐藏文件
- 也就是:伪 flag 不是终点,而是提示链的入口
4. rundll32 / res/4t 是怎么解的
在签名清单里能看到一个资源名 res/4t,经过 apktool 还原后,对应的是:
对应的 native 解密函数是 sub_979970,逻辑不是 AES,而是:
- 输入长度至少
521
- 把前
520 字节挪到文件末尾
- 然后用伪 flag 整串循环异或
- 检查结果是否为 PNG/JPEG magic
写成一句话就是:
先轮转 520 字节,再用伪 flag 循环 XOR
解出来是一张伪装成 Vim 界面的 note 图。
5. note 图真正提示了什么
note 图里最关键的几句是:
- 第一个密钥是伪造的,但不是毫无意义
- 真正的密钥不该隐藏在混淆的逻辑之中
- 真相不在逻辑迷宫里
- 要关注开发者无法擦除的签名
AES-256-CBC
EREB0S
这几句的意思是:
- 不要继续只盯 native 控制流。
- 真 payload 不在普通资源、普通代码里。
- 应该去原始 APK 容器的签名区域找东西。
6. 真正的 payload 在 APK Signing Block 里
这里是整题最关键的一步。
如果只有 jadx_out 和 apktool_out,是看不到这部分数据的,因为:
- APK Signing Block 位于 ZIP Central Directory 之前
- 它不是普通 ZIP entry
- 解包工具不会把它还原到目录树里
对原始 APK 直接读尾部,可以找到:
继续解析 signing block 内的 entries,会发现一个自定义块:
把它按 ASCII 看,就是:
EREB
这和剧情里的:
正好对上。
这个自定义块里存的是一段 JSON:
{"seed":"RVJFQjBTXzIxNDcwMzI3IQ==","archive":"RsxG28SwnXELKT6/i42/+npxR+t4H/ToXMxLpRrQZk9L8tVZQU5wVMjpI13kkxZlXgP2q9b7KEL2NRlfgXsy2g=="}
7. seed 和 archive 的作用
7.1 seed
seed base64 解码后得到:
EREB0S_21470327!
长度正好是 16 字节,因此可以直接作为:
7.2 AES key
伪 flag 去掉外层 flag{} 后得到:
N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!
长度正好是 32 字节,因此可以直接作为:
7.3 archive
archive base64 解码后得到 AES 密文。
于是整个解密参数就齐了:
- 算法:
AES-256-CBC
- Key:
N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!
- IV:
EREB0S_21470327!
- Ciphertext:
archive base64 解码结果
8. 解密得到真 flag
使用上述参数解密 archive,去掉 PKCS#7 padding,得到:
flag{Cr@ckme_JigUr0_5ZPoJie_MT8B5_2O26:05:1G_xV9qL7n_L0L}
这条字符串也能反过来通过 native 真校验:
sub_375940(true_flag) 的结果
- 正好等于
sub_968096 比较的四段目标 digest
因此这是最终 flag。
9. 题目设计总结
这题的设计非常完整,分成了两层:
第一层:伪 flag 线
sub_132918 / sub_350158
- 还原出伪 flag
- 用伪 flag 解出隐藏日志与 note 图
第二层:真 flag 线
- note 图提示不要只盯逻辑本身
- 转去原始 APK 的 Signing Block
- 在自定义块里拿到
seed 和 archive
- 用伪 flag 内核作为 AES-256 key
- 最终解出真 flag
所以伪 flag 的定位不是“纯干扰项”,而是:
用来打开真正解题路径的中间钥匙
10. 最终答案
flag{Cr@ckme_JigUr0_5ZPoJie_MT8B5_2O26:05:1G_xV9qL7n_L0L}