猿人学 App 第六题「session保持检测」逆向:绕过 native 反调试/伪 pinning 拆解请求算法(未完待续)
本文最后更新于:2026年9月7日 上午
[猿人学 App 逆向系列]第六题「session保持检测」,难度标”简单”、实为地狱。取数逻辑整个在 native
onCreate里:一个把 libcurl + OpenSSL 静态链接进去、goron 混淆、带反调试 + 伪 pinning 的原生 HTTP 客户端。
本文记录一条完整的攻坚链:反 root 机制分析 → 磁盘 .so 打补丁绕过伪 pinning → stealth-frida 复活反检测 → OpenSSL 符号全 hook → Stalker 调用图 → Ghidra 反编译,逐字段拆解这套”session保持”的请求算法。诚实声明:最后一个字段
data(96 字节自定义 cipher)我没能完全还原——本文如实标注为”未完待续”,把踩到的墙和确认的边界写清楚。逆向本就不总是圆满,记录过程本身有价值。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 题目形态:一切都在 native onCreate
- 第一道墙:反 root(只对 Frida 注入崩)
- 第二道墙:伪 pinning(bundled CA blob)—— bind-mount 磁盘 .so 打补丁
- 第三道墙:反 Frida —— stealth-frida(rusda)复活
- 一把钥匙开所有锁:OpenSSL 符号全 hook
- 拆解 session保持 算法
- 上 Ghidra:确认 MT19937 + AES
- 未完成的
data:诚实的边界
一、题目形态:一切都在 native onCreate
第 6 题有独立包 com.yuanrenxue.challenge.six,ChallengeSixActivity 的 onCreate 本身是 native:
1 | |
strings libsix.so 一扫就露馅:
1 | |
即:native 里用静态链接的 libcurl 自己发 HTTPS 取数,业务全在 so,且 goron 混淆(控制流平坦化 + 字符串加密 + blr x8 GOT 间接调用)。
反汇编定位到 curl 配置点:
1 | |
二、第一道墙:反 root(只对 Frida 注入崩)
直接开第 6 题会崩。反汇编 crash 链,发现 4 处直接 svc #0 系统调用(faccessat 查 su/magisk 文件、读 /proc/self/*),绕开 libc——所以 hook access() 这类思路无效。检测到异常后不显式 crash(),而是污染 OLLVM 平坦化的状态变量,导致 dispatch 越界 SIGSEGV。
但关键实验发现:用 root am start 直接开第 6 题不崩,只有 Frida 注入进程时才崩。也就是说反 root 对”设备已 root”宽容,只对”进程里有 frida-agent”敏感(扫 maps/线程名)。
教训:纯网络抓包对它完全隐形——反 root 只盯进程内注入。
三、第二道墙:伪 pinning —— bind-mount 磁盘 .so 打补丁
POST /api/app6 的证书校验分两条路:一条 VERIFYPEER=0(app_stop 等框架请求走这条,能 MITM),一条 CAINFO 指向 APK 里 assets/cacert.pem(Mozilla CA 包)以 CAINFO_BLOB 内存加载——app6 数据请求走这条,拒绝 mitmproxy 的自签证书。这就是伪 pinning。
破法很干净:libsix.so 是解压到磁盘 /data/app/.../lib/arm64/libsix.so 加载的(不是从 APK 直读)。把校验分支 NOP 掉即可,无需重签/重装/丢 token:
1 | |
反篡改不自校验 .text(patch 后不崩),mitm 直接解密。抓到明文请求:
1 | |
服务器回 403 "session保持检测"——因为 mitm 重新序列化 HTTP/2 破坏了 session 连续性(直连不经 mitm 时是 200)。但我们已经拿到真实请求模板。
四、第三道墙:反 Frida —— stealth-frida(rusda)复活
要看内部算法必须 hook,但 Frida 注入就崩(反 root)。解法是 stealth-frida:把 frida-server + 注入 agent 里所有 frida/gum/gmain/gum-js-loop 签名串改名重编。
对照 Frida 反检测三件套,Pixel 6 + Sentry 检测场景选 **rusda**(三层魔改、实测过 Sentry):
1 | |
attach 后 app 存活、不崩——反 frida 被绕过,可任意 hook。 万能钥匙到手。
五、一把钥匙开所有锁:OpenSSL 符号全 hook
枚举 libsix.so 符号:7130 个,是完整 OpenSSL 不是 stripped BoringSSL!直接按名 hook:
1 | |
配合 nghttp2_submit_request(拿 HTTP/2 header 名值对)、0x11a144(POST body)、Stalker(调用图),逐字段对拍。
六、拆解 session保持 算法
同一请求里 hook 到 crypto + header,逐个对应上:
1 | |
那些看似 decoy 的”元素周期表”头其实携带 AES key/iv 材料 + 被哈希成 sign——服务器用头里的字母表反推 key/iv、验哈希,这就是”session保持”的本质(session 态全塞在头里,服务端可复算)。
七、上 Ghidra:确认 MT19937 + AES
Stalker 拿到 body 构造函数的完整调用图后,按 crypto 指令密度排序 + Ghidra 反编译(analyzeHeadless + Java 脚本,注意 imageBase=0x100000 要给地址加偏移),确认了两个核心:
MT19937(梅森旋转)——生成 11 个字母表 + nonce:
1 | |
AES-128-CBC 函数——FUN_00218900(out, plaintext, len, key, iv):
1 | |
八、未完成的 data:诚实的边界
到这里 page / aluminium / beryllium / AES字段 / key,iv来源 / 11字母表 全部拆清、离线可复现。唯独 body 里的 data(96字节)没能还原,如实记录我撞到的墙:
data 确定不是什么(Ghidra + 动态双向排除):
- ❌ 不是
AES(page_b64)——那个只有 80 字节,且值对不上; - ❌ 不是 MT19937 随机字节(任何字节序/偏移/低字节都不匹配);
- ❌ 不是
page XOR MT流密码; - ❌ 不是
uniform_int_distribution输出(那个范围 0-57,是洗牌用); - ❌ Stalker 追到的 251 个函数里,没有一个是字节级 cipher(只有 MT + 字符串解密 XOR + 跳转表分发)。
为什么卡住:data 的生成函数经 goron 间接调用(blr x8,x8 从 GOT 动态算)到达——Ghidra 静态 XREF 全失效(GOT slot 无静态引用者);动态 hook 每个疑似点都落在 std::string/vtable 机制上空转;它大概率在 onCreate 更早处、经我追不到的间接跳转算出,且用了非标准自定义算法(不走任何 OpenSSL 原语)。
这不是”没努力”,是这个字段被 goron 间接 + C++ 对象化 + 非标准自定义算法三重埋到了当前工具链(stealth-frida + 符号 hook + Stalker + Ghidra)的极限。逆向到这一步,诚实标注边界比硬凑一个”看起来完整”的结论更有价值。
TODO:逐个反编译 onCreate 全部 goron 间接目标定位 data cipher 核心,或换更强的去混淆/符号执行手段。留待后续。
小结
第 6 题这条链,值钱的是方法论:
- 反 root 先做空对照——搞清它到底盯什么(这题只盯 Frida 注入,网络抓包无视它);
- 伪 pinning 别硬 MITM——so 是磁盘加载的,
bind-mount打补丁绕证书校验,不重签不丢 token; - 反 Frida 上 stealth-frida(rusda/Florida)——版本对齐、非默认端口,一 attach 满盘皆活;
- 静态链接 OpenSSL 是送分——符号没 stripped,按名 hook
EVP_*/SHA256_*/MD5_*逐字段对拍; - Ghidra 兜底穿透 C++/goron——
analyzeHeadless+ Java 脚本批量反编译,认出 MT19937/AES; - 啃不动就诚实标注——
data这最后一层留白,写清边界。
一句话:这题的 CTF 精髓是”绕过 native 反调试/伪 pinning,把 session保持 的请求算法看明白”——这条链我走通了;data 那 10% 是纯运气式深挖,留作未完待续。