资讯动态

JS逆向实战:破解动态网站加密签名,实现数据采集

发布时间:2026/8/8 6:34:49 来源:尧图企业网站定制
1. 项目概述为什么我们需要逆向分析群人员数据最近在做一个数据分析项目需要采集某个社交平台群组的人员信息比如成员昵称、入群时间、发言活跃度等。一开始我像往常一样打开浏览器开发者工具找到网络请求准备用Python的requests库直接模拟请求。结果页面数据是加载出来了但关键的成员列表数据却空空如也只有一个div标签里写着“正在加载中...”。刷新几次发现每次请求的URL都一样但返回的HTML结构里就是没有我想要的数据。这立刻让我意识到我遇到了一个典型的JavaScript动态渲染反爬场景。目标网站并没有在初始的HTML响应中直接嵌入数据而是通过后续的JavaScript代码异步地向服务器发起另一个请求获取到数据后再动态地填充到页面上。对于爬虫来说直接请求初始页面只能拿到一个“空壳”真正的数据藏在后续的、由JavaScript触发的网络请求里。这就是标题中“JavaScript逆向分析”的由来。我们得像个侦探一样去分析前端JavaScript代码是如何工作的找到它真正获取数据的那个“秘密通道”通常是某个特定的API接口并破解这个通道的“准入规则”比如请求参数是如何加密生成的。这个过程就是JS逆向。它不再是简单的“请求-解析”模式而是一场与前端工程师的智力博弈。今天我就把这次采集群人员数据过程中对JavaScript进行逆向分析的全过程记录下来重点分享思路、工具和踩过的坑。2. 逆向分析前的环境与思路准备在动手“开干”之前搭建一个顺手的分析环境和理清分析思路至关重要。这能让你在后续复杂的代码海洋中不至于迷失方向。2.1 核心工具链搭建浏览器与编辑器工欲善其事必先利其器。对于JS逆向以下几样工具是你的“标配”Chrome/Edge 开发者工具这是主力侦察兵。我们主要用到其中的几个面板Network网络这是最重要的面板没有之一。它会记录页面加载过程中所有的网络请求XHR/Fetch、JS、CSS等。你需要在这里找到那个真正携带数据的请求。记得勾选“Preserve log”保留日志并禁用缓存防止请求记录被刷新页面清空。Sources源代码用于查看和调试前端JavaScript文件。你可以在这里给JS代码打上断点让代码执行到特定位置时暂停然后查看当时所有变量的值这是理解代码逻辑的终极手段。Console控制台可以执行任意的JavaScript代码用于测试你从源码中扣出来的函数是否工作正常或者直接调用某些全局函数来获取数据。Application应用可以查看和操作本地存储、Session Storage、Cookies等这些信息常常是请求头或参数的一部分。代码编辑器用于整理和运行我们“扣”出来的JavaScript代码。Visual Studio Code是绝佳选择轻量且插件丰富。你需要安装两个关键插件JavaScript (ES6) code snippets提供代码高亮和智能提示。Code Runner可以让你快速运行选中的JS代码片段方便测试。Node.js环境我们的Python爬虫最终需要能执行JavaScript代码来生成加密参数。虽然Python有PyExecJS或js2py等库但在分析调试阶段一个本地的Node.js环境更加直接和稳定。你可以在Node.js环境中用node命令直接运行你重构的JS文件验证其输出是否正确。注意很多新手会忽略“Preserve log”这个选项导致页面跳转或刷新后关键的XHR请求记录消失了怎么也找不到。这是逆向分析中第一个容易踩的坑。2.2 通用逆向分析思路拆解面对一个看似复杂的动态网站不要慌按照以下步骤一步步推进大部分问题都能找到突破口定位关键请求在目标页面比如群成员列表页打开开发者工具的Network面板刷新页面。在纷繁的请求列表中重点关注类型为XHR或Fetch的请求。通过预览Preview或响应Response标签页查看其返回内容。那个返回格式为JSON、且内容包含成员列表数据的请求就是我们的“目标请求”。记下它的请求URL和请求方法。分析请求参数与载荷点击目标请求查看它的“Headers”和“Payload”如果是POST请求或“Query String Parameters”如果是GET请求。你需要找出哪些参数是固定的哪些是每次请求都会变化的。特别关注那些看起来像一串无规律字符串的参数它们很可能是加密或编码后的结果。搜索参数生成位置这是逆向的核心。在Sources面板中使用全局搜索CtrlShiftF功能搜索上一步中找到的关键参数名例如sign、token、_t等。这能帮你快速定位到生成这个参数的JavaScript代码片段。调试与扣代码找到相关代码后通过打断点、单步执行F10、步入F11等方式理清该参数的生成逻辑。目标是将其生成逻辑通常是一个函数完整地“扣”出来形成一个可以在Node.js或Python环境中独立运行的JS代码块。模拟与集成将扣出的JS代码在Node.js环境中运行测试确保它能生成与浏览器中一致的参数。最后将这个逻辑集成到你的Python爬虫中使用requests库携带所有必要的参数包括这个生成的加密参数去模拟请求从而拿到数据。3. 实战定位并分析群成员API请求现在让我们把上述思路应用到具体的“采集群人员数据”场景中。3.1 初探网络请求识别数据接口打开目标群组的成员列表页面确保开发者工具的Network面板是开启状态。我观察到页面加载后成员列表是一个个逐渐渲染出来的这明显是异步加载。在Network面板中过滤XHR请求我很快发现了一个可疑的请求名称:get_member_list方法:POST状态: 200URL:https://api.example.com/group/member/list(此为示例实际URL不同)点击这个请求查看它的Response果然看到了结构清晰的JSON数据里面包含了成员ID、昵称、头像、角色、入群时间等所有我需要的字段。目标锁定3.2 解密请求载荷寻找加密参数接下来查看这个POST请求的Payload在Chrome中可能显示为“Request Payload”或“Form Data”。我发现它并不是简单的group_id123这样的形式而是类似这样{ gid: 123456, page: 1, size: 20, t: 1648792832000, sign: a1b2c3d4e5f6...很长一串..., nonce: xyz789 }其中gid、page、size这些是业务参数很好理解分别是群ID、页码和每页大小。t看起来是一个13位的时间戳毫秒级这也很常见用于防止请求被缓存或重放。nonce一个随机字符串通常也是防重放攻击用的。sign这个参数最长也最杂乱看起来像是经过某种加密或哈希计算得到的签名。这十有八九就是我们需要逆向的核心。网站服务器会用它来验证请求的合法性如果签名不对即使其他参数正确请求也会被拒绝。3.3 逆向关键全局搜索签名sign在Sources面板使用全局搜索CtrlShiftF功能在所有加载的JS文件中搜索关键词sign。搜索结果可能会有很多条我们需要有策略地筛选优先查看包含“sign”赋值语句的代码比如sign 、data.sign 、params[sign] 这样的行。关注疑似加密函数调用搜索MD5、SHA、Hmac、encrypt、CryptoJS、signature等关键词这些往往是生成签名的函数库或方法。结合请求URL查找有时签名函数就定义在请求发起代码的附近。可以尝试搜索请求URL的一部分如member/list。经过一番搜索和排查我在一个名为app.c7a3b8df.js的经过Webpack打包的JS文件中找到了一段关键代码。通过格式化工具美化代码后我看到了类似下面的结构// 示例代码已简化 function getSign(params) { var key 一个固定的密钥字符串; // 1. 将参数按字典序排序 var sortedParams Object.keys(params).sort().map(function(k) { return k params[k]; }).join(); // 2. 拼接密钥 var stringToSign sortedParams key key; // 3. 进行MD5哈希并转为大写 var sign md5(stringToSign).toUpperCase(); return sign; } // 在发起请求的地方被调用 var requestData { gid: groupId, page: pageNum, size: pageSize, t: Date.now(), nonce: generateNonce() }; requestData.sign getSign(requestData);分析过程我在这段代码的入口getSign函数第一行打上了断点。然后回到页面触发一次成员列表的加载比如翻到下一页。代码果然在断点处暂停了。我逐步执行观察每一步中sortedParams、stringToSign和最终sign的值并与Network面板中实际发送的请求参数进行比对完全吻合。至此签名算法逻辑已经清晰。实操心得面对经过打包压缩的JS文件第一步永远是点击代码面板左下角的{}美化代码按钮。美化后的代码虽然变量名可能还是混淆的如a,b,c但结构清晰便于阅读和打断点。4. 扣取与重构JavaScript签名函数找到了算法下一步就是把它“搬”到我们自己的环境中。这个过程俗称“扣代码”。4.1 完整扣取依赖函数上面的示例代码中用到了一个md5函数和一个generateNonce函数。我们不能只扣getSign必须确保它依赖的所有函数和变量都一并扣取。定位md5函数在同一个JS文件中搜索function md5或者md5 function。很可能找到的是一个现成的MD5算法实现或者是对CryptoJS.MD5的调用。如果是后者我们还需要处理CryptoJS这个外部库。定位generateNonce函数同样搜索这个函数名它可能就是一个生成随机字符串的简单函数。在我的实际案例中md5是网站自己实现的一个函数代码较长但独立。我将其整个函数体复制出来。generateNonce函数则是生成一个8位的随机数字符串。4.2 在Node.js环境中构建独立JS文件新建一个signature.js文件将扣取的所有代码整合进去。关键是要创建一个独立、不依赖浏览器环境的模块。// signature.js - 重构后的独立签名生成模块 // 1. 扣取的MD5函数 (此处为示意实际代码很长) function md5(inputString) { // ... 完整的MD5算法实现 ... return hexResult; // 返回16进制字符串 } // 2. 扣取的生成随机数函数 function generateNonce() { return Math.random().toString(36).substr(2, 8); } // 3. 核心的签名生成函数 function getSign(params) { var key 一个固定的密钥字符串; // 注意这个密钥是硬编码在JS里的 // 参数排序拼接 var sortedKeys Object.keys(params).sort(); var sortedParams sortedKeys.map(function(k) { // 重点这里要根据原代码逻辑判断值是否为对象或数组原代码可能是 params[k]也可能是 JSON.stringify(params[k]) return k params[k]; }).join(); var stringToSign sortedParams key key; var sign md5(stringToSign).toUpperCase(); return sign; } // 4. 导出给Node.js环境使用的主函数 function generateRequestParams(groupId, pageNum, pageSize) { var params { gid: groupId.toString(), page: pageNum.toString(), size: pageSize.toString(), t: Date.now().toString(), nonce: generateNonce() }; params.sign getSign(params); return params; } // 如果是Node.js环境导出模块 if (typeof module ! undefined module.exports) { module.exports generateRequestParams; }4.3 本地测试与验证在终端中使用Node.js运行这个文件进行测试node -e const gen require(./signature.js); console.log(gen(123456, 1, 20));观察输出的sign值。然后回到浏览器的Console面板在页面上下文下手动调用网站的原始getSign函数如果它是全局的或者更简单地直接复制一组浏览器实际发送的参数从Network面板Payload里复制在你的Node.js脚本中写死这些参数gid,page,size,t,nonce运行getSign函数看生成的sign是否与浏览器中的完全一致。必须完全一致一个字符都不能差。如果不一样检查参数排序规则是否正确拼接时用的符号、是否正确密钥key是否正确t和nonce的值格式是否一致是否都是字符串MD5的结果是否需要十六进制hex输出是否需要转为大写5. 集成到Python爬虫并完成数据采集签名算法验证通过后最后一步就是让Python爬虫能够使用这个算法并发起请求。5.1 方案选择PyExecJS vs 直接调用Node让Python执行JS通常有两种主流方案PyExecJS一个Python库可以调用系统安装的JavaScript运行时如Node.js来执行JS代码。优点是使用简单像调用普通函数一样。import execjs # 读取JS文件 with open(signature.js, r, encodingutf-8) as f: js_code f.read() # 编译JS代码 ctx execjs.compile(js_code) # 调用函数 params ctx.call(generateRequestParams, 123456, 1, 20) print(params)直接调用Node.js子进程更底层性能稍好但需要自己处理输入输出。import subprocess import json def get_params_by_node(gid, page, size): # 构造Node.js命令 cmd [node, -e, f const gen require(./signature.js); console.log(JSON.stringify(gen({gid}, {page}, {size}))); ] result subprocess.run(cmd, capture_outputTrue, textTrue, timeout5) if result.returncode 0: return json.loads(result.stdout.strip()) else: raise Exception(fNode.js执行失败: {result.stderr})如何选择对于快速验证和简单项目PyExecJS更便捷。对于性能要求高、调用频繁的生产环境或者JS代码非常复杂、PyExecJS可能兼容性不佳时推荐使用直接调用Node.js子进程的方式。我本次项目就采用了后者因为它更稳定可靠。5.2 构建完整的Python爬虫现在我们可以组装完整的爬虫了。除了签名别忘了模拟必要的请求头Headers特别是User-Agent、Content-Type、Referer等这些可以从浏览器Network面板中直接复制。import requests import json import time from your_node_runner import get_params_by_node # 假设你把调用Node的函数封装在这里 def fetch_group_members(group_id, total_pages): 采集指定群组的成员数据 base_url https://api.example.com/group/member/list session requests.Session() # 设置通用请求头 session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Content-Type: application/json;charsetUTF-8, Referer: https://www.example.com/group/, # 可能还需要Cookie此处省略 }) all_members [] for page in range(1, total_pages 1): print(f正在采集第 {page} 页...) # 1. 调用JS算法生成加密参数 try: request_params get_params_by_node(group_id, page, 20) # 每页20条 except Exception as e: print(f生成第{page}页参数失败: {e}) break # 2. 发送POST请求 try: # 注意根据API要求参数可能在查询字符串也可能在请求体。本例在请求体。 response session.post(base_url, jsonrequest_params, timeout10) response.raise_for_status() # 检查HTTP错误 except requests.exceptions.RequestException as e: print(f请求第{page}页失败: {e}) break # 3. 解析响应 try: data response.json() if data.get(code) 0: # 假设返回码0表示成功 members data.get(data, {}).get(list, []) all_members.extend(members) print(f 获取到 {len(members)} 条成员记录。) else: print(f 接口返回错误: {data.get(message)}) break except json.JSONDecodeError: print(f 解析JSON响应失败。原始响应: {response.text[:200]}) break # 4. 礼貌性延迟避免请求过快 time.sleep(1.5) return all_members if __name__ __main__: members fetch_group_members(123456, 5) # 采集前5页 print(f总共采集到 {len(members)} 条成员数据。) # 可以将数据保存为JSON文件 with open(group_members.json, w, encodingutf-8) as f: json.dump(members, f, ensure_asciiFalse, indent2)5.3 数据清洗与存储拿到原始数据后通常还需要进行清洗和结构化存储。例如时间戳转换为可读日期过滤无效数据等。import pandas as pd from datetime import datetime def clean_member_data(raw_members): cleaned [] for member in raw_members: cleaned.append({ member_id: member.get(uid), nickname: member.get(nickname, ).strip(), role: member.get(role, 成员), # 群主、管理员、成员等 # 将毫秒时间戳转换为 datetime 对象 join_time: datetime.fromtimestamp(int(member.get(join_time, 0)) / 1000) if member.get(join_time) else None, last_active: datetime.fromtimestamp(int(member.get(last_active, 0)) / 1000) if member.get(last_active) else None, }) return pd.DataFrame(cleaned) # 使用 df clean_member_data(members) print(df.head()) # 保存为CSV df.to_csv(group_members_cleaned.csv, indexFalse, encodingutf-8-sig)6. 逆向过程中的常见问题与调试技巧逆向分析很少一帆风顺以下是几个我踩过坑后总结出的核心技巧和常见问题解决方案。6.1 问题排查清单问题现象可能原因排查思路与解决方案生成的sign与浏览器不一致1. 参数顺序错误。2. 参数值类型不一致字符串/数字。3. 密钥(key)错误或遗漏。4. 拼接字符串的格式如空格、换行、编码有差异。5. 依赖的全局变量如window,document,CryptoJS未定义。1.断点比对在浏览器中在签名函数入口和关键步骤打上断点记录每一步生成的中间字符串。在本地Node.js环境中用相同的输入参数运行代码打印出每一步的中间结果进行逐字符比对。2.类型检查确保所有参数在拼接前都转换为字符串且格式与浏览器中完全一致例如时间戳是13位数字的字符串。3.环境补全如果扣出的代码依赖浏览器对象需要在Node.js环境中模拟。例如如果用到CryptoJS需要npm install crypto-js并在JS文件开头引入const CryptoJS require(crypto-js);。Node.js执行JS报错xxx is not defined扣取的JS代码中使用了浏览器或特定环境下的全局变量/函数。1.搜索该变量在原始JS文件中搜索这个变量名看它是如何定义的。它可能是另一个函数也可能是来自某个外部库。2.模拟或替换如果是简单的工具函数如生成随机数可以直接用Node.js原生函数替换。如果是复杂库如加密库需要在Node.js项目中安装对应的npm包并引入。请求返回403 Forbidden或签名无效1. 请求头Headers不完整或不正确。2. Cookie或Token失效/缺失。3. 签名算法已更新但你的代码未同步。1.完整复制Headers从浏览器中复制所有请求头特别是Cookie,Authorization,X-Requested-With,Origin等。2.维护会话使用requests.Session()保持会话自动处理Cookie。3.验证时效性检查签名算法中是否使用了动态变化的值如服务器下发的临时Token。这类值需要先通过一个前置请求获取。代码被重度混淆无法阅读网站使用了Webpack等打包工具并进行了代码混淆变量名替换为a,b,c控制流扁平化等。1.使用反混淆工具尝试使用像de4js这样的在线工具或本地工具进行初步反混淆但效果可能有限。2.重点追踪不要试图理解全部代码。在Network面板找到入口请求在发起请求的代码附近打上“XHR断点”或“事件监听器断点”直接定位到关键函数。3.Hook技术对于更复杂的情况可以学习使用浏览器插件或Frida等工具Hook住关键函数如JSON.parse,encodeURIComponent, 加密函数直接获取输入输出绕过复杂的代码逻辑分析。6.2 高级调试技巧XHR/Fetch断点与“油猴”脚本当搜索关键词找不到或者代码混淆严重时可以尝试更高级的定位方法XHR/Fetch断点在Sources面板的右侧找到“XHR/Fetch Breakpoints”区域。你可以点击“”号添加一个包含部分URL的断点例如包含member/list。这样当任何JavaScript代码发起匹配该URL的请求时执行就会自动暂停并且调用栈会直接指向发起请求的那行代码。这是定位请求发起源头的最有效方法之一。使用“油猴”脚本进行Hook如果你熟悉一点JavaScript可以编写一个简单的Tampermonkey脚本在页面加载时注入用于Hook关键函数。例如Hookwindow.fetch和XMLHttpRequest.prototype.send这样所有网络请求的参数和响应都能被你捕获和修改便于分析。// 示例Hook XMLHttpRequest的send方法 (function() { var originalSend XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.send function(body) { // 在这里打印或修改请求体 console.log(XHR Request Body:, body); console.trace(); // 打印调用栈找到是谁发起的请求 return originalSend.apply(this, arguments); }; })();6.3 关于法律与道德的最终提醒在进行任何网络数据采集尤其是涉及逆向工程时必须时刻保持警惕遵守robots.txt检查目标网站的robots.txt文件看是否明确禁止爬取其相关路径。尊重数据所有权明确你采集的数据用途。用于个人学习、研究分析是相对安全的灰色地带但用于商业盈利、公开传播或任何可能侵犯用户隐私、损害网站利益的用途则风险极高。控制请求频率在代码中务必添加合理的延时如time.sleep避免对目标服务器造成DoS攻击式的压力。过于频繁的请求不仅不道德还可能导致你的IP被永久封禁。查看用户协议仔细阅读网站的用户协议或服务条款其中往往有关于数据抓取的明确规定。敏感数据避让切勿采集个人的身份证号、手机号、详细住址、金融信息等极度敏感的数据。逆向分析是一项强大的技术它帮助我们理解系统如何工作并解决实际问题。但能力越大责任越大。请务必在法律和道德框架内负责任地使用这项技术。本次关于群成员数据采集的逆向分析过程就记录到这里核心是思路和方法的分享具体的目标网站和密钥已做脱敏处理。希望这份详实的记录能为你打开JS逆向的大门。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价