猿人学 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

一、请求模型(静态反编译)

ChallengeElevenFragmentPOST /api/app11单字段 data = native String data(int page)(libeleven.so):

1
2
service.OooO0oO(data(page));          // @POST("/api/app11") @Field("data")
private native String data(int i); // static { System.loadLibrary("eleven"); }

刷新 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 跑壳的解密 stubDT_INIT)来自解密:结果它疯狂 mmapsvc #0,syscall 0xDE)约 90 万次、疑似反模拟死循环——不实际。于是换思路:既然壳在真机上一定会把代码解密到内存,那就从内存里 dump

三、内存 dump 脱壳

在 App 里打开第 11 题 → System.loadLibrary("eleven") 触发壳解密。root 看 /proc/pid/maps

1
2
6ca9ac9000-6ca9b09000 r-xp ... /libeleven.so     # 打包数据(udf),文件映射
6ca9b09000-6ca9b89000 r-xp 00000000 00:00 0 # ← 解密后的真实代码(匿名 r-x 区)

解密后的原始代码,就落在紧邻文件映射之后的匿名可执行区(本例 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
2
3
page 0   -> co0uYLIacJcRYTguYLKj
page 1 -> cT0uYLIacJcRYTguYLKa
page 100 -> cT4WrJgjroIRcL/scTgjYJ4=

观察:同页同时刻确定性;页码位数越多、密文越长(== 补位)→ 明文里含 str(page)。字符集像 Base64,但顺序被打乱。

五、算法还原:自定义 Base64

密文是自定义字母表 Base64。字母表就藏在解密后的 rodata 里(一段 64 字符、是标准表的置换):

1
2
CUST = 45AogfZw/9+FcYrSVH1TKM7k3BebEtqCIGPJyQD60lUXpNmhWuRLds8jainzv2Ox
STD = ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

把密文按 CUST→STD 映射后做标准 Base64 解码,明文赫然是:

1
2
page 0   -> "0:1788232511757"
page 100 -> "100:1788232511760"

明文 = f"{page}:{毫秒时间戳}"。再测服务器对时间戳的态度:now / 1 小时前 / 30 天前 / 2020 年 / 甚至 0——全部照常返回数据,说明不校验时间戳新鲜度。于是时间戳可以随便填,整个 data 生成就是一行纯 Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import base64, requests

CUST = "45AogfZw/9+FcYrSVH1TKM7k3BebEtqCIGPJyQD60lUXpNmhWuRLds8jainzv2Ox"
STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
_ENC = str.maketrans(STD, CUST)

def data(page, ms=0): # ms 服务器不校验,默认 0
return base64.b64encode(f"{page}:{ms}".encode()).decode().translate(_ENC)

def fetch(page):
j = requests.post("https://www.python-spider.com/api/app11",
data={"data": data(page)},
headers={"User-Agent": "okhttp/4.9.2"}, timeout=15).json()
return [int(x["value"].strip()) for x in j["data"]]

print(fetch(1)) # 纯 Python 现造 data 参数 → 直连公网取回该页数据

到这一步就彻底离机了:不需要设备、不需要 Frida、不需要 so,一行 data(page) 即可任意翻页。

小结

这题的”upx壳”只是把 data 的生成逻辑藏进加壳 native——真读懂后,它不过是 {页码}:{时间戳} 的自定义 Base64,连时间戳都不验。方法论上值得记的两点:对抗性的壳别硬啃离线脱壳,直接从进程内存 dump 解密后代码;以及 Frida 17 无 Java 桥时,靠 JNI 函数表照样能 hook / 主动调用 native 拿样本。


猿人学 App 第十一题「upx壳」离机复现:内存 dump 脱壳 + 自定义 Base64 还原
https://kingjem.github.io/2026/09/01/猿人学App第十一题upx壳离机复现-内存dump脱壳与自定义Base64/
作者
Ruhai
发布于
2026年9月1日
许可协议