当前位置: 首页 > news >正文

JS逆向实战:从抓包到Python复现,破解莫莫铺子sign签名算法

1. 项目概述:从抓包到逆向,破解莫莫铺子的数据之门

最近在分析一个名为“莫莫铺子”的数据接口时,遇到了一个典型的签名参数sign。对于从事数据采集和分析的朋友来说,这种场景再熟悉不过了:你兴致勃勃地打开开发者工具,抓取到目标接口,却发现关键的请求参数被一个看似随机的字符串保护着,直接复制链接无法获取数据。这个sign参数,就是服务端用来验证请求合法性、防止数据被随意抓取的“门锁”。今天,我就以“莫莫铺子”这个实际案例,带大家完整走一遍js逆向的实战流程,从抓包定位、代码分析到最终用Python复现签名算法,手把手教你如何解开这扇门。

这个过程不仅仅是学会一个网站的破解,更重要的是掌握一套通用的js逆向方法论。无论你面对的是电商平台、内容社区还是其他数据服务,其核心的签名逻辑往往有迹可循。我们将重点关注如何在海量的JavaScript代码中,精准定位到签名函数,如何理清函数内部的变量依赖关系,以及如何将js逻辑无误地翻译成Python代码。整个思路适用于绝大多数基于前端JavaScript生成动态参数的场景,是数据工程师和爬虫开发者必须掌握的硬核技能。

2. 逆向前的准备工作与环境搭建

2.1 目标分析与工具选择

在开始逆向之前,明确目标和准备好趁手的工具是成功的一半。我们的目标是莫莫铺子网站(示例域名为mmpz.ttzhuijuba.com)上某个数据列表接口的sign参数。这个参数通常作为URL的查询字符串(Query String)的一部分,每次请求都会变化,但其生成逻辑是固定的。

工欲善其事,必先利其器。以下是本次实战的核心工具栈:

  1. 浏览器开发者工具(Chrome DevTools):这是我们的主战场。特别是Network(网络)面板和Sources(源代码)面板。Network 面板用于捕获和分析网络请求,Sources 面板用于调试 JavaScript 代码。
  2. 代码美化工具:线上环境的js代码通常被压缩(minified),变量名都是a, b, cn, r, i等单字母,可读性极差。Chrome DevTools 自带的“Pretty Print”功能(通常显示为{}图标)可以一键将压缩代码格式化,是逆向分析的救命稻草。
  3. 全局搜索(Ctrl+Shift+F):Sources 面板支持在所有加载的js文件中进行全局文本搜索,这是定位关键函数和字符串的最有效手段。
  4. Python 环境:用于最终复现算法。需要安装requests库用于发起网络请求,hashlib库用于实现MD5等哈希算法。

注意:在进行任何逆向操作前,请务必遵守网站的robots.txt协议,并尊重数据版权。本教程仅用于技术学习与交流,请勿用于任何侵犯他人权益或违反相关法律法规的用途。高频、大量的请求可能会对目标服务器造成压力,请在测试时控制请求频率。

2.2 关键请求的捕获与初步观察

打开目标网站,进入需要采集数据的页面(例如商品列表页)。打开 Chrome DevTools 的 Network 面板,并勾选“Preserve log”(保留日志),防止页面跳转时请求记录被清空。然后刷新页面或进行翻页操作。

在纷繁复杂的请求列表中,我们需要找到那个携带了sign参数、并且返回了核心数据(如JSON格式的商品列表)的XHRFetch请求。通过查看请求的PreviewResponse选项卡,可以快速判断。找到目标请求后,点击它,查看Headers选项卡下的Query String Parameters部分。

以案例中的请求为例,其URL可能长这样:

https://openapi.dataoke.com/api/goods/get-goods-list?version=v1.2.4&appKey=612bcfe884763&pageId=1&pageSize=100&...&sign=5dd20b08158402032845b45f5b67a349

这里我们清晰地看到sign=5dd20b08158402032845b45f5b67a349。我们的任务就是找出这个5dd20b08158402032845b45f5b67a349是如何由其他参数(如version,appKey,pageId等)计算出来的。

3. 逆向核心思路与签名函数定位

3.1 两种搜索策略的抉择

定位签名函数通常有两种思路:

  1. 搜索URL中的特征路径或参数名:例如搜索“get-goods-list”“pageId”。这种方法有时能直接定位到发起请求的Ajax代码段,但现代前端工程化项目往往将接口配置和业务逻辑分离,找到的代码可能离生成sign的核心函数还很远。
  2. 直接搜索签名参数名或关键字:这是更直接有效的方法。既然我们的目标是sign,那么就在所有JavaScript文件中全局搜索“sign”“sign:”“makeSign”等关键词。

