如果只看表面,动漫站点的反爬无非是“Cookie里加个签名”。但真正上手之后才发现,卡住你的根本不是某个加密算法,而是一整套环境对抗方案。最近技术社区里频繁讨论的 chameleon 反爬 JS,就是一个典型例子。它不是大家熟悉的 OB 混淆那种“静态读代码难”,而是把字符串混淆、动态代码生成、环境指纹检测、定时刷新放在一起,让分析者就算定位到了关键代码,也很难在 Node.js 里还原出和浏览器一致的结果。
不少有五六年经验的逆向工程师,遇到这种组合式反爬同样会卡壳。原因并不复杂:过去做 JS 逆向是“读懂算法”,现在做 JS 逆向已经变成“模拟一个完整的浏览器可信环境”。本文就以这个案例为切入点,把现代反爬 JS 的完整分析流程拆开讲清楚,包括如何定位加密入口、如何识别环境检测、如何搭建可用的调试环境。
先说清楚边界:本文只讨论技术分析方法论和通用调试技巧,不提供针对任何具体站点的破解代码。做安全研究和防御验证时,请务必遵守法律法规,只分析自己拥有或被授权测试的目标。
1. 这篇文章真正要解决的问题
1.1 为什么“老手”也会被恶心到
先给一个判断:现代反爬已经从“单点加密”进化成了“多层组合检测”。以前做 JS 逆向,遇到的大多是某个签名算法比较复杂,核心工作是把算法流程还原出来,再用 Python 或 Node.js 重写一遍。这种模式下,只要耐心足够,早晚能把代码啃下来。
但以 chameleon 为代表的这类反爬 JS,思路完全变了。它不指望用一个算法挡住你,而是让你“根本跑不起来”。整个执行过程包含多层设计:
- 代码加了好几层混淆,字符串全部还原成数组索引,静态分析耗时巨大;
- 关键逻辑不是一次性生成,而是运行时动态生成,每次请求都可能不同;
- 代码执行前先探测运行环境,读取浏览器指纹、插件列表、自动化工具标记;
- 检测通过后才返回结果,而且这个结果带时间戳和会话绑定,过期就失效。
这种设计的核心思想,是把逆向工程变成一场“不断追环境”的拉锯战。你花三天还原了算法,第二天对方换一个动态分支,之前的分析又得推倒一部分重来。这才是真正让人崩溃的地方。
1.2 谁是本文的目标读者
- 爬虫工程师:需要理解为什么自建抓取任务总会拿到“环境异常”或“签名过期”;
- 前端工程师:想从攻击者视角审视自己站点的反爬设计是否真正有效;
- 安全测试人员:在做授权渗透或 JS 漏洞分析时,需要一套系统的调试方法;
- 逆向初学者:想搞清楚“补环境”到底是什么意思,为什么要补,补到什么程度。
如果你只是想要一个现成的破解脚本,这篇文章不适合你。本文讲方法和判断,不提供对特定站点有效的完整绕过方案。更准确地说,这篇文章讲的是“当遇到一个陌生反爬 JS 时,怎么一步步分析它”。
1.3 读完你能得到什么
- 能从抓包请求出发,快速定位加密逻辑所在的 JS 文件和对应函数;
- 能识别常见反调试手段和环境检测方式,知道在 DevTools 里如何观察;
- 能搭建一个最小可用的 Node.js 补环境,让样本 JS 在本地跑起来;
- 能建立一套“先分析、后验证、再自动化”的工程化思路。
2. 反爬JS的核心原理与chameleon的典型套路
2.1 JS逆向到底在做什么
JS 逆向分析,简单说就是:从压缩混淆过的 JavaScript 代码中,还原出服务端数据接口所需的加密参数生成过程。
大多数网站的业务数据要通过接口返回,而接口又要求前端必须计算出正确的参数。于是前端代码里就藏着“参数怎么生成”的逻辑。反爬要做的事,是让“只有真实浏览器环境”才能正确生成这些参数。它把前端代码变成了一把锁,而逆向就是研究这把锁的弹子结构。
一个容易忽略的事实是:现代反爬已经不只是密码学问题,而是一个环境可信度问题。服务端校验的往往不只是参数值本身,还包括生成参数的环境是否可信。
2.2 常见混淆与反调试分类
| 分类 | 典型手法 | 分析难度 |
|---|---|---|
| 代码压缩 | 变量名缩短、换行移除 | 低 |
| 字符串混淆 | 字符串拆分成数组,按索引取值 | 中 |
| 控制流平坦化 | 把表达式拆成状态机,增加分支 | 中高 |
| 虚拟机保护 | 把字节码交给自定义解释器执行 | 高 |
| 环境检测 | 检查 navigator、window 等对象 | 中 |
| 反调试 | 无限 debugger、定时器、内存检测 | 高 |
| 动态生成 | 每次执行都生成不同代码 | 高 |
大多数现代反爬不会只用其中一种,而是按组合方式使用。chameleon 这类 JS 更偏向“动态生成+环境检测+反调试”的组合。
2.3 chameleon反爬JS的典型设计
从公开讨论看,chameleon 反爬 JS 有这几个特点:
环境指纹采集。它会在代码执行早期读取大量环境变量,比如 navigator.userAgent、screen.width、canvas 指纹、WebGL 信息、插件列表、时间戳等,然后把这些信息编码进后续的加密参数里。这样一来,即使你把加密逻辑原样搬到 Node.js 里运行,只要指纹不一致,服务端也能识别出来。
动态算法选择。混淆代码里同时存在多套加密路径,实际执行哪一套由环境参数决定。分析时如果只盯着其中一套,下次运行可能就走另一套了。这也是“变色龙”这个名字的含义:代码会适配它看到的运行环境。
会话绑定与时效控制。最终生成的 Cookie 或签名里会带上时间、会话序号、环境摘要,服务端可以快速判断这个请求是否来自可信浏览器。签名一旦过期,前一次的分析结果立刻失效。
2.4 为什么补环境是核心难点
“补环境”是指在 Node.js 里模拟出一个假浏览器,让 JS 代码在运行时取到的 window、navigator、document 等对象都和真实浏览器一致,从而得到和浏览器一致的生成结果。
难点在于,这类反爬 JS 会不断探测环境对象之间的引用关系。比如 document.defaultView !== window 会被识破,navigator.userAgent 和屏幕宽度不匹配也会直接进入错误分支。每次补完环境,运行一次,又发现缺一个变量,这种循环是这类逆向里最常见的折磨。
更进阶的检测还会检查属性描述符、原型链、toString 输出等细节。比如真实浏览器里 navigator.webdriver 是 undefined,而很多自动化工具会把它设成 true。这种微小的差异,足以让整个环境检测分支走错。
3. 环境准备与前置条件
3.1 浏览器与调试工具
- 推荐使用 Chrome 或 Edge,最好准备一个独立的测试用户目录,避免本地插件影响指纹;
- DevTools 是主要调试阵地,重点使用 Sources、Network、Console 三个面板;
- 建议安装一个脚本注入插件,比如 Tampermonkey,方便在页面加载前挂 Hook 脚本。
如果是做长期分析,可以准备一个带远程调试端口的 Chrome 实例。远程调试协议可以在自动化工具里直接调用,这对后续写验证脚本很有帮助。
3.2 抓包工具
- 浏览器开发者工具的 Network 面板能覆盖大多数场景;
- 如果需要分析移动端页面,或者想看 HTTPS 解密后的完整请求,可以准备 Fiddler 或 Charles;
- 抓包时优先关注 XHR 和 Fetch 请求,找到返回真实数据的接口后,再反向追 JS 调用链。
抓包这一步的关键不是把所有请求都拷贝下来,而是从请求参数里找到“哪些值是需要逆向生成的”。通常值得关注的参数有 sign、token、nonce、payload、ts 等。
3.3 Node.js 运行环境
- Node.js 版本建议使用 LTS,太老的版本可能不支持新语法;
- 如果样本 JS 使用了较新的 ES 特性,需要保证本地 V8 版本不要太旧;
- 可以选用 jsdom 在 Node 里补充 DOM 环境,但不是必须,很多场景手动补环境反而更快。
执行复杂混淆 JS 时,不建议依赖在线执行工具。原因是这类代码经常包含无限循环、内存爆破等反调试逻辑,在自己的机器上反而更好控制。
3.4 辅助工具
- AST 解析工具,用于格式化混淆代码并辅助自动化修改;
- VS Code 或同类编辑器,需要支持大文件搜索和正则替换;
- Python 环境,用于最终参数送检、服务端响应验证,以及后续自动化接入。
3.5 环境自检
打开 DevTools Console,执行下面代码,确认可以正常注入脚本:
// 环境自检脚本 console.log('%cHook Ready', 'color:green;font-size:16px;'); console.log('navigator.webdriver =', navigator.webdriver); console.log('userAgent =', navigator.userAgent);如果能在页面加载前注入 Hook,说明后续的调试分析可以正常进行。如果 navigator.webdriver 输出为 true,说明当前环境带有自动化标记,分析前需要先解决这个干扰项。
4. 现代反爬JS的分析流程拆解
4.1 抓包定位关键接口
打开目标页面,触发一次数据刷新,在 Network 面板里找到真正返回数据的接口。不要把注意力浪费在图片、CSS、字体文件上,优先过滤 XHR 和 Fetch 请求。
假设接口是:
https://api.example.com/v1/play/list?page=1&sign=xxx这里 sign 的值就是逆向的目标。在请求头里可能还会看到自定义的 headers,比如 x-time、x-sign、x-fingerprint,这些都是服务端用来校验的参数。
4.2 从Initiator追JS调用链
在 Network 面板里点击请求的 Initiator 列,可以看到这个请求由哪个 JS 文件、哪一行发起的。顺着调用链往上翻,通常能找到发起请求的封装函数,再往上就是加密参数的生成处。
常见做法是在可疑函数位置打上断点,刷新后单步执行,观察 sign、token 等变量在什么时候被赋值。如果断点经常被跳过,可以先在参数生成处打一个条件断点,等参数变成非空时再停下来。
4.3 识别混淆类型
拿到反爬 JS 文件后,不要急着逐行读。先看几个特征:
- 变量名是否全部变成 a、b、c、_0x 之类,可能是普通变量名压缩;
- 是否出现大段数组定义和字符串索引访问,可能是字符串数组混淆;
- 是否出现 switch-case 拼接的循环状态机,可能是控制流平坦化;
- 是否出现 WebAssembly 或自定义字节码解释器,可能用了虚拟机保护;
- 是否在文件开头就大量读取 navigator、screen、canvas 等对象,说明有环境检测。
从 chameleon 这类反爬 JS 的公开讨论看,它的混淆层次比较多,静态分析成本很高。更高效的方式是先格式化代码,再通过运行时 Hook 绕过一部分静态分析。
4.4 定位加密入口
静态分析困难时,运行时 Hook 是更高效的手段。常用的 Hook 点包括:
- document.cookie 的 setter;
- localStorage、sessionStorage 的写入方法;
- XMLHttpRequest.prototype.open 和 send;
- window.fetch;
- Function、eval、setTimeout。
只要在页面加载前把这些对象包装一层,记录调用栈和参数,就能知道加密函数何时被调用、输入是什么。
这里面最关键的是调用栈。调用栈能告诉你“这个参数是谁生成的”,而不是让你猜。很多复杂的反爬 JS,最终都能靠这种方式找到加密入口函数。
4.5 Hook关键函数
比较高效的做法,是写一个内容脚本,在页面加载前注入。Hook 代码一旦发现目标参数被写入,就打印参数值和调用栈。
由于这类反爬代码经常使用 Function.prototype.toString 检测函数是否被篡改,所以 Hook 本身也要尽量隐蔽。常用的办法是保留原生函数引用,在包装函数内部调用原生实现,同时记录日志。
4.6 补环境运行
把摘取出来的 JS 片段放到 Node.js 里运行,首先要面对的就是各种环境缺失。报错信息会明确告诉你缺了什么:
- xxx is not defined,说明环境对象缺失;
- xxx is not a function,说明方法没有实现;
- 某个属性值是 undefined 但浏览器里有值,说明需要补属性。
补环境的顺序建议是:window、navigator、document、location、screen,再到 canvas、performance 等更细的对象。每补一个,就运行一次,看下一步还会报什么错。
4.7 验证一致性
把 Node.js 里生成的结果,与浏览器里实际抓到的结果做对比。如果一致,说明加密流程跑通了;如果不一致,大概率是环境指纹没有完全模拟。
验证时不要只看参数值是否相同,还要看多次运行结果的随机性是否一致。有些反爬 JS 会基于当前时间生成随机因子,如果本地时间和服务器时间偏差过大,同样会导致失败。
5. 完整示例与代码实现
5.1 示例1:Cookie写入Hook
// 文件名:hook-cookie.js // 用法:通过浏览器插件或 DevTools 注入到页面加载前 (() => { const setCookie = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').set; const getCookie = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').get; Object.defineProperty(document, 'cookie', { get() { return getCookie.call(this); }, set(value) { if (value && value.includes('sign=')) { console.log('[Cookie Hook] 捕获关键Cookie:', value); console.log('[Cookie Hook] 调用栈:', new Error().stack); window.__capturedCookie = value; } return setCookie.call(this, value); } }); })();这段代码拦截 document.cookie 的写操作。当脚本写入包含 sign= 的 Cookie 时,控制台会打印参数值和调用栈。通过调用栈,可以直接看到是哪个函数写入了这个 Cookie,从而缩小分析范围。
5.2 示例2:Function.prototype.toString检测Hook
// 文件名:hook-fn-tostring.js // 目的:识别JS是否在检测“函数是否为原生函数” (() => { const originalToString = Function.prototype.toString; Function.prototype.toString = function (...args) { // 如果调用方想查看这个函数是否为 [native code],说明可能存在反调试 const result = originalToString.call(this, ...args); if (this.name && result.includes('[native code]')) { console.log(`[Native Hook] ${this.name} 被检测了`); } return result; }; })();反爬 JS 经常通过 Function.prototype.toString.call(fn) 来检测某个函数是否被篡改。如果函数被包装过,toString 输出不会包含 [native code],环境就会被判定为不可信。这个 Hook 会暴露“哪个函数被检测”,帮助你快速定位反调试点。
5.3 示例3:Node.js最小补环境
// 文件名:env.js // 用途:给样本 JS 提供一个最小可用的浏览器假环境 const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; global.window = global.window || {}; global.navigator = global.navigator || { userAgent, language: 'zh-CN', languages: ['zh-CN', 'en'], platform: 'Win32', vendor: 'Google Inc.', webdriver: false, plugins: [], maxTouchPoints: 0, hardwareConcurrency: 8, deviceMemory: 8, }; window.window = window; window.navigator = navigator; global.document = global.document || { cookie: '', referrer: '', title: '', readyState: 'complete', createElement(tagName) { return { tagName: String(tagName).toUpperCase(), style: {}, setAttribute() {}, getAttribute() { return null; }, appendChild() {}, }; }, }; window.document = document; module.exports = { window, navigator, document };这是补环境的第一步。如果样本 JS 继续读取 canvas、screen、performance 等属性,还需要继续扩展这个文件。补环境是一个渐进的过程,不需要一开始就把所有浏览器 API 都实现完。
5.4 示例4:Python调用本地JS生成签名
# 文件名:run_js.py # 说明:仅用于演示 Python 调用本地 JS 的基本方式 import execjs with open('sdk.js', encoding='utf-8') as f: js_code = f.read() ctx = execjs.compile(js_code) # 假设 sdk.js 中暴露了 getSign(data) 函数 # 实际项目里函数名和环境复杂程度不同,需要以调试结果为准 sign = ctx.call('getSign', '12345') print(sign)如果目标 JS 补完环境后能直接执行,就可以在 Python 里通过 execjs 或子进程调用 Node.js 来生成签名。需要注意的是,execjs 对复杂 ES6 语法和大型混淆代码的支持并不稳定。更稳妥的做法是在 Node.js 里跑完整环境,对外暴露一个 HTTP 接口,由 Python 侧调用这个接口获取参数。
5.5 代码运行说明
上面四个示例不是“开箱即用”的完整破解方案,而是分析过程中最常用的工具片段。建议按这个顺序使用:
- 在浏览器中注入 Hook,定位加密入口;
- 把目标 JS 放到 Node.js 补环境里运行;
- 对比浏览器和本地环境的差异;
- 用 Python 或接口方式做参数接入。
6. 运行结果与效果验证
6.1 验证Hook是否注入成功
在浏览器打开一个空白页,注入 hook-cookie.js,然后手动执行:
document.cookie = 'sign=abc123';如果 Console 打印了捕获信息,说明 Hook 生效。如果没有任何输出,需要检查注入脚本是否在页面加载前执行。
6.2 验证Node环境与浏览器环境差异
在浏览器 Console 执行:
console.log(JSON.stringify({ userAgent: navigator.userAgent, platform: navigator.platform, language: navigator.language, webdriver: navigator.webdriver, screen: [window.screen.width, window.screen.height], }));然后在 Node.js 的 env.js 里也输出同样的结构,逐字段对比。差异点就是下一步需要补的环境属性。
6.3 如何判断“逆向正确”
最直接的判断标准是请求能被服务端接受,返回正常数据。但在此之前,可以先看三个中间指标:
- 加密参数格式是否和浏览器抓包时一致;
- 服务端是否返回“参数错误”“签名过期”“环境异常”等提示;
- 多次执行的结果是否具有和浏览器一致的随机性和时效性。
6.4 失败时第一步查哪里
如果请求一直失败,优先检查三处:
- Cookie 或请求头是否和浏览器完全一致;
- 时间戳字段是否使用了目标网站服务器时间,而不是本地时间;
- 是否存在只在浏览器里才会执行的异步初始化逻辑,比如页面加载后几秒才开始计算参数。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 控制台出现无限 debugger | 反调试机制 | 在 Sources 面板定位 debugger 位置,看触发条件 | 使用 DevTools 的“Deactivate breakpoints”或通过代码替换移除调试语句 |
| 内存频繁爆掉 | JS 内做内存消耗检测或死循环陷阱 | 观察 CPU 和内存占用,定位循环代码 | 打断点或使用 AST 还原,避免直接长时间运行 |
| Node.js 运行报错 xxx is not defined | 补环境不完整 | 按报错变量名搜索 JS 源码 | 在 env.js 中补充对应对象和属性 |
| 加密结果与浏览器不一致 | 环境指纹参与计算 | 对比浏览器和 Node 环境输出 | 逐步补 canvas、WebGL、screen 等指纹属性 |
| 参数第一次有效,第二次失效 | 会话绑定或一次性令牌 | 检查 Cookie、localStorage 是否有变化 | 保留前序请求的会话状态 |
| 执行结果有随机性 | 动态算法选择 | 多次运行记录差异 | 收集多套分支,分析选择逻辑 |
| 抓包看不到数据接口 | 请求被 Service Worker 缓存或走 WebSocket | 检查 Network 里的 WS 连接,或清缓存刷新 | 用 Hook 拦截全局 fetch 和 XHR |
8. 最佳实践与工程建议
8.1 合法合规永远是第一位
做任何 JS 逆向之前,先确认三个问题:
- 目标站点是否允许爬虫访问,是否声明了使用条款;
- 你是否拥有该站点的测试授权;
- 你的请求是否会影响到目标业务的正常运行。
未授权绕过技术措施可能涉及违法风险,这不是套话,而是底线。本文所有内容都应被理解为研究自动化工具检测与前端安全防御的参考资料,而不是攻击教程。
8.2 把分析过程流程化
不要一上来就把整个 JS 粘贴到 Node.js 里跑。建议按照“抓包定位 -> 调用链追踪 -> Hook 定位 -> 补环境 -> 一致性对比”的顺序来,每步只做一件事,这样排错最快。
尤其要养成记录的习惯。每一步的报错、环境变量、测试结果都要有记录,否则遇到多轮迭代时,很容易忘记之前改过什么。
8.3 工程化维护
如果目标站点的反爬会定期更新,你的分析代码也需要做版本管理。建议:
- 用 Git 管理分析脚本和环境补丁;
- 把补环境代码拆成独立模块,方便复用;
- 记录每次反爬升级的变化点,逐渐积累自己的常见检测特征库。
8.4 站在防御视角看反爬
这个案例对前端工程师和风控工程师也有启发。反爬设计的核心不是“永不破解”,而是提高攻击成本。以下几点很关键:
- 把关键逻辑做成动态代码生成,比静态混淆有效得多;
- 环境指纹校验必须放在服务端,不能只在前端判断;
- 参数要有时间戳、会话绑定和服务端二次校验;
- 风控要结合请求频率、行为轨迹、设备指纹,形成多维判断。
单独看 chameleon 反爬 JS 的某个加密算法,难度并不高。真正让分析者头疼的,是它把多种检测组合起来,互相补充,让模拟成本不断上升。
8.5 爬虫工程的自我修养
如果你在做大规模数据采集,更务实的路线是:
- 优先使用官方 API 和授权数据源;
- 控制请求频率,尊重目标站点的服务条款;
- 做好失败重试和数据校验;
- 不要把全部精力放在对抗反爬上,数据质量和稳定性才是核心价值。
反爬对抗本质上是成本博弈。只要提高对方的攻击成本,反爬设计就成功了一半。同样,作为数据分析方,如果发现自己需要投入巨大成本才能绕过一套反爬,也要冷静评估这条路是否真的值得。
9. 总结与后续学习方向
回到开头的问题:为什么现代反爬会让很多有经验的逆向工程师也头疼?
答案不是某个加密算法突然变难了,而是反爬思路从“让代码读不懂”变成了“让代码跑不起来,跑起来也对不上”。chameleon 这类反爬 JS 只是一个缩影,它背后是环境检测、动态生成、会话绑定、服务端风控的组合。你花大量时间还原的算法,可能只是一次性分支。真正决定成败的,是对整个浏览器环境的模拟精度。
对于想继续深入的人,几个方向值得投入:
- AST 抽象语法树,用来批量处理混淆代码;
- V8 引擎和 JavaScript 虚拟机原理,理解虚拟化保护;
- 浏览器渲染机制和指纹生成原理,理解环境检测;
- 服务端风控体系设计,理解反爬的最终决策逻辑。
最后再提醒一次:技术分析可以很有趣,但请把它用在合法合规的范围内。如果遇到一个值得研究的反爬案例,先确认授权,再动手调试。希望这篇文章能把 JS 逆向的完整思路讲清楚。下次再看到 chameleon 这类反爬时,你至少知道从哪里下手,也更容易判断哪些环节是真正卡住你的关键。
建议收藏备用,遇到类似问题可以按这套流程排一遍。