进阶:补环境与反爬对抗
“补环境”是 JS 逆向工程中的核心高级技术,主要用于应对瑞数(RuiShu)、Akamai、F5 等强对抗型反爬系统。其核心逻辑是在 Node.js 等非浏览器环境中,手动模拟出浏览器特有的对象(BOM、DOM)和行为,欺骗服务器端的检测脚本。
1. 核心原理与调试手段
1.1 为什么需要补环境?
浏览器执行 JS 的上下文(window)与 Node.js 执行 JS 的上下文(global)差异巨大。反爬 JS 代码会检查这些差异(如 window.navigator 是否存在,document.cookie 是否可写)来判断当前环境是否为真实浏览器。
1.2 神器:Recursive Proxy (递归代理)
这是补环境最关键的调试工具。通过 Proxy 拦截所有对象属性的读取,我们可以准确知道反爬代码到底检测了什么。
jsx
// 简易递归代理框架,用于捕获缺少的环境
function getProxy(obj, name) {
return new Proxy(obj, {
get: function(target, prop) {
// 排除一些 Node 内部调用的干扰属性
if (typeof prop === 'symbol' || prop === 'inspect') {
return target[prop];
}
console.log(`[读] ${name}.${String(prop)}`);
const val = target[prop];
// 如果属性值是对象,继续递归代理
if (typeof val === 'object' && val !== null) {
return getProxy(val, `${name}.${String(prop)}`);
}
return val;
},
set: function(target, prop, value) {
console.log(`[写] ${name}.${String(prop)} = ${value}`);
target[prop] = value;
return true;
}
});
}
// 挂载到 window
window = getProxy({}, "window");2. BOM (浏览器对象模型) 模拟重点
2.1 Window 与 Global 隔离
- 循环引用:在浏览器中,
window.window === window,window.self === window,[window.top](http://window.top) === window(在无 iframe 时)。这必须严格模拟。 - 变量泄漏:Node.js 中的
global变量如果不小心泄漏到 JS 执行环境中(例如process对象),会立刻暴露身份。务必删除或隐藏 Node 特有变量。
2.2 Navigator (身份指纹)
webdriver: 高危。自动化工具通常为true。需设置为undefined或false。plugins/mimeTypes: 许多检测会遍历插件长度。如果为空数组[],评分会降低。platform/userAgent: 必须与 HTTP 请求头完全一致。
2.3 Location (地址栏)
- 很多加密算法会依赖
location.href或location.search参与哈希计算。如果location为空或 URL 不对,生成的 cookie/token 将无效。 - 模拟时要解析目标 URL 的 protocol, host, pathname, search, hash。
2.4 Screen & Performance
- Screen:
width,height,colorDepth。注意多显示器场景的模拟。 - Performance: [
performance.now](http://performance.now)()是高频检测点,用于检测脚本执行耗时或检测环境卡顿。
3. DOM (文档对象模型) 模拟重点
3.1 Document 对象
- Cookie 钩子:必须实现 getter/setter。瑞数等反爬会通过 JS 设置 cookie,然后立刻读取校验。
- Referrer: 必须与请求头中的 Referer 保持一致。
createElement:- 反爬脚本常用
div = document.createElement('div')然后检查div.innerHTML默认值,或者div.appendChild的行为。 - 动态创建的元素(如
canvas)也需要具备相应的属性和方法(如toDataURL)。
- 反爬脚本常用
3.2 HTML 元素原型链 (Prototype Chain)
检测重灾区。
简单模拟:
var div = {}(错误,容易被div instanceof HTMLDivElement识破)深度模拟:
jsx// 构造原型链 var HTMLDivElement = function() {}; var HTMLElement = function() {}; HTMLDivElement.prototype = Object.create(HTMLElement.prototype); // ... 更多继承 Object.defineProperty(window, 'HTMLDivElement', { value: HTMLDivElement });
4. 高级对抗:Native 函数伪装 (toString 保护)
反爬脚本会检查核心函数(如 eval, setTimeout, btoa)的 toString() 结果。
- 检测逻辑:
window.atob.toString()应该返回"function atob() { [native code] }"。如果是 JS 模拟的,通常会返回 JS 源码。 - Hook 方案:
jsx
// 保护 toString 的通用方案
(() => {
const $toString = Function.prototype.toString;
const mySocket = 'function() { [native code] }'; // 或者是具体的 name
// Hook Function.prototype.toString
Function.prototype.toString = function() {
// 如果当前函数是被我们需要保护的函数,返回 native code
if (this === window.atob || this === window.setTimeout) { // 示例逻辑
return `function ${this.name}() { [native code] }`;
}
// 否则返回原始结果
return $toString.call(this);
};
// 注意:别忘了保护 toString 自身!
// Function.prototype.toString.toString() 也必须返回 native code
})();5. 实战案例详解
5.1 瑞数 (RuiShu) - 4/5/6 代区分与逆向
瑞数的核心逻辑是 动态代码 (VMP) + 环境检测。
- 流程:请求首页 -> 返回 HTML (含
<meta>和<script>) -> 浏览器执行 JS (生成 cookie) -> 携带 Cookie 二次请求。 - 区分特征:
- 4 代:通常有“二次 Cookie 生成”的过程。第一次生成的 Cookie 是假的(长度较短),JS 会再次生成一个真 Cookie(长度很长),然后刷新页面。
- 5 代:去掉了“假 Cookie”步骤。直接生成真 Cookie(通常以
FSSBBIl1Uqz等开头,名称动态)。 - 6 代:在 5 代基础上加强了 VMP 混淆,增加了更多环境检测点(如
Math.random随机数序列检测),Hook 难度极大。
- 逆向思路:
- 定位入口:Hook
document.cookie或window.eval。瑞数通常通过eval执行解密后的 VM 代码。 - 扣代码 vs 补环境:瑞数逻辑过于复杂(几万行 VM 调度),硬扣算法(还原 VMP)成本极高。推荐补环境。
- 固定随机数:瑞数会利用
Math.random和Date.now参与加密。在 Node.js 中必须重写这两个函数,固定返回值,以便调试时输出一致。
- 定位入口:Hook
- 调试代码 (Cookie Hook):
jsx
// Hook Cookie Setter 定位加密位置
(function() {
'use strict';
var cookie_cache = document.cookie;
Object.defineProperty(document, 'cookie', {
get: function() {
return cookie_cache;
},
set: function(val) {
console.log('正在设置 Cookie:', val);
debugger; // 在此处断点,查看调用栈
cookie_cache = val;
return val;
}
});
})();5.2 Akamai (2.0/3.0) - 传感器数据
Akamai 的核心是收集传感器数据 (sensor_data),生成 _abck Cookie。
- 特征:请求中包含
_abckcookie,POST 请求提交巨大的sensor_data字符串。 - 逆向思路:
- 鼠标/键盘轨迹:Akamai 极度依赖用户行为。必须模拟真实的鼠标移动(贝塞尔曲线)、点击间隔和键盘输入。直接复制粘贴的 payload 必挂。
- 环境一致性:
navigator属性必须与 TLS 指纹匹配。 - 被动检测:Akamai 会检测开发者工具(DevTools)是否打开。
- 代码片段 (模拟鼠标移动数据):
jsx
// 伪代码:生成贝塞尔曲线轨迹
function generateBezierPath(start, end, steps) {
// ...计算贝塞尔控制点...
return path; // [{x:1, y:1, t:100}, {x:2, y:3, t:110}, ...]
}
// sensor_data 中记录的是轨迹的差值编码5.3 Cloudflare 5秒盾 (Turnstile / IUAM)
Cloudflare 的“5秒盾”本质上是 TLS 指纹校验 + JS 算力挑战 (PoW)。
- 核心痛点:
- TLS 指纹:Python 的
requests库发出的 TLS 握手包(Client Hello)特征明显,会被 CF 直接拦截(403 Forbidden),甚至不触发 5秒盾 JS。 - JS Challenge:如果是 Turnstile(类似验证码),需要计算复杂的数学题(PoW)。
- TLS 指纹:Python 的
- 解决方案:
- TLS 层面 (最关键):放弃
requests,使用支持模拟浏览器 TLS 指纹的库。- Python:
tls_client,curl_cffi - Go:
cycletls
- Python:
- JS 层面:
- 如果通过 TLS 伪装后仍遇到 5秒盾,需要解析页面中的 JS 挑战(通常是 AES 运算或数学计算),算出
cf_clearanceCookie。 - 捷径:使用“穿云API”类服务,或使用
DrissionPage(控制浏览器) 绕过。
- 如果通过 TLS 伪装后仍遇到 5秒盾,需要解析页面中的 JS 挑战(通常是 AES 运算或数学计算),算出
- TLS 层面 (最关键):放弃
- 代码示例 (使用 tls_client 绕过检测):
python
import tls_client
session = tls_client.Session(
client_identifier="chrome_120", # 模拟 Chrome 120 的 TLS 指纹
random_tls_extension_order=True
)
res = session.get(
"https://example.com",
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."
}
)
print(res.status_code) # 通常能直接绕过 4036. 工具与框架选择
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| :--- | :--- | :--- | :--- |
| JSDOM | 成熟,API 全 | 并不是真正的浏览器,特征指纹非常多 (如 canvas 实现差异) | 简单反爬,验证逻辑 |
| vm2 + 手动补环境 | 纯净,无多余特征,可控性最高 | 开发成本极高,缺什么补什么,容易漏 | 瑞数 4/5/6,Akamai,高难度对抗 |
| 浏览器自动化 (Puppeteer/Playwright) | 真实环境,无需补环境 | 效率低,并发差,容易被检测指纹 (webdriver, CDP) | 少量数据抓取,无法破解 JS 时 |
| unidbg (Android) | 模拟安卓环境 | 很多网页反爬算法与 App 也就是一套逻辑,有时有奇效 | 主要是 App 逆向,Web 辅助 |