猿人学 App 第六题「session保持检测」逆向:绕过 native 反调试/伪 pinning 拆解请求算法(未完待续)

本文最后更新于:2026年9月7日 上午

[猿人学 App 逆向系列]第六题「session保持检测」,难度标”简单”、实为地狱。取数逻辑整个在 native onCreate 里:一个把 libcurl + OpenSSL 静态链接进去、goron 混淆、带反调试 + 伪 pinning 的原生 HTTP 客户端。
本文记录一条完整的攻坚链:反 root 机制分析 → 磁盘 .so 打补丁绕过伪 pinningstealth-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.sixChallengeSixActivityonCreate 本身是 native

1
2
static { System.loadLibrary("six"); }
public native void onCreate(Bundle bundle); // 整个取数 + 填 20 个 TextView 都在这里

strings libsix.so 一扫就露馅:

1
2
3
https://curl.haxx.se/...          # libcurl 静态链接
https://www.openssl.org/... # OpenSSL 静态链接
github.com/amimo/goron.git # goron 混淆(和第4题同款)

即:native 里用静态链接的 libcurl 自己发 HTTPS 取数,业务全在 so,且 goron 混淆(控制流平坦化 + 字符串加密 + blr x8 GOT 间接调用)。

反汇编定位到 curl 配置点:

1
2
3
4
CURLOPT_URL          @0x13f1b8   -> https://www.python-spider.com/api/app6
CURLOPT_POSTFIELDS @0x11a13c -> POST
CURLOPT_SSL_VERIFYPEER=0 / VERIFYHOST=0 (一条分支)
CURLOPT_CAINFO @0x13f294 (另一条分支,指向 bundled CA)

二、第一道墙:反 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
2
3
4
5
6
7
8
# 0x13f1c8 的 tbz(证书校验分支) -> NOP,强制走 VERIFYPEER=0
# /data/app 的 apk_data_file 被 SELinux 挡写,改用 patch 副本 + bind-mount 覆盖
su 0 sh -c '
cp /data/local/tmp/libsix.bak /data/local/tmp/libsix_p.so
printf "\x1f\x20\x03\xd5" | dd of=/data/local/tmp/libsix_p.so bs=1 seek=$((0x13f1c8)) count=4 conv=notrunc
chcon u:object_r:apk_data_file:s0 /data/local/tmp/libsix_p.so
mount -o bind /data/local/tmp/libsix_p.so /data/app/.../lib/arm64/libsix.so
'

反篡改不自校验 .text(patch 后不崩),mitm 直接解密。抓到明文请求:

1
2
3
4
5
6
7
8
9
POST /api/app6   (HTTP/2, curl/8.7.1)
# 一整套"元素周期表"命名的头:
aluminium: <base64(MD5 hex)> sulphur: <base64(MD5 hex)>
beryllium/neon/nitrogen: <SHA256 hex>
boron/carbon/fluorine/helium/hydrogen/lithium/
magnesium/oxygen/phosphorus/silicon/sodium: <各不相同的打乱 base62 字母表>
body:
data: <192 hex = 96字节>
page: base64("11259374:<ms>:<32位nonce>")

服务器回 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
2
3
# rusda-server 版本与 frida-python 对齐,非默认端口
su 0 sh -c 'setenforce 0; /data/local/tmp/rusda-server -l 127.0.0.1:8765'
adb forward tcp:8765 tcp:8765

attach 后 app 存活、不崩——反 frida 被绕过,可任意 hook。 万能钥匙到手。

五、一把钥匙开所有锁:OpenSSL 符号全 hook

枚举 libsix.so 符号:7130 个,是完整 OpenSSL 不是 stripped BoringSSL!直接按名 hook:

