电商接口sign签名逆向实战:从MD5加密到Python复现 1. 项目概述电商sign签名与MD5加密的逆向挑战最近在分析一个电商平台的接口时遇到了一个典型的防护手段请求参数中携带了一个名为sign的签名值并且整个签名算法似乎与MD5加密紧密相关。对于前端工程师或者安全爱好者来说这种场景再熟悉不过了。平台通过在前端JavaScript代码中植入一套复杂的签名算法对请求的关键参数进行加密生成一个唯一的sign值。服务器收到请求后会用同样的算法重新计算一遍签名如果两者不一致就直接拒绝请求以此来防止请求被篡改或重放。这不仅是保护数据完整性的常规操作更是对抗爬虫和恶意刷单的第一道防线。今天我们就来一次实战演练手把手拆解这个“黑盒”看看如何定位签名函数、还原算法逻辑并最终在本地复现整个签名过程。无论你是想学习JS逆向的思路还是工作中遇到了类似的需求这篇从实战中总结的经验或许能给你提供一条清晰的路径。2. 逆向目标分析与环境准备2.1 明确逆向目标与核心难点我们的目标非常明确找到生成sign参数的JavaScript函数理解其输入、处理和输出逻辑并最终能用Python或其他语言独立实现相同的签名算法。听起来简单但实际操作中会遇到几个典型的“坑”。首先代码混淆与压缩。生产环境的JS代码通常被压缩成单行变量名被替换成a, b, c, d等无意义字符函数结构也被扁平化可读性极差。这需要我们具备一定的“脑补”能力能在一团乱麻中识别出关键的控制流和数据结构。其次算法逻辑分散。签名算法很少会集中在一个函数里。它可能分散在多个模块中涉及全局变量的存取、多个工具函数的调用甚至可能动态加载某些代码片段。我们需要像侦探一样顺着sign这个线索顺藤摸瓜找到所有相关的代码块。最后环境依赖检测。有些签名算法会依赖浏览器特有的对象或属性比如window、document、navigator.userAgent或者一些加密库生成的随机数。如果我们的本地复现环境缺少这些依赖计算出的签名就会对不上。因此分析时必须留意算法是否与浏览器环境强绑定。2.2 工具链选择与配置工欲善其事必先利其器。一套顺手的工具能极大提升逆向效率。以下是我个人在多次实战后固定下来的工具组合你可以直接“抄作业”。1. 浏览器开发者工具 (Chrome DevTools)这是主战场务必熟练掌握。Sources面板用于查看、搜索和调试JavaScript源码。重点关注XHR/fetch Breakpoints功能可以给包含特定关键词如“sign”的网络请求打上断点是定位签名入口最直接的方法。Network面板记录所有网络请求。筛选出目标请求查看其Headers和Payload确认sign参数的位置和格式。右键请求选择Copy - Copy as cURL或Copy as Node.js fetch可以方便地导出请求信息用于后续测试。Console面板用于执行JavaScript代码片段测试我们还原的函数。可以通过copy()函数快速复制对象到剪贴板。2. 代码格式化与美化工具面对压缩的代码第一步就是格式化。Chrome DevTools 自带的Pretty print按钮通常显示为{}符号是首选。对于特别复杂的混淆可以尝试在线工具或VS Code插件但要注意代码安全避免泄露敏感信息。3. 本地调试与复现环境Node.js用于在本地执行还原出的JS代码。这是测试算法是否独立于浏览器的关键。确保安装好。Python环境最终我们通常会用Python来实现签名算法集成到爬虫或自动化脚本中。需要准备requests库用于发送请求hashlib库用于MD5等哈希计算。文本编辑器/IDEVS Code 或 WebStorm用于管理和分析从浏览器中复制出来的大量JS代码片段。注意整个逆向过程必须在法律允许和道德规范的范围内进行仅用于学习交流和技术研究切勿对目标网站进行恶意攻击或高频访问以免对其正常服务造成影响。3. 逆向实战定位与解析签名函数3.1 从网络请求锁定关键断点一切从观察开始。打开目标电商网站的商品列表页或搜索页开启开发者工具的Network面板并勾选Preserve log。进行一次操作比如点击搜索在纷繁的请求中找到我们的目标接口。通常获取商品数据的接口是XHR或Fetch类型。找到后点击该请求查看Headers和Payload。在Payload通常是Form Data或Request Payload中仔细寻找sign、token、_signature等命名的参数。记下它的值同时记录下同一请求中的所有其他参数特别是那些看起来是动态生成的如时间戳t、随机数nonce等。这些很可能都是签名算法的原材料。接下来在Sources面板中找到并点击XHR/fetch Breakpoints右边的号添加一个断点规则。由于我们不知道具体是哪个JS文件发出的请求最有效的方法是添加一个包含目标接口URL关键词的断点例如*api*product*。设置好后再次触发请求浏览器就会在发起这个网络请求的瞬间暂停并跳转到发起请求的JavaScript代码处。3.2 调用栈分析与关键函数定位当断点触发代码执行暂停后我们的“侦探”工作才真正开始。此时不要看眼前那一行代码而是立刻将目光投向右侧的Call Stack调用栈面板。调用栈展示了从当前暂停点往回追溯的函数调用链最上面是刚刚被调用的函数越往下越是根源。我们的目标签名函数很可能就在这个调用链的某个环节中。我们需要从栈顶开始逐层点击查看。查找的线索有以下几个函数名寻找包含sign、encode、encrypt、md5、hash等关键词的函数名。参数名查看函数的作用域变量寻找包含data、params、payload等对象里面很可能有我们熟悉的请求参数。可疑操作在函数体内如果看到Object.keys遍历对象、数组的sort方法、字符串的拼接或Array.join以及类似CryptoJS.MD5或require(‘crypto’).createHash(‘md5’)的调用那这里就非常可疑了。一旦找到一个高度疑似签名生成的函数就在该函数的第一行打上断点然后取消之前的XHR断点刷新页面重新触发请求。如果成功在我们的新断点处暂停并且能看到入参是请求参数对象返回值就是那个sign字符串那么恭喜你找到了“命门”。3.3 算法逻辑还原与关键代码提取找到签名函数后不要急着复制全部代码。很多代码是无关的框架代码或业务逻辑。我们需要做的是“萃取”。首先理清输入输出。在断点暂停时在Console里打印函数的入参确认它就是我们要签名的原始参数对象。同时记录下函数返回的sign值与Network面板中的值进行比对确保一致。其次单步执行理解流程。使用F10单步跳过和F11单步进入逐步执行函数。重点关注参数排序签名通常要求参数按字母序排序注意是Object.keys(params).sort()的过程。拼接规则参数名和值是如何拼接成待签名字符串的常见格式有key1value1key2value2或者更复杂的key1value1key2value2。是否加入了固定的分隔符或盐值salt编码处理拼接后的字符串是否进行了encodeURIComponent或Base64编码加密调用最终将字符串传递给哪个加密函数是原生的btoa、CryptoJS.MD5还是其他自定义的哈希函数记下函数名和调用方式。最后提取最小可执行代码块。将签名函数及其直接依赖的函数比如一个用于拼接字符串的sortedQueryString函数或一个自定义的hex_md5函数的代码复制出来。如果依赖了浏览器全局对象如window、location需要评估是否可在Node.js中模拟或替换。将这些代码保存到一个单独的.js文件中为下一步的本地验证做准备。4. 签名算法复现与本地验证4.1 构建独立的Node.js测试环境将上一步提取的JS代码保存为sign.js。在文件开头我们需要模拟浏览器环境缺失的部分。最常见的需要模拟的对象是window和navigator。// 模拟浏览器全局对象如果代码中用到了的话 if (typeof window undefined) { global.window {}; global.navigator {}; // 如果算法用到了userAgent可以在这里定义 global.navigator.userAgent Mozilla/5.0 ...; }然后将我们提取的核心签名函数暴露出来。例如如果原函数是function getSign(params) {...}我们修改为function getSign(params) { // ... 提取的算法逻辑 } // 导出函数方便测试 module.exports getSign;创建一个测试文件test.jsconst getSign require(./sign.js); // 构造一个与真实请求完全一致的参数对象 const testParams { keyword: 手机, page: 1, size: 20, t: Date.now(), // 时间戳 appKey: xxxxxx // 如果有的话 }; console.log(测试参数, testParams); const mySign getSign(testParams); console.log(本地计算签名, mySign); console.log(预期签名从Network面板复制, 预期的sign值字符串);运行node test.js观察输出的签名是否与浏览器中抓取到的签名一致。如果不一致进入排查阶段。4.2 算法细节还原与Python实现在Node.js环境中调试通过后我们就可以将算法翻译成Python了。这个过程需要非常仔细因为JS和Python在字符串处理、字典排序、编码等方面有细微差别。1. 参数排序与拼接JS中Object.keys(obj).sort()默认按字符编码顺序排序。Python中dict.keys()返回的是视图需要转为列表再排序sorted(params.keys())。确保排序结果一致。 JS中数组拼接常用arr.join(‘’)。Python中对应‘’.join([f’{k}{v}‘ for k, v in sorted_items])。注意值是否需要str()转换或URL编码。2. MD5哈希计算JS中可能使用CryptoJS.MD5(string).toString()。Python中使用hashlibimport hashlib sign_str ‘key1value1key2value2’ md5 hashlib.md5() md5.update(sign_str.encode(‘utf-8’)) # 注意编码 sign md5.hexdigest() # 得到16进制小写字符串 # 或者 sign md5.hexdigest().upper() 如果原签名是大写务必注意字符串编码JS通常使用UTF-16而Python的encode(‘utf-8’)是UTF-8。大多数情况下对于ASCII字符两者一致但如果待签名字符串包含中文必须确认JS端的处理方式。一个技巧是在JS签名函数中将待签名字符串用unescape(encodeURIComponent(str))等方式显式转换为UTF-8字节流后再MD5这样在Python端更容易对齐。3. 盐值Salt与密钥有些签名会在参数字符串前后拼接一个固定的盐值或密钥。这个盐值可能硬编码在JS里也可能通过其他接口获取。在Python实现中需要原封不动地加上。一个完整的Python签名函数雏形如下import hashlib import urllib.parse import time def generate_sign(params, app_secretNone): 生成签名 :param params: 参数字典 :param app_secret: 密钥盐值 :return: 签名字符串 # 1. 参数排序 sorted_keys sorted(params.keys()) # 2. 拼接键值对 sign_list [] for key in sorted_keys: value params[key] # 根据实际情况决定是否进行URL编码 # encoded_value urllib.parse.quote(str(value)) encoded_value str(value) sign_list.append(f{key}{encoded_value}) sign_str .join(sign_list) # 3. 拼接盐值如果存在 if app_secret: # 可能是 sign_str app_secret也可能是 app_secret sign_str app_secret sign_str app_secret sign_str app_secret # 4. MD5哈希 m hashlib.md5() m.update(sign_str.encode(utf-8)) # 编码是关键 sign m.hexdigest() # 5. 可能还需要二次处理如转大写、截取部分字符等 # sign sign.upper() # sign sign[:16] return sign # 测试 test_params { keyword: 手机, page: 1, size: 20, t: int(time.time() * 1000) # 模拟JS的Date.now() } secret your_app_secret_here # 需要从JS代码中找出 my_sign generate_sign(test_params, secret) print(f生成的签名: {my_sign})4.3 本地验证与线上请求测试本地算法实现后需要用真实的请求进行验证。使用之前从Network面板Copy as cURL复制的命令将其转化为Pythonrequests代码。替换掉其中的sign参数为我们本地计算的值然后发送请求。关键是比较响应结果。如果服务器返回了正常的数据恭喜你成功了如果返回了签名错误如code: 1001, msg: ‘signature invalid’则需要仔细排查以下问题参数遗漏或多余检查你的参数字典是否和浏览器发送的完全一致。特别注意一些隐藏的、默认的参数或者由SDK自动添加的参数。编码问题这是最常见的坑。对比JS端和Python端在MD5之前那个待签名的字符串是否完全一致包括每个字符、空格、标点。可以在JS签名函数里用console.log(‘待签名字符串:’, signStr)在Python里用print(‘待签名字符串:’, sign_str)输出对比。特别注意中文、空格、特殊符号的URL编码差异。盐值或密钥错误确认拼接的盐值是否正确是否经过了某种变换如自身先MD5一次。时间戳格式JS的Date.now()是毫秒级时间戳13位Python的int(time.time()*1000)与之对应。而time.time()是秒级10位。务必统一。MD5输出格式JS的CryptoJS.MD5(...).toString()默认输出16进制小写字符串。Python的hexdigest()也是小写。检查是否需要转大写.upper()。5. 常见问题排查与进阶技巧5.1 高频问题速查表问题现象可能原因排查思路本地签名与浏览器签名完全不同根本未找到正确的签名函数参数集合错误。重新检查调用栈确认断点是否打在真正的签名函数上。对比浏览器请求的Payload和自己构造的参数字典确保键值对完全一致。签名部分一致但末尾几位不同编码问题特别是中文或特殊符号。时间戳等动态参数取值瞬间不同。在JS和Python中分别打印MD5之前的原始字符串进行逐字符比对包括不可见字符。确保动态参数在比对时是冻结的用固定值测试。服务器返回“签名过期”签名算法中包含了时间戳且服务器对时间戳有有效期校验如5分钟。检查请求中的时间戳参数如t,_t。在Python请求中使用当前时间生成时间戳确保在服务器有效期内。算法依赖浏览器环境对象签名函数中使用了window.btoa,location.href等。在Node.js复现时需要模拟这些对象。btoa可以用Buffer.from(str).toString(‘base64’)替代。location相关值可能需要从请求URL中提取并硬编码模拟。算法被深度混淆无法阅读变量名和函数名被替换控制流被扁平化或加入垃圾代码。尝试使用反混淆工具需谨慎注意安全。更可靠的方法是不追求完全理解每一行而是通过断点调试记录输入到输出的完整数据变换流程用“黑盒测试”的方式复现。5.2 进阶技巧与心得Hook技术辅助定位对于难以直接断点的场景可以在Console中注入Hook代码。例如拦截所有JSON.stringify或XMLHttpRequest.send调用在其中判断参数是否包含sign然后打上调试断点。这需要对JavaScript原型链有一定了解。// 示例Hook XMLHttpRequest的send方法 var oldSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send function(body) { if (body body.indexOf(sign) -1) { debugger; // 自动断点 console.log(捕获到含sign的请求体:, body); } return oldSend.apply(this, arguments); };关注Webpack模块加载器现代前端多用Webpack打包模块代码被包裹在(function(modules) {…})([…])的结构中。找到签名函数后它可能是一个模块的导出。需要找到该模块的编号和导出方式有时需要手动构造一个类似的模块环境来执行函数。动态盐值Token的处理有些签名算法不仅使用固定盐值还会用一个动态的Token这个Token可能来自之前的某个登录接口或初始化接口。这意味着签名不能完全离线计算需要先请求一个接口获取Token再用来计算后续请求的签名。分析时需要关注网络请求的先后顺序和参数传递链条。保持代码更新与维护网站的签名算法不是一成不变的可能会定期更新。因此完全复现的代码最好封装成独立的服务或模块并加入监控和告警机制。当请求大量失败时能第一时间意识到可能是签名算法变了需要重新进行逆向分析。逆向工程是一个需要耐心和细心的过程每一次成功破解都是一次对前端安全机制和JavaScript运行原理的深入理解。它没有一成不变的公式但核心思路——观察、定位、分析、复现、验证——是相通的。希望这次针对电商sign签名的实战演练能为你打开JS逆向的大门在以后遇到类似加密时能够从容应对。记住思路和工具比单纯的代码复制更重要。