ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

使用Unidbg模拟执行阿里系so库:生成x-sign与x-mini-wua签名实战

2026/9/20 10:33:25 拓冰建站 浏览量
使用Unidbg模拟执行阿里系so库:生成x-sign与x-mini-wua签名实战 如果你抓过阿里系App的包大概率会在某个请求头里见过x-sign、x-mini-wua这两个名字。跟普通的query参数不一样这两个值是跟着每次请求动态算出来的而且几十个字符背后往往是一整套JNI调用链牵扯好几个so库。之前我想绕过它们试过纯Java算法还原、试过Frida Hook最后都因为so库内部大量的环境检测和加固逻辑而放弃。直到换用Unidbg才把这条链路彻底跑通。这篇东西面向的读者是那些已经给阿里系App做过基础抓包、知道x-sign大概长什么样但还没用Unidbg跑通native层的小伙伴。我会从环境搭建开始把so提取、JNI方法定位、Unidbg初始化、补环境、参数构造、返回值解析整个过程写一遍并附上可复制的完整Java代码。你不需要有很深的逆向功底但至少要对Android开发、JNI这些概念有个基本认知。1. 为什么Unidbg是跑阿里系签名的首选方案1.1 纯Java还原和Frida Hook各自的问题先说说我一开始走过的弯路不然你不知道坑在哪。第一反应是纯Java还原。思路很直接把so里生成签名的算法逆向出来然后用Java重新实现一遍。但这个方案在阿里系场景下基本属于硬刚。x-sign、长x-mini-wua这类参数的生成流程通常在加固过、混淆过、带VMP片段的so里汇编指令本身就可能被虚拟化还原难度和时间成本都高得吓人。就算你花两周还原出一个版本对方服务端稍作参数调整你又得重新逆一轮。第二个方案是Frida Hook。这个方案在单机调试时很爽能直接看so内部函数的输入输出但放到批量化场景就麻烦了需要一台已root的手机或模拟器需要维持Frida Server运行应用升级后脚本可能失效而且要处理so本身的注入检测、反调试、检测Frida特征等一堆问题。如果你只是自己研究还好一旦想着稳定批量跑维护成本非常高。1.2 Unidbg的模拟执行原理在PC上虚拟出一台手机Unidbg的思路和上面两条都不同。它不跑完整Android系统而是用Unicorn引擎在Java进程里模拟ARM/ARM64指令把so文件当作一段可以执行的CPU指令来跑。怎么理解呢你可以在PC上创建一个虚拟手机内存空间把目标so加载进去然后让CPU一条一条执行so里的汇编指令。关键是它还顺带帮你模拟了Android环境里的很多东西JNI调用、JavaVM、DalvikVM、libc系统函数、文件系统等等。当so里执行到JNI_OnLoad或者某个native方法时Unidbg会收到JNI回调然后你把so想调用的Java层方法在Java侧实现或mock掉。这样so根本分不清自己是在真机还是PC上该算的签名照样算出来。1.3 为什么用Unidbg做这件事最舒服不需要root、不需要真机、不需要模拟器一台主力开发机就能跑。调用速度很快同一个so可以连续调用几千次做参数对比、回归验证都很方便。可以在so内部下Hook点把中间态参数打出来排查问题比真机开Frida更方便。跨平台Java能跑的地方基本都能跑后续做成一个个独立签名服务也方便。对你来说Unidbg解决的核心矛盾是你不想逆算法但你又得调用算法。那就不逆直接用模拟执行的方式把算法跑起来。2. 环境搭建IDEA、JDK、Unidbg依赖引入2.1 版本清单与说明我自己的主力环境如下你照着来大概率不会翻车JDK 11Unidbg新版对JDK版本有要求JDK 8在部分分支会出兼容问题Maven 3.6及以上IntelliJ IDEA社区版就够macOS / Linux / Windows 均可但Windows下需要确保本机装好了VC运行库因为Unicorn引擎有native动态库需要加载如果你之前一直是Android开发注意Unidbg跑的是纯Java进程不需要SDK也不需要连接设备。2.2 通过JitPack引入依赖Unidbg目前主仓库是zhkl0228/unidbg在Maven Central没有稳定的官方制品最省事的办法是通过JitPack引入。pom.xml里加一下repositories repository idjitpack.io/id urlhttps://jitpack.io/url /repository /repositories dependencies dependency groupIdcom.github.zhkl0228/groupId artifactIdunidbg/artifactId version0.9.8/version /dependency /dependencies需要注意Unidbg版本迭代很快API变动也比较频繁。如果你拉到的版本和我下面写的代码有接口差异优先看这个版本的源码和demo。如果JitPack拉不下来直接git clone仓库用IDEA打开它的unidbg-android模块在src/test/java目录下看官方样例这是最不容易出错的入门路径。2.3 快速验证环境是否正常依赖引入后先写一个最小验证确保Unicorn引擎的native库能正常加载import com.github.unidbg.Emulator; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; public class UnidbgEnvCheck { public static void main(String[] args) { Emulator? emulator AndroidEmulatorBuilder.for32Bit() .setProcessName(com.example.check) .build(); System.out.println(Unidbg环境正常当前模拟架构 emulator.getPointerSize() * 8 bit); } }如果能打印出架构信息说明依赖、native库、本机环境都OK。这一步都过不去的话先去处理依赖问题别急着往下走。3. 提取so与定位Native方法入口3.1 解包apk找so这一步放在Unidbg初始化之前做是因为你后面所有代码都要围绕目标so的实际情况来写。先把apk后缀改成zip解压或者直接用命令行unzip app.apk -d /tmp/apk_extracted解开后到/tmp/apk_extracted/lib/arm64-v8a或lib/armeabi-v7a里翻一翻。阿里系App里跟签名相关的so常见的有libsgmain.so、libsgsecuritybody.so、libsecurityguard.so之类的名字但这不是绝对规律不同App、不同版本可能不一样。我一般用jadx里的代码引用去反推so归属而不是靠猜文件名。3.2 用jadx定位Java层Native方法拿jadx打开apk全局搜索x-sign附近出现的类找到声明了native方法的Java类。比如你可能会看到类似这样的代码public class SecurityGuard { public static native byte[] getXSign(byte[] input); public static native byte[] getMiniWua(byte[] input); }注意这些native方法所在的全限定类名后面Unidbg加载时就要用这个类名去解析。把类名、方法名、方法签名记下来这是字符串匹配的关键信息。3.3 用readelf核对导出的JNI符号拿到so文件后在Linux/macOS终端下执行readelf -sW libsgmain.so | grep Java_com正常情况下你会看到类似Java_com_xxx_SecurityGuard_getXSign Java_com_xxx_SecurityGuard_getMiniWua这种导出符号格式就是JNI的标准命名Java_ 包名中的_替换为_ 类名 _ 方法名。但注意阿里系很多so可能做了加固或者用RegisterNatives动态注册导出表里可能找不到Java_com_开头的符号。这种时候说明方法不是通过静态导出注册的而是so启动时在JNI_OnLoad里调用RegisterNatives手动绑定的。这个场景我在第5.4节单独讲。4. Unidbg初始化与补环境让so以为自己在手机上4.1 最小初始化代码Unidbg用起来最核心的结构就这几步创建模拟器、配置内存、创建DalvikVM、hook JNI、解析目标类。我先把最基础的写出来后面补环境的部分再往里面塞。import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.DalvikVM; import com.github.unidbg.linux.android.dvm.DvmClass; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.memory.Memory; import java.io.File; public class AliSignExecutor { private final AndroidEmulator emulator; private final VM vm; private final DvmClass securityGuard; public AliSignExecutor() { // 1. 创建32位模拟器进程名尽量用目标App的包名 emulator AndroidEmulatorBuilder.for32Bit() .setProcessName(com.taobao.xxx) .build(); // 2. 设置内存和系统库解析器23表示Android 6.0的API level Memory memory emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); // 3. 创建DalvikVM传入apk或dex文件 vm emulator.createDalvikVM(new File(app.apk)); vm.setJni(new DynamicJniHandler()); vm.setVerbose(false); // 4. 解析目标类类名替换成你反编译出来的类 securityGuard vm.resolveClass(com/xxx/SecurityGuard); } }这里解释几个关键点for32Bit()大部分so在armeabi-v7a里都有优先用32位跑稳定性和易用性都更好。arm64-v8a的so在部分Unidbg版本上支持还不够完整。setProcessNameso内部可能会读进程名做白名单校验把这个设置成目标App的包名能减少一些麻烦。AndroidResolver(23)负责帮你加载so依赖的系统库比如libc、liblog、libandroid等。createDalvikVM(File apk)传入apk文件会让Unidbg自动解析dex解析消耗会大一点你也可以只传一个空的dex文件后续纯手动resolveClass。4.2 处理JNI回调动态JNI Handlerso在跑的过程中一定会回调Java层方法比如读取某个字段、调用某个静态方法、拿context等等。你不能让这些回调都返回空否则so大概率崩溃。写一个继承JniHandler的类来兜底import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.memory.Memory; public class DynamicJniHandler extends JniHandler { Override public DvmObject? callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, DvmMethod dvmMethod, VarArg varArg) { String name dvmMethod.getName(); System.out.println(callStaticObjectMethod: dvmClass.getClassName() # name); if (getApplicationContext.equals(name)) { return vm.resolveClass(android/app/Application).newObject(); } if (getPackageName.equals(name)) { return new StringObject(vm, com.taobao.xxx); } return super.callStaticObjectMethod(vm, dvmClass, dvmMethod, varArg); } Override public DvmObject? callObjectMethod(BaseVM vm, DvmObject? dvmObject, DvmMethod dvmMethod, VarArg varArg) { String name dvmMethod.getName(); System.out.println(callObjectMethod: dvmObject.getObjectType() # name); return super.callObjectMethod(vm, dvmObject, dvmMethod, varArg); } Override public boolean callBooleanMethod(BaseVM vm, DvmObject? dvmObject, DvmMethod dvmMethod, VarArg varArg) { String name dvmMethod.getName(); if (isAppRooted.equals(name)) { return false; } return super.callBooleanMethod(vm, dvmObject, dvmMethod, varArg); } }一开始可以先全量打印这些回调看看so到底要调什么。打印出来的内容能帮你快速定位so卡在哪个环境检查上。等跑通之后再把日志关掉提升速度。4.3 用HookZz处理so内部的线程和系统函数阿里系so很喜欢在native方法里开线程做校验或者直接读取系统的/proc/self/maps、检查frida-server进程、检查/system/bin/su文件等等。这些在Unidbg环境里很容易导致崩溃或卡死。常规做法是用HookZz对关键函数做替换。比如把pthread_create给替换成什么都不干让so里的并发逻辑失效import com.github.unidbg.hook.hookzz.HookZz; import com.github.unidbg.hook.hookzz.ReplaceCallback; import com.github.unidbg.hook.hookzz.HookStatus; HookZz hookZz HookZz.getInstance(emulator); hookZz.replace(emulator, pthread_create, new ReplaceCallback() { Override public HookStatus onCall(Emulator? emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { // 直接返回0模拟线程创建成功但不做任何事 return new HookStatus(0, 0); } });再比如getenv有时候会被so用来做环境判断直接返回null也不一定行需要看实际情况决定是放行还是替换。我的建议是先跑看崩溃点在哪再针对崩溃点做hook。不要一开始就hook掉一堆函数否则会掩盖掉真实的逻辑路径。5. x-sign和长x-mini-wua的完整Java调用示例5.1 参数构造你要输入什么拿到一个目标App的请求x-sign和x-mini-wua在header里是按一定规则算出来的。具体输入参数是什么取决于so暴露的native方法怎么定义。常见的一种模型是方法接收一个byte[]这个数组由业务参数、时间戳、设备ID等拼接而成。怎么确定输入格式我的笨办法是在jadx里看native方法调用处的Java代码看调用方在调用之前构造了什么字符串、按什么顺序拼接。把那一段Java代码还原到测试工程里生成同样的输入字节数组再喂给Unidbg调用native方法对比抓包得到的x-sign。如果对不上就在Unidbg里用HookZz hook打印memcpy或者目标函数的入参看so收到的实际字节是什么。5.2 完整可复制的执行类下面给一个相对完整的Java类。注意类名、方法名、方法签名都要替换成你自己反编译出来的值这里只是把调用链路写清楚。import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.DalvikVM; import com.github.unidbg.linux.android.dvm.DvmClass; import com.github.unidbg.linux.android.dvm.VM; import com.github.unidbg.memory.Memory; import java.io.File; import java.nio.charset.StandardCharsets; public class AliSignExecutor { private final AndroidEmulator emulator; private final VM vm; private final DvmClass securityGuard; public AliSignExecutor() { emulator AndroidEmulatorBuilder.for32Bit() .setProcessName(com.taobao.xxx) .build(); Memory memory emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm emulator.createDalvikVM(new File(app.apk)); vm.setJni(new DynamicJniHandler()); vm.setVerbose(false); securityGuard vm.resolveClass(com/xxx/SecurityGuard); } /** * 调用x-sign签名方法 */ public String getXSign(String rawData) { byte[] input rawData.getBytes(StandardCharsets.UTF_8); // [B 表示byte[]这是JNI方法签名 byte[] result securityGuard.callStaticJniMethodObject(emulator, getXSign([B)[B, input); return bytesToHex(result); } /** * 调用长x-mini-wua签名方法 */ public String getMiniWua(String rawData) { byte[] input rawData.getBytes(StandardCharsets.UTF_8); byte[] result securityGuard.callStaticJniMethodObject(emulator, getMiniWua([B)[B, input); return bytesToHex(result); } /** * byte[]转十六进制字符串 */ private static String bytesToHex(byte[] bytes) { if (bytes null) return null; StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b 0xff)); } return sb.toString(); } public static void main(String[] args) { AliSignExecutor executor new AliSignExecutor(); String raw appKeyxxxtimestamp1700000000000deviceIdyyy; System.out.println(x-sign: executor.getXSign(raw)); System.out.println(x-mini-wua: executor.getMiniWua(raw)); } }代码逻辑不复杂核心就是callStaticJniMethodObject(emulator, 方法签名, 参数)。这个方法会通过Unidbg的VM帮你在so里找到对应的native实现并执行。返回的byte[]是JNI层直接导出的字节转成hex就是请求头里常见的值。5.3 返回结果不一定是Hex这里我必须多提醒一句bytesToHex是我最常见的写法但不要默认所有App都这样返回。有的App返回的是Base64编码有的是把byte数组截断后拼接有的还会在返回前做一层AES解密。判断标准只有一个——跟你抓包到的真实header对照。如果hex对不上试试Base64或者把返回的byte数组直接按UTF-8打印看是不是ASCII字符串。5.4 遇到RegisterNatives动态注册怎么处理前面说了阿里系so很多不是静态导出Java_com_符号而是在JNI_OnLoad里用RegisterNatives动态注册。这种情况下securityGuard.callStaticJniMethodObject直接按方法名找会找不到方法。处理方式有两类第一类在Unidbg里hook掉JNI_OnLoad阶段的RegisterNatives调用把注册信息打印出来。简单来说就是hookjni_register_natives或者monitorJNIEnv结构体里相关函数指针。Unidbg自带了不少JNI Hook工具你可以先从打印入手确认so把哪个符号注册到了哪个方法名上。第二类用IDA静态分析so在JNI_OnLoad里找到传给RegisterNatives的方法指针记录方法在so中的偏移然后用Module.callFunction直接按偏移调用。这个方式更底层但绕过了一整个JNI查找过程也是最稳的。我建议新手先走第一类把注册关系搞清楚再考虑第二类。6. 保姆级避坑我在跑阿里系签名时踩过的坑6.1 JNI方法签名写错导致找不到方法callStaticJniMethodObject里的方法签名是JNI签名格式方法名(参数类型)返回类型参数和返回里的byte[]要写成[BString要写成Ljava/lang/String;。最容易出错的点就是分号必不必须、类名斜杠方向、参数个数对不对。我的排查习惯是开vm.setVerbose(true)看日志。如果签名不对Unidbg会明确提示找不到对应方法还会把已注册的方法列表打出来。照着列表比对很快能找出签名问题。6.2 32位和64位so混用for32Bit()跑armv7 sofor64Bit()跑armv8 so但Unidbg对64位的支持成熟度总体不如32位。如果目标App只有arm64-v8a的so没有armeabi-v7a你要么去旧版本App里找32位so要么硬上64位模拟。实际经验是阿里系绝大多数App都保留了32位so建议优先用32位。6.3 so内部创建线程导致卡死或崩溃这是最经典的一个坑。so为了做异步上报或二次校验经常在native方法里pthread_create。Unidbg虽然能模拟线程但多线程场景下同步很容易出问题。遇到这种情况就把pthread_createhook成直接返回。如果线程创建后还要执行什么关键逻辑再考虑通过HookZz替换到Java线程里手动触发。6.4 环境检测设备指纹、root检测、调试检测阿里系so里环境检测是一套一套的。getenv、/proc/self/maps、/system/bin/su文件是否存在、当前进程是否被调试、frida-server特征字符串这些都可能被查到。Unidbg天然没有这些文件有些检测会直接返回安全但有些检测会因为读取不到路径而返回异常值反而触发so的兜底逻辑。我的应对办法是把常见的fopen、openat按需hook对敏感路径直接返回空或伪造内容。用emulator.getMemory().addHookListener去观察so的内核调用看到它读哪个文件再决定怎么返回。保持vm.setVerbose(false)之外单独给敏感调用加日志。6.5 时间戳不一致导致签名结果对不上有些so生成签名时会带上当前时间戳你在PC上调用时的时间和抓包时的时间不同结果自然不一样。这种情况不能简单怪Unidbg。处理方法是先用抓包报文里的时间戳拼接输入参数再调用签名方法如果结果一致说明时间戳参与计算而且你的输入拼接顺序是对的。6.6 依赖so缺失导致加载失败阿里系签名so通常不是单文件会有好几个so互相依赖。如果只往Unidbg里加载了主so加载时会报dlopen failed: library xxx.so not found。解决办法是把依赖so全部放到同一目录下在创建VM后显式加载依赖memory.loadLibrary(libsgsecuritybody.so);6.7 返回的byte数组要在Java侧二次处理有的native方法返回的并不是最终签名而是一个密文还需要拿这个密文去另一个Java方法里做变换或者拼接其他参数后再处理。遇到这种不是调用一次就完事的场景回到jadx里多看几行业务代码把native返回后的处理流程完整还原出来。6.8 不要跑一个异常就直接加hook这个算方法论层面的坑。我看到很多人一看到so崩溃就狂加hook、乱替换函数最后所有函数都被替换成空实现反而永远跑不通。正确顺序是先不加任何hook跑一遍记录崩溃堆栈根据堆栈定位具体是哪个函数、哪个上下文导致的崩溃只针对这个点做最小化修复跑通后再逐步打开开关验证逻辑是否完整。我一直用这个流程难得翻车。7. 这方法能做什么、不能做什么边界与合规Unidbg跑通阿里系签名这件事本身是技术研究范畴——理解安全SDK的算法组织形式、学习JNI调用链、掌握模拟执行思路这些对我个人能力提升帮助很大。公众号和社区里大量文章也都在讲这类技术它确实是Android安全研究里绕不开的一课。但工具是中性的用在哪完全取决于使用者。它可以用来对你自己的App做安全测评判断安全SDK在模拟环境下是否存在漏洞也可以用来做SDK选型对比看看不同厂商的防护强度还可以单纯作为一种学习手段把黑盒调用变成白盒理解。它不应该用来做这几件事未经授权批量抓取平台用户数据伪造请求刷单或薅羊毛绕过对方反作弊体系去攻击生产环境或者把别人APP的核心签名能力包装成服务去售卖。这些行为既违法又是不道德的。还有一个现实因素值得知道阿里系服务端风控不是只看一个x-sign值还会结合设备指纹、请求频率、账号行为、IP风险分来做综合判断。就算你在Unidbg里把签名跑得和真机一模一样服务端依然可能通过其他维度识别出这不是真实用户设备。所以就算你出于恶意目的搞定了Unidbg长远看也未必能绕开风控。想稳定合法地做技术研究正路永远是给自己准备好授权范围。最后说点实际感受我把这套东西跑通后最大的感受不是签名能算了这个结果而是模拟执行和逆向还原在思维方式上的巨大差异。Unidbg让我把精力从指令级还原中解放出来更关注调用关系、数据流和环境交互学到的通用性反而更强。如果你接下来想继续深入可以从这几个方向扩展第一深入理解RegisterNatives动态注册的机制很多时候你不需要去还原算法只需要把注册逻辑摸清楚就能拿到完整调用链第二研究Unidbg里的HookZz和Dobby框架把so里各种系统调用、字符串读取、随机数生成都管控起来签名调用的稳定性能再上一个台阶第三把Unidbg封装成HTTP服务让它支持多并发调用方便你后续做参数矩阵测试和自动化验证。最后再分享一个实用小技巧在Unidbg里跑任何so之前先把vm.setVerbose(true)打开让它把每次JNI调用都打出来。你可能觉得日志太吵但在早期调试阶段这些日志比任何调试器都好用它会帮你快速理解so的调用节奏和依赖顺序。等你完全跑通后再关闭性能会好很多。