news 2026/9/26 2:36:31

App请求签名与加密码还原实战:从抓包识别到本地复现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
App请求签名与加密码还原实战:从抓包识别到本地复现

简介:这份资源面向移动应用、小程序与网站开发者,聚焦数字签名与加密的实战代码整理,覆盖自如、小红书、蛋壳公寓、瑞幸咖啡等生活服务类App的签名与加密实现思路,适合需要研究接口安全、逆向分析或加固方案的中高级开发者参考。压缩包共15个文件,以11个Python脚本为主,辅以2个JavaScript文件、1份README说明和1份LICENSE授权文件,整体约26KB,按项目模块分目录组织,包含各App的初始化与入口脚本,便于对照阅读不同平台的签名逻辑。目前已有435人学习下载。读者可从中获取多款主流App的签名算法实现片段、加密参数构造方式以及模块化目录结构,用于理解请求签名生成、参数加密与校验流程,也可作为自研接口安全方案的对照素材,快速定位关键代码并迁移到自己的项目中。

1. 从自如、小红书到瑞幸:App 请求签名与加密码到底在防什么

抓过自如房源、小红书笔记、蛋壳公寓账单、瑞幸咖啡优惠券接口的人,大概率都撞过同一堵墙:请求发出去,返回的不是数据,而是一句冷冰冰的「签名错误」或「invalid sign」。你明明把 URL、Header、Body 都抄对了,可服务端就是不认。问题不在参数,在于你没带对那串由客户端动态算出来的签名或加密码。App、小程序、网站这三类客户端,只要涉及登录态、订单、优惠、房源这类有价值的数据,几乎都会在请求里塞一个由密钥、时间戳、随机数和业务参数共同算出来的校验值。它防的不是普通用户,而是脚本、爬虫和批量薅羊毛。这篇文章讲的就是这套机制怎么识别、怎么还原、怎么在本地复现出可用的签名,适合做数据采集、接口联调、自动化测试的工程师,也适合想搞懂自家 App 接口为什么被刷的安全同学。核心词就三个:签名、加密码、请求校验。下面从识别到落地一步步拆。

2. 先判断目标用的是签名还是加密码:四种常见形态

2.1 从抓包结果反推校验类型

拿到一个 App 或小程序的请求,第一步不是急着写代码,而是看它到底把什么当成了「凭证」。常见的校验形态有四种,识别方式差别很大。

第一种是明文签名,请求里直接带sign、signature、_sign这类字段,值通常是一串 32 位或 64 位十六进制。这种最常见,也最好还原,因为算法基本就是 MD5、SHA-1、SHA-256 或 HMAC 系列。

第二种是加密码,字段可能叫encrypt、data、cipher,值是一段 Base64 或十六进制密文,长度明显比原文长。这种是先把业务参数整体加密再传输,服务端解密后校验,常见 AES、DES、RSA 混合使用。

第三种是 Header 签名,签名不在 Body 里,而在X-Sign、Authorization、X-Auth-Token这类请求头中,往往还配合时间戳X-Timestamp和随机串X-Nonce。

第四种是协同签名,客户端只负责一部分计算,真正的签名由服务端下发的动态密钥或 SDK 内部完成,比如某些加固后的 App 会把签名逻辑放进 so 库或小程序底层框架里。

判断方法很直接:把同一个请求的 Body 改一个无关紧要的字段,重放一次。如果返回签名错误,说明签名覆盖了全部或部分业务参数;如果还能通,说明签名只跟时间戳和固定密钥有关。这一步能帮你省掉大量瞎猜的时间。

2.2 用最小改动定位签名覆盖范围

定位签名覆盖范围,是还原工作的分水岭。我一般会做一组对照实验,每次只改一个变量,观察服务端反应。

改动项观察结果推断
只改时间戳报签名错误时间戳参与签名
只改随机数 nonce报签名错误nonce 参与签名
只改业务参数报签名错误业务参数参与签名
只改 Header 顺序通常不影响签名与字段顺序无关
改 Body 里未参与签名的字段请求成功该字段被排除在签名外

这张表看着简单,但血泪经验是:很多人一上来就假设「所有参数都参与签名」,结果算出来的值永远对不上。实际上不少接口会把sign本身、文件流字段、以及某些展示用字段排除在外。你要做的是先确定参与集合,再确定拼接顺序,最后确定算法。

