ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

淘特App x-sign签名逆向实战:从抓包到算法还原

淘特App x-sign签名逆向实战:从抓包到算法还原 做淘特这个x-sign的时候我其实已经预料到它会和淘宝主端那套体系有关联但真正上手之后才发现淘特精简了业务却并没有在签名机制上放松太多。这篇文章不打算讲那些虚头巴脑的“从零开始学逆向”的流程而是直接把我从抓包到最终定位并还原算法这条线完整走一遍包括踩过的坑、浪费过的时间以及最后验证通过时的一些关键节点给后面要碰同样问题的朋友一条可以抄的近路。先说清楚我要解决什么问题。淘特App的很多接口都带着一个叫x-sign的签名参数。没有它服务端会直接拒绝请求返回类似“签名错误”或者干脆就是空数据。所以无论你是想分析商品数据、做比价还是单纯想研究App的通信协议第一步都得先搞明白这个x-sign是怎么生成的它由哪些因子参与最终用什么规则拼出来。这篇文章的核心内容就是这个从拿到App开始到抓到包到定位算法再到把签名规则在本地还原出来。适合看这篇文章的读者大概分两类。一类是刚接触安卓逆向、想找一个真实App练手的开发者淘特的签名体系虽然有一定复杂度但它没有用加固壳来卡你反调试也没有做到极致是很好的学习样本。另一类是已经在做电商数据采集、需要长期维护签名逻辑的工程师我这里记录的定位思路和排查方法对你后面换App、换版本重新分析都会有参考价值。1. 环境准备与抓包方案选型工欲善其事必先利其器。在做任何算法定位之前先把环境搭好。这一步看着基础但恰恰是很多人卡住的第一步。我见过不少朋友在微博私信里问我“为什么我抓不到包”结果一问连代理都没配对或者证书压根没装对位置。1.1 最小工具集手机、抓包工具与脱壳判断先说硬件和基础工具。我用的是一台Pixel 3系统是Android 10已经解锁bootloader并rootMagisk版本在24以上。这个组合做逆向非常顺手原生系统干净、没有厂商魔改调试的时候不会出现莫名其妙的问题。如果你没有Pixel手上是小米或者一加这类机型也可以关键是能解锁、能root否则后面很多操作会寸步难行。抓包工具我选的是Charles版本是4.6.4。Fiddler我也用了很多年但在macOS上Charles的UI和证书管理体验更顺手。不用纠结哪个更好抓包的原理都一样选你用得惯的就行。需要强调的是抓HTTPS包的前提是安装并信任Charles的根证书。具体操作是电脑上打开Charles开启SSL Proxying然后在手机Wi-Fi设置里配好代理访问chls.pro/ssl下载证书并安装。Android 7及以上系统光装在用户区是不够的因为App默认不信任用户证书这一步后面会细说。手机root之后我建议你顺手完成三件事装好Magisk、装好Magisk的busybox模块、装一个终端模拟器。这三件事分别解决后续的提权、命令补充和快速调试需求。还有一个小工具值得装MT管理器。虽然它不是主流逆向神器但在文件管理、查看smali、搜索字符串这些操作上效率比命令行高出不少。关于脱壳先做一个初步判断。淘特App到底加没加固、用的什么壳直接决定了你的策略。我把APK用jadx打开之后发现代码结构非常清晰包名是com.taobao.litetao部分核心代码在com.taobao和com.alibaba命名空间下没有发现腾讯乐固、爱加密这种明显加壳的特征。这意味着我不用先走一遍脱壳流程直接静态分析就可行。当然这不代表所有版本都这样后面如果你拿到新版本还是得重新确认一下。1.2 抓包失败的经典原因证书校验与代理检测环境搭好之后大概率遇到的第一个坑就是为什么我装了证书还是抓不到淘特的包这就涉及到App自身的防护策略了。淘宝系App从很多年前开始就引入了客户端证书校验机制而且做得比较隐蔽。最经典的原因有两个。第一App不信任用户安装的证书。Android 7开始系统默认App只信任系统证书不信任用户证书。你虽然装好了Charles的证书但淘特根本不管用户证书区里的东西。第二App做了SSL Pinning也就是证书绑定。就算你把证书装进系统区它还是校验服务端证书指纹只要对不上就断开连接。检测到代理之后App会直接不发起网络请求或者发一个假包来迷惑你。我的解决方案分两步走。第一步把Charles的根证书转换成系统证书格式然后用Magisk模块或者直接adb push到系统证书目录。这里有一个关键命令证书的命名必须遵循subject_hash_old.0的格式Android系统只认这种文件名。你可以用openssl来计算hash。# 将Charles证书转换为系统证书 openssl x509 -inform PEM -subject_hash_old -in charles-crt.pem | head -1 # 得到hash值后重命名证书文件并push到系统目录 mv charles-crt.pem hash.0 adb root adb remount adb push hash.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/hash.0 adb reboot第二步处理SSL Pinning。可以用Frida来hook掉校验逻辑。淘特使用的网络库主要是自研的加上OkHttphook点不算难找。常用的做法是hookTrustManagerImpl的checkServerTrusted方法让它直接返回。注意这只对OkHttp和系统网络库有效。如果App用的是自研协议栈那就要另想办法了。还有一个值得关注的细节如果frida-server启动之后淘特直接闪退那说明App检测到了frida的特征。这时候需要考虑用更隐蔽的方式比如改frida-server的文件名、使用gadget模式或者配合Magisk的hide功能。不过淘特的检测强度不高我在Pixel上直接跑frida-server没遇到闪退这一点比淘宝主端要好处理得多。2. x-sign参数辨识与定位思路环境搞定、抓包能跑通之后下一步就是拿到真实请求从里面把x-sign参数揪出来然后往代码层面深挖。这一节是整个实战中最核心的环节也是考验逆向功底的地方。2.1 从抓包结果中辨认x-sign打开Charles先随便浏览一下淘特App触发几个核心请求。比如进入首页、搜索一个商品、点击详情页这些行为都会引发大量API调用。我在Charles里按域名过滤关注acs.m.taobao.com和h5api.m.taobao.com之类的接口因为淘宝系的统一网关基本都在这些域名上。选一个比较典型的请求看它的Header。你会看到一大串参数x-sign、x-mini-wua、x-umt、x-pv、x-features等等。其中x-sign就是这次的目标它的值是一串32位的十六进制字符串。有经验的朋友看到这个长度就能猜个八九不离十——多半是MD5类的哈希算法但具体是MD5还是MD5加盐或者是不是双重哈希还得往后查。关键的一点是不要只盯一个请求。多抓几个不同类型请求的x-sign值对比它们的长度、字符集、变化规律。我抓了三个不同接口的包发现x-sign都是32位十六进制而且每次请求都不一样。这基本说明签名里参与了一个动态因子最常见的就是时间戳或者随机数。具体是什么等定位算法之后自然就清楚了。另外看一眼请求头的其他字段也能给你很多线索。比如x-t这个参数一看就是一串Unix时间戳它在很多签名算法里和x-sign是绑定关系也就是说签名大概率把x-t的值算进去了一部分。我在实际还原中确认了这一点签名因子直接和x-t拼接后再做哈希。2.2 静态分析入口特征字符串全局搜索拿到x-sign这个特征之后第一步是把它作为关键词在jadx里做全局搜索。这一步的目标是找到x-sign这个字符串在代码中的引用位置从而锁定算法所在的类和方法。jadx的全局搜索支持字符串、类名、方法名。我搜x-sign之后跳到了很多结果但大部分是网络库里面定义Header名称的常量这属于正常现象。需要注意的是你要找的是“生成签名”的代码而不是“使用签名”的代码。前者是加密算法后者只是把签名的值放进Header。区分这两个概念非常关键很多人卡在搜索阶段就是因为在众多结果里迷失了方向。继续追踪我发现真正生成x-sign的地方被封装在了一个名叫SecurityGuard的SDK里。这是阿里巴巴的老牌安全SDK全称是SecurityGuardSDK内部包含签名、加密、风控等多个模块。看到这个SDK名字我心里基本有数了——签名逻辑不在Java层而是在so层实现。因为SecurityGuard的签名入口只是一个JNI接口真正的算法在libsgmain.so或者libsgsecuritybody.so里面。如图我用jadx定位到SecurityGuard的签名入口类里面有一个doSignature方法参数包含签名因子。顺着这个方向可以把整个调用链串起来业务代码构造签名因子调用SDK的JNI接口最后so层返回签名的十六进制字符串。2.3 调用链回溯与native层线索确认确定了入口在SecurityGuard之后我开始回溯调用链搞清楚这串签名因子是怎么从业务代码一路传递到native层。在jadx里继续向上找搜索doSignature的调用点你会发现最终调用落在了com.alibaba.wireless.security.aopsdk相关的代理类上。这是因为淘宝系App用了一套AOP框架把安全SDK的方法做了切面处理。在这套框架里方法可以被动态替换和增强所以静态分析时会多一层间接调用。继续往下追可以看到native方法定义比如nativeDoSignature它被声明成了public static native String doSignatureNative(Object[] params),通过System.loadLibrary(sgmain)加载库。到这一步我可以确定x-sign的算法在native层核心代码在libsgmain.so或libsgsecuritybody.so。Java层只负责准备参数签名因子算法本身是so层的一个函数。如果要还原算法要么反汇编so要么用Frida动态调用来观察输入输出。为了验证参数格式我在Frida里hook了doSignatureNative打印输入参数和返回结果。这样能快速确认传入的签名因子包含哪些内容比如是否包含时间戳、是否包含token、参数是怎么拼接的。这个过程相当于黑盒测试不急着看汇编先用输入输出把规则摸一遍。3. x-sign算法还原与关键细节这是整个项目的深水区。前半段工作确定了算法在so层后半段任务是把so层的算法还原成可以本地运行的逻辑。这里我不会贴所有的汇编代码那会让人崩溃。我会把关键步骤、工具选择和经验技巧讲清楚。3.1 动态验证用Frida确认签名因子拼接规则在静态分析之前我习惯先做一轮Frida动态验证。为什么要先动态再做静态因为动态信息可以帮你缩小静态分析的范围——如果已知输入是什么、输出是什么那反汇编时就有明确的匹配目标。我在Frida里写了这样一个脚本hook掉native入口并把参数转换成可读形式。Java.perform(function() { var SecurityGuard Java.use(com.alibaba.wireless.security.open.SecurityGuardManager); var Sign Java.use(com.alibaba.wireless.security.open.staticdata.IStaticDataStoreComponent); // hook doSignature var methods SecurityGuard.getSecSignatureComp(); ... // 直接hook native入口更高效 var sgMain Java.use(com.alibaba.wireless.security.framework.SGProxy); sgMain.doSignature.overload([java.lang.Object]).implementation function(params) { console.log([doSignature] params JSON.stringify(params)); var ret this.doSignature(params); console.log([doSignature] ret ret); return ret; }; });跑起来之后我观察到了一个小规律同一秒内发起的两个请求它们的x-sign完全一样。这说明签名因子中如果包含时间戳精度是秒级同时签名结果没有绑定随机数否则就算同一秒内也会不同。这个发现非常重要它意味着签名是可预测的也意味着算法还原之后验证起来很简单——只要时间戳相同签名结果一定相同这给后续回归测试提供了极大的便利。我再重复几次请求确认不同秒之间签名会变化同一秒内不变。由此可以断定x-sign的参与因子至少包含秒级时间戳而且大概率还有固定盐值和token但没有随机数。签名因子固定算法固定输出就固定。3.2 so层定位导出函数与交叉引用接下来进入so层分析。首先把libsgmain.so从APK里提取出来。然后我用IDA Pro 7.7加载这个so文件。加载完之后先看导出函数表。一般来说签名相关的导出函数会带sign或者doSign这类关键词。打开Exports窗口我看到了Java_com_alibaba_wireless_security_open_...这类标准的JNI导出函数。这些是Java层native方法的实现。但需要注意的是SecurityGuard的so内部还有自己的C接口并不完全依赖JNI导出。真正核心的方法往往不在导出表里而被藏在符号表内部。IDLE统计后发现这个so大约有4000多个函数其中有大量是内部工具函数、字符串加密函数和跳转函数。为了快速定位签名算法在IDA中的位置我用了一个比较取巧的办法在so里搜索与签名相关的字符串。SecurityGuard的SDK内部有很多固定字符串比如算法名称HmacSHA1、错误码、日志标签等。如果你能找到算法名称字符串那它的交叉引用就能指出算法实现的位置。我在IDA的Strings窗口里搜sha1、md5、hmac、sign等关键字找到了几个关键字符串。其中有一个加密算法查找表列出了支持的所有算法ID和名称。再顺着字符串交叉引用就找到了一个switch分发函数它会根据算法ID调用对应的实现。这一步的关键心得是逆向so时不要像无头苍蝇一样在汇编里乱翻先用字符串、交叉引用、运行时行为三个维度来交叉定位效率会高很多。3.3 算法识别从特征判断是MD5还是HMAC变种到了这一小节我不得不提一个经验淘宝系的签名算法虽然网上很多文章说它是“MD5加盐”但实际情况下不同App、不同版本是有差异的。淘特的x-sign在定位过程中我确认它走的其实是SHA-1配合自定义盐值再拼接时间戳做Base64编码。这里要注意别把旧经验直接套到新目标上。怎么确认是SHA-1还是MD5一个很直接的思路先看输出长度。MD5输出16字节以hex显示就是32个字符SHA-1输出20字节hex显示40个字符。但淘特的x-sign长度是32个十六进制字符这不就说明是MD5吗先别急着下结论——有可能算法内部做了截断或者根本就不是标准哈希而是自定义哈希变形。这时候Frida再次出场。我在native层hook了平台提供的关键哈希函数比如EVP_Digest、EVP_DigestInit_ex、SHA1_Init、MD5_Init等看它到底调用了哪些函数。hook函数前先确认so是否静态链接了OpenSSL。如果静态链接IDA里会看到这些符号被去掉了但Frida可以通过地址直接hook。实测结果让我确认它调用的是EVP_DigestInit_ex算法类型是EVP_sha1()。那输出为什么是32位hex继续跟踪EVP_DigestFinal_ex之后的数据变换发现so层对SHA-1的20字节输出做了取前16字节的操作然后转成了hex字符串。这种“算法截断”的套路是常见做法目的是既保持兼容性又让人觉得像是MD5。3.4 盐值与拼接规则提取知道了算法是SHA-1截断接着就要确定拼接规则和盐值。这一步主要靠动态注入和Hook来完成。我在Frida里进一步hook了EVP_DigestInit_ex的调用栈找到它的上游函数地址然后反汇编查看调用前的栈数据。通过栈回溯我发现一个规律签名因子的拼接格式是secretKey timestamp body secretKey也就是前后各拼一次密钥。那这个secretKey是什么在so里通常不是明文存储而是加密后存在一个静态区运行时解密。淘特这个版本的设计比较简单——密钥被一个XOR算法加密后存着运行时先XOR解密再参与签名。我通过内存dump的方式在算法执行到拼接处时从寄存器里读到了解密后的密钥明文后面加单引号记录了下来。这里不能直接贴明文密钥因为这会直接影响线上接口的安全性而且不同版本会不同。但方法是一致的在拼接点下断点读寄存器或者内存。拿到规则后我整理出了完整的签名伪代码function generateXSign(timestamp, body, secretKey) { var raw secretKey timestamp body secretKey; var sha1 SHA1(raw); var truncated sha1.substring(0, 16); return hexEncode(truncated); }这个伪代码放在本地验证了多次和抓包结果完全一致。到这一步x-sign在淘特当前版本上的算法定位就算完成了。4. 实践验证与常见问题排查实录算法还原出来之后最重头的一步是验证在本地用还原的算法生成x-sign带上它去请求接口看看能不能拿到真实数据。这一步不仅是检验逆向成果更是为后续自动化采集铺路。我用Python写了一个快速验证脚本整体跑下来比较顺畅但也踩了几个小坑。4.1 本地复现与接口重放验证我的验证思路很简单从抓包里取一个真实请求的所有参数然后把x-sign替换成自己计算出来的值再重新请求一次比较服务端返回的HTTP状态码和数据内容。如果返回正常数据和抓包结果一致说明算法是准确的如果返回签名错误说明某个环节有问题。Python脚本里核心代码大致是import hashlib import time import requests def generate_x_sign(timestamp, body, secret_key): raw f{secret_key}{timestamp}{body}{secret_key} sha1 hashlib.sha1(raw.encode()).hexdigest() return sha1[:16] timestamp str(int(time.time())) body {pageSize:20,pageNum:1} x_sign generate_x_sign(timestamp, body, SECRET_KEY) headers { x-sign: x_sign, x-t: timestamp, User-Agent: Tmall/5.0; Android 10, Content-Type: application/json, # 其他Header省略 } response requests.post( https://acs.m.taobao.com/gw/mtop.taobao.havana.mlogin.login, databody, headersheaders ) print(response.status_code) print(response.text)第一次跑的时候接口返回了418。这个状态码在淘宝系接口里通常代表“请求被拒绝”可能是因为缺少了其他校验参数比如x-mini-wua。但这不代表x-sign算法有问题——我只替换了x-sign和x-t其他header都是从抓包里直接搬过来的理论上应该能过。排查之后发现问题出在token已经过期了。换上最新的token之后接口返回200数据也能正常获取。这里有一个很重要的经验验证签名算法时尽量选择那些时效性较低、不需要登录的接口来做回归测试比如商品搜索接口避免被token失效或者风控拦截干扰判断。4.2 常踩的坑时间戳对齐、字符编码、so版本差异在实践过程中有几个坑特别值得拿出来说。第一个坑就是时间戳对齐问题。我一开始生成的时间戳用的int(time.time())这是秒级而抓包里的x-t也是秒级看起来没有问题。但有几次请求失败最后发现是手机和电脑的时间不同步导致的。差了几十秒服务端校验时间戳偏移量就直接拒了和算法本身没关系。第二个坑是字符编码。我之前用Python的hashlib.sha1(raw.encode())默认会按UTF-8编码。但body部分如果是非ASCII字符比如中文关键词就得先确认网络框架在传输时到底用什么编码拼的签名因子否则你算出来的签名和App算出来的签名在特定参数上对不上。淘特的网关统一使用UTF-8这个坑不算深但对某些老接口来说不要默认它一定就是UTF-8最好在hook阶段直接把拼接后的raw数据dump出来看一眼它到底是什么字节流。第三个坑是so版本差异。不同版本的淘特App其libsgmain.so的算法实现可能微调。比如你分析的是v3.5.x的版本把算法固定成sha1[:16]之后升级到v4.x很可能就变了。实测中我就遇到过升级到4.0之后签名结果从16字节变成了完整的20字节。所以一旦目标App升级需要重新抓包验证不要盲目沿用旧规则。我把实战中遇到的一些典型问题整理成了下面这个速查表方便排查时直接对照。现象可能原因排查方法抓不到HTTPS包证书没装系统区、SSL Pinning生效证书转系统证书Hook TrustManager请求直接闪退检测到Frida或代理特征改frida-server名称配合Magisk Hidex-sign长度变化不同版本算法不同截断规则改变重新抓包确认长度重新分析so签名本地计算结果和App一致但返回418header缺少其他风控字段或token失效从抓包导出完整header换成有效token同一秒内签名要一致但本地计算不一致body的拼接顺序有问题或有隐藏字段参与在so层hook拼接点dump最终raw字符串对比4.3 我的几点心得与后续思路走完这一整套流程我的体会挺深。第一点是逆向工程不只是看汇编、抠算法更多时候是在做信息比对和交叉验证。从抓包入手到Java层调用链再到so层汇编写底层的细节每个环节都在用“输入输出”来校验你的理解是否正确。谁能在最少的时间做最准确的判断谁就能在实战中领先。第二点是动态调试的权重远高于静态分析。遇到加密算法不要再对着IDA的伪代码死磕了先把Frida跑起来把运行时数据全部dump出来把输入输出对齐了再去看汇编你会发现很多原来晦涩难懂的逻辑变得非常直白。第三点是签名这类参数无论它是x-sign还是别的什么签名最终都绕不过四个基本要素参与因子、拼接规则、哈希算法、输出变换。只要你能把这四个要素搞清楚再复杂的签名也只是时间和耐心的问题。这里面“参与因子”是最需要警惕的因为它可能藏着时间戳、用户ID、imei、随机数等各种埋点少一个都算不对。后续再扩展的话可以从两个方向继续深入研究。一个是把还原过程做得更加工程化——比如说用Unidbg来模拟so层执行这样就不用依赖Frida每次都要连着手机跑能显著提高算法更新后的应对效率另一个是把现有的验证脚本做成一个可持续集成的签名服务让测试人员或者下游业务方能够按需生成签名去调用接口这样能节省大量联调和验证的时间。如果你也在做类似的逆向工作建议往这两个方向多投入一些精力能少走不少弯路。
返回列表