1
2
3
4
5
6
7
// Frida 17: Process.findModuleByName; 无 Java 桥
var m = Process.findModuleByName("libsix.so");
["EVP_DigestUpdate","EVP_EncryptInit_ex","EVP_EncryptUpdate",
"SHA256_Update","SHA256_Final","MD5_Update","MD5_Final",
"AES_set_encrypt_key","EVP_CipherInit_ex"].forEach(function(n){
Interceptor.attach(m.findExportByName(n), {...});
});

配合 nghttp2_submit_request(拿 HTTP/2 header 名值对)、0x11a144(POST body)、Stalker(调用图),逐字段对拍。

六、拆解 session保持 算法

同一请求里 hook 到 crypto + header,逐个对应上:

1
2
3
4
5
6
7
11"元素字母表"头(boron/carbon/.../sodium) = 各自随机打乱的 base62(62字符)
page = base64("11259374:<ms毫秒>:<32位随机nonce>")
aluminium = base64(hex(MD5(silicon 头的字母表))) ✓ 离线复现验证通过
beryllium = hex(SHA256(boron 头的字母表)) ✓ 离线复现验证通过
key = 某个字母表头的前 16 字符
iv = 另一个字母表头的前 16 字符
AES-128-CBC(page_b64, key, iv)80字节密文 ✓ 逐字节对上 EVP 输出

那些看似 decoy 的”元素周期表”头其实携带 AES key/iv 材料 + 被哈希成 sign——服务器用头里的字母表反推 key/iv、验哈希,这就是”session保持”的本质(session 态全塞在头里,服务端可复算)。

七、上 Ghidra:确认 MT19937 + AES

Stalker 拿到 body 构造函数的完整调用图后,按 crypto 指令密度排序 + Ghidra 反编译(analyzeHeadless + Java 脚本,注意 imageBase=0x100000 要给地址加偏移),确认了两个核心:

MT19937(梅森旋转)——生成 11 个字母表 + nonce:

1
2
3
// FUN_0023c3e4: 铁证 0x9908b0df(MAG01) + 624/397 + tempering 掩码
*(state + idx*8) = (upper & 1) * 0x9908b0df ^ state[(idx+397)%624] ^ (y>>1);
uVar3 = temper ^ (temper & 0x9d2c5680) ^ ((...) & 0xefc60000); // tempering

AES-128-CBC 函数——FUN_00218900(out, plaintext, len, key, iv)

1
2
3
4
ctx = EVP_CIPHER_CTX_new();
EVP_CipherInit_ex(ctx, EVP_aes_128_cbc(), ...); // key_len==16, iv_len==16
EVP_CipherUpdate(ctx, out, &outl, plaintext, len);
EVP_CipherFinal_ex(...); // page_b64(76) -> 80字节密文

八、未完成的 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 题这条链,值钱的是方法论

  1. 反 root 先做空对照——搞清它到底盯什么(这题只盯 Frida 注入,网络抓包无视它);
  2. 伪 pinning 别硬 MITM——so 是磁盘加载的,bind-mount 打补丁绕证书校验,不重签不丢 token;
  3. 反 Frida 上 stealth-frida(rusda/Florida)——版本对齐、非默认端口,一 attach 满盘皆活;
  4. 静态链接 OpenSSL 是送分——符号没 stripped,按名 hook EVP_*/SHA256_*/MD5_* 逐字段对拍;
  5. Ghidra 兜底穿透 C++/goron——analyzeHeadless + Java 脚本批量反编译,认出 MT19937/AES;
  6. 啃不动就诚实标注——data 这最后一层留白,写清边界。

一句话:这题的 CTF 精髓是”绕过 native 反调试/伪 pinning,把 session保持 的请求算法看明白”——这条链我走通了;data 那 10% 是纯运气式深挖,留作未完待续。


猿人学 App 第六题「session保持检测」逆向:绕过 native 反调试/伪 pinning 拆解请求算法(未完待续)
https://kingjem.github.io/2026/09/07/猿人学App第六题session保持检测逆向-未完待续/
作者
Ruhai
发布于
2026年9月7日
许可协议