提示:改参数重放时,务必保持时间戳新鲜。很多接口的时间戳窗口只有 60 秒,过期后即使签名正确也会被拒,容易让你误判算法错了。

2.3 小程序和 App 的差异点在哪

小程序和 App 虽然都叫「客户端」,但签名逻辑的藏身之处完全不同。

App 的签名逻辑通常在 Java/Kotlin 或 OC/Swift 层,加固后可能下沉到 native 层。你用 jadx 反编译能看到 Java 层的调用链,但关键算法可能只是一个 native 方法声明,真正的实现藏在.so里。这时候要么用 Frida 动态 hook,要么直接找 so 里的导出函数。

小程序的签名逻辑则跑在 JavaScript 里,但代码经过打包和混淆,变量名全是a、b、c。微信小程序的包体可以在本地缓存目录找到,解包后是app-service.js这类文件。还原时重点看wx.request调用前的参数组装,签名函数往往就在附近。网站相对最透明,前端 JS 直接可读,但可能被压缩成一行,需要格式化后再找sign关键字。

三类客户端的共同点是:签名一定发生在请求发出前的最后一刻,且依赖的密钥要么硬编码,要么从服务端动态获取。找到密钥,等于拿到后悔药。

3. 还原签名算法:从关键字到可运行脚本

3.1 静态搜索与动态 hook 的配合

还原算法的第一步是找到计算入口。静态搜索的关键字包括:sign、signature、md5、sha、hmac、aes、encrypt、secret、appKey、appSecret。在 App 里,这些字符串可能被混淆,但算法库的调用特征很难完全抹掉,比如MessageDigest.getInstance("MD5")这种系统 API 调用。

如果静态搜索找不到,就上动态 hook。Frida 是常用工具,思路是 hook 常见的加密函数,打印入参和返回值。下面是一段 hook Java 层 MD5 的示例:

// Frida hook Java 层 MessageDigest,观察签名输入输出 Java.perform(function () { var MessageDigest = Java.use('java.security.MessageDigest'); var update = MessageDigest.update.overload('[B'); update.implementation = function (bytes) { // 打印待摘要的原始字节,转成字符串便于观察 console.log('MD5 input: ' + bytesToHex(bytes)); var result = update.call(this, bytes); return result; }; var digest = MessageDigest.digest.overload(); digest.implementation = function () { var res = digest.call(this); console.log('MD5 output: ' + bytesToHex(res)); return res; }; }); function bytesToHex(bytes) { var hex = []; for (var i = 0; i < bytes.length; i++) { hex.push(('0' + (bytes[i] & 0xff).toString(16)).slice(-2)); } return hex.join(''); }

这段脚本的逻辑是:在MessageDigest.update被调用时打印输入字节,在digest返回时打印摘要结果。参数说明上,overload('[B')表示匹配字节数组参数,bytesToHex负责把字节转成可读十六进制。跑起来后,你在 App 里触发一次请求,就能看到签名前的原始字符串长什么样。这一步的价值在于,它直接告诉你拼接格式,比如是appKey=xxx&timestamp=xxx&nonce=xxx还是key+value+key+value的顺序。

3.2 用 Python 复现一个 HMAC-SHA256 签名

拿到拼接格式和密钥后,用 Python 复现是最快的验证方式。下面是一个 HMAC-SHA256 的典型实现:

import hmac import hashlib import time import uuid def build_sign(params: dict, app_secret: str) -> str: # 1. 过滤空值和 sign 字段本身 filtered = {k: v for k, v in params.items() if v is not None and k != 'sign'} # 2. 按 key 的字典序排序 sorted_keys = sorted(filtered.keys()) # 3. 拼接成 key=value&key=value 形式 raw = '&'.join(f'{k}={filtered[k]}' for k in sorted_keys) # 4. 用 app_secret 做 HMAC-SHA256 sign = hmac.new( app_secret.encode('utf-8'), raw.encode('utf-8'), hashlib.sha256 ).hexdigest() return sign # 构造请求参数 params = { 'appKey': 'your_app_key', 'timestamp': str(int(time.time() * 1000)), 'nonce': uuid.uuid4().hex[:16], 'userId': '123456', 'page': '1' } params['sign'] = build_sign(params, 'your_app_secret') print(params)

