
最近在复盘淘特App的接口签名机制正好把x-sign参数从抓包到算法还原的整个链路又重新走了一遍。这个x-sign几乎出现在每个业务请求头里可以说是淘特客户端请求签名体系中最核心的一个参数核心作用就是确认请求来自正规客户端、参数没有被中间人篡改。做接口分析、爬虫采集、安全性评估的工程师基本都会碰到它。这篇文章我按自己实际操作的顺序来写从环境搭建、抓包绕过到so库定位、JNI函数分析再到签名算法还原与本地验证整个过程尽量展开来讲。适合刚接触安卓逆向的朋友也适合已经做过基础逆向、想系统梳理签名参数定位思路的同行按我这个流程走一遍基本能少踩一半坑。1. 项目整体思路与目标拆解1.1 这个x-sign到底是什么先明确我们要处理的对象。x-sign是淘特客户端在发送HTTP请求时自动附加在Header里的一个签名串通常和x-mini-wua、x-uid等参数一起出现。它本质上是对请求的method、path、query参数、body内容、时间戳以及客户端本地埋入的密钥做一系列运算后得到的十六进制字符串。服务端拿到请求后会用同样的算法重新计算一遍比对结果是否一致从而判断请求是否被篡改、是否来自伪造客户端。所以整个逆向的核心就一件事找到客户端本地生成x-sign的完整计算逻辑包括使用了哪些输入、经过了几轮变换、最后怎么编码输出。只要这些点全部确认清楚就可以脱离App在任何语言环境下复现签名构造出合法请求。1.2 技术选型为什么是Frida加Jadx加IDA这套组合选工具这件事很多人会纠结。我直接说结论这套任务最稳定的组合是Jadx负责静态看Java层调用关系IDA Pro负责分析核心so库的JNI导出函数Frida负责动态hook和打印关键入参返回值。如果不想用IDA那至少也要用Ghidra顶一顶否则纯靠看汇编还原算法会非常痛苦。实测下来淘特App的签名核心逻辑不在Java层Java层只是加载so并传入参数真正运算全部在native层完成。所以行程基本是先用Jadx定位Java层的调用点再用Frida确认传入native层的参数内容最后用IDA打开so文件顺着JNI函数往下梳理计算流程。整个过程没有一步可以省少了动态确认很容易在前面就判断错方向。1.3 从抓包到算法还原的完整链路我习惯把整个项目拆成四个阶段环境准备、抓包验证、算法定位、算法还原。环境准备阶段解决手机和电脑之间的代理连通、证书信任、反调试绕过抓包验证阶段搞清楚签名参数长什么样、改了参数服务端什么反应算法定位阶段从脱壳、字符串交叉引用到Frida hook逐步确定核心函数算法还原阶段则用IDA静态分析加Python代码验证最后做到本地生成签名能通过服务端校验。这四个阶段每一步都有独立的坑但整体又是串在一起的。前面抓包没搞定后面所有分析都无从谈起so库定位不准还原出来的算法大概率是错的。这篇文章我会把每个阶段的实操细节都写清楚。2. 环境准备与基础工具链2.1 抓包工具配置的三个关键点我用的抓包方案是Charles加一台Android真机。第一次做淘特逆向的朋友很多折在这一步因为默认配置下根本抓不到App的HTTPS请求。核心原因有两个App做了证书校验而且系统根证书里没有安装你的代理证书。配置步骤归纳下来三步。第一步电脑上打开Charles的SSL Proxying添加Host为淘特涉及的所有域名Port填443第二步手机连上同一个局域网设置HTTP代理指向电脑IP和Charles默认的8888端口第三步把Charles生成的CA证书用adb push到手机在系统设置里手动信任。注意Android 7以上系统默认不信任用户级证书如果App只是默认校验那装用户证书勉强能行但淘特这类App通常做了SSL Pinning所以还要额外用Frida绕过证书固定这个放到后面讲。提示抓包时优先关掉手机上的其它代理工具很多一次抓包失败是因为360、网易mumu这类工具自带代理占了端口。2.2 绕过模拟器检测与SSL Pinning我在真机上操作时遇到两个拦路虎一个是App检测到代理环境就拒绝发送业务请求另一个是抓包工具虽然能看到TCP流但TLS握手直接被客户端掐断。前者本质是环境检测后者才是证书校验。对于环境检测最省事的方法是直接用真机而不是模拟器这样大部分检测都能过。如果坚持用模拟器那么需要修改build.prop里的ro.product.model、ro.product.brand等字段还要关闭开发者选项中可疑的USB调试痕迹。对于SSL PinningFrida的脚本可以Hook系统的TrustManagerImpl把服务端证书校验逻辑替换为信任任意证书。常见的脚本网上很多核心就是Java.perform里重写checkServerTrusted方法。实测用这种方式后Charles很快就能看到明文请求内容。不过这里要注意一点绕过SSL Pinning之后不代表就完事了因为淘特部分接口是WebSocket或是HTTP/2长连接Charles对这种流量的呈现不如普通的HTTPS请求直观需要打开Charles的WebSocket选项卡才能看到完整报文。2.3 Jadx静态分析前的准备先脱壳直接用Jadx打开最新版淘特APK大概率只会看到一层壳入口真正的业务代码都在加固后的so或解密后的dex里。这就必须在静态分析前先做脱壳处理。我推荐两条路。一条是使用frida-dexdump这一类内存dump工具在App运行到业务界面时把内存里已经解码的dex dump出来另一条是BlackDex它可以主动触发壳的解密流程然后自动转储dex文件兼容性比前者更好。脱壳后把dump出来的dex文件拖进Jadx重新导入所有类名和关键字符串就都正常可见了。脱壳这个环节最影响后续效率。很多人一上来就跳过脱壳直接搜字符串结果什么都搜不到还误以为走错了方向。先花几分钟检查apk包名下面的classes.dex数量是否有异常如果只有一个很小的入口dex基本可以判断被加固了老老实实先dump再说。3. 抓包与签名参数特征分析3.1 明暗请求对比哪个参数在变抓包连通后我这里用Charles带出的会话举例。打开淘特首页随便点一个商品详情会看到几个请求。对比相同接口在不同时间发出的请求Header里大部分字段都是固定的只有x-sign和x-mini-wua一直在变。x-sign是一个32位的十六进制字符串看起来很像MD5输出但经过后续分析会发现它不是直接对某个字符串做MD5而是经过了一层自定义编码后的结果。另外一个重要特征是请求体的变化会直接导致x-sign变化。这符合签名的基本逻辑参与运算的输入一变输出必须跟着变。我最初尝试直接修改请求URL里的一个参数然后用原始x-sign重放服务端很快返回了签名校验失败之类的错误进一步确认x-sign和整个请求实体是强绑定的。3.2 排序与拼接签名前的数据标准化在对x-sign做还原时最容易忽略的一点是数据标准化。客户端不会拿原始请求的所有字段直接拼字符串去算签名而是会先做一系列整理。实际操作中我发现URL路径要经过规范化query参数要按key的字典序排序body内容可能还要截断或按固定格式重组。这个标准化规则没人告诉你只能通过反复控制变量来测。做法是固定住其他参数只改一个字段看x-sign变化是否和预期一致再通过构造不同顺序的query参数去试探排序规则。几次尝试后基本可以确定排序是按参数名的ASCII码从小到达执行的拼接方式是keyvalue中间用“”连接最后再加上一个固定盐值。这个结论在还原阶段起了大作用。3.3 初始特征与哈希算法猜测看到32位固定长度输出我的第一反应是MD5。但直接用常见字符串组合做MD5对比发现完全对不上。后来用x-sign本身去搜索源码字符串也没有直接出现。这说明两层原因第一可能最终输出前又做了异或或置换第二参与计算的原始字符串并不是直接可见明文可能经过了二进制层级的编码。这个阶段不要急着下结论记录好特征就行。真正缩小范围还是要靠下面的native层定位。4. 算法定位从壳到核心so4.1 定位native方法搜索关键字和JNI注册表分析脱壳后的Jadx工程里直接搜索“x-sign”通常能看到一个迁移函数它把请求参数传给某个native方法这个方法通过System.loadLibrary加载了一个名为libsgmain.so或libsgsecuritybody.so的库。这两个so库在淘宝系App里经常出现x-sign的核心算法基本都集中在libsgmain.so中。打开libsgmain.so的是IDA或Ghidra第一件事是查看导出函数。很多核心逻辑不会直接以fixSign之类的名称导出而是隐藏在JNI_OnLoad动态注册表格里。所以正确顺序是先看JNI_OnLoad找到RegisterNatives调用解析函数对应的Java全限定名和native函数指针这样才能知道哪个导出地址对应哪个Java方法。我这次就是这么定位的在JNI_OnLoad里找到注册表看到其中一个方法名为doSign对应地址为0xABC123用这个地址进入反汇编窗口后才真正开始算法分析。4.2 Frida动态Hook确认入参与输出位置静态定位只解决了“函数在哪”的问题但算法细节还是不清楚。这时我用Frida做动态注入直接Hook doSign这个native函数打印进入函数时的参数以及返回结果。脚本大概长这样var target Module.findBaseAddress(libsgmain.so).add(0xABC123); Interceptor.attach(target, { onEnter: function(args) { console.log([] doSign called); console.log(arg0(jstring): Java.vm.getEnv().getStringUtfChars(args[0], null).readCString()); console.log(arg1(jstring): Java.vm.getEnv().getStringUtfChars(args[1], null).readCString()); console.log(arg2: args[2]); }, onLeave: function(retval) { console.log([] doSign return: Java.vm.getEnv().getStringUtfChars(retval, null).readCString()); } });执行后能看到传入的参数里包含了一串看似拼接后的字符串这个字符串其实就是客户端对请求做标准化之后的原始串。把这段原始串记下来后面验算签名时可以直接对比。同时我也hook了System.loadLibrary确认so文件确实是从App私有目录下加载的没有二次释放到其他位置。4.3 so文件加壳与反调试的应对方案分析libsgmain.so的过程中我发现它并不是完全裸露的文件里存在一些ollvm的混淆痕迹某些关键函数还有反调试检测。用IDA直接F5查看时会发现控制流被打散成switch-case形式看伪代码很容易被绕晕。应对手段是交叉使用动态插桩。我用Frida去枚举native层调用的所有子函数通过打印调用栈判断当前执行到了哪里。如果在某一段指令上反复跳转那大概率是混淆壳的调度器如果频繁读取/proc/self/status检测调试状态则需要先在Frida脚本里把ptrace调用一并Hook掉。另外还可以用Unicorn模拟执行来绕过反调试。把so文件加载到Unicorn里从doSign入口开始模拟同时Hook内存读写逐步trace每一条指令的输入输出这种方法对处理片段级加密逻辑特别有效就是速度慢。但如果你也到了需要抠细节的阶段这招很值得尝试。5. 签名核心算法还原5.1 JNI静态结构从签名函数到算法骨架在IDA中顺着doSign的伪代码梳理发现整个流程可以归纳为三步第一步准备数据把Java层传入的原始串转成byte数组第二步调用内部子函数做核心运算这个子函数内部多次调用了类似MD5_update、MD5_final的结构第三步把运算结果转成32位十六进制字符串返回。看起来像是标准MD5但为什么之前用标准MD5对不上原因出在第一步。函数并不直接拿原始Java字符串计算而是先从so内部的一个固定偏移地址取出一段密钥再把原始串和密钥按特定顺序拼接。这个密钥每个版本都可能不同所以哪怕你抓到的明文原始串完全正确不拿到这段密钥也还原不出来。5.2 定位密钥和变换逻辑动态跟踪与内存搜索密钥在so文件里通常是静态存储的以二进制形式藏在.rodata段。IDA里可以查看doSign函数中引用的固定地址例如通过ADRP加ADD指令加载的地址一般就是密钥所在位置。我沿着引用的地址翻看数据段看到一段64字节的二进制前16字节像标准MD5的初始IV但后面的数据明显经过了自定义混淆。为了确认这段数据在运行时是否被修改我用Frida在函数执行前把内存中的密钥dump出来再在函数执行后dump一次。对比发现前后完全一致说明算法没有动态改写密钥。那么还原逻辑就简单了只要把这段固定密钥提取出来按IDA显示的拼接顺序组合就能得到正确的M5输入串。5.3 纯Python复刻签名流程拿到密钥和拼接规则后我用Python写了一个本地验证脚本。脚本结构很直接先复现Java层拼好原始串的标准化过程再按so里的顺序拼接密钥最后调用hashlib计算MD5。为了排除遗留问题我用抓包时记录的原始输入串和标准输出对比。import hashlib import time def generate_xsign(raw_string: str) - str: fixed_key bytes.fromhex(XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX) data raw_string.encode(utf-8) fixed_key md5_value hashlib.md5(data).hexdigest() return md5_value raw_string GET/api/xxxa1b2... local_sign generate_xsign(raw_string) print(local :, local_sign)最开始跑出来和抓包结果不完全一致排查后发现是我少算了一个字段拼接原始串时客户端还会把当前时间戳按毫秒精度追加在末尾而抓包的明文请求里时间戳和签名是一同发出的所以重放时必须保证时间窗口内有效。把时间戳加进去后本地生成的签名和线上抓到的x-sign完全吻合说明核心还原逻辑已经成立。6. 常见问题与故障排查实录6.1 抓包阶段为什么看不到HTTPS明文最常见的问题是手机装了证书但Charles依然看不到明文。这个现象绝大多数不是配置问题而是App的SSL Pinning还没绕过或者系统版本高导致用户证书不被信任。先确认安装时选择的证书类型Android 7以上必须把用户证书“移动”到系统证书目录否则部分App根因就是证书写入失败。如果确认证书没问题那就是单纯的SSL Pinning。建议先看App有没有使用OkHttp或者系统默认的TrustManager越底层的校验越好Hook。如果App用了自研TLS库那么Hook点就不在Java层要去native看openssl的回调复杂度会直线上升。6.2 Hook阶段Frida脚本不生效怎么处理Frida脚本不生效的情况我也遇到过几次。一种是so库没有在入口点加载需要在App运行到业务请求后再attach或者在Java层直接调用System.loadLibrary后再注入另一种是so文件被重定位导致算出来的基址加偏移不对。解决方法是启动时先用Module.findBaseAddress获取实际加载基址不要硬编码。还有一个隐蔽问题有时通过Interceptor.attach到native函数后参数打印出来全是空这不是没Hook到而是因为该函数被VMP虚拟化保护入口地址只是个调度器真正的逻辑全部在解释器里执行。面对这种情况继续在静态上抠已经没有意义最好直接改用Unicorn模拟跑一遍完整流程把所有字节码指令dump下来做分析。6.3 还原阶段本地签名结果对不上怎么办本地签名结果对不上九成原因是输入串没拼对。我复盘自己遇到过的问题典型的漏项集中在三处请求路径没有考虑带不带尾部斜杠、query参数排序规则没完全按ASCII码执行、body内容为空时客户端是否还会拼一个空标记。建议把抓包时识别到的原始串完整保存下来在Python脚本里先把原始串打印出来和当时的请求报文逐字符对比这种问题很快就能暴露。另外一类是时间戳和随机数问题。淘宝系签名里经常混入时间戳导致签名值每分钟都在变化。如果排错时发现同一个输入串在不同时刻算出来的签名不同就去内存里搜一下有没有读取当前时间的方法被调用。搜到后先固定一个时间值再跑签名这样就能把所有变量逐一排除。6.4 避坑经验速查表问题现象根本原因解决思路Charles看不到请求明文SSL Pinning或证书不受信任Frida Hook TrustManager或使用系统证书目录能抓到请求但返回值校验失败重放时时间戳过期实现签名时同步生成时间戳并写入请求参数搜索x-sign字符串无结果APK被加固dex未脱壳用frida-dexdump或BlackDex脱壳后再分析Frida Hook入口不触发so库未加载或基址计算错误先调用loadLibrary再Module.findBaseAddressIDA伪代码全是switch-caseollvm混淆控制流配合Frida动态跟踪或Unicorn模拟执行Python签名结果偏差输入串拼接规则有遗漏保存线上原始串逐字符对比固定时间戳排障7. 扩展从x-sign逆向到通用签名分析思路7.1 同一体系下的x-mini-wua和x-uid搞定x-sign之后你会发现自己顺手把同一套体系中很多其他参数也打通了。淘特App请求头里的x-mini-wua和x-uid虽然名字不同但生成位置几乎都在libsgmain.so或者它的兄弟so里。也就是说只要你已经会定位x-sign的入口函数就能用同样的方法去Hook另几个参数分析它们的输入输出再看它们之间是独立计算还是从x-sign结果中派生出来的。我在实际操作中验证过x-mini-wua不仅仅依赖请求参数还融合了设备指纹信息计算链路比x-sign长而且返回长度的不固定性更高。它的算法里会出现base64编码、自定义字符表置换等步骤跟x-sign单纯走MD5的风格完全不同。所以做完x-sign后再用这套通用思路排查x-mini-wua会节省大量重新学习工具的时间。7.2 通用定位方法高频参数、反射调用、堆栈回溯三板斧如果你拿到的不是淘特而是其他任意一款App想快速定位它某个签名参数的算法核心方法其实就三板斧。第一板斧在Jadx里搜索这个参数名字找到构造请求头的代码位置第二板斧顺着这个参数找出传给native方法的调用点Hook这个native方法确认它是否就是最终计算函数第三板斧如果参数值最终由so返回就在JNI函数上做堆栈回溯把so内部调用的子函数全部列出来逐一分析。这套打法在绝大多数常规App上都能奏效。只有遇到代码虚拟化或者非常严重的加固时才会失效那时候就需要引入专门的脱壳机和指令trace工具本质上还是追数据流和控制流思路并没有变。7.3 签名逆向之外的接口风控意识最后说一点偏工程经验的东西。逆向签名并不代表可以无限请求大多数App还有独立的设备维度风控比如同一设备短时间内的高频请求会触发滑块验证或黑名单。x-sign算法还原解决的是“请求是不是伪造”的问题但解决不了“请求频次是否异常”的问题。所以做接口分析时我会建议把签名生成模块当成普通SDK一样封装外部预留添加业务参数的入口。在测试阶段控制好并发和频率尽量用真实网络环境而不是内网代理连接到生产环境否则很容易在还没有分析完响应数据时就被风控踢下线。这个坑踩一次就能记住越是后端校验严格的服务越要用模拟真实用户行为的节奏来调试。8. 收尾一点个人体会这次做淘特App x-sign从抓包到算法定位整体走下来我最大的感触是逆向工程真正花时间的不是某个单一环节而是反复在“静态分析-动态验证-日志对比”之间来回切换。一开始我以为定位到doSign就能直接出结果结果被key和拼接规则卡了快两天后来老老实实把IDA里的交叉引用梳理了一遍才找到问题。如果你也想复现这个流程我的建议是每完成一个阶段就做一次记录尤其是抓包时的原始串、so文件的加载基址以及IDA里看到的伪代码截图。这些信息单独看都很零散但还原算法时它们全是关键坐标。另一个小技巧是写一个自动化的Frida脚本把Hook点、参数打印、返回结果一次性输出到文件比每次都在控制台看更好用也能避免漏掉某个关键分支。希望这篇记录能帮你少走几步弯路抓到属于你自己的那个“sign”。