逆向xhs签名参数x-mns与x-s-com:从JS混淆到Python复现的完整实战
1. 项目概述为什么我们要关注xhs的签名参数最近在和一些做数据分析和内容运营的朋友聊天发现大家对一个平台的数据获取都挺头疼的尤其是涉及到一些动态生成的、用于接口验证的签名参数。比如我们今天要聊的这个“x-mns”和“x-s-com”。这两个参数对于需要与目标平台进行自动化、程序化交互的开发者来说就像是一把锁的钥匙。没有它们你发起的请求在服务器看来就是“非法闯入”直接给你一个403或者更复杂的验证挑战。我花了些时间把这两个参数的生成逻辑完整地逆向了出来。这个过程本质上是一场与前端JavaScript代码的“捉迷藏”。平台为了保护其接口会将核心的签名算法隐藏在经过混淆、压缩的JS代码中我们的任务就是在这片“代码丛林”里找到那条生成签名的路径并用我们能理解的语言比如Python将其复现。这不仅是一个技术活更像是一次侦探游戏考验的是耐心、细心和对JavaScript运行机制的理解。接下来我就把这次“探案”的全过程包括踩过的坑、找到的关键代码以及完整的复现方案毫无保留地分享给你。无论你是前端安全研究者、数据分析师还是对爬虫逆向感兴趣的朋友相信都能从中获得直接的帮助。2. 逆向环境准备与核心思路拆解工欲善其事必先利其器。在开始逆向之前搭建一个顺手的调试环境至关重要。这能让你在追踪代码执行流时事半功倍。2.1 工具链选择与配置我主要使用 Chrome DevTools因为它与浏览器内核深度集成对JavaScript的调试支持最为原生和强大。有几个关键设置你需要提前调整开启“停用缓存”在 Network 面板勾选 “Disable cache”。这能确保你每次刷新页面都能获取到最新的、未缓存的JavaScript文件避免调试旧代码。配置格式化代码面对被压缩成一行、变量名被混淆的代码第一步就是“美化”。在 Sources 面板找到混淆的JS文件点击左下角的{}Pretty-print按钮。这会让代码恢复缩进和基本结构虽然变量名还是a、b、c但可读性已大幅提升。使用“搜索”功能格式化后使用Ctrl Shift FWindows/Linux或Cmd Opt FMac开启全局搜索。这是定位关键字符串如 “x-mns”、 “x-s-com”、 “sign”、 “encrypt” 等最直接的方法。注意有些平台会使用“反调试”技巧例如在代码中插入debugger语句或检测控制台是否打开。如果遇到页面无限暂停可以在 DevTools 的设置中尝试禁用“停用断点”或使用条件断点来绕过。2.2 逆向核心思路由外而内顺藤摸瓜逆向这类签名参数一个非常有效的策略是“由外而内”定位请求在 Network 面板中筛选 XHR/Fetch 请求找到携带了x-mns和x-s-com这两个关键请求头的接口。记录下这个请求的完整URL、方法通常是POST、以及所有的请求头和请求体。全局搜索关键参数名在格式化后的所有JS资源中全局搜索x-mns和x-s-com。运气好的话你能直接找到它们被赋值的地方比如headers[x-mns] someFunction()。这是最理想的入口。追踪函数调用栈如果直接搜索不到或者赋值语句是headers[x-mns] t这种非常简短的就需要在请求发起前打上“XHR/Fetch 断点”。在 DevTools 的 Sources - XHR/Fetch Breakpoints 里添加包含接口URL关键词的断点。当请求发起时执行流会自动暂停此时调用栈Call Stack会显示当前函数是如何被一层层调用的。从栈顶往下回溯你就能找到负责构造请求头、生成签名的函数。分析签名函数找到疑似生成签名的函数后重点分析它的参数和返回值。它的输入通常包含请求的URL路径、请求方法GET/POST、请求体或其摘要、时间戳、一个随机数等。输出就是x-mns和x-s-com的值。你需要用调试器跟踪这个函数的每一步计算记录下中间变量的值。我的这次逆向过程就是通过“XHR断点”成功定位到了一个名为sign的内部函数它正是整个签名生成的核心。3. 关键JS代码解析与算法还原找到了核心函数就像拿到了藏宝图的关键碎片。接下来我们需要在调试器中一步步执行理解每一行代码的作用并最终用清晰的逻辑将其还原。3.1 核心sign函数拆解以下是我逆向后经过反混淆和重命名关键变量还原出的核心JavaScript逻辑。为了便于理解我将其分块解析// 假设的 sign 核心函数结构 function generateSign(apiPath, method, requestBody, timestamp, nonce) { // 1. 规范化请求数据 let sortedParams ; if (method GET apiPath.includes(?)) { // 对GET请求的查询参数按字母顺序排序并拼接 let queryPart apiPath.split(?)[1]; let params new URLSearchParams(queryPart); let keys Array.from(params.keys()).sort(); sortedParams keys.map(k ${k}${params.get(k)}).join(); } else if (method POST requestBody) { // 对POST请求体如果是JSON则对JSON对象的键进行排序 // 注意这里取决于具体实现有时是排序后字符串化有时是直接对原始字符串操作 try { let bodyObj JSON.parse(requestBody); let orderedBody {}; Object.keys(bodyObj).sort().forEach(key { orderedBody[key] bodyObj[key]; }); sortedParams JSON.stringify(orderedBody); } catch (e) { // 如果不是JSON可能直接使用原始字符串或进行其他处理 sortedParams requestBody; } } // 2. 构造待签名字符串 // 这是最关键的一步格式通常是固定模板拼接 let stringToSign [ method.toUpperCase(), apiPath.split(?)[0], // 去除查询参数的纯路径 sortedParams, timestamp, nonce ].join(\n); // 注意分隔符可能是\n、|或需根据实际情况确定 // 3. 使用密钥进行HMAC-SHA256签名 // 密钥secret是逆向的难点它可能硬编码在JS中也可能由其他接口动态获取 let secret 硬编码或动态获取的密钥字符串; let hmacDigest CryptoJS.HmacSHA256(stringToSign, secret); let signResult hmacDigest.toString(CryptoJS.enc.Hex); // 输出16进制字符串 // 4. 生成最终的 x-mns 和 x-s-com // x-mns 通常是签名结果本身或者签名结果加其他信息 // x-s-com 可能包含时间戳、随机数、版本号等元信息用特定分隔符连接 let xMns signResult; let xSCom [timestamp, nonce, v1].join(:); // 示例格式 return { x-mns: xMns, x-s-com: xSCom }; }代码逻辑解读数据规范化无论GET还是POST平台都需要一个确定性的数据表示来进行签名防止因参数顺序不同导致签名不一致。所以要对查询参数或请求体JSON的键进行排序。构造待签名字符串将HTTP方法、API路径、规范化后的参数、时间戳、随机数按特定顺序和分隔符拼接成一个字符串。这个拼接规则和分隔符是算法的核心必须完全一致。HMAC-SHA256签名使用一个密钥Secret对上一步的字符串进行HMAC-SHA256运算得到一个二进制摘要并转换为16进制字符串。密钥的获取是逆向的另一大难点它可能被隐藏在代码的某个常量里也可能通过一个复杂的函数计算得出。组装请求头将签名结果和元数据分别放入x-mns和x-s-com。x-s-com的格式也需要精确还原。3.2 如何定位并提取“密钥”密钥是签名的“盐”找不到正确的密钥前面的步骤都白费。在我的逆向案例中密钥是通过一个函数动态生成的而不是简单的字符串。寻找密钥的方法有搜索关键词在JS代码中搜索secret、key、hmac、SHA256等。跟踪函数调用在CryptoJS.HmacSHA256调用处打断点观察第二个参数即密钥的值是什么它来自哪个变量然后回溯这个变量的赋值过程。Hook 关键函数在控制台使用Object.defineProperty或Proxy来Hook住CryptoJS对象或JSON.stringify等方法记录下调用时的参数。例如let _stringify JSON.stringify; JSON.stringify function(...args) { console.log(JSON.stringify called with:, args); debugger; // 可选触发断点 return _stringify.apply(this, args); };这能帮你确认请求体在签名前是否被处理过。在我的实际逆向中发现密钥是通过一个名为getDynamicSecret的函数结合当前页面的某个Cookie值和固定字符串拼接而成。这增加了逆向的复杂度但也是一种常见的安全措施。4. 使用Python完整复现签名流程理解了算法下一步就是用服务端语言这里以Python为例将其复现出来实现脱离浏览器环境的自动化请求。这里假设我们已经知道了密钥是固定的this_is_a_demo_secret实际应用中你需要替换成逆向出的真实密钥。4.1 依赖安装与基础函数首先确保安装了hmac、hashlibPython标准库和requests库。time和random用于生成时间戳和随机数。import hashlib import hmac import json import time import random import string from urllib.parse import urlparse, parse_qs, urlencode def generate_nonce(length16): 生成指定长度的随机字符串作为nonce chars string.ascii_letters string.digits return .join(random.choice(chars) for _ in range(length)) def canonicalize_query_string(url): 规范化GET请求的查询字符串按参数名排序并重新拼接 parsed_url urlparse(url) if not parsed_url.query: return # 解析查询参数 params parse_qs(parsed_url.query, keep_blank_valuesTrue) # 注意parse_qs返回的是 {key: [value1, value2]}需要展平。这里假设参数不重复。 sorted_params {} for key in sorted(params.keys()): # 取第一个值如果有多值情况需根据平台实际处理 sorted_params[key] params[key][0] # 重新编码为规范化的查询字符串 canonical_query urlencode(sorted_params) return canonical_query def canonicalize_request_body(body_str): 规范化POST请求体假设为JSON if not body_str: return try: body_dict json.loads(body_str) # 对JSON对象的键进行排序 sorted_body json.dumps(body_dict, sort_keysTrue, separators(,, :)) return sorted_body except json.JSONDecodeError: # 如果不是JSON原样返回根据实际情况调整 return body_str4.2 核心签名生成函数这是对前面JS逻辑的精确Python翻译。def generate_xhs_sign(api_url, http_method, body_strNone, secretthis_is_a_demo_secret): 生成 x-mns 和 x-s-com 签名 :param api_url: 完整的API请求URL :param http_method: HTTP方法GET 或 POST :param body_str: 请求体原始字符串POST请求时传入 :param secret: HMAC签名使用的密钥 :return: 包含 x-mns 和 x-s-com 的字典 # 1. 生成时间戳和随机数 timestamp str(int(time.time() * 1000)) # 毫秒级时间戳 nonce generate_nonce(16) # 2. 规范化数据 parsed_url urlparse(api_url) api_path parsed_url.path # 获取纯路径部分 canonical_data if http_method.upper() GET: canonical_data canonicalize_query_string(api_url) elif http_method.upper() POST and body_str: canonical_data canonicalize_request_body(body_str) # 其他方法如PUTDELETE根据平台规则处理 # 3. 构造待签名字符串 (关键格式必须与JS端完全一致) # 假设格式为Method\nPath\nCanonicalData\nTimestamp\nNonce string_to_sign \n.join([ http_method.upper(), api_path, canonical_data, timestamp, nonce ]) # 4. 计算HMAC-SHA256签名 # 注意密钥和消息都需要是字节串 secret_bytes secret.encode(utf-8) message_bytes string_to_sign.encode(utf-8) hmac_obj hmac.new(secret_bytes, message_bytes, hashlib.sha256) x_mns hmac_obj.hexdigest() # 获取16进制签名结果 # 5. 组装 x-s-com (假设格式为 timestamp:nonce:version) x_s_com f{timestamp}:{nonce}:v1 return { x-mns: x_mns, x-s-com: x_s_com, timestamp: timestamp, # 有时也需要单独传timestamp nonce: nonce }4.3 实战发起一个带签名的请求现在我们可以用上面的函数来模拟一个真实的API调用。import requests def make_signed_request(): # 示例API请替换为实际目标API api_url https://api.example.com/some/endpoint method POST payload { keyword: 旅行, page: 1, page_size: 20, sort: hot } body_str json.dumps(payload) # 生成签名 secret 逆向获取的真实密钥 # 替换这里 headers generate_xhs_sign(api_url, method, body_str, secret) # 将签名添加到请求头 request_headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Content-Type: application/json, x-mns: headers[x-mns], x-s-com: headers[x-s-com], # 有些接口可能还需要单独传递timestamp和nonce # x-timestamp: headers[timestamp], # x-nonce: headers[nonce] } # 发起请求 if method.upper() POST: resp requests.post(api_url, headersrequest_headers, databody_str) else: resp requests.get(api_url, headersrequest_headers) print(fStatus Code: {resp.status_code}) print(fResponse: {resp.text[:500]}) # 打印前500字符 return resp.json() if __name__ __main__: result make_signed_request()5. 逆向过程中的常见问题与排查技巧逆向不是一帆风顺的你会遇到各种“坑”。下面是我总结的一些典型问题及解决方法。5.1 签名验证失败为什么我的签名不对这是最常见的问题。请按以下清单逐一核对可能原因排查方法解决方案待签名字符串格式错误这是最可能的原因。用调试器在JS中打印出stringToSign的精确值与你的Python代码生成的进行逐字符比对。检查分隔符是\n、密钥错误检查JS中传递给HMAC函数的密钥是否与你使用的一致。在JS签名计算处打条件断点打印密钥值。确保密钥提取正确。如果是动态密钥完整复现其生成逻辑。数据规范化不一致GET参数排序规则POST的JSON是排序键还是原样日期数字等格式是否统一仔细分析JS中的canonicalize或sort逻辑。对于JSON注意JSON.stringify可能默认不排序需要手动处理。编码问题待签名字符串或密钥的编码。JS内部使用UTF-16Python默认UTF-8。尝试在Python中将字符串明确编码为utf-8。对于中文字符确保编解码一致。时间戳/随机数格式时间戳是秒还是毫秒随机数长度和字符集是什么匹配JS中的Date.now()毫秒或Math.floor(Date.now()/1000)秒。随机数用相同算法生成。遗漏了其他签名参数签名是否还依赖其他请求头如User-Agent或固定字符串仔细阅读JS代码看是否有其他变量被拼接进了stringToSign。实操心得建立一个“黄金标准”测试用例。在浏览器中成功发起一次请求记录下该请求的所有输入URL、方法、请求体、时间戳、随机数和输出的x-mns、x-s-com。然后用你的Python代码完全使用这些输入包括时间戳和随机数而不是重新生成看是否能计算出完全相同的签名。如果一致说明你的算法逻辑正确如果不一致就用这个固定输入进行调试能极大缩小问题范围。5.2 如何处理复杂的JS混淆与反调试代码动态加载关键签名函数可能不在初始加载的JS中而是通过eval、Function构造函数或动态插入的script标签加载。在Network面板过滤“JS”类型关注在接口请求前瞬间加载的JS文件。字符串混淆字符串可能被拆散、加密或隐藏在数组中。搜索时尝试搜索字符串的一部分。观察代码中是否有明显的“解密函数”它通常接收一个数字或简单字符串返回一个复杂字符串。环境检测与反调试无限Debugger在Sources面板右侧的断点列表中找到并禁用这些断点。或者在控制台输入Function.prototype.constructor function() {};重写构造函数可能不总是有效。控制台检测可以尝试在页面加载前通过开发者工具的“Overrides”功能用空的函数覆盖console.log或检测函数。最根本的方法使用Node.js Puppeteer或Playwright等无头浏览器环境。在这些环境中可以更自由地注入脚本、Hook函数且不受浏览器开发者工具状态的干扰。你可以让无头浏览器执行到签名步骤然后直接导出关键函数或计算结果。5.3 密钥是动态的怎么办如果密钥不是硬编码而是每次动态计算你需要找到计算逻辑。常见模式有基于Cookie或LocalStoragesecret md5(localStorage.getItem(token) 固定盐值)。从其他接口获取先请求一个getSecret接口返回一个有时效性的密钥。前端代码混淆生成一个非常复杂的函数输入是当前时间、页面URL等输出一个密钥。这种情况逆向难度最大需要耐心跟踪整个函数。应对策略如果动态密钥的生成逻辑过于复杂一个取巧但稳定的方案是直接使用无头浏览器执行整个页面逻辑然后通过Hook或上下文暴露的方式直接获取计算好的密钥或最终的签名值。这样你就不需要完全理解其生成算法。6. 进阶自动化与长期维护策略逆向成功一次不代表一劳永逸。平台会更新算法会变更。如何让你的代码更健壮、更容易维护封装与抽象将签名生成逻辑封装成一个独立的类或模块如XHSSigner。对外只暴露get_headers(url, method, body)这样的简单接口。内部处理所有细节时间戳生成、随机数、密钥管理、算法实现。密钥管理不要将密钥硬编码在代码中。使用配置文件、环境变量或密钥管理服务。如果密钥是动态获取的在这个模块内部实现获取和缓存的逻辑并处理过期刷新。算法变更监控定期健康检查编写一个简单的测试脚本定期用你的签名代码去调用一个简单的接口如获取个人资料。如果连续失败则触发告警。特征监控监控目标网站的主要JS文件哈希值或文件大小是否发生变化。变化可能意味着前端代码更新。准备降级方案如果自动签名失效是否有备用方案如手动更新密钥、切换其他接口考虑使用无头浏览器作为后备对于签名逻辑极其复杂且频繁变更的情况可以准备一个基于Puppeteer的“签名服务”。当纯算法签名失败时自动回退到启动一个无头浏览器实例让浏览器去完成整个签名过程然后提取结果。这牺牲了性能但保证了最高的成功率。逆向工程是与平台防御机制的持续博弈。核心是理解HTTP协议、JavaScript语言特性和常见的加密哈希算法。保持耐心善用工具从一次成功的逆向中总结出方法论你就能应对更多的挑战。这次对x-mns和x-s-com的逆向其思路和方法同样适用于分析其他App或网站的签名参数希望这份详细的记录能成为你工具箱里的一件利器。