逻辑说明:先剔除sign自身和空值,再按字典序排序,拼成标准查询串,最后用密钥做 HMAC-SHA256。参数上,app_secret是核心密钥,timestamp用毫秒级,nonce用随机十六进制。常见翻车点是排序规则,有的接口按参数名 ASCII 升序,有的按参数出现顺序,还有的只对 value 排序。你必须用抓到的真实请求反推,不能想当然。

3.3 加密码的还原:AES 与 RSA 的分工

加密码比签名多一层,因为你要先解密才能看到业务参数。常见组合是 AES 加密业务数据,RSA 加密 AES 密钥。还原时先找 AES 的 key 和 iv,再看它们是不是被 RSA 保护。

from Crypto.Cipher import AES from Crypto.Util.Padding import unpad import base64 def aes_decrypt(cipher_text: str, key: bytes, iv: bytes) -> str: # Base64 解码密文 encrypted = base64.b64decode(cipher_text) # 创建 AES-CBC 解密器 cipher = AES.new(key, AES.MODE_CBC, iv) # 解密并去除 PKCS7 填充 decrypted = unpad(cipher.decrypt(encrypted), AES.block_size) return decrypted.decode('utf-8') # 假设 key 和 iv 已从客户端提取 key = bytes.fromhex('0123456789abcdef0123456789abcdef') iv = bytes.fromhex('fedcba9876543210fedcba9876543210') plain = aes_decrypt('你的密文字段', key, iv) print(plain)

参数说明:key长度必须是 16、24 或 32 字节,对应 AES-128/192/256;iv长度固定 16 字节;模式常见 CBC,也有 ECB。如果解密出来是乱码,先检查 key 和 iv 是否取错,再检查填充方式。RSA 部分通常只用来保护 AES 密钥,用私钥解密即可,但私钥往往不在客户端,这时候要么找服务端下发的动态密钥,要么放弃纯静态还原。

注意:加密码的还原难度明显高于签名,因为密钥可能一次一密。遇到动态密钥,优先考虑 hook 密钥生成函数,而不是硬啃算法。

4. 避坑与排查:签名还原路上最常见的五个翻车点

4.1 时间戳单位搞错导致签名永远过期

现象:本地算出来的签名和抓包值一模一样,但请求返回「签名过期」或「timestamp invalid」。

原因:客户端用毫秒,你用了秒;或者客户端用秒,你用了毫秒。差 1000 倍,签名窗口直接失效。

解决:抓包看timestamp字段的位数。13 位是毫秒,10 位是秒。统一后再算签名,并且确保本地时间和服务器时间偏差在允许窗口内。

4.2 参数拼接顺序与编码方式不一致

现象:算法、密钥、参数都对,签名就是差几位。

原因:拼接顺序不是字典序,或者 value 做了 URL 编码而你没做,或者空格被编码成了+而不是%20。

解决:用 hook 打印出的原始字符串逐字符对比。重点看分隔符是&还是空字符串,value 是否 encode,中文是否 UTF-8。这一步没有捷径,只能靠原始输入反推。

4.3 密钥硬编码在 so 里,Java 层搜不到

现象:jadx 里翻遍所有类,找不到appSecret或sign的赋值逻辑。

原因:签名逻辑下沉到 native 层,Java 只留一个 native 方法声明。

解决:用 Frida hook native 层的常见加密函数,比如MD5_Update、SHA256_Update、EVP_DigestUpdate。或者直接 hookJNI_OnLoad附近的注册函数,找到 native 方法地址后反汇编。工具上 IDA 配合 Frida 是常规组合。

4.4 小程序包体更新后签名逻辑变了

现象:昨天还能用的脚本,今天全部签名错误。

原因:小程序热更新或发版后,签名算法、密钥、拼接格式任一改变都会导致失效。

解决:建立版本监控,每次抓包先比对sign字段长度和请求参数结构。如果长度从 32 位变 64 位,说明算法从 MD5 换成了 SHA-256。不要假设一套逻辑能永久用。

