猿人学 App 第二十二题「书中自有黄金屋」离机复现:STOMP + native 魔改 TEA
本文最后更新于:2026年9月2日 下午
[猿人学 App 逆向系列]第二十二题「书中自有黄金屋【stomp协议】」。这题把两样东西叠在一起:传输走 STOMP over WebSocket,payload 由 native 加密。
完整链路:反汇编libtwentytwo.so还原一个 TEA 变种 → Frida 逐位对拍验证 → 抓 App 的 WebSocket 帧(解 RFC6455 掩码)补齐 payload 的 base64 包装 → 手写 STOMP 客户端离机取数。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 请求模型:STOMP 订阅 + 发送
- native encrypt:反汇编还原 TEA 变种
- 输入块是什么:page + 一个”奇怪的时间戳”
- Frida 逐位对拍
- 最后一公里:抓 WS 帧解掩码,发现 base64
- 纯 Python STOMP 客户端
一、请求模型:STOMP 订阅 + 发送
ChallengeTwentyTwoFragment 用的是 STOMP over WebSocket(naiksoftware/StompProtocolAndroid):
1 | |
混淆字符串用 JVM 预言机解出来:
1 | |
NativeLib:
1 | |
所以核心是搞清 libtwentytwo.so 的 encrypt(int)。
二、native encrypt:反汇编还原 TEA 变种
libtwentytwo.so 的 JNI 是 RegisterNatives 动态注册(不是 Java_... 导出名)。读 JNI_OnLoad 里的 JNINativeMethod 表,encrypt/(I)Ljava/lang/String; 绑定到内部符号 f1。
反汇编 f1,核心是个循环,一眼 TEA/XTEA——因为出现了魔数 0x9E3779B9(TEA delta) 和 32 轮:
1 | |
但它不是标准 XTEA:标准 XTEA 每半轮之间会更新 sum、且用 sum 的不同位去索引 key;这里两个半轮共用同一个 sum(轮末才 +delta)、key 固定 k0/k1、k2/k3,还把移位项直接 k + (v<<4) 相加再异或。总之是一套自定义的 TEA 变种,照着汇编逐行转写即可。
三、输入块是什么:page + 一个”奇怪的时间戳”
f1 里两个输入字:
v0 = page(JNI 传入的 int);v1 = get_timestamp()。
反汇编 get_timestamp:它拿一个时间值,乘以除 1000 的魔数(0x20C49BA5E353F7CF + 右移 7)做除以 1000,结果存进 **32 位寄存器 w0**。
坑就在这:Frida hook 出来 v1 = 1624913135,可设备当前时间是 2026 年(1788331385),差了 5 年——一开始以为是”秒”,怎么都对不上。用模运算反推才明白:底层时间取的是微秒,/1000 得到毫秒,再塞进 32 位寄存器发生截断,所以
1 | |
1788331308271 ms & 0xFFFFFFFF = 1624913135 ——对上了。**”截断到 32 位”这一步,不反汇编 get_timestamp 是猜不出来的。**
四、Frida 逐位对拍
光靠读汇编容易在细节上翻车,拿 Frida 做真机对拍最稳。注意本机 Frida 17 移除了 Module.findBaseAddress,要用 Process.findModuleByName:
1 | |
真机跑一次翻页:IN page=1 v1=1624913135 → OUT 1581750949 3716016438。拿同样的输入喂给 Python 实现,输出逐位一致——算法确认无误。
五、最后一公里:抓 WS 帧解掩码,发现 base64
算法对了,可手写 STOMP 客户端 SEND 过去却服务端毫无反应(而”只订阅不发”能收到 App 触发的广播,说明订阅没问题)。差别一定在 SEND 帧本身。
明文 ws:// 不走 mitm,而且 WebSocket 客户端帧按 RFC6455 是掩码的,socket 层直接读是乱码。hook libc sendto 拿原始字节,自己解掩码:
1 | |
解出来 App 真正发的是:
1 | |
两个之前漏掉的点:① SEND body 是 base64("v0:v1"),不是裸的 “v0:v1”;② 帧里没有 content-length。补上这两点,服务端立刻回数据。
六、纯 Python STOMP 客户端
1 | |
100 页取齐,求和即通关。
小结
第二十二题是这一批里最”厚”的一题,三层皮都得揭:
- 传输层:STOMP over WebSocket——订阅
/topic/api22-receive收广播,SEND到/topic/api22触发; - 算法层:native
encrypt是魔改 TEA(delta 标准、key 内联、但轮结构非标准),v1还是被截断到 32 位的 epoch 毫秒——这个只有反汇编get_timestamp才看得出; - 封装层:payload 要 base64、
SEND帧不带 content-length——这两点靠抓 WS 帧 + 解掩码才补齐。
方法论上最值得记的是:读汇编 + Frida 对拍确认算法,hook sendto 解 RFC6455 掩码看清应用层真正发的字节。当”算法明明对了却收不到响应”时,别怀疑算法,去看封装的最后一层。