公司动态

JS逆向实战:破解动态Cookie反爬与补环境全流程

📅 2026/8/30 17:17:36
JS逆向实战:破解动态Cookie反爬与补环境全流程
说实话干逆向这行五年多遇到过的反爬也算不少。UA检测、字体反爬、加速乐、Akamai、动态Token多少都有接触。但最近在维护一个数据采集任务时被某个动漫资源站的动态Cookie脚本狠狠上了一课。请求接口时参数全对却依然返回403浏览器里却一切正常。反复对比才发现问题出在一个叫chameleon的反爬JS脚本上。这篇文章就把这次JS逆向的过程完整拆解一遍包含抓包定位、断点调试、补环境执行和常见坑点希望能给正在搞JS逆向和反爬实战的读者一点帮助。1. 这次踩坑到底坑在哪动态Cookie与chameleon1.1 樱花动漫反爬为什么会让人头大很多动漫资源站为了防止资源被批量采集都会在服务端做一系列校验。最常见的是校验 User-Agent、Referer、Cookie但大部分都是静态校验只要把浏览器里的请求头复制过来就能通过。这次遇到的樱花动漫反爬恶心之处在于它不仅仅是校验 Cookie而是校验一个动态生成的 Cookie 字段。这个字段由前端JS动态生成带有时间戳和签名而且和浏览器环境指纹绑定。也就是说哪怕你把浏览器里的 Cookie 原封不动复制到爬虫里过一段时间或者换一个IP它就会失效。我第一次用 Python requests 直接请求详情页时返回的是 403。后来用 Selenium 模拟浏览器才看到正常请求里多了一个chameleon字段的 Cookie。顺着这个字段去 Sources 里全局搜索才找到那段反爬JS。1.2 JS逆向的核心目标这里先明确一个概念JS逆向并不是“破解某个网站”这么狭隘。它的核心目标是理解前端JS中与数据生成、加密、签名相关的逻辑并在自己的程序中还原或复用这段逻辑。在爬虫场景下我们需要回答三个问题某个请求参数或 Cookie 是如何生成的它依赖了哪些浏览器环境变量window、document、navigator它在什么时候失效服务端如何校验樱花动漫的chameleon脚本做的就是一件事生成一个带有环境指纹和时间戳的签名 Cookie。只要能复现它的生成逻辑就能绕过第一层反爬。2. 反爬机制全景四道常见关卡在细讲实战之前先把动漫资源站常见的反爬机制梳理一遍。这样后面定位问题时心里会比较有数。关卡检测点常见绕过方式第1关User-Agent、Referer、Origin构造完整浏览器请求头第2关动态Cookie/Token执行JS生成合法Cookie第3关字体反爬 / 文本加密解析字体映射表第4关浏览器指纹与行为检测补环境 / 浏览器自动化2.1 第一关常规请求头检测这类检测最简单服务端只检查请求头里的 User-Agent 是否包含“Mozilla”Referer 是否来自本站Origin 是否正确。很多新手写爬虫时只带一个默认的python-requestsUA自然会被拦截。这一关不需要逆向直接把浏览器里的完整请求头复制过来即可。但需要注意的是部分站点会校验 UA 和 Cookie 中的其他字段是否能够对上比如 Cookie 里有一个device_id它的生成方式可能是 UA 的哈希。这种情况下单纯复制请求头就不够了。2.2 第二关动态Cookie与Token校验这是樱花动漫这次遇到的核心关卡。服务端无法仅凭静态请求头判断请求是否来自真实浏览器所以在前端JS里动态生成一个签名 Cookie。真实浏览器访问时会自动执行这段JS生成 Cookie 后随请求发送给服务端。爬虫如果不执行这段JS拿不到合法的 Cookie就会被识别为异常请求。动态 Cookie 通常有两个特点带时间戳有效期很短通常只有几十秒到几分钟。内容由算法生成可能包含环境指纹、随机数、盐值等。2.3 第三关字体反爬与内容混淆有些站点会把页面的数字、文字渲染成自定义字体页面源码里看到的是乱码或 Unicode 私有区字符必须下载 woff 字体文件解析字形映射后才能还原真实文本。这一招对搜索引擎和爬虫都有一定的干扰效果。但樱花动漫的主体内容是视频播放页并不依赖大量文字展示所以字体反爬在这个场景下不是主力。不过这并不代表其他资源站不会用遇到文本类采集时仍然需要留意。2.4 第四关浏览器指纹与行为检测这是现在比较高级的反爬手段。JS脚本在页面加载时采集 Canvas 指纹、WebGL 信息、时区、语言、屏幕分辨率、插件列表等环境信息然后打包发送给服务端。如果服务端发现某个环境指纹的请求频率异常就会直接拒绝。更狠的是这类检测会和动态 Cookie 绑定。比如chameleon的签名算法里就掺杂了 Canvas 指纹如果直接用 Node.js 执行 JS 而没有补环境生成的指纹和真实浏览器不一致服务端依然能识别。3. 环境准备与工具清单在开始分析之前先把环境准备好。本文示例以常规开发环境为例具体版本需要根据你的项目实际情况调整。3.1 开发环境操作系统Windows 10/11、macOS、Linux 均可本文命令以 Windows 和 macOS 通用写法为主。Python3.8 及以上用于写最终的请求脚本。Node.js建议 16 及以上因为部分JS脚本使用了较新的 ES6 语法。Chrome 浏览器用于抓包和调试。Chrome DevTools内置无需安装。3.2 工具选择抓包工具优先使用 Chrome DevTools 的 Network 面板其次可以使用 Fiddler 或 Charles 来抓取 HTTPS 流量。全局搜索DevTools 里的CtrlShiftF可以在所有加载的JS文件中搜索关键字非常关键。JS运行环境Node.js 配合vm模块来补环境或者直接用 Python 里的PyExecJS调用系统 Node.js。3.3 安装依赖Python 侧需要安装 requests 和 PyExecJS。PyExecJS 只是一个调度器它本身不包含 JS 引擎需要系统里有 Node.js 才能工作。pip install requests PyExecJS安装完成后可以用下面这段代码验证 JS 引擎是否可用import execjs print(execjs.get().name)如果输出Node.js (V8)说明 PyExecJS 已经能够使用 Node.js 运行时。如果输出的是其他内容优先检查 Node.js 是否安装成功。4. 从复现问题到定位chameleon完整实战4.1 第一步先用普通请求复现403先写一个最普通的 Python 请求带上了浏览器 UA模拟访问一个播放详情页。import requests url https://example.com/detail/12345.html headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://example.com/ } resp requests.get(url, headersheaders) print(resp.status_code) print(resp.text[:500])预期结果状态码 403响应体里可能包含“访问异常”“校验失败”等提示或者直接返回空白页。4.2 第二步抓包对比找出差异点打开 Chrome DevTools清空 Network 面板刷新页面找到详情页对应的文档请求。对比浏览器请求和 Python 请求最明显的差异有两个Cookie 字段。请求头里额外的自定义 Header。对于樱花动漫这次的情况差异集中在 Cookie。浏览器请求中多了一个类似chameleonxxx的字段而 Python 请求没有。为了确认这个 Cookie 是动态生成的可以连续刷新两次页面对比它的值是否变化。如果每次都不一样说明它由前端JS实时计算生成。4.3 第三步搜索可疑参数在 DevTools 里按CtrlShiftF全局搜索chameleon。正常情况下搜索结果会指向某个JS文件并且能看到类似这样的代码document.cookie chameleon getChameleonSign();如果直接搜索字段名搜不到可以尝试搜索以下关键词setCookiecookie signtoken搜索到之后点击进入对应JS文件定位到函数调用处。4.4 第四步分析chameleon.js的签名逻辑真实站点的chameleon.js通常会做混淆和压缩直接读源码会很痛苦。这里把它简化成一个便于理解的版本展示它的核心逻辑。// chameleon.js —— 简化教学版仅用于说明签名逻辑 var chameleon (function () { function getCanvasFingerprint() { var canvas document.createElement(canvas); var ctx canvas.getContext(2d); ctx.textBaseline top; ctx.font 14px Arial; ctx.fillText(chameleon, 2, 2); return canvas.toDataURL().length; } function getTimestamp() { return new Date().getTime(); } function sign() { var fp getCanvasFingerprint(); var t getTimestamp(); var raw fp | t | secret_salt; return t _ md5(raw); } return { sign: sign }; })(); document.cookie chameleon chameleon.sign();真实的签名函数会比这个复杂但核心结构差不多采集环境指纹。拼接时间戳和盐值。做哈希或加密。写入 Cookie。secret_salt是写死在JS里的盐值实际项目中可能还会从服务端接口动态获取。4.5 第五步Hook document.cookie 快速定位如果脚本做了混淆直接读代码会非常累可以先用 Hook 的方式定位写入 Cookie 的位置。在页面加载前注入下面的代码当脚本尝试设置 cookie 时自动触发断点。Object.defineProperty(document, cookie, { set: function (val) { debugger; console.trace([Cookie Hook], val); return val; } });操作方式在 DevTools 的 Sources 面板里找到 Snippets。新建一个 Snippet粘贴上面的代码。在页面加载之前运行这个 Snippet。刷新页面。如果脚本执行了document.cookie ...就会触发断点并且 Console 会打印调用堆栈。这样就能定位到具体是哪一行代码写入了 Cookie。4.6 第六步在Node.js中补环境执行拿到签名函数后最直接的方式是把它复制到本地在 Node.js 中执行。但由于脚本依赖document、canvas、navigator等浏览器对象直接执行会报错。这时候需要补环境。补环境的思路是在 Node.js 的vm模块中构造一个假的浏览器环境对象让脚本认为自己在浏览器里运行。// local_env.js const fs require(fs); const vm require(vm); const crypto require(crypto); // 简单实现一个 md5实际项目中如果脚本里自带 md5则不需要这个 function md5(str) { return crypto.createHash(md5).update(str).digest(hex); } // 模拟浏览器环境 const sandbox { md5: md5, navigator: { userAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, language: zh-CN, platform: Win32 }, document: { createElement: function () { return { getContext: function () { return { textBaseline: , font: , fillText: function () {} }; }, toDataURL: function () { return data:image/png;base64,xxxxxx; } }; } }, Date: Date, console: console, setTimeout: setTimeout }; sandbox.window sandbox; const code fs.readFileSync(./chameleon.js, utf-8); vm.createContext(sandbox); vm.runInContext(code, sandbox); console.log(生成的Cookie:, sandbox.document.cookie);这段代码的关键点在于必须把脚本依赖的全局对象都放到sandbox里。window指向 sandbox 自身。如果脚本还依赖location、screen、localStorage等对象也需要一并补充。补环境是一个试错的过程。每次运行报错就去查看缺少哪个变量然后补上。缺什么补什么。4.7 第七步在Python中调用并验证如果不想用 Node.js 脚本也可以把chameleon.js和补环境代码整合成一个完整的 JS 文件然后用 PyExecJS 调用。把补环境代码和业务代码写进同一个文件后在 Python 中执行import execjs import requests # 编译JS文件 with open(chameleon_full.js, encodingutf-8) as f: js_content f.read() ctx execjs.compile(js_content) result ctx.eval(chameleon.sign()) print(签名结果:, result) # 使用签名结果请求目标页面 session requests.Session() session.headers.update({ User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://example.com/ }) session.cookies.set(chameleon, result, domainexample.com) resp session.get(https://example.com/detail/12345.html) print(resp.status_code)如果一切顺利状态码会从 403 变成 200。如果还是 403说明签名中依赖的环境指纹没有被完整还原需要回到第 4.6 节继续补环境。5. 更恶心的反调试无限Debugger与混淆樱花动漫的chameleon脚本不仅做了签名还自带反调试机制。打开 DevTools 后会一直停在 debugger 断点或者页面进入死循环这其实是JS反调试的常见套路。5.1 无限Debugger的绕过无限 Debugger 的实现方式很多常见的是在代码里循环调用debugger指令setInterval(function () { debugger; }, 1000);手动点击“继续执行”之后下一次断点又会出现非常烦人。比较快的解决方法是在 Sources 面板中找到对应的JS脚本。右键点击断点所在行号选择 “Never pause here”。如果脚本被压缩成一行先格式化再定位到debugger关键字所在行。另外也可以在脚本执行前在 Console 里输入下面的代码禁用debugger的自动断点效果。注意这只是一种思路不保证对所有站点生效window._debuggerEnabled false;更稳妥的做法是使用 Fiddler 或 Charles 的响应替换功能把JS文件里的debugger关键字替换成空字符从源头去掉反调试代码。5.2 反格式化检测有些脚本检测到代码被格式化后会改变行为。因为格式化会在代码里插入大量换行和缩进脚本通过判断自身源码是否包含特定数量的换行符来识别。应对方式有两种不直接格式化而是用 AST 工具对变量名和逻辑做还原。在本地运行时把脚本源码保存为文件利用 Node.js 的vm模块执行而不是依赖浏览器。由于这种检测只影响浏览器调试场景在 Node.js 补环境脚本中基本不会触发。5.3 使用AST还原代码如果脚本做了变量名混淆比如把函数名改成_0x3f2a这种形式可以借助 AST 工具对代码进行还原。常用的工具是babel通过解析、遍历、替换节点把混淆后的代码转换成可读性更高的代码。例如可以写一个简单的 Babel 插件来替换FunctionExpression的默认名称。但在实际逆向中AST 还原需要结合具体混淆器来定制规则很难给出一个通用的完整脚本。对于大多数场景如果只是想获取签名结果其实不需要完全还原代码。能通过补环境运行成功就已经足够。6. 常见问题与排查思路问题现象常见原因解决思路补环境后生成结果和浏览器不一致缺少某个环境变量或补的指纹值不对打印浏览器和Node中的关键变量逐一对比使用签名Cookie后仍然403签名过期或Cookie与IP/UA绑定使用同一个Session保持IP和UA不变PyExecJS执行JS时崩溃Node版本过低或JS使用了ES6新特性升级Node或改用Node子进程执行站点提示“请求被篡改”时间戳或签名参数被服务端二次校验检查服务器时间和本机时间是否一致反爬JS频繁更新目标站点识别到异常请求或逆向行为将JS文件按版本保存更新后快速替换6.1 补环境结果不一致怎么办这种情况最常出现在 Canvas 指纹或 WebGL 指纹相关逻辑中。因为浏览器渲染的 Canvas 数据和 Node.js 里模拟的全然不同。解决方法是在浏览器中执行一次签名函数记录输出结果。在 Node.js 中执行同样函数对比中间变量。如果差别来自 Canvas就模拟成和浏览器一致的数据。但这里有一个前提如果服务端存储了浏览器端上报的真实指纹单纯靠伪造数据很难通过验证。遇到这种情况建议改用浏览器自动化方案在真实页面里执行JS获取结果。6.2 403问题仍然存在怎么办如果签名和 Cookie 都正确仍然 403需要检查几个点请求头是否完整尤其是 Accept、Accept-Language、Sec-Fetch-* 等字段。Cookie 是否带有其他字段比如Hm_lvt_xxx、UM_distinctid等。请求频率是否过高触发服务端频控。此时可以先用浏览器手动访问一次把完整请求头和 Cookie 复制到 Python 里如果能通说明只是缺字段如果还是 403说明存在更深的指纹检测。7. 最佳实践与工程建议7.1 合规与风险控制逆向和反爬本身是攻防技术但使用范围要注意。用于学习JS语法、研究加密算法是没问题的但不能用于批量抓取版权内容、用户隐私数据也不能给目标服务器造成过大压力。在实际项目中建议遵守目标网站的 robots 协议。控制请求频率避免高并发抓取。不传播完整破解代码尤其是涉及版权内容的逆向脚本。7.2 工程化与版本管理动态 Cookie 脚本是会变的。目标站点只要更新一次chameleon.js之前的脚本就可能失效。为了降低维护成本建议把逆向脚本纳入版本管理例如js/ chameleon/ v1.0/chameleon.js v1.0/local_env.js v1.0/chameleon_full.js v1.1/chameleon.js v1.1/local_env.js这样当旧版本失效时可以快速切换。7.3 频率控制与异常处理爬虫请求不要写成一梭子全跑完建议增加随机延时和指数退避。import time import random def get_with_retry(session, url, max_retry3): for i in range(max_retry): resp session.get(url) if resp.status_code 200: return resp time.sleep(random.uniform(1, 3) * (2 ** i)) return None出现大批量 403 时第一反应不是继续重试而是暂停程序、检查签名是否失效。7.4 使用代理时的Cookie绑定问题很多动态 Cookie 会和 IP 绑定。如果使用代理池每次切换 IP 后旧 Cookie 就失效了。一个可行的做法是每次拿到新 IP 后先用干净的 Session 访问首页让 JS 生成新 Cookie。再使用这个 Session 请求详情页。不要在多个 IP 之间共享同一个 Cookie。7.5 监控与告警在生产环境中建议对关键请求的状态码、成功率和耗时做监控。一旦成功率下降立即触发告警而不是等任务跑完才发现数据全是 403。8. 总结与下一步学习路线这次樱花动漫的chameleon反爬本质上是一次典型的 JS 动态 Cookie 逆向。流程归纳下来就是用普通请求复现异常确认问题出在 Cookie 或参数。用 DevTools 抓包对比找出差异字段。用全局搜索定位 JS 文件。用 Hook 和断点辅助定位关键函数。在 Node.js 或 PyExecJS 中补环境执行。将生成的 Cookie 带入 Python 请求中验证。如果你已经能独立完成这个流程下一步可以继续学习 JavaScript 混淆与 AST 还原了解 VMP 虚拟机保护的对抗思路或者把 JS 逆向的能力迁移到安卓端接触更底层的逆向分析。这次被樱花动漫恶心到其实不是坏事儿反而逼着我把chameleon这类反爬脚本的套路整理了出来。如果你也遇到类似的动态 Cookie 问题不妨按上面的排查清单走一遍应该能少走不少弯路。