深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

用 unidbg 摸清一个加固客户端的请求签名(学习笔记)

起因

某个 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)。几个坑值得记:

  1. 构建:源码分支用 Maven 构建(国内镜像快很多)。用 JDK 17 编译会撞上 Module 与 java.lang.Module 的歧义(上游是按 JDK 8 写的),加两处显式 import 就能过;install 阶段会被 maven-gpg-plugin 拦住,加 -Dgpg.skip=true。
  2. 后端 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))。
  3. 建模拟器:64 位 + AndroidResolver,并且一定要 vm.setJni(...),否则跑到一半会 Please vm.setJni(jni)。

四、让它自己把结构讲出来

整篇最有复用价值的一步:不读汇编,直接跑 JNI_OnLoad,把 native 的启动协议打出来。第一次运行就能看到两件关键的事:

JNIEnv->FindClass(.../SecException)
JNIEnv->FindClass(.../adapter/common/HttpUtil)
CallStaticObjectMethod(...MainPlugin, getMainPluginClassLoader() => java.lang.ClassLoader)
JNIEnv->RegisterNatives(<bridge class>, ..., 1)
RegisterNative(<bridge class>, doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;, 0x…)

三条收获:

  • 插件会反向向 Java 侧索要 ClassLoader——这说明插件类的加载权在 native 手里,也解释了”为什么插件类不在 APK 的 dex 里”;
  • RegisterNatives 直接把总入口长什么样告诉你(一个 doCommandNative(int cmd, Object[] args) 式的命令分发),比逆汇编高效得多;
  • 结合组件编号表,可以确认”签名/加密/令牌”是同一套命令分发的不同分支。

给 native 补几个 JNI 回调(ClassLoader 交握、静态字段读取、异常构造)之后,JNI_OnLoad 就能完整跑完。

整个启动协议长这样:

graph TD
A["AndroidEmulator<br/>(aarch64 + Unicorn2 后端 + AndroidResolver)"] --> B["loadLibrary:加载引导 .so"]
B --> C["callJNI_OnLoad"]
C --> D{"native 反向向 Java 要<br/>ClassLoader"}
D -- "getMainPluginClassLoader" --> E["Java 侧给一个壳<br/>并把 loadClass 映射到 dex"]
E --> F["RegisterNatives<br/>注册 doCommandNative(int, Object[])"]
F --> G["命令分发<br/>签名 / 静态加密 / 令牌 / 安全体"]
G --> H["按 cmd 调用"]
H --> I["结构化错误:9901 未初始化 / 9905 参数错"]
I --> J["说明初始化在框架层(dex)<br/>纯 unidbg 走到这里就到头了"]

五、直调命令号:错误码也是信息

直接调那个命令分发函数、扫一批候选命令号,返回的都是结构化错误,比如:

  • 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。装之前都确认过来源与许可证。

文章作者:阿文
文章链接: https://www.awen.me/post/3f7c1a92.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️