4.5 忽略设备指纹和风控参数

现象:签名完全正确,但请求返回「环境异常」或直接封号。

原因:除了签名,服务端还校验设备 ID、安装 ID、行为轨迹等风控参数,这些参数也在请求里且参与签名。

解决:把风控参数一并纳入签名计算,并保证每次请求的指纹一致。如果风控参数由 SDK 动态生成,需要 hook 生成函数而不是伪造固定值。

5. 进阶:把签名逻辑做成可复用的本地服务

5.1 用 Flask 暴露一个签名接口

当你需要批量请求时,把签名逻辑封装成本地 HTTP 服务是最省事的做法。这样采集脚本、测试脚本、Postman 都能调用同一套逻辑,改算法时只改一处。

from flask import Flask, request, jsonify import hmac import hashlib import time app = Flask(__name__) APP_SECRET = 'your_app_secret' def calc_sign(params: dict) -> str: filtered = {k: v for k, v in params.items() if k != 'sign' and v is not None} raw = '&'.join(f'{k}={filtered[k]}' for k in sorted(filtered)) return hmac.new(APP_SECRET.encode(), raw.encode(), hashlib.sha256).hexdigest() @app.route('/sign', methods=['POST']) def sign_api(): data = request.get_json() # 自动补时间戳和随机数 data.setdefault('timestamp', str(int(time.time() * 1000))) data['sign'] = calc_sign(data) return jsonify(data) if __name__ == '__main__': app.run(host='127.0.0.1', port=5000)

逻辑说明:接口接收业务参数,自动补时间戳,计算签名后原样返回。参数上,APP_SECRET从配置文件读取更安全,端口按需改。调用方拿到返回的 JSON 直接作为请求参数即可。这个服务的价值在于把「签名」从脚本里解耦出来,多个项目共用。

5.2 验证签名正确性的三个手段

写完签名逻辑,别急着上量,先做三组验证。

第一,用抓包到的真实请求做回归。把当时的参数、时间戳、nonce 原样输入,看输出是否与抓包sign完全一致。一致说明算法和拼接都对。

第二,做时间窗口测试。把时间戳往前调 30 秒、60 秒、120 秒,观察服务端在哪个点开始拒绝,确认窗口边界。

第三,做参数扰动测试。随机改一个参与签名的参数,确认签名变化;改一个不参与签名的参数,确认签名不变。这能验证你的参与集合是否准确。

5.3 我踩过的坑和现在的习惯

我最早做这类还原时,总想一次把算法完全静态分析出来,结果在 so 库上耗了两天。后来养成习惯:先动态 hook 拿到输入输出,用真实数据反推算法,再回头静态确认。动态优先,静态验证,这个顺序帮我省了大量时间。

另一个习惯是给每个目标建一个「签名档案」,记录算法、密钥来源、拼接格式、时间戳单位、参与字段列表、失效日期。下次接口一变,对照档案就能快速定位差异。签名还原不是一劳永逸的事,它更像持续维护的适配工作。把每次踩坑记下来,比记住某个具体算法更有用。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 2:36:07

数据库课程设计实战:员工考勤管理系统从需求到SQL实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:35:44

QT QTextEdit 自动滚动到底部怎么关?TaoToken 配置骨架与验证清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 2:34:48

SoLab AI逆向工作台:集成DEX/SO/Flutter的安卓逆向分析利器

很多做安卓安全研究、App合规检测、恶意代码分析的朋友&#xff0c;应该都有过这样的体会&#xff1a;拿到一个APK&#xff0c;第一件事就是用jadx打开看一眼Java层代码&#xff0c;再用IDA或者Ghidra去啃Native库&#xff0c;遇到Flutter应用更是头疼&#xff0c;Dart AOT编译…

作者头像 李华
网站建设 2026/9/26 2:29:17

Substrate区块链开发框架:从核心原理到无分叉升级实战

1. 从“substrate”这个词说起&#xff1a;它到底是什么&#xff0c;能解决什么问题第一次看到“substrate”这个标题&#xff0c;很多人脑子里蹦出来的第一反应可能是“底层”“基底”“培养基”这类模糊概念。这个词本身确实是个跨领域的高频术语——在材料科学里它指衬底&am…

作者头像 李华