猿人学 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
2
service.OooO0O0(getDeviceId(), page);   // @POST("/api/app13") @Field("device_id") @Field("page")
public native String getDeviceId(); // libthirteen.so,JNI_OnLoad 动态注册
  • 端点:POST /api/app13,表单 device_id + page(登录后拦截器附加 token)。刷新 page=1、加载更多 page++。

所以整题的核心,就是把 native getDeviceId() 弄清楚。

二、用 Unicorn 模拟 getDeviceId

libthirteen.so 被 goron/OLLVM 混淆、方法名运行时解密,jadx/CFR 都啃不动 sign 一类函数。于是用 Unicorn 忠实模拟,要点:

  1. 定位函数:模拟 JNI_OnLoad,在 RegisterNatives 处截获方法表 → 拿到 getDeviceId @ 0x47944(签名 ()Ljava/lang/String;)。
  2. JNI / libc 打桩JNIEnv 用函数表逐槽分派(NewStringUTF 截获返回串);实现 clock_gettime、AArch64 变参 __vsnprintf_chk / __vsprintf_chk / vasprintfstrftimerandom_device 等。
  3. goron 重定位坑R_AARCH64_ABS64 导入符号要写 stub + addend(goron 会在调用点把 addend 抵消回 stub),漏加 addend 会跳飞。
  4. 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
2
4EA873FFEC6DBE37DF93 · EC7090 · [ 随 MAC 变的 38 位 ] · B97F43A62E0D85C
固定前缀(app盐) 时间戳区 MAC 派生区 固定尾(app盐)
变量 是否影响 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 → 那段的映射彻底反出来。密集采样后发现:

  1. 6 个 MAC 字节各占一组 4 字符group_i = BASE_i XOR delta(mac[i])

  2. 6 组共享同一个 delta 函数(只是各组常量基 BASE_i 不同):

    1
    2
    delta(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 → 0x7760xff → 0x7676 全部吻合,与模拟器逐字节对拍一致;

  3. 时间戳区只有 16bit(装不下完整毫秒),实测服务器不校验时间戳新鲜度 → 固定一张旧模板即可,只替换 MAC 6 组。

于是 gen_device_id() = 取固定模板 + 按随机 MAC 做 6 组 XOR 替换,一行现造,运行期不再需要 so 和 Unicorn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import os, requests

# all-zero-MAC、固定时间戳的模板;仅 MAC 6 组会被替换
TEMPLATE = "4EA873FFEC6DBE37DF93EC7090F47601AF844301FBB1943F3C75246E886225001B97F43A62E0D85C"
T = [0,1,2,3,4,5,6,7,8,9, 0x71,0x72,0x73,0x74,0x75,0x76] # 16 项共享表
POS = [28, 34, 40, 46, 52, 58] # 6 个 MAC 组起始位置

def _delta(m): return (T[m >> 4] << 8) | T[m & 0xf]

def gen_device_id(mac=None):
if mac is None: mac = os.urandom(6) # 随机 MAC = 一台新设备
s = list(TEMPLATE)
for i in range(6):
v = int(TEMPLATE[POS[i]:POS[i]+4], 16) ^ _delta(mac[i])
h = f"{v:04X}"
for k in range(4): s[POS[i]+k] = h[k]
return "".join(s)

def fetch(page): # 每页换新 MAC,绕过封设备
j = requests.post("https://www.python-spider.com/api/app13",
data={"device_id": gen_device_id(), "page": str(page)},
headers={"User-Agent": "okhttp/4.9.2"}, timeout=15).json()
return [int(x["value"].strip()) for x in j["data"]]

print(fetch(1)) # 现造 device_id → 直连公网取回该页数据

到这一步,整条链路已经离机、零依赖:不碰手机、不加载 so、不跑 Unicorn,一行现造合法 device_id 就能任意翻页。

小结

这题的”封设备”,本质是把一个并不复杂的算法(模板 + MAC 异或查表)藏进 goron 混淆的 native 里。拿到手的方法论:先用 Unicorn 把真实代码离机跑起来拿样本 → 单变量对照锁定结构 → 反成纯 Python。其中最关键的两个洞察是:socket(AF_NETLINK) 是读本机网卡而非连服务器;以及 device_id 里真正的”设备身份”是 MAC 段,轮换它即可当新设备。


猿人学 App 第十三题「封设备」离机复现:device_id 从 Unicorn 模拟到纯 Python 还原
https://kingjem.github.io/2026/09/01/猿人学App第十三题封设备离机复现-deviceid纯Python还原/
作者
Ruhai
发布于
2026年9月1日
许可协议