[原创] Deep Archive {JiGuro} CrackMe 逆向分析:不止一个的秘密
一、基本信息
二、核心陷阱:一明一暗,双层校验
作者在核心验证函数 sub_968096 中设计了双层校验,第一层是精心构造的干扰项(Decoy),用于误导和筛选。这完美呼应了故事中的那句提示:“不要轻易相信,你看到的一切”,“秘密可能不止一个”。
| 层级 |
算法 |
产物(Flag) |
返回值 |
意义 |
| 第一层 |
Base64 + XOR |
flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!} |
2 |
干扰项 / 钥匙 |
| 第二层 |
APK签名块 + AES-256-CBC |
flag{Cr@ckme_JigUr0_5ZPoJie_MT8B5_2O26:05:1G_xV9qL7n_L0L} |
1 |
真Flag |
三、静态分析:从Java到Native
3.1 APK 解包与结构
首先使用 apktool 解包APK,观察其结构。除了常规的lib和res,res/raw/目录下有一个名为 rundll32 的1194KB文件,内部是混淆数据。
decoded/
├── lib/.../libdeeparchive.so # 核心Native库
├── res/raw/
│ ├── rundll32 # 混淆数据文件
│ └── *.mp3 # 各种音效(成功/失败/背景音乐)
└── smali/com/jiguro/.../MainActivity.smali (153KB)
3.2 Java层逻辑梳理
在 MainActivity.smali 中,声明了10个Native方法,核心验证方法是 sub_968096。
- 按钮点击:触发
dm/l1.smali 中的 onClick 事件。
- 验证线程:启动
dm/xt.smali 线程,在其中调用 sub_968096。
- 结果处理:返回值通过
dm/pg.smali 处理,分为三种情况。
关键返回值对照表:
| 返回值 |
界面反馈 |
含义 |
| 1 |
按钮显示“[ 已获取最高权限 ]”,背景变绿,播放成功音效,触发隐藏剧情 |
完全正确 |
| 2 |
按钮显示“[ 检测到未知凭证 ]”,背景变橙色 |
仅通过干扰项 |
| 其他 |
按钮恢复,显示“[ 权限验证失败 ]” |
完全错误 |
四、Native决战:libdeeparchive.so 深度解析
4.1 函数调用链
通过分析 libdeeparchive.so (x86_64, stripped),定位到核心函数 sub_968096 的内部流程:
sub_968096(用户输入)
│
├─ 1. 调用 sub_350158() 【第一层:XOR校验】
│ ├─ 失败 → return 0
│ └─ 成功 → 进入第二层
│
└─ 2. 调用 sub_375940() 【第二层:自定义SHA-256校验】
├─ 失败 → return 2 (仅第一层通过,落入陷阱)
└─ 成功 → return 1 ★ (两层全通过,获得真Flag)
4.2 第一层:解密干扰项
数据被分割存放:6段Base64片段在数据段,而前缀 9r28/DC8 以立即数形式嵌入在代码段。XOR密钥在偏移 0xe30。
解密过程(Python):
import base64
# 1. 拼接所有片段得到完整Base64串
b64_full = '9r28/DC8PBsB+ZaxUQ4O+MLiq6g5gWkbZf/CnU0/Epf3o+n2ao8='
# 2. Base64解码
decoded = base64.b64decode(b64_full)
# 3. XOR密钥(16字节循环)
xor_key = bytes.fromhex('90d1dd9b4bf20c443197f3ee126f60a7')
# 4. 逐字节异或
flag_decoy = bytes([decoded[i] ^ xor_key[i % 16] for i in range(len(decoded))])
print(flag_decoy.decode())
输出:
flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!}
看起来真的很像答案,但是无法通过
再看一眼 No one can reverse this program!
????很好,落入圈套了
4.3 第二层:干扰项就是钥匙——真正的Flag藏在哪里?
这是整个CrackMe最精彩的设计。 干扰项 flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!} 不是死胡同——它本身就是通往真Flag的钥匙。
关键线索:rundll32 与 note 图片
当输入干扰项时,sub_968096 返回 2,应用在 dm/pg.smali 中进入 Partial 分支。这个分支会:
- 调用
sub_460339() 输出9条系统日志(其中明确写着 当前凭证已被标记为[伪造授权])
- 读取
res/raw/rundll32(1.14MB 加密数据)
- 用干扰项作为 XOR 密钥,对
rundll32 解密,得到一张 note 图片(PNG格式)
Note 图片中的关键提示:
- 当前的"密钥"是伪造的,真正的密钥藏在更深的地方
- 只依赖Native逻辑是不够的
- 去原始 APK 的签名块(Signing Block)里找
AES-256-CBC / EREB0S
"不要轻易相信你看到的一切" —— 故事和提示都在告诉你:从静态分析SO找到的flag是假的,答案需要从更隐秘的地方获取。
关键线索:APK Signing Block 中的隐藏数据
APK 文件结构为:ZIP Entries → APK Signing Block → Central Directory → EOCD。
APK Signing Block 位于 ZIP Central Directory 之前,不属于普通 ZIP Entry,因此 apktool / jadx 解包后完全不可见——只能在原始 APK 文件中直接读取。
在原始 APK 的 Signing Block 中,作者嵌入了一个自定义 ID-Value 对:
| 字段 |
值 |
| 自定义 ID |
0x45524542 (ASCII: EREB,呼应故事中空间站"EREBOS",内部编号 45524542) |
| Value |
{"seed":"RVJFQjBTXzIxNDcwMzI3IQ==","archive":"RsxG28SwnXELKT6/i42/+npxR+t4H/ToXMxLpRrQZk9L8tVZQU5wVMjpI13kkxZlXgP2q9b7KEL2NRlfgXsy2g=="} |
解密真Flag:AES-256-CBC
从 Signing Block 提取到的数据就是解密的全部原料:
干扰项 = "flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!}" ← 你自己的发现
│
├─ 去掉 flag{} 包装 → AES-256 Key(恰好 32 字节)
│ Key = "N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!"
│
└─ Signing Block 中的 seed
Base64 解码 → AES IV(恰好 16 字节)
IV = "EREB0S_21470327!"
解密代码(Python):
import base64
from Crypto.Cipher import AES
# 从干扰项提取 AES-256 密钥(恰好 32 字节)
decoy = "flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!}"
key = decoy[5:-1] # "N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!" — 32 bytes = AES-256
# 从 APK Signing Block 提取
seed_b64 = "RVJFQjBTXzIxNDcwMzI3IQ=="
archive_b64 = "RsxG28SwnXELKT6/i42/+npxR+t4H/ToXMxLpRrQZk9L8tVZQU5wVMjpI13kkxZlXgP2q9b7KEL2NRlfgXsy2g=="
iv = base64.b64decode(seed_b64) # "EREB0S_21470327!"
ciphertext = base64.b64decode(archive_b64)
# AES-256-CBC 解密
cipher = AES.new(key.encode(), AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
# 去除 PKCS#7 padding
real_flag = plaintext[:-plaintext[-1]].decode()
print(real_flag)
# flag{Cr@ckme_JigUr0_5ZPoJie_MT8B5_2O26:05:1G_xV9qL7n_L0L}
验证:自定义 SHA-256
Native 中 sub_375940() 实现了自定义初始常量的 SHA-256,用于在做最终校验时比对。但这不是找到Flag的途径——你不可能从哈希值反向推出输入。
| 常量 |
标准 SHA-256 |
本CrackMe |
| H0~H3 |
6a09e667, bb67ae85, 3c6ef372, a54ff53a |
9ea9f03b, 428dd41f, 2671b8c3, 0a559ca7 |
| H4~H7 |
510e527f, 9b05688c, 1f83d9ab, 5be0cd19 |
0d0d0d0d, 00000000, 00000000, 00000000 |
# 验证:真Flag通过自定义SHA-256,拿到预置的期望哈希
custom_sha256(real_flag)
# → 906dd0355d5a646b5a2425b04ed2b42e03d7f10de2cf53a049e5e589734b209e
# 这个值在 SO 的 sub_375940 中与硬编码期望值比对 → 匹配 → return 1
五、完整解题路线图
[1] 静态分析 libdeeparchive.so
├─ 在数据段搜集 Base64 片段 + 立即数前缀
├─ Base64 解码 → XOR(密钥 0xe30)
└─ 得到干扰项: flag{N0_0ne_Can_R3v3rse_Th1s_Pr0gr4m!}
│
[2] 输入干扰项 → 返回 2 (Partial) │
├─ 应用触发 rundll32 解密(干扰项作 XOR key) │
└─ 得到 note 图片:"去原始 APK 的 Signing Block 找"│
│ │
[3] 解析原始 APK Signing Block ←──────────────────────┘
├─ 找到自定义块 ID=0x45524542 ("EREB")
└─ 提取 seed 和 archive
│
[4] AES-256-CBC 解密 │
├─ Key = 干扰项去掉 flag{}(恰好32字节)
├─ IV = Base64(seed) = "EREB0S_21470327!"
└─ 解密 archive → flag{Cr@ckme_JigUr0_5ZPoJie_MT8B5_2O26:05:1G_xV9qL7n_L0L}
│
[5] 输入真Flag → sub_968096 返回 1 → 完全通关 ✓
设计精髓:干扰项是"钥匙"而非"答案"。只有真正理解 rundll32 → note → Signing Block → AES 这一整条链路,才能找到真Flag。作者用一个逆向工程师最熟悉的 SO 静态分析作为第一层诱饵,真Flag则藏在 APK 文件结构中一个解包工具看不见的角落。