动态DEX加载+反射+DES硬编码密钥:安卓三层逆向实战

四、 DES 加密验证 ------ 动态 DEX + DES ECB

题目信息

|----------|-------------------------------|
| 题目名称 | DES加密验证 |
| 题目分类 | REVERSE |
| 题目难度 | 初级 |
| 题目分值 | 350 |
| 附件 | CrackMe_2_5.zip (Android APK) |
| 安装命令 | adb install -t xx.apk |

题目展示

4-1 DES 加密验证 题目页面

题目分析

这道题是四道里结构最复杂的一题,但每一层都不难。整体可以拆成 Java 层动态 DEX 加载 + JNI 反射桥接 + Native 层 DES ECB 加密三层结构。下面分层来看。

1. APK 结构

解压 APK 后核心文件如下:

复制代码
classes2.dex              # Material Design 库

classes3.dex              # 主逻辑 (com.cr.crackme2.MainActivity)

classes4.dex              # DataBinding 生成类

assets/

  classes3.dex            # 动态加载的 DEX (com.cr.test.wide)

  myde.bin                # 另一个版本的 DEX (也包含 wide 类)

  my2.dex                 # 大型第三方库 DEX

lib/x86/libcrackme2.so    # Native 库

注意 assets 目录下还藏了一个 classes3.dex ------ 这是真正包含验证逻辑的 DEX,但它在 APK 安装后并不会被默认加载,而是由 MainActivity 在运行时通过反射动态加载。

4-2 jadx 打开 APK 分析主逻辑

2. 动态 DEX 加载机制

com.cr.crackme2.MainActivity 在启动时会执行一套典型的 DEX 动态加载流程:

• 从 assets/classes3.dex 读取 DEX 文件字节流;

• 保存为内部存储中的 mydex.dex;

• 通过反射获取 ActivityThread.currentActivityThread();

• 拿到 mPackages 中的 mClassLoader;

• 创建自定义 DexClassLoader 加载 mydex.dex;

• 从加载的 DEX 中反射获取 com.cr.test.wide 类;

• 调用 wide.init() 完成初始化。

用户输入 flag 后点击 "开始验证" 按钮,触发的是 wide.verify() 方法 ------ 注意,这个方法在动态加载的 DEX 里,不在主 DEX 中。

3. 反射桥接 callNativeMethod

反汇编 assets/classes3.dex 中的 wide.verify(),可以看到它先取输入框文本,然后调用一个 callNativeMethod 把验证工作转发出去:

复制代码
invoke-virtual  {v0,v5}, findViewById         # 获取输入框

invoke-virtual  {v0}, getText                  # 获取文本

invoke-virtual  {v0}, toString                 # 转字符串

invoke-direct   {v0,v5}, callNativeMethod      # 调用 native 验证

move-result     v2

if-eqz          v2, FAIL                      # v2==0 -> 失败

const-string    v4, "恭喜,这是..."            # 成功分支

invoke-static   {v0,v6,v3}, make              # 构建显示内容

invoke-virtual  {v0}, show

return-void

# 失败分支

const-string    v4, "flag..."                  # 失败提示

callNativeMethod 自己又是一层反射,最终调到 MainActivity.verifyFlag 这个 native 方法:

java 复制代码
Class<?>  clazz  = Class.forName("com.cr.crackme2.MainActivity");

Method    method = clazz.getMethod("verifyFlag", String.class);

Object    result = method.invoke(null, userInput);

return ((Boolean) result).booleanValue();

也就是说,验证逻辑最终落到 lib/x86/libcrackme2.so 中的 verifyFlag 函数(地址 0x24140)。

4. Native DES 分析

反汇编 verifyFlag,整体流程是标准的 "加密 + hex 化 + 比较" 三段式:

java 复制代码
GetStringUTFChars(env, input, &isCopy)    # 获取 UTF-8 输入

strlen(input)                              # 计算长度

malloc(length)                             # 分配缓冲区

des_ecb_encrypt(input, length, key, out)   # DES ECB 加密

bytesToHex(out, hexStr)                    # 转十六进制字符串

