恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
从抓包到Python复现:App接口sign签名逆向实战解析
首页
资讯中心
/
从抓包到Python复现:App接口sign签名逆向实战解析
从抓包到Python复现:App接口sign签名逆向实战解析
发布时间:2026/10/14 4:02:58
哦豁又逆出来一个 sign。说实话这种“头秃半小时、调试一下午、最后豁然开朗”的感觉做接口逆向的人应该都懂。最近我接到某物流应用的数据采集需求对方要求拿物流轨迹和网点信息但所有请求头里都带一个独立生成的 sign 字段服务端校验不过就直接拒绝。这年头接口不放个签名等于把数据裸奔在公网上所以这类操作我早就习惯了。这篇文章就围绕这轮逆向过程展开怎么定位 sign 的生成位置、怎么把签名算法还原出来、过程中踩了哪些坑、最后怎么用一个 Python 模块把这套逻辑固化下来。不管你是刚接触接口签名分析的小白还是整天跟抓包和反编译打交道的同行这篇应该都有点参考价值。先说明一下以下内容主要面向授权范围内的接口调试和自有数据分析分享的是通用思路而不是教你乱刷别人接口。1. 先拆思路为什么选择动态调试而不是纯静态分析1.1 目标确认抓包先搞清楚 sign 是什么、怎么变接到这个需求之后我第一步没有直接拿反编译工具去翻代码而是先做了一件很基础的事抓包把请求长什么样看明白。我用的抓包方案是在本地起一个代理把手机或模拟器的流量引过来具体是让设备走同一个局域网再把 HTTP 代理指到电脑上。很多 App 起手就会校验代理所以我提前打了证书并确认目标 App 没有做代理检测。抓到的请求大概长这样POST /api/v1/waybill/query HTTP/1.1 Host: api.xxx.com Content-Type: application/json User-Agent: okhttp/4.9.3 sign: f8a4e1c6c5b6a2d4e8f7a1b2c3d4e5f6 timestamp: 1718503600 nonce: ad8f3f2c1esign 是一个 32 位的十六进制字符串按长度和字符集来看大概率是 MD5 或某种摘要算法的输出。我随手把请求参数里的“运单号”改了一个数字重新发包发现 sign 立刻变了说明 sign 跟请求正文的内容强相关。把 timestamp 改掉sign 也跟着变说明时间戳也参与了签名。这一步的目的很明确先搞清楚 sign 会随哪些变量变化再决定去哪里找代码。如果 sign 是一个固定值那就简单得多了多半是客户端硬编码的直接搜索字符串即可但这里它随参数和时间变化几乎可以断定是一个动态生成的算法接下来要做的就是定位这段生成逻辑。1.2 为什么选择动态分析而不是硬啃静态代码定位动态签名通常有两条路一条是纯静态分析用反编译工具把 APK 的 dex 还原成 Java 代码再搜关键字找加密实现另一条是动态分析用调试工具在运行时 Hook 可疑函数直接观察输入和输出。最开始我尝试的是纯静态。用反编译工具打开 APK 后我先搜“sign”结果一下就出来了几个类但点进去才发现代码被做了混淆类名全都变成了 a、b、c方法名也全是 a()、a(String) 这种。想通过静态阅读把整个调用链理清楚就像在灯光很暗的房间里找一根掉在地上的针虽然理论上可行但效率极低尤其是当项目里还嵌套了 JSON 序列化、参数拼接、编码转换等等操作的时候。所以我换了一条路直接用动态分析。原理很简单APP 最终发出请求的时候一定会在某个对象上调用一个方法来计算签名。只要我在运行时把这个方法“拦下来”打印它的入参和返回值就能直接看到签名计算的前后数据。这种方法的优势在于不需要完全理解整个应用的业务逻辑只需要找到一个关键锚点剩下的交给运行时的真实数据。生活里有个很贴切的类比如果冰箱不制冷与其把说明书从第一页看到最后一页不如拿个温度计放进冰箱再听听压缩机有没有转。动态分析就是这个温度计它能让你直接观察到“哪里在工作”而不是靠猜整个系统怎么设计的。我这次的实践也验证了这个判断动态 Hook 让我不到二十分钟就定位到了最终的签名拼接代码。2. 核心细节解析sign 通常怎么生成、从哪里开始查2.1 常见的签名算法模式速览接口签名在设计上其实有不少惯用套路懂了这些套路之后定位 code 会快很多。根据我的经验市面上最常见的签名生成方式大致分为三类算法形态典型特征常见场景参数排序 盐 时间戳做 MD5sign 长度固定为 32 位字符集是 0-9a-f中小型 App、企业内部系统参数拼接 HMAC-SHA256sign 长度 64 位对大小写敏感有专职安全团队的产品包含随机数 nonce哈希前先做 Base64/URL 编码sign 每次生成都不同甚至不依赖参数变化风控较严格的平台我这个目标明显是第一类32 位 MD5 的特征非常典型。但这只是第一层猜测最终还是要靠代码验证。关于“盐”的存放位置这里有个很容易忽略的细节。很多开发者会把盐硬编码在 Java 层这类盐在反编译后直接就能看到但有的团队会把盐放到 so 文件里或者拆成几段存在不同类中运行时再拼接。如果用户在静态代码里没有直接看到明显字符串别急着放弃先全局搜一遍“abc”“123”“!#”之类的常见盐值也可以搜 Base64 解码后的可见字符串。2.2 如何在代码里找到那个计算 sign 的入口找到入口是这次逆向的核心环节。我总结了一个基本的排查路径适合大多数情况先用抓包数据确定请求发送的 API 层类。通常发送请求的 Retrofit Service 接口方法名会和请求路径有一定对应关系比如query()方法对应/api/v1/waybill/query。在反编译代码里全局搜索 sign 相关关键字。注意除了sign还要搜sig、sgn、encrypt、signature、digest因为不同开发者的命名习惯完全不一样。搜索timestamp和nonce很多情况下生成 sign 的那段代码会紧挨着时间戳和随机数生成逻辑。如果方法体很小打着日志直接看逻辑如果方法体很大先看它调用了哪些工具类再逐个点击进入。这里有个搜索技巧想说一下与其直接搜 sign不如搜字典里不太可能被混淆的关键字符串。比如目标参数里包含“appId”或“versionCode”这类业务字段名往往会被原样保留因为后端要按这个名字取值。搜到之后在那个类里继续往下翻很快就能看到拼接一串字符串再调用加密方法的地方。我这次就是用“timestamp”作为突破口。搜索之后看到某个类里有一个方法先获取系统时间戳再调了一个md5Encrypt()方法返回值被塞进了请求头。当时我心里就很有底了剩下的事就是把这段逻辑完完整整地抠出来。3. 实操过程从抓包到复现签名的完整链路3.1 抓包与初步反编译前面说过我先把包抓下来拿到了真实的请求头和请求体。这里再补充一些细节抓包用的工具我不固定Charles、Fiddler 或者 mitmproxy 都行只要能看明文请求即可。有的 App 会做证书校验这时候还需要在手机里安装代理证书并确认目标进程信任用户证书。APK 的反编译过程就不展开说了工具选对就行。我拿到的 APK 没有做加固直接用 jadx 打开就能看到 Java 层代码这一点已经帮我省了脱壳的麻烦。如果遇到加固壳比如常见厂商的免费壳或商业壳就得先过脱壳这一关。关于脱壳我的经验是优先尝试内存 dump其次才是静态脱壳因为加固壳的静态还原经常会有函数抽取不完整的问题。反编译后我按 2.2 的路径做了一遍搜索最终锚定了一个叫SignManager的类。名字都没混淆说明这家 App 的逆向难度属于入门偏下级别。代码如下public class SignManager { private static final String SALT s3cr3t_k3y_2024; private static final String APP_ID 10086; public static String buildSign(MapString, Object params, long timestamp, String nonce) { StringBuilder sb new StringBuilder(); // 先把参数按 key 排序 ListString keys new ArrayList(params.keySet()); Collections.sort(keys); StringBuilder raw new StringBuilder(); for (String key : keys) { raw.append(key).append().append(params.get(key)).append(); } raw.append(timestamp).append(timestamp).append(); raw.append(nonce).append(nonce); raw.append(APP_ID).append(SALT); return MD5.md5(raw.toString()); } }看到这段代码的时候我已经能想象出这个 App 的开发者在写这行Collections.sort(keys)时的表情了他们确实有安全意识知道要排序拼接但把盐明文放在 Java 层等于把钥匙挂在了家门口。3.2 Hook 定位参数拼接点静态代码已经给出了一个大致的算法模型但实际生成 sign 时传入的参数顺序、参数值长什么样光靠静态阅读还是不确定。为了保险起见我用 Frida Hook 了一下这个类。先上脚本核心逻辑很简单拦截buildSign方法打印所有入参和返回值同时打印调用堆栈确认调用来源。Java.perform(function () { var SignManager Java.use(com.example.app.SignManager); SignManager.buildSign.overload(java.util.Map, long, java.lang.String) .implementation function (params, timestamp, nonce) { var stack Java.use(android.util.Log) .getStackTraceString(Java.use(java.lang.Throwable).$new()); console.log([入参] timestamp timestamp); console.log([入参] nonce nonce); console.log([入参] params params.toString()); console.log([堆栈] stack); var ret this.buildSign(params, timestamp, nonce); console.log([返回值] ret); return ret; }; });把脚本跑起来之后我马上看到了一组真实的入参数据。参数里除了运单号还有clientType、version、channel这些固定值拼接顺序也验证了确实是先按 key 排序再把时间戳和随机数接在后面最后加上 APP_ID 和盐。有一个小细节必须强调签名算法里如果出现了nonce随机数那我们做本地复现的时候必须保证重放请求时使用的是同一个 nonce否则签出来的 sign 是不匹配的。也就是说如果你没有从请求包中读出原始 nonce而是自己新建了一个随机数那签名必然对不上。这在后面测试时很容易造成误判。3.3 用 Python 复现算法并本地验证拿到了完整的拼接规则之后剩下的就是写脚本复现。我习惯用 Python 写一个独立的签名模块这样可以脱离 App 环境直接生成合法签名。核心代码大概长这样import hashlib import time import random import string APP_ID 10086 SALT s3cr3t_k3y_2024 def gen_sign(params: dict, timestamp: int, nonce: str) - str: # 按 key 排序拼成 keyvalue 的形式 items sorted(params.items()) raw .join(f{k}{v} for k, v in items) # 补充动态字段 raw ftimestamp{timestamp}nonce{nonce} # 末尾拼接固定值和盐 raw APP_ID SALT # 计算 MD5 return hashlib.md5(raw.encode(utf-8)).hexdigest() def generate_nonce(length: int 16) - str: chars string.ascii_lowercase string.digits return .join(random.choice(chars) for _ in range(length)) if __name__ __main__: params { clientType: android, version: 2.8.1, waybillNo: SF1234567890, } ts int(time.time()) nonce generate_nonce() sign gen_sign(params, ts, nonce) print(sign, ts, nonce)写上这段脚本之后我拿刚才 Hook 到的同一组参数去算了一遍 MD5和返回值对比完全一致。这里的完整链路就打通了客户端拼接规则 排序后的参数键值对 时间戳 随机数 APP_ID 盐然后 MD5。服务端大概率也是用同一套规则校验所以只要能生成和服务器一致的 sign请求就算合法了。我拿这个模块去请求了一次目标接口原来总是提示“sign invalid”现在直接返回了 200。看到 200 的那一刻说实话比喝了一大口冰可乐还舒服。4. 常见问题与排查技巧实录4.1 一张速查表常见签名相关报错逆向签名或者调试签名接口最怕的就是各种奇奇怪怪的报错。我把实际操作中遇到的问题整理成了一个速查表方便你排查时直接用现象可能原因处理思路请求返回 sign invalid拼接规则不对时间戳或 nonce 与请求不一致对比算法输出与抓包值定位差异字段sign 算出来跟抓包不一致拼接顺序、大小写、URL 编码差异检查 key 排序方式、字符串编码、Base64 尾巴改了时间戳立即失效服务端对时间有窗口校验每次请求重新生成时间戳别一次性写好缺少 nonce 却生成乱码随机数生成规则不一致从抓包中提取原 nonce或复制客户端格式Hook 没有效果检测到调试环境 / 方法被 inline更换 Hook 时机尝试在启动早期注入反编译看不到 sign 逻辑代码在 so 层或被混淆先考虑命令行追踪加密函数的调用栈表里的每一项后面其实都有不少故事挑几个重点展开说一说。4.2 几个容易踩的坑编码、大小写与排序陷阱第一个坑是大小写问题。很多后端校验时会统一转成小写再比较但有的团队比较粗心直接按大写存库。如果你用 Python 自带的hexdigest()拿到的是小写而服务端期望大写就会莫名奇异地不匹配。我的建议是先用抓包值验证大小写不要想当然。第二个坑是拼接顺序里的 JSON 嵌套。如果参数里有对象或数组比如{address: {city: 北京}}不同语言对字典排序后的字符串表现完全不同。Java 的Map.toString()输出的是{city北京}而 Python 的str(dict)输出的是{city: 北京}两个字符一不一样MD5 直接就变了。如果你遇到的是这种嵌套型参数最好的方案是直接复用 App 的 JSON 序列化逻辑而不是自己拼接。第三个坑是 URL 编码。有的签名算法会先对参数值做 URL 编码再拼接这时候空格会变成%20或两者的 MD5 完全不同。我当时在验证另一个项目时就被坑过一次肉眼看起来两个字符串一样但实际一个里面有空格一个里面是%20。排查这种问题时最好的办法是把抓包得到的原文复制出来再用计算机逐字节对比。第四个坑更隐蔽函数参数类型。比如时间戳在 Java 层是long但在拼接字符串时会以十进制形式出现没有问题。可如果你写 Python 时用了浮点数比如int(time.time())和time.time()就差了一个.0拼出来的字符串尾部也会多一个0。不要笑这种错误真的会浪费掉半小时。4.3 遇到 so 层混淆时的兜底方案如果你遇到的 sign 算法被放在了 so 文件里静态分析的难度会直线上升。因为 so 层面可能做了指令变换、反调试、PTRACE 检测甚至把关键字符串做了加密。碰到这种硬骨头我一般不会选择硬啃汇编而是采用一个比较“懒”但很可靠的办法直接用 Frida 在 so 层 Hook 关键导出函数看入参和返回值。如果 so 层的函数也被混淆到没有导出符号那我还有一个下策直接在 Java 层的网络请求出口把整个请求对象 dump 出来看到发送给服务器的内容里已经带上了 sign就不需要知道 sign 怎么来的直接用原包的输出即可。当然这种方案写起来更“脏”因为它依赖 App 本身的计算能力一旦算法更新或 App 退出登录就会受影响。所以我的建议很明确能还原算法就还原算法还原不了才考虑“原包代算”。前者具备可持续性后者通常是应急手段。5. 个人心得与最后提醒5.1 还原出来的 sign 能稳定用多久签名逆向完成之后很多人最关心的问题是这套东西能稳定用多久实话说没有标准答案。我给一个粗略经验作为参考如果后端没有做风控升级和版本检测算法可能几个月不动但如果团队意识到签名被破解他们会不定期换盐、改拼接顺序甚至整体升级成 HMAC那前一天还能跑的脚本第二天就会全线报错。我通常会把签名逻辑单独封装成一个模块并且把盐、拼接规则、时间戳策略等做成可配置参数。这样一旦规则变更我只需要更新配置文件而不是在业务代码里到处找硬编码。另外还要给请求加一点合理的频率控制别把对方的接口当自己家内网来打。无节制的并发请求不仅容易触发风控封 IP还可能给自己带来不必要的麻烦。5.2 边界意识与合规提醒最后我要认真地说一点体会逆向签名本身是一种技术能力但这把刀怎么用决定了它是工具还是风险。我做的这些项目都是基于客户授权的业务系统、自己参与过的项目或者公开接口的兼容实现。拿到一个签名算法之后我不会把它用在未授权的数据获取上更不会用它去绕过对方的风控去刷量或伤害其他用户。如果你是想学习签名算法分析我的建议是拿自己的 App、沙箱环境或者授权范围之内的应用来练手。技术没有善恶使用场景才决定性质。尽量在合法合规的框架内做研究会让这条路走得更稳。这次从抓包到复现总共花了大概三个小时。中间最磨人的不是算法本身而是排查大小写和编码的那段时间。以后再做类似逆向时我会先做一个字符串逐字节对比工具把抓包原值和本地生成值并排显示这样能在三十秒内发现差异位置而不是拿着两份长字符串在终端里人眼找不同。这种小工具不复杂但真的能省下不少时间你也可以试试。