猿人学 App 第三题「so 动态注册」离机复现:Looper 反模拟与一场"字节全对却被拒"的追凶

本文最后更新于:2026年9月1日 下午

[猿人学 App 逆向系列]第三篇。第三题「众里寻他千百度【so动态注册】」的 sign 由 native 生成。
这题是目前最曲折的一道:sign 逐字节逆出来了、模拟器和真机对拍一致,服务器却拒绝一切请求。本文完整记录这场追凶——从”设备时钟快 78 秒”,到最终揪出 native 里的 Looper.myLooper() 反模拟,再到在 Unicorn 里绕过它、做成真离线客户端。

⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。

本文目录

  • 请求模型 + so 动态注册(真身与诱饵)
  • Unicorn 模拟 sign:与真机对拍一致
  • 追凶(一):字节全对,服务器却拒
  • 追凶(二):设备时钟快了 78 秒
  • 追凶(三):app 的 sign 和我的不一样 —— Looper 反模拟
  • 绕过 Looper,做成真离线

一、请求模型 + so 动态注册

ChallengeThreeFragment 翻页:

1
2
3
byte[] raw = new ChallengeThreeNativeLib().sign(page);   // native
String sign = toHex(raw); // 大写 hex "0123456789ABCDEF"
service.OooO(page, sign); // @POST("/api/app3") @Field("page") @Field("sign")

ChallengeThreeNativeLibstatic { System.loadLibrary("three"); } public native byte[] sign(int);

侦察 libthree.so:不加壳、不 goron;sign 有一个静态导出 Java_..._sign。但题名叫「so动态注册」——真身其实是 JNI_OnLoadRegisterNatives 注册的那个。用 Unicorn 模拟 JNI_OnLoad、在 RegisterNatives 处截获方法表,果然:

1
sign -> 0x2104c  (签名 (I)[B)

静态导出是诱饵,动态注册的 0x2104c 才是真身(RegisterNatives 会覆盖同名静态导出)。

二、Unicorn 模拟 sign:与真机对拍一致

用 Unicorn 从 0x2104c 跑起来,打桩 JNIEnv/libc,捕获它 NewByteArray + SetByteArrayRegion 写出的 20 字节。函数里能看到 MD4 的 IV/常量,输入是 sprintf("%d", page)time() 秒。

对拍:Frida 在真机上直接调 0x2104c 取 20 字节,喂模拟器相同的秒——逐字节一致。看起来收工了。

三、追凶(一):字节全对,服务器却拒

把模拟器现算的 sign(转大写 hex)打 /api/app3

1
{"status":0, "message":"Access forbidden;please check request"}

逐字节正确、当前秒现算现提交、±300s 全扫、紧循环 25 次……全拒。而 app1/app2 从同一主机同一 IP 都正常,排除 IP 被封。诡异。

四、追凶(二):设备时钟快了 78 秒

先确认 app 自己能不能加载第三题——截图一看:界面一直转圈、加载不出。再一比时钟:

1
device: 1788242989   host/server: 1788242911   -> 设备快了 78

sign 内嵌的是 native time()(设备时钟)。设备快 78s → sign 带的是”未来时间” → 服务器判定不新鲜 → 连 app 自己都过不了。root 下把设备时钟校准:

1
adb shell su -c "date @$(host_epoch)"

再进第三题——app 出数据了(”请计算1-100页所有数字之和” + 一列数字)。机制确认:sign(page, 正确的秒) 有效。

五、追凶(三):app 的 sign 和我的不一样 —— Looper 反模拟

可我主机的模拟器 sign 还是被拒。于是做了最关键的一步对比:hook app 主线程真实调用的 sign,和我模拟器/Frida(都在非主线程)产出的 sign 放一起——

1
2
app  (主线程) : 12c6bd63f761505b9569848c...
emu (模拟器) : 21080c9f52666229956984... (任何秒都对不上)

不一样! 反汇编 sign 核心,真相浮出水面:

1
2
3
4
bl   #0x20424          ; x0 = env->CallStaticObjectMethodV(Looper, myLooper)
cbnz x8, valid_path ; myLooper() != null -> 走【有效】MD4 路径
; 否则(null,即模拟器/Frida 这种没有 Looper 的线程):
bl #0x1f770 ; 对中间值多做一次变换 -> sign 被【污染】失效

字符串常量坐实:android/os/Looper + myLooper + ()Landroid/os/Looper;

这是专门的反模拟/反 Fridasign 回调 Looper.myLooper(),只有在有 Looper 的线程(app 主线程)才生成有效 sign;在模拟器、或 Frida 主动调用所在的工作线程(都没有 Looper)上,sign 被故意搞坏。难怪逐字节”对拍一致”的是两个同样无效的版本。

六、绕过 Looper,做成真离线

对症下药:让模拟器的 JNIEnv 在这次回调上返回非空即可走有效分支。先打日志看 sign() 实际调了哪些 JNIEnv 索引:

1
[6=FindClass, 113=GetStaticMethodID, 115=CallStaticObjectMethodV, 176=NewByteArray, 208=SetByteArrayRegion]

于是把 索引 115(CallStaticObjectMethodV)桩成非空(伪造一个 Looper 指针):

1
if idx == 115: ret(0x6000); return   # 假 Looper,绕过 myLooper()==null 的污染分支

再对拍——emu-match-offset = 0,模拟器产出的 sign 与 app 主线程完全一致

最后补上”时钟”这一环:服务器严格按自己的时钟校验 sign 内嵌的秒,而开发机时钟可能有偏差(我这台走代理,偏了 ~1.4s)。客户端做自校准——围绕本机当前秒试 now, now-1, now-2 …,命中一次就记住这个偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from yuanrenxue_level3_emu import gen_sign   # Unicorn 模拟 sign(page, sec),已绕过 Looper

def fetch(page, s, cal=[None]):
offs = [cal[0]] if cal[0] is not None else [0,-1,-2,-3,1]
for _ in range(15):
for off in offs:
sec = int(time.time()) + off
j = s.post("https://www.python-spider.com/api/app3",
data={"page":page, "sign":gen_sign(page,sec).upper()},
headers={"User-Agent":"okhttp/4.9.2"}, timeout=8).json()
if str(j.get("status")) == "1":
cal[0]=off
return [int(x["value"].strip()) for x in j["data"]]
time.sleep(0.25)

跑通——纯离线(Unicorn 模拟 + 自校准时钟,不碰手机)逐页取数。

小结

第三题把”反离线”写进了 native 本身:

  1. so动态注册:真身藏在 RegisterNatives,静态导出是诱饵;
  2. Looper 反模拟myLooper()==null(模拟器/Frida 工作线程)时故意产出无效 sign——这也解释了为什么”模拟器和 Frida 对拍一致”具有迷惑性,因为它俩都是无效版本;
  3. 时钟强校验:sign 内嵌 time() 秒,设备时钟一偏,连 app 自己都过不了。

破法对应三步:Unicorn 跑 JNI_OnLoad 揪真身 → 把 CallStaticObjectMethodV 桩成非空绕过 Looper → 自校准时钟。这场”字节全对却被拒”的追凶,最大的教训是:对拍一致 ≠ 正确,当两边都跑在”被反制的环境”里时,它们会一致地错。


猿人学 App 第三题「so 动态注册」离机复现:Looper 反模拟与一场"字节全对却被拒"的追凶
https://kingjem.github.io/2026/09/01/猿人学App第三题离机复现-Looper反模拟/
作者
Ruhai
发布于
2026年9月1日
许可协议