猿人学 App 第四题离机复现:Unicorn 忠实模拟 goron so 造 UDP 包
本文最后更新于:2026年9月1日 下午
猿人学 App 第四题(UDP / native,难度”困难”)的纯离机复现记录:不驱动 App,逆向
libfour.so的请求逻辑。libfour.so被 goron/OLLVM 混淆,请求包由双 PRNG 造出。本文记录先看穿响应结构(头部即”位置表”)省掉逆 PRNG,再用 Unicorn 忠实模拟echo_cli造包、本机直连公网取值的全过程。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 手上的静态素材
- 协议与响应(最关键的发现)
- goron 造包:用 Unicorn 忠实模拟
- 取值流程
- 为什么第四题没做成纯 Python
一、手上的静态素材
从设备 pull 出 base.apk 后就再没碰过手机:
base.apk→lib/arm64-v8a/libfour.so- 工具:
unicorn+lief+capstone(模拟 / 反汇编 native),都是 pip 可装的纯 Python 库
二、协议与响应(最关键的发现)
ChallengeFourActivity.onCreate 是 native,逻辑全在 libfour.so(arm64,goron/OLLVM:控制流扁平化 + 运行期填充的间接跳转表)。硬编码服务器 47.95.8.136:12000(UDP),关键符号:echo_cli(造包+发送)、get_timestamp、xorshift64、RC4::*、minstd_rand(LCG 48271)+ uniform_int_distribution<uint8>。
先看响应(1024 字节)。最关键的发现是它的头部就是”位置表”——key 和 value 都散布在响应里,取值根本不用逆请求侧的 PRNG:
1 | |
而且本机公网能直连 12000 端口,连一个全零包都能收到 1024 字节响应。所以只剩一个问题:如何构造一个携带指定时间戳的请求包。
三、goron 造包:用 Unicorn 忠实模拟
echo_cli 用 get_timestamp() 的毫秒时间戳派生 PRNG、组 255 字节包、sendto 发送。这段被 goron 的间接跳转(realaddr = *(table+off) + delta 大常量算术)包裹,纯静态啃代价高。于是**用 Unicorn 模拟 echo_cli**:
- 按 program header 映射各 LOAD 段;
- 应用重定位(踩坑点):
R_AARCH64_RELATIVE:*addr = addend(填 goron 跳转表,addend 常是0xffffffff…负值靠 64 位回绕落回模块内);R_AARCH64_ABS64(导入符号):***addr = stub + addend** —— goron 会在调用点+(magic - state)把 addend 抵消回stub。漏加 addend 会直接跳飞,这是最大的坑;
- 打桩:导入函数(malloc/memcpy/inet_addr/socket/clock_gettime/…)用 Python 实现或返回 0;
get_timestamphook 返回目标时间戳;sendtohook 截获 255B 请求包并停机; - JNIEnv:
env* → 函数表 → 统一 stub,所有 JNI 调用返回 0(造包不依赖 JNI 返回值); - 置
x0=0, x1=env, x2=obj,从echo_cli跑到sendto,拿到确定性请求包(同一时间戳稳定复现,不同时间戳不同)。
关键片段(重定位):
1 | |
get_timestamp / sendto 的 hook:
1 | |
四、取值流程
题目要”获取某个指定时刻的当前数值”,只要把目标时间戳喂给被模拟的 get_timestamp,就得到对应的请求包:
1 | |
服务器接受任意时间戳并返回 f(时间戳),所以把题目给的固定时刻喂进去即得确定结果——全程离机,只依赖 unicorn 造包 + 本机 UDP 直连。
五、为什么第四题没做成纯 Python?
顺手也把造包往纯 Python 方向挖了挖:它不用 RC4;用两个 PRNG——xorshift64 链(种子=完整毫秒 ts)+ minstd_rand(种子≈秒级 ts//1000,同秒同流)+ libc++ uniform_int_distribution<uint8>;包体是交错 XOR 块(每 16 字节 = 明文^掩码 与 明文 字节交错),且调用次数随数据变长(不同 ts 下 xorshift 286 vs 297 次)。
结论:这段造包是一段定制、混淆、双 PRNG + 变长循环的逻辑,不是标准算法。对这类 OLLVM native,用 CPU 模拟器执行真实机器码才是稳健、忠实、同样离机的正解——依赖只有 unicorn/lief,都是 pip 可装的纯 Python 可调库。
小结
这题的方法论对照上一篇第五题很有意思:先判断”是不是标准算法”。第五题的签名是标准 MD4 的变体,能干净还原成纯 Python;而第四题的造包是定制的双 PRNG 逻辑,就别硬啃混淆——用 Unicorn 把真实机器码离机跑起来。另外一个省时的洞察是:很多”看起来随机”的响应包,其实是位置表 + 对称解密,取值根本无需逆请求侧的 PRNG。