猿人学 App 第十三题「封设备」离机复现:device_id 从 Unicorn 模拟到纯 Python 还原
本文最后更新于:2026年9月1日 下午
猿人学 App 第十三题「东风夜放花千树【封设备】」的离机复现记录:目标是不驱动 App,只靠逆向把
device_id的生成算法搞清楚。
反爬点是”封设备”——请求要带 native 生成的device_id,服务器按其中某段当”设备身份”限流。本文记录从 Unicorn 忠实模拟getDeviceId,到单变量对照锁定算法结构,最后反成一张 16 项查表的纯 Python 的全过程。⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
- 请求模型(静态反编译)
- 用 Unicorn 模拟 getDeviceId
- 单变量对照:锁定「设备身份」
- 「封设备」的真相
- 反成纯 Python(免 so / Unicorn)
一、请求模型(静态反编译)
ChallengeThirteenFragment 的翻页逻辑很直白,device_id 来自 native:
1 | |
- 端点:
POST /api/app13,表单device_id+page(登录后拦截器附加token)。刷新 page=1、加载更多 page++。
所以整题的核心,就是把 native getDeviceId() 弄清楚。
二、用 Unicorn 模拟 getDeviceId
libthirteen.so 被 goron/OLLVM 混淆、方法名运行时解密,jadx/CFR 都啃不动 sign 一类函数。于是用 Unicorn 忠实模拟,要点:
- 定位函数:模拟
JNI_OnLoad,在RegisterNatives处截获方法表 → 拿到getDeviceId @ 0x47944(签名()Ljava/lang/String;)。 - JNI / libc 打桩:
JNIEnv用函数表逐槽分派(NewStringUTF截获返回串);实现clock_gettime、AArch64 变参__vsnprintf_chk / __vsprintf_chk / vasprintf、strftime、random_device等。 - goron 重定位坑:
R_AARCH64_ABS64导入符号要写stub + addend(goron 会在调用点把 addend 抵消回 stub),漏加 addend 会跳飞。 - NETLINK 应答(关键洞察):getDeviceId 会
socket(AF_NETLINK) + sendto + recvfrom向内核发RTM_GETLINK / RTM_GETADDR读本机网卡 MAC / IP——它不是在连服务器!脚本伪造 netlink dump 应答让它跑完,就能拿到device_id。
跑通后,观察格式串可知:device_id 是若干字节用 %lX 逐字节拼成的 80 位大写十六进制(40 字节),里头混了毫秒时间戳、MAC、random_device。
三、单变量对照:锁定「设备身份」
有了模拟器就能固定一个输入、只动另一个,逐字符 diff,把结构拆清楚:
1 | |
| 变量 | 是否影响 device_id |
|---|---|
| 毫秒时间戳 | ✅ 改变时间戳区(6 位十六进制) |
| 本机 MAC | ✅ 改变中段 38 位 |
| random_device | ❌ 调用了但不进 id(反模拟诱饵) |
而且各段互相独立、无扩散:改 MAC 只动中段、改时间戳只动时间戳区,常量段纹丝不动。
⚠️ 踩坑:一开始误判 MAC / random 都不影响 id——根因是**模拟器的 netlink 应答把 MAC 写死成
02:00:00:00:00:00**(环境变量没接进去),所以”改 MAC”其实没改到。把 MAC 真正接进 netlink 应答后,中段立刻随之变化,才看清 MAC 就是那段”设备身份”。
四、「封设备」的真相
把 MAC 接对后再打服务器,行为就清楚了:
| 场景 | 结果 |
|---|---|
| 同一 device_id(同 MAC)连续翻页 | 约 2 次后 设备号已被封锁 |
| 换新 MAC 的 device_id | 正常返回,可直接跳任意页 |
| 随机 UUID / 乱改 device_id | Access forbidden;please check request |
即:服务器把 device_id 里的 MAC 段当”设备身份”,每设备限量后封;同时逐字节 checksum 校验整串,乱造无效。破解 = 每翻一页现造一个随机 MAC 的 device_id,服务器就当成一台新设备。
五、反成纯 Python(免 so / Unicorn)
既然各段独立,就能把 MAC → 那段的映射彻底反出来。密集采样后发现:
6 个 MAC 字节各占一组 4 字符,
group_i = BASE_i XOR delta(mac[i]);6 组共享同一个 delta 函数(只是各组常量基 BASE_i 不同):
1
2delta(m) = (T[m>>4] << 8) | T[m & 0xf]
T = [0,1,2,3,4,5,6,7,8,9, 0x71,0x72,0x73,0x74,0x75,0x76] # 16 项已验证
m=0x7f → 0x776、0xff → 0x7676全部吻合,与模拟器逐字节对拍一致;时间戳区只有 16bit(装不下完整毫秒),实测服务器不校验时间戳新鲜度 → 固定一张旧模板即可,只替换 MAC 6 组。
于是 gen_device_id() = 取固定模板 + 按随机 MAC 做 6 组 XOR 替换,一行现造,运行期不再需要 so 和 Unicorn:
1 | |
到这一步,整条链路已经离机、零依赖:不碰手机、不加载 so、不跑 Unicorn,一行现造合法 device_id 就能任意翻页。
小结
这题的”封设备”,本质是把一个并不复杂的算法(模板 + MAC 异或查表)藏进 goron 混淆的 native 里。拿到手的方法论:先用 Unicorn 把真实代码离机跑起来拿样本 → 单变量对照锁定结构 → 反成纯 Python。其中最关键的两个洞察是:socket(AF_NETLINK) 是读本机网卡而非连服务器;以及 device_id 里真正的”设备身份”是 MAC 段,轮换它即可当新设备。