在本次莫莫铺子的案例中,第一种方法效果不佳。搜索URL路径找到的代码位置无法清晰追踪到sign的生成。因此,我们果断采用第二种策略。

3.2 全局搜索与断点调试实战

在 Chrome DevTools 的 Sources 面板中,按下Ctrl+Shift+F,在搜索框中输入sign。搜索结果可能会非常多,因为sign是一个常见单词。我们需要进行筛选:

  • 忽略注释和字符串:有些结果可能出现在注释或无关的日志字符串里。
  • 关注赋值和函数调用:寻找类似sign =sign:(在对象中)、makeSign(getSign(这样的模式。
  • 结合格式化工具:对可疑的js文件点击{}进行美化,让代码结构清晰。

在案例中,搜索后我们找到了两处高度可疑的代码:

  1. sign: n("7bc9")
  2. 一个名为makeSign的函数被调用。

对于第一处n("7bc9"),我们可以通过在其所在行打上断点(点击行号),然后刷新页面来触发断点。在 Console 中打印n("7bc9")的值,发现它返回的是一段原生函数代码,这显然不是我们想要的sign值。这是一个很好的排除法实践。

接着,我们聚焦到makeSign函数。在它的定义或调用处打上断点。刷新页面,当代码执行到断点时,程序会暂停。此时,我们可以利用Scope(作用域)面板查看当前作用域内的所有变量,也可以将鼠标悬停在变量上查看其值,或者在 Console 中直接输入变量名进行查看。

3.3 理解 makeSign 函数的逻辑

当断点命中在makeSign函数内部时,我们逐步分析。假设我们看到类似如下的代码结构(经过格式化后):

function makeSign(s) { var e = this.config.appSecret; var n = Object.keys(s).sort().map(function(key) { return key + "=" + s[key]; }).join("&") + e; return md5(n); }

这段代码非常典型地揭示了一个MD5签名流程:

  1. s:是一个对象,包含了除sign之外的其他请求参数(如appKey,version,pageId等)。
  2. e:从this.config.appSecret获取一个密钥(appSecret)。这是一个常量,是签名算法的关键。
  3. n:将参数对象s的键值对按字母顺序排序,拼接成key1=value1&key2=value2...的字符串格式,然后在末尾拼接上密钥e
  4. 最后,对这个拼接后的字符串n进行MD5哈希计算,并将得到的十六进制字符串作为sign返回。

至此,签名算法的核心逻辑已经浮出水面。但还有关键一步:s这个参数对象和this.config.appSecret这个密钥,它们是从哪里来的?

4. 关键变量溯源与算法完全还原

4.1 追踪 this.config 与参数对象 s

逆向工程就像侦探破案,需要顺藤摸瓜。我们已经知道了makeSign这个“凶手”的作案手法(MD5),现在需要找到它的“凶器”(密钥)和“作案材料”(参数)。

首先,在makeSign函数内部或调用栈的上下文中,查找this.config的定义。在 Sources 面板中,可以搜索this.config =config:。在案例中,发现this.config是在某个初始化函数中通过Object.assign合并而来的,其appSecretappKey等值来源于一个固定的配置对象或传入的参数e。通过查看调用栈(Call Stack),找到初始化该模块的地方,最终确定this.config.appSecret是一个固定的字符串,比如“your_app_secret_here”这个appSecret是服务端和前端约定的私钥,不会在网络请求中明文传输,是签名安全性的基石。

接下来,追踪参数对象s。在调用makeSign(s)的地方,查看s是如何构建的。通常,它会包含接口的公共参数和业务参数。在案例中,我们看到类似这样的代码:

var s = p(p({}, { version: this.config.version, appKey: this.config.appKey }), r);

这里的p函数可能是类似Object.assign{...spread}语法的工具函数,用于合并对象。this.config.versionthis.config.appKey是固定值。r则可能是包含了当前请求的业务参数(如pageId,cids等)的对象。所以,s最终就是由固定公共参数和动态业务参数合并而成的一个完整参数对象。

4.2 完整算法还原与 Python 实现

现在,我们已经掌握了所有信息:

  • 签名算法MD5
  • 待签名字符串格式:将所有请求参数(除sign外)按参数名升序排序,用&连接成key=value的形式,最后拼接上appSecret
  • 关键常量appKey,appSecret,version(通常也固定)。

我们可以用 Python 完美复现这个逻辑:

import hashlib import urllib.parse def generate_sign(params, app_secret): """ 生成莫莫铺子接口的 sign 参数 :param params: dict, 所有请求参数(不包含sign本身) :param app_secret: str, 密钥 :return: str, 计算得到的sign值 """ # 1. 对参数按键名进行升序排序 sorted_params = sorted(params.items(), key=lambda x: x[0]) # 2. 将排序后的参数拼接成 key1=value1&key2=value2... 的格式 # 注意:值需要进行URL编码,确保与js端行为一致。js中encodeURIComponent的行为可用quote替代。 query_string = '&'.join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in sorted_params]) # 3. 在字符串末尾拼接上 app_secret string_to_sign = query_string + app_secret # 4. 计算 MD5 哈希值(32位小写十六进制) m = hashlib.md5() m.update(string_to_sign.encode('utf-8')) sign = m.hexdigest() return sign # 示例使用 if __name__ == '__main__': # 这些是常量,从js代码中分析得到 APP_KEY = '612bcfe884763' APP_SECRET = 'your_app_secret_here' # 此处需要替换为真实的appSecret VERSION = 'v1.0.2' # 这些是动态的业务参数 dynamic_params = { 'pageId': 1, 'pageSize': 100, 'cids': 1, 'tmall': 1, 'commissionRateLowerLimit': 3, 'hasCoupon': 1, 'keyWords': '' } # 构建完整的参数字典(包含固定参数和动态参数) all_params = { 'appKey': APP_KEY, 'version': VERSION, **dynamic_params # Python 3.5+ 的字典解包语法 } # 生成签名 sign = generate_sign(all_params, APP_SECRET) print(f"生成的 sign 为: {sign}") # 构建最终的请求URL(注意:sign本身不参与签名计算,所以最后加上) base_url = "https://openapi.dataoke.com/api/goods/get-goods-list" # 将参数转换为查询字符串,注意sign单独处理 query_without_sign = '&'.join([f"{k}={urllib.parse.quote(str(v), safe='')}" for k, v in sorted(all_params.items())]) final_url = f"{base_url}?{query_without_sign}&sign={sign}" print(f"完整的请求URL: {final_url}")

实操心得:在拼接参数字符串时,一个极易踩坑的细节是URL 编码。JavaScript 中的encodeURIComponent会对更多字符进行编码(如!,',(,),*等),而 Python 的urllib.parse.quote默认行为略有不同。使用quote(str(v), safe='')可以确保编码行为与encodeURIComponent基本一致,避免因编码差异导致服务端计算的签名与客户端不一致。在实际逆向时,最好在js调试器中打印出拼接后的待签名字符串,与Python生成的字符串进行逐字符比对,这是排查签名错误的最有效方法。

5. 常见问题排查与进阶技巧

5.1 签名验证失败的排查清单

即使算法还原了,第一次尝试时签名错误也是家常便饭。请按照以下清单逐一排查:

问题现象可能原因排查方法
返回“签名无效”或类似错误1.appSecret错误:密钥不对。
2.参数缺失或多余:参与签名的参数集合不对。
3.参数值错误:动态参数的值与实际请求不符(如时间戳过期)。
4.排序规则不一致:未按字母顺序升序排序。
5.编码问题:参数值未进行正确的 URL 编码。
6.拼接格式错误:键值对连接符不是&,或末尾拼接密钥的格式不对。
1. 在js调试器中,在签名函数前打印出this.config.appSecret和最终待签名的原始字符串。
2. 在Python中,在计算MD5前也打印出待签名字符串。
3.逐字符对比这两个字符串(注意隐藏字符、空格、换行)。完全一致才能保证签名相同。
请求成功但返回空数据或错误码1.请求头(Headers)问题:缺少必要的User-AgentRefererCookie
2.参数逻辑错误:某些参数有固定值范围或依赖关系。
1. 用浏览器正常访问,抓取成功的请求,完整复制其Headers(尤其是Cookie),在Pythonrequests中模拟。
2. 仔细阅读接口可能的文档或通过多次请求观察参数规律。
无法定位签名函数1.代码被混淆或加密:变量名和函数名被替换,逻辑被隐藏。
2.签名在 WebAssembly 或 Worker 中计算:更复杂的保护手段。
1. 尝试搜索签名结果的常量特征(如MD5是32位十六进制)。
2. 搜索MD5encrypthash等算法关键词。
3. 使用“XHR/fetch Breakpoints”在发起特定URL的请求时断住,然后回溯调用栈。

5.2 应对代码混淆与加密的进阶策略

对于简单的混淆(变量名缩短),我们使用的“搜索关键词+断点调试”方法依然有效。但如果遇到更复杂的保护:

  1. Hook 关键函数:使用浏览器插件或控制台代码,劫持标准的JavaScript函数。例如,在控制台执行以下代码可以监控所有MD5调用:

    var _md5 = window.md5 || function(){}; window.md5 = function(data) { console.trace('MD5 called with data:', data); var result = _md5(data); console.log('MD5 result:', result); return result; };

    你需要根据实际情况找到md5函数挂载的对象(可能是CryptoJS或某个自定义对象)。

  2. 追查网络请求栈:在 Network 面板中找到目标请求,右键选择“Copy -> Copy as cURL”有时能直接看到浏览器构建的完整命令,但看不到sign生成过程。更有效的是,在该请求的“Initiator”标签页查看调用栈,一层层点击进去,可能找到离签名生成更近的代码。

  3. 还原混淆代码:如果代码被严重混淆,可以尝试使用js反混淆工具(如jsnice,de4js等)进行初步还原,提高可读性。但要注意,完全自动化的还原可能引入错误,需结合人工分析。

5.3 保持爬虫健壮性的建议

逆向成功只是第一步,让爬虫长期稳定运行需要考虑更多:

  • 密钥与配置外置:将appKey,appSecret,version等常量放在配置文件或环境变量中,便于管理和更换。
  • 错误重试与降级:网络请求可能失败,签名也可能因服务端变更而失效。代码中应加入重试机制和完备的日志记录,一旦签名失败能快速报警。
  • 关注前端更新:前端JavaScript代码可能会更新,签名算法也可能改变。需要有一套机制(如定时访问、校验签名)来监控这种变化。
  • 尊重robots.txt与速率限制:严格遵守目标网站的爬虫协议,并在代码中设置合理的请求间隔(如time.sleep),避免对服务器造成冲击。

逆向工程是一个需要耐心和细心的过程。莫莫铺子sign参数的解密案例是一个经典的MD5签名场景,掌握了这套“抓包 -> 搜索 -> 断点 -> 溯源 -> 还原”的方法论,你就能应对市面上大多数前端参数加密的挑战。核心永远是对比验证:确保你的Python代码每一步生成的中间结果,都与浏览器中JavaScript执行的结果完全一致。

http://www.cnnetsun.cn/news/3195358.html

相关文章:

  • F统计量详解:回归模型整体显著性的判断原理与实操
  • League Akari:重构英雄联盟游戏体验的智能工作流引擎
  • Unity ECS 与 MonoBehaviour 对比评测:3类场景下的内存与CPU开销分析
  • Unity游戏开发实战:有限状态机FSM实现NPC 4种行为状态切换
  • DeepSORT 核心模块解析:卡尔曼滤波与匈牙利算法在 MOT 中的 3 种匹配策略
  • Kubernetes Redis 7.2 集群部署:NFS存储+StatefulSet 6节点配置详解
  • Python Playwright控件定位全解析:从基础选择器到高级场景实战
  • AI智能体开发实战:从App到技能服务的范式转移
  • GitHub Actions集成Makefile实现CI/CD自动化
  • 如何将3D模型完美转换为Minecraft建筑:ObjToSchematic深度指南
  • CoppeliaSim 4.6 工业机器人仿真:从 URDF 导入到 TCP 轨迹规划的 7 个步骤
  • 通信工程转码 Java 后端:从 0 到 1 的 6 个月学习路径与 3 个核心项目
  • Unity ECS 1.0 实战:从 MonoBehaviour 迁移 3 个核心系统到 JobSystem
  • 微信QQ防撤回补丁原理与实战:十六进制编辑实现消息留存
  • AI大模型伦理实践:从偏见、透明度到责任的全链路应对策略
  • 3种主流Linux发行版部署NI-VISA对比:CentOS 7 vs Ubuntu 20.04 vs openSUSE
  • JUnit测试套件配置错误诊断与修复指南
  • BOINC 与 Docker 容器化部署对比:4 种方案在家庭服务器上的性能与易用性评测
  • VNC 开机自启失败排查:3 类常见错误与 Systemd 日志分析指南
  • 治愈系工作台:终端配色不只是好看,还要降低视觉疲劳
  • OpenCore 0.7.0+ AllowToggleSIP 功能详解:3种SIP状态切换方案对比
  • 企业级JMeter部署实战:从环境搭建到自动化压测全流程
  • 10分钟搞定REPENTOGON脚本扩展器:让《以撒的结合》焕发新生
  • OpenCV 4.8.0 setMouseCallback 实战:3种图像标注工具实现与性能对比
  • 小米穿戴表盘DIY终极指南:如何用Mi-Create轻松打造个性化智能手表界面
  • GPT-4V 多模态美学分析:从婚礼照片到哈勃影像的 3 种情感计算路径
  • 【Bug已解决】Claude Code Monorepo 下 CLAUDE.md 同级目录未加载解决方案
  • PostGIS 3.5.2 空间查询性能优化:5个核心函数与索引策略实测
  • Paperxie 期刊论文智能写作|科研人问答实录,拆解普刊 / 核心 / SCI 专属写稿神器
  • Linux 进程守护:nohup 与 信号处理差异详解(SIGHUP/SIGINT)