实战解析如何逆向破解FastMoss电商网站店铺排名的fm-sign加密参数最近在分析一些电商数据平台时我发现FastMoss的店铺排名接口数据非常有价值无论是用于市场分析还是竞品研究。但和许多现代Web应用一样它的API接口被一层加密参数保护着这个参数就是fm-sign。对于需要自动化获取数据的中高级开发者或爬虫工程师来说直接请求会返回错误必须破解这层加密逻辑才能拿到数据。今天我就把自己逆向分析fm-sign参数的完整过程、踩过的坑以及最终实现的解决方案毫无保留地分享出来。整个过程不涉及任何敏感操作纯粹是前端JavaScript代码的逻辑分析与复现旨在帮助大家理解现代Web应用常见的参数加密与验证机制。1. 目标确立与初步侦察在开始任何逆向工程之前明确目标和进行初步侦察是至关重要的第一步。盲目地扎进代码里往往事倍功半。1.1 明确分析目标与接口我的目标是获取FastMoss网站上TikTok相关店铺的排名列表数据。通过浏览器开发者工具F12的“网络”Network面板可以清晰地看到页面加载时发出的所有请求。经过筛选我很快定位到了核心的数据接口https://www.fastmoss.com/api/shop/shopList/这个接口是一个GET请求但它携带了一系列参数其中就包括我们的目标——fm-sign。此外还有_time时间戳、cnonce随机数、page、pagesize等常见参数。初步判断fm-sign很可能是一个基于这些请求参数计算出来的签名用于防止请求被篡改或重放。提示在分析网络请求时重点关注XHR和Fetch类型的请求数据接口通常位于其中。使用请求的Copy as cURL功能可以快速获取请求的所有细节方便后续在Python或其他环境中进行测试复现。1.2 排除干扰锁定加密点拿到cURL命令后我第一时间将其转化为Python的requests代码进行测试。最初的怀疑对象有两个一是Cookie中可能包含认证或签名信息二是那个显眼的fm-sign请求头。为了验证我设计了一个简单的排除实验import requests headers { fm-sign: 一个示例签名值, user-agent: Mozilla/5.0..., # 其他必要头部... } params { page: 1, pagesize: 10, _time: 1727184797, cnonce: 57869802, # 其他参数... } # 测试1携带Cookie和fm-sign # response requests.get(url, headersheaders, paramsparams) # 测试2将Cookie设为None仅保留fm-sign response requests.get(url, headersheaders, cookiesNone, paramsparams) print(response.json())当我把cookies明确设置为None后请求依然成功返回了数据。这个结果清晰地表明服务端验证的核心是fm-sign而非Cookie。我们的逆向目标就此锁定。2. 深入前端定位加密函数既然确定了fm-sign是关键下一步就是找出它在浏览器中是如何被计算出来的。这需要我们从网络请求的“战场”转移到JavaScript代码的“源码战场”。2.1 全局搜索与断点调试在现代浏览器的开发者工具中“源代码”Sources面板是我们的主战场。我使用了以下几种策略来定位加密代码关键词搜索在源代码文件中全局搜索fm-sign。这是最直接的方法通常能快速找到设置该请求头的地方。XHR/Fetch断点在开发者工具的“调试器”中可以设置“XHR/fetch断点”指定当请求URL包含shopList时自动暂停。这样代码会在发起请求的那一刻中断方便我们回溯调用栈。事件监听器断点有时加密发生在请求发起前的事件回调中可以尝试在“事件监听器断点”中勾选XHR相关事件。通过搜索fm-sign我很快找到了类似下面的代码片段p m.encryptParams({...d}, h); i[fm-sign] p;这行代码就是加密的入口将一些参数d和一个可能额外的值h传入encryptParams函数计算结果赋值给fm-sign。2.2 关键函数提取与初步分析我点击进入了这个encryptParams函数其核心结构如下function encryptParams(e) { let t arguments.length 1 void 0 ! arguments[1] ? arguments[1] : , n window.Object.keys(e).sort(), o ; n.forEach(t { o t e[t] this.salt }); let r d()(o t).toString(), a , i 0, l r.length - 1; for (; i r.length !(i l); i, l--) a (window.parseInt(r[i], 16) ^ window.parseInt(r[l], 16)).toString(16); return a r.substring(i) }即使不做深入分析也能看出这个函数的几个关键操作对传入的对象e的键进行排序。按“键值salt”的格式拼接成一个字符串。注意这里的this.salt它是一个关键的盐值需要找到它的定义。将拼接后的字符串传入一个名为d的函数进行计算得到一个结果r。对r这个字符串进行一种“首尾异或”的变换。返回最终结果。至此加密逻辑的轮廓已经清晰。接下来需要解决两个问题salt的值是什么函数d()到底做了什么3. 算法拆解与本地复现将浏览器中的代码剥离出来在本地Node.js或JavaScript环境中复现是验证我们理解是否正确的最佳方式。3.1 定位盐值Salt与依赖函数在浏览器中我顺着encryptParams函数的上下文查找很快在更外层的代码或同一个对象中找到了salt的定义它通常是一个固定的字符串例如this.salt asjdfoaur3ur829322; // 示例实际值需在网站代码中确认这个盐值用于增加签名的复杂性防止简单的参数拼接被猜测。接下来是函数d()。在源代码中搜索function d或d 发现它通常被赋值为一个来自某个大型库或内部工具函数的结果。直接执行提取的代码会报错ReferenceError: d is not defined。这说明d是一个外部依赖。3.2 推断并验证哈希算法观察d()(o t).toString()的用法这非常像常见哈希函数如MD5、SHA1的调用方式MD5(string).toString()。输出结果r是一个32位的十六进制字符串这强烈暗示它就是MD5哈希值。为了验证我在浏览器的控制台和本地同时进行测试浏览器控制台在加密逻辑执行到r被赋值后手动输入r查看其值并用d()(1).toString()计算字符串“1”的哈希。本地Node.js环境使用标准的crypto-js库计算CryptoJS.MD5(1).toString()。我得到了如下对比结果测试字符串浏览器中d()(str).toString()结果本地CryptoJS.MD5(str).toString()结果是否一致1c4ca4238a0b923820dcc509a6f75849bc4ca4238a0b923820dcc509a6f75849b是test098f6bcd4621d373cade4e832627b4f6098f6bcd4621d373cade4e832627b4f6是两次结果完全一致这确凿地证明了d()就是标准的、未经过魔改的MD5哈希函数。网站可能使用了CryptoJS、或者自己实现的标准MD5但算法本身是通用的。3.3 完整算法逻辑还原现在我们可以完全还原fm-sign的生成算法了。整个过程可以总结为以下步骤参数准备收集所有需要发送的查询参数如page, pagesize, _time, cnonce等构成一个对象params。键排序与拼接将params对象的所有键key按字母顺序排序。遍历排序后的键按键名 键值 盐值(salt)的顺序拼接成一个长字符串。如果encryptParams函数有第二个参数代码中的t则将其追加到拼接字符串的末尾。计算MD5对上述拼接得到的字符串计算标准的MD5哈希值得到一个32位的十六进制字符串记为md5_str。首尾异或变换初始化一个空字符串result_prefix。设置两个指针i指向md5_str的开头l指向末尾。循环条件为i l。在每次循环中将md5_str[i]和md5_str[l]分别从十六进制字符转为十进制数字然后进行按位异或XOR运算再将结果转回十六进制字符追加到result_prefix。i向后移动一位l向前移动一位。生成最终签名最终fm-sign的值是result_prefix md5_str.substring(i)。由于循环在i l时停止md5_str.substring(i)取的是变换未处理的后半部分或中间字符。这个“首尾异或”的操作是一种简单的混淆可能旨在让签名看起来不那么像直接的MD5。4. 代码实现与实战应用理论分析完毕接下来就是将其转化为可用的代码。我将分别提供JavaScriptNode.js和Python两种语言的实现方便不同技术栈的开发者使用。4.1 JavaScript/Node.js 完整实现在Node.js环境中我们需要安装crypto-js库npm install crypto-js。// fm_sign_generator.js const CryptoJS require(crypto-js); // 配置盐值必须与目标网站一致 const SALT asjdfoaur3ur829322; // 请替换为实际找到的盐值 /** * 生成FastMoss店铺列表接口的fm-sign参数 * param {Object} params - 请求的查询参数对象 * param {string} [extraStr] - encryptParams函数的第二个参数通常为空字符串 * return {string} 计算得到的fm-sign值 */ function generateFmSign(params, extraStr ) { // 1. 对参数键进行排序 const sortedKeys Object.keys(params).sort(); // 2. 拼接键值对和盐值 let concatenatedString ; sortedKeys.forEach(key { concatenatedString key params[key] SALT; }); concatenatedString extraStr; // 3. 计算MD5 const md5Hash CryptoJS.MD5(concatenatedString).toString(); // 4. 首尾异或变换 let prefix ; let i 0; let l md5Hash.length - 1; while (i l) { const frontHex parseInt(md5Hash[i], 16); const backHex parseInt(md5Hash[l], 16); prefix (frontHex ^ backHex).toString(16); i; l--; } // 5. 组合最终签名 const fmSign prefix md5Hash.substring(i); return fmSign; } // 实战测试 const testParams { page: 1, pagesize: 10, order: 1,2, region: US, _time: Math.floor(Date.now() / 1000), // 使用当前时间戳 cnonce: Math.floor(Math.random() * 100000000) // 生成随机数 }; console.log(生成的fm-sign:, generateFmSign(testParams)); console.log(测试参数:, testParams);4.2 Python 完整实现Python的实现逻辑完全一致我们使用hashlib库进行MD5计算。# fm_sign_generator.py import hashlib import time import random # 配置盐值必须与目标网站一致 SALT basjdfoaur3ur829322 # 注意在Python3中hashlib需要bytes类型 def generate_fm_sign(params, extra_str): 生成FastMoss店铺列表接口的fm-sign参数 :param params: dict, 请求的查询参数字典 :param extra_str: str, encryptParams函数的第二个参数通常为空字符串 :return: str, 计算得到的fm-sign值 # 1. 对参数键进行排序 sorted_keys sorted(params.keys()) # 2. 拼接键值对和盐值 concatenated_string for key in sorted_keys: # 确保所有值都转换为字符串进行拼接 concatenated_string str(key) str(params[key]) SALT.decode(utf-8) concatenated_string extra_str # 3. 计算MD5 md5_hash hashlib.md5(concatenated_string.encode(utf-8)).hexdigest() # 4. 首尾异或变换 prefix i 0 l len(md5_hash) - 1 while i l: front_hex int(md5_hash[i], 16) back_hex int(md5_hash[l], 16) prefix hex(front_hex ^ back_hex)[2:] # hex()返回0xf取[2:]得到f i 1 l - 1 # 5. 组合最终签名 fm_sign prefix md5_hash[i:] return fm_sign # 实战测试与请求 if __name__ __main__: import requests # 构造请求参数 test_params { page: 1, pagesize: 10, order: 1,2, region: US, _time: int(time.time()), # 当前Unix时间戳 cnonce: random.randint(10000000, 99999999) # 8位随机数 } # 生成签名 fm_sign_value generate_fm_sign(test_params) print(f生成的 fm-sign: {fm_sign_value}) print(f请求参数: {test_params}) # 组装请求头 headers { fm-sign: fm_sign_value, user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, referer: https://www.fastmoss.com/shop-marketing/tiktok, # 根据实际情况添加其他必要头部如lang, region等 } url https://www.fastmoss.com/api/shop/shopList/ # 发送请求 try: response requests.get(url, headersheaders, paramstest_params, timeout10) response.raise_for_status() # 检查HTTP错误 data response.json() print(请求成功) print(f返回数据条数: {len(data.get(data, [])) if isinstance(data, dict) else N/A}) # 此处可以进一步处理data except requests.exceptions.RequestException as e: print(f请求失败: {e}) except ValueError as e: print(f解析JSON响应失败: {e})4.3 关键细节与调试技巧在实现和调试过程中以下几个细节至关重要盐值Salt的准确性这是签名能否成功的第一关。务必从网站源代码中准确提取一个字符的错误都会导致签名无效。可以通过在浏览器加密函数中console.log(this.salt)来确认。参数顺序与类型JavaScript对象的Object.keys().sort()排序是基于字符串的默认字典序。确保你的实现以完全相同的方式排序。同时参数值在拼接前必须转换为字符串数字1和字符串1拼接结果不同。时间戳_time与随机数cnonce这两个是典型的防重放攻击参数。_time通常是当前的Unix时间戳秒级服务器会校验其有效性如允许一定时间误差。cnonce是一个随机数增加每次请求签名的唯一性。编码问题在Python中确保拼接的字符串在计算MD5前正确编码为UTF-8字节。hashlib.md5()接受的是bytes对象。验证方法最直接的验证方式是运行你的生成代码将得到的fm-sign与浏览器开发者工具中捕获的同参数请求的fm-sign进行比对。完全一致则说明算法复现成功。5. 扩展思考与安全实践成功逆向一个签名算法固然有成就感但更重要的是从中学习并应用到更广泛的场景和更安全的自身开发实践中。5.1 此类签名机制的常见变体FastMoss使用的是一种相对经典的“参数排序盐值哈希简单混淆”的签名方案。在实际中你可能会遇到更复杂的变体变体类型描述可能遇到的例子哈希算法不同使用SHA256、SHA1、HMAC等替代MD5。CryptoJS.HmacSHA256(message, secret).toString()拼接方式复杂化加入请求方法、请求路径、特定分隔符。GET/api/datak1v1k2v2[salt]混淆/加密增强对哈希结果进行Base64、AES加密等二次处理。btoa(md5Str)或CryptoJS.AES.encrypt(md5Str, key)动态盐值/密钥盐值不是固定字符串可能来自其他接口或随时间变化。需要先请求一个/getToken接口获取临时密钥。包含请求体对于POST请求请求体JSON或FormData也需要参与签名计算。将JSON字符串序列化后拼接或单独哈希。遇到这些情况时逆向的思路是相通的定位入口 - 提取逻辑 - 分析依赖 - 本地复现 - 对比验证。只是每一步可能需要更耐心的调试和更广泛的知识储备如各种加密算法。5.2 逆向工程中的道德与法律边界作为一名开发者我们必须清醒地认识到逆向工程的边界目的正当性我们的活动应仅限于技术学习、安全研究、兼容性开发或获取已公开但被接口限制的自身数据。用于个人学习或提升自动化工作效率通常是可接受的。尊重服务条款明确违反网站robots.txt协议或用户服务条款的行为存在风险。避免滥用严禁对目标网站进行高频、并发请求这等同于DDoS攻击会严重影响对方服务器正常运行并可能引发法律问题。务必在代码中设置合理的延迟如time.sleep。数据使用限制获取的数据应谨慎使用不得用于侵犯他人隐私、不正当竞争或任何非法活动。注意本文所有技术讨论仅用于教育目的旨在帮助开发者理解Web安全机制。在实际应用中请务必遵守目标网站的相关规定并承担起合理使用的责任。5.3 从攻击到防御设计更健壮的API签名站在防御者角度研究如何破解签名恰恰能帮助我们设计出更难被逆向的API保护机制。如果你需要为自己的服务设计签名可以考虑以下几点使用非对称加密客户端使用私钥签名服务器用公钥验签。即使算法公开没有私钥也无法伪造。这是最安全但实现较复杂的方式。引入时间窗与一次性令牌严格校验时间戳并让随机数nonce在时间窗内唯一服务器缓存已使用的nonce有效防止重放。关键逻辑放在后端将部分签名计算步骤放在后端前端通过另一个接口获取临时密钥或部分计算结果增加逆向链路的长度和难度。代码混淆与反调试对前端JavaScript进行高强度混淆、压缩并增加反调试代码如检测开发者工具是否打开增加静态分析和动态调试的成本。风控系统联动签名验证不应是唯一防线。结合IP频率、用户行为序列、设备指纹等风控手段对异常请求进行拦截。逆向fm-sign的过程是一次典型的Web前端安全攻防演练。它不仅仅关乎一个参数的计算更涉及对网络协议、JavaScript运行机制、加密算法和系统设计思维的深入理解。我在最后测试时发现成功获取数据的那一刻感觉之前所有在调试器里逐行跟踪、在控制台反复验证的付出都是值得的。记住核心思路永远是“观察 - 假设 - 验证”而耐心和细致是完成这类任务最重要的工具。