猿人学 App 第十一题「upx壳」离机复现:内存 dump 脱壳 + 自定义 Base64 还原
本文最后更新于:2026年9月1日 下午
猿人学 App 第十一题「学诗谩有惊人句【upx壳】」的离机复现记录。请求参数
data由一个 UPX 加壳的libeleven.so生成。本文记录识别 upx 壳 → 从进程内存 dump 出解密后的代码 → 定位data()→ Frida 取样 → 还原出自定义 Base64 算法的全过程。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 请求模型(静态反编译)
- 识别 upx 壳
- 内存 dump 脱壳
- 定位 data() 并 Frida 取样
- 算法还原:自定义 Base64
一、请求模型(静态反编译)
ChallengeElevenFragment:POST /api/app11,单字段 data = native String data(int page)(libeleven.so):
1 | |
刷新 page=1、加载更多 page++。整题核心就是弄清 native data(page) 生成的那个字符串。
二、识别 upx 壳
拿 lief / capstone 看磁盘上的 libeleven.so:.text 全是 udf(未定义指令)、节内容不可读、notes 损坏。搜字符串能看到 UPX! 与 $Info: This file is packed——是 UPX 壳,但 UPX0 / UPX1 节名被抹掉了(标准 upx -d 靠这俩节名识别,抹掉即拒绝脱壳)。
试着离线用 Unicorn 跑壳的解密 stub(DT_INIT)来自解密:结果它疯狂 mmap(svc #0,syscall 0xDE)约 90 万次、疑似反模拟死循环——不实际。于是换思路:既然壳在真机上一定会把代码解密到内存,那就从内存里 dump。
三、内存 dump 脱壳
在 App 里打开第 11 题 → System.loadLibrary("eleven") 触发壳解密。root 看 /proc/pid/maps:
1 | |
解密后的原始代码,就落在紧邻文件映射之后的匿名可执行区(本例 base+0x40000 起、0x80000 大小)。用 dd if=/proc/pid/mem 把它 dump 出来,反汇编即为明文(能看到 data、(I)Ljava/lang/String; 等字符串,说明脱壳成功)。
四、定位 data() 并 Frida 取样
在 dump 里搜 JNINativeMethod 表——它由三个指针组成:name → "data"、sig → "(I)Ljava/lang/String;"、fnPtr。据此定位 data() @ 模块基址 + 0x41cb4。
反汇编 data():大量 std::ostringstream 拼接、一个 /1000 的时间戳魔数——纯静态啃 C++ 代价高。直接 Frida 主动调用取样更快:Frida 17 已移除 Java 桥,但 hook / 调用 native 不需要它;拿到 env 后用 JNI 函数表手动调 GetStringUTFChars 读返回串即可。
主动调 data(page) 得到样本:
1 | |
观察:同页同时刻确定性;页码位数越多、密文越长(== 补位)→ 明文里含 str(page)。字符集像 Base64,但顺序被打乱。
五、算法还原:自定义 Base64
密文是自定义字母表 Base64。字母表就藏在解密后的 rodata 里(一段 64 字符、是标准表的置换):
1 | |
把密文按 CUST→STD 映射后做标准 Base64 解码,明文赫然是:
1 | |
即 明文 = f"{page}:{毫秒时间戳}"。再测服务器对时间戳的态度:now / 1 小时前 / 30 天前 / 2020 年 / 甚至 0——全部照常返回数据,说明不校验时间戳新鲜度。于是时间戳可以随便填,整个 data 生成就是一行纯 Python:
1 | |
到这一步就彻底离机了:不需要设备、不需要 Frida、不需要 so,一行 data(page) 即可任意翻页。
小结
这题的”upx壳”只是把 data 的生成逻辑藏进加壳 native——真读懂后,它不过是 {页码}:{时间戳} 的自定义 Base64,连时间戳都不验。方法论上值得记的两点:对抗性的壳别硬啃离线脱壳,直接从进程内存 dump 解密后代码;以及 Frida 17 无 Java 桥时,靠 JNI 函数表照样能 hook / 主动调用 native 拿样本。