起因
某个 App 的接口里带着一组长得很像签名的参数(kps_wg、sign_wg、vcode 这一类,阿里系客户端常见风格)。我最初的直觉是:
这些都是”客户端身份校验”,换个 UA 假装成官方客户端,是不是就能绕过限速?
花了两天把链路摸了一遍,结论完全相反。这篇只记方法和踩坑,不点名具体 App,也不写可被直接拿去用的细节——就是一次学习记录。
一、先量,再逆
动手之前,我其实有一个更实际的问题:某个云盘下载只有三四百 KB/s,我怀疑”被识别成第三方客户端所以限速”。于是先做了一轮对照实验:
| 变量 | 结果 |
|---|---|
| 走本机代理 | ~380 KB/s |
| 绕过本机代理、直连中继 | 基本一样 |
| 直连云厂商 CDN | 一样 |
| 4 个文件并发 | 合计还是这个数(不是连接级限速) |
| 换 3 种 UA(含官方客户端 UA) | 完全没变化 |
| 换一台同局域网、同账号的机器 | 9 MB/s |
结论很清楚:慢跟”识不识别客户端”没关系,是本机那条链路的问题。先量链路,别先假设被风控——这一步省下的时间,比后面所有逆向加起来都多。
这也是我这次最想记的一条:逆向之前先确认”值不值得逆”。
二、静态勘察:能读到什么,读不到什么
目标 App 的请求签名由一套加固组件负责。静态看下来是这样的结构:
- 插件式加固:
lib<name>.so只是引导壳,真正的逻辑在一个带版本号的插件.so里(例如libxxxso-6.8.xxxxx.so这种命名),插件还带一份pkgInfo描述符记录版本和插件类名; - 两个
.so都是 stripped、动态符号表为空,字符串加密——sign、appkey、doCommand之类的关键字一个都搜不到; - JNI 走
JNI_OnLoad里的RegisterNatives动态注册,所以文件里没有Java_com_xxx_yyy这种导出符号; - 能扫到
Fptrace_s这类反调试痕迹。
顺手能读到的还有”组件编号表”(签名、静态数据加密、令牌、安全体……按编号分发),以及业务侧拼装请求参数的代码——拼装逻辑是明文的,算法在 native 里。
三、unidbg 环境搭建(踩坑记录)
要让它跑起来,我用的是 unidbg(Apache-2.0)。几个坑值得记:
- 构建:源码分支用 Maven 构建(国内镜像快很多)。用 JDK 17 编译会撞上
Module与java.lang.Module的歧义(上游是按 JDK 8 写的),加两处显式import就能过;install阶段会被maven-gpg-plugin拦住,加-Dgpg.skip=true。 - 后端 native 库:0.9.x 的 unicorn2 后端,JNI 入口其实是制品里的
libunicorn.so,而旧制品libunicorn_java.so是过期绑定,加载会报undefined symbol: helper_div_i32。解决办法是把libunicorn.so放进java.library.path,并显式addBackendFactory(new Unicorn2Factory(true))。 - 建模拟器:64 位 +
AndroidResolver,并且一定要vm.setJni(...),否则跑到一半会Please vm.setJni(jni)。
四、让它自己把结构讲出来
整篇最有复用价值的一步:不读汇编,直接跑 JNI_OnLoad,把 native 的启动协议打出来。第一次运行就能看到两件关键的事:
|
三条收获:
- 插件会反向向 Java 侧索要
ClassLoader——这说明插件类的加载权在 native 手里,也解释了”为什么插件类不在 APK 的 dex 里”; RegisterNatives直接把总入口长什么样告诉你(一个doCommandNative(int cmd, Object[] args)式的命令分发),比逆汇编高效得多;- 结合组件编号表,可以确认”签名/加密/令牌”是同一套命令分发的不同分支。
给 native 补几个 JNI 回调(ClassLoader 交握、静态字段读取、异常构造)之后,JNI_OnLoad 就能完整跑完。
整个启动协议长这样:
|
五、直调命令号:错误码也是信息
直接调那个命令分发函数、扫一批候选命令号,返回的都是结构化错误,比如:
9901:未初始化;9905:参数错。
这组错误码本身很有用——它说明插件初始化是由 App 的框架层驱动的,而框架层是 dex,我们没有执行它的能力。
六、unidbg 的边界(这次最大的认知)
unidbg 只能从宿主侧调用 native 方法;而被加固组件的框架代码(组件实现、初始化序列)在 dex 里,宿主调不动。
于是形成一个死结:
要触发插件解密它自带的 dex,得先跑框架代码;而框架代码正是 dex。
我试过两条补救:
- 扫描模拟器内存找
dex\n035/cdex头——JNI_OnLoad之后、裸调十余个命令号期间,一次都没命中; - 挂钩 libc 的
open/openat/stat/mkdir——全程零文件访问,说明初始化根本没开始。
经验:模拟加固插件时,先判断”初始化在 native 还是 dex”。如果在 dex 里,纯 unidbg 的静态模拟链就断了,得换动态手段(root + hook,或在模拟器里跑真机 App)。
七、结论
- 这类客户端签名的本质是设备绑定密钥 + 时效令牌:一个令牌绑账号信息,一个令牌绑时间戳/票据,密钥在加固 so 里、和设备绑定;
- 因此换 UA、换 IP、换机器都”骗不过”它;反过来说,服务端认的是那套令牌,而不是”你是不是官方客户端”;
- 本次真正的目标(下载慢)根本不需要逆向,量一次链路就解决了。
八、合规声明
以上全部观察都在我自己的账号、自己的设备上完成,目的是理解加固方案的工作原理,没有绕过任何服务端校验,也没有对外提供可直接使用的实现细节。这篇笔记只记录方法论与踩坑,供同样在做安全学习的同学参考。
附:用到的工具
unidbg(Apache-2.0)、jadx(Apache-2.0),以及系统自带的 unzip / strings / readelf / nm。装之前都确认过来源与许可证。
评论
0 条评论