# 将 hexStr 与预期值比较...

DES 密钥的定位有点绕。汇编里有这么一条:

java 复制代码
lea    -0x47d7b(%ebx), %esi    # 密钥地址

其中 %ebx 是 GOT 基址 0x56e04,密钥偏移算出来是 0xf089。读 .rodata 偏移 0xf089 处的 8 字节,得到 3132333435363738 ------ ASCII 字符串 "12345678"。这就是 DES 密钥,硬编码在 .so 里。

5. Flag 提取

既然算法是 DES ECB + hex 比较,那么 .rodata 中一定藏着 hex 编码后的预期密文。在 .rodata 偏移 0xea05 处确实发现了一段连续的 hex 字符:

4-3 .rodata 偏移 0xea05 处的 hex 数据

这段 hex 直接解码就能拿到 Flag ------ 因为 verifyFlag 函数里 hex 化后的预期比较值就是 Flag 本身经过 DES 加密后的产物,但出题人图省事直接把 Flag 用 hex 编码塞进了 .rodata,根本没用 DES 真的算一遍。

用随波逐流或 Python 脚本解码都可以:

4-4 * 随波逐流解码得到* Flag

python 复制代码
# 直接从 .so 文件中提取 flag

with open('lib/x86/libcrackme2.so', 'rb') as f:

    f.seek(0xea05)  # .rodata 偏移

    hex_chars = b'0123456789abcdef'

    result = b''

    while True:

        b = f.read(1)

        if not b or b[0] not in hex_chars:

            break

        result += b

    flag = bytes.fromhex(result.decode())

    # 去除 PKCS7 填充

    pad = flag[-1]

    if pad <= 16:

        flag = flag[:-pad]

    print(flag.decode())

Flag

|-------------------------------------------------------------------------------------|
| FLAG flag{b527e2621131134ec22251cfbca75e8c9f5ae4f41371871fd55911927f66a1b4} |

技术总结

这道题在四道里考察面最广,但每一层都是套路,分层击破即可:

层级 技术点
Java 层 动态 DEX 加载(DexClassLoader + 反射 ActivityThread)
JNI 层 Native 方法声明 verifyFlag,反射桥接 callNativeMethod
Native 层 DES ECB 加密(密钥硬编码 "12345678")+ hex 编码比较
数据存储 Flag 以 hex 编码形式直接存于 .rodata 段,未真正使用 DES 加密结果

题目最有意思的地方在于:虽然出题人搭了三层结构(动态加载 + 反射 + DES),但 Flag 本身是用 hex 编码直接写死在 .rodata 里的,所以只要会 strings + 偏移定位就能秒出答案,DES 部分反而成了摆设。这种 "看起来很复杂、其实有捷径" 的设计在 CTF 里挺常见的。

相关推荐
被摘下的星星21 分钟前
Java 中 .length、.length() 以及 .size() 的区分
java·开发语言
代码不停26 分钟前
子数组问题
java·算法
深念Y27 分钟前
B站封面与元数据抓取完整方案
逆向
岁月宁静29 分钟前
一、《从零手撸 Agent》 我用 10 行代码跑通了第一次大模型调用(顺便踩了 4 个坑)
前端·python·agent
大连好光景30 分钟前
【AI Agent案例开发项目01解读】
chrome·python·langchain
青 春 记 忆33 分钟前
零基础入门python43:Django接口测试与库存边界
python·django·后端开发
2501_9160074736 分钟前
Flutter与游戏App加固避坑指南:如何为混合开发与高交互应用选对安全方案?
安全·flutter·游戏·ios·小程序·uni-app·iphone
IT小白杨38 分钟前
TikTok小店防封号浏览器实战:2026年多店铺环境隔离与账号安全运营技术解析
开发语言·数据库·安全·php·chrome devtools·指纹浏览器
Patrick在香港39 分钟前
Python 拉取 C&SD 官方 API:香港 2022 年已跨过“超老龄线“,而抚养比正在爬回 1961
android·c语言·python·数据分析·时序数据库·数据可视化·香港