最近在做一个采集项目时,接到了一个让人头疼的需求:目标站点用了 Incapsula 的防护,请求正常发过去,返回的是 325 状态码的挑战页面,里面夹着一大坨看不太懂的 JS。页面提示几秒后会自动跳转,但那是在真实浏览器里才会发生的事。我这个用 Requests 拉数据的脚本,直接被卡死在了第一步。
关键在页面源码里的一个 cookie 名:reese84。当时我对 Incapsula 这套机制还比较陌生,但凭直觉意识到,只要把这个 cookie 的生成逻辑还原,后面的采集链路就通了。于是从 JS 反混淆开始,一路做到动态 Token 生成,前后花了差不多三天时间。这篇文章把整个过程完整记录一下,从工具选择、代码拆解到 Node.js 侧复现,全部展开写,希望能帮到同样卡在 reese84 上的朋友。
1. reese84 防护到底在防什么:先看懂它的运行链路
想要绕过一套防护,前提是真正搞懂它为什么存在、怎么工作的。Incapsula(现在叫 Imperva)这套防护在商业风控领域算是硬骨头,它区别于普通 WAF 的地方在于,不仅识别 IP、UA、Header,还会在浏览器端执行一段 JS,通过代码运算生成一个加密 Token,再由页面请求把 Token 带回服务端校验。reese84 就是这段 JS 写的 cookie 名。
1.1 一个典型的 Incapsula 挑战流程
我第一次访问目标站点时,服务端返回的是 403 状态码的 HTML,标题大概是 "Just a moment...",页面里有一段内联的混淆 JS 和几个外链脚本。这段 JS 会在浏览器里做三件事:
- 采集当前环境的浏览器指纹信息(UA、Canvas、WebGL、时区、语言等);
- 把采集到的数据经过一系列编码、加密、编码的操作;
- 最终生成一个名称为
reese84的 cookie 写入浏览器,然后触发页面刷新。
刷新后的请求带着reese84cookie 重新访问,服务端验签通过后,才会真正返回业务页面。
这和我们常见的 CSS 挑战(计算一个值后拼到 URL)最大的区别是:它不是一个简单的数学题,而是一个多步骤的 JS 执行过程,而且生成 Token 的算法是高度混淆的。每个用户、每次访问生成的 Token 都不一样,依赖浏览器指纹,换个环境直接失效。
1.2 从用户视角看一次完整交互
用浏览器手动访问时,几乎感受不到这个过程。页面会短暂卡一下,然后自动跳转。但如果用抓包工具观察,能看到这个流程:
| 请求阶段 | 请求内容 | 响应特征 |
|---|---|---|
| 第1次请求 | GET / | 返回 403,响应体为 challenge HTML |
| 浏览器执行JS | 加载外链脚本、内联脚本 | 采集指纹并计算 Token |
| 第2次请求 | GET /,携带 reese84 cookie | 服务端校验,返回 200 和真实页面 |
这个第二阶段的请求实际上同时携带了很多 header 用于验证,包括 user-agent、accept-language、cookie 等。Incapsula 的校验是综合性的,任何一项对不上都可能导致验证失败。
1.3 为什么 Selenium 和 Requests 都容易翻车
说到这,你可能会想:用 Selenium 模拟浏览器打开页面,等它生成 cookie 不就行了?理论上可以,但实践中有两个大坑:
- Incapsula 的 JS 会检测浏览器是否被自动化工具控制,比如
navigator.webdriver标记、Chrome DevTools Protocol 的连接特征、window 组件顺序等,Selenium 默认配置很容易暴露。 - 它的 Token 校验和 IP、UA、指纹是绑定的。如果用 Selenium 生成的 cookie 拿到 Requests 里用,很可能因为 UA 不一致等问题直接被拒绝。
Requests 直接请求就更不用说了,没有执行 JS 的能力,拿不到 reese84,等于被锁在门外。
我当时的目标很明确:把整段 JS 的算法还原,在 Node.js 里模拟浏览器环境跑出同一个 cookie,然后带着 cookie 去请求目标接口。这条路走通后,比 Selenium 快好几个量级,稳定性也更高。
2. 逆向前的准备工作:工具、环境、抓素材
在正式开始读代码之前,有几个准备工作值得花时间做。很多人一上来就试图用肉眼读懂混淆 JS,结果看了半小时就放弃了。我的建议是:先把素材抓全,把环境搭好,再把代码拆干净,最后才是读逻辑。
2.1 需要的工具清单
我自己在本地搭建的工具组合很简单,但干活效率很高:
- Chrome DevTools 元素面板与网络面板:主要用于定位脚本资源、观察页面运行时对 DOM 和 window 对象的操作。
- 浏览器开发者工具的 overrides 功能:可以在本地修改 JS 文件,实现 hook 埋点,观察关键函数的参数和返回值。
- 抓包工具 Charles:用于分析完整的请求时序,尤其是挑战页加载的所有 JS 文件。
- Node.js 环境:最终复现 Token 生成逻辑的地方,建议用 18 以上版本,自带 fetch,方便后续测试。
- Babel 工具链:用于 JS 反混淆的 AST 层面还原,后面第三部分会详细说明。
2.2 把挑战页的 JS 完整抠出来
当你打开挑战页面时,Network 面板里会看到几个脚本,有的来自内联,有的来自独立域名。最好是把所有脚本内容和 HTML 结构完整保存下来,之后再统一分析。实际做法分三步:
第一步,直接保存响应体。在 Network 面板找到第一个文档请求(通常状态码是 403),右键保存响应内容到本地,命名为challenge.html。
第二步,保存所有外链 JS。同样的方式,把扩展名是 js 的响应体保存下来。有时候脚本内容是从动态接口返回的,response header 里面 content-type 是application/javascript或类似,不影响保存。
第三步,用一段小脚本整理资源名。把 html 里的<script>标签的 src 属性提出来,和保存的 JS 文件一对一对上,搞清楚每个脚本的作用。
我用的一个小技巧是,在 Chrome 里打开请求页面后,直接在 Console 执行一段代码,把脚本 src 列表打印出来:
Array.from(document.querySelectorAll('script')).map(s => s.src).filter(Boolean)这样能快速看到页面加载了哪些脚本。之后再逐一从 Network 面板对照保存即可。
2.3 先观察时序,再动代码
很多朋友一上来就盯着 JS 看,容易陷入细节。我更建议先观察「cookie 是什么时候写入的」。在 DevTools 里启动录制,勾选Preserve log,然后刷新挑战页。接下来打开 Application 面板的 Cookies,刷新一下看看reese84是什么时候出现的。
有了这个时间点,再去 Network 面板里对比对应时间的请求。一般会发现:在 reese84 写入前,浏览器会先请求一个或多个外链脚本;reese84 写入后,页面自动刷新,携带 cookie 再次请求主页。这个时序能帮你快速定位核心脚本是哪个,省去盲目读代码的大量时间。
另外建议在 DevTools 里直接开启 Script Profiler 或者 Performance 录制,可以看到 JS 执行的调用栈顺序。我用这个方式定位到了很多关键函数的入口,比纯静态阅读快得多。
3. JS 反混淆一步步拆解:从字符串还原到控制流恢复
采集完素材之后,真正费功夫的环节来了:反混淆。Incapsula 的 reese84 防护脚本号称花费了大量精力做混淆,事实也确实如此。我拿到的脚本里包含了变量名混淆、字符串数组化、控制流平坦化和自执行函数嵌套等多种手段。这里我把自己的拆解流程详细展开。
3.1 识别混淆类型:先看清楚敌人长什么样
拿到 JS 文件后,第一步不要急着还原,先格式化。在 DevTools 里打开 pretty print,或者本地用prettier格式化一下。格式化后观察代码结构,判断混淆类型。
我看到的脚本呈现几个典型特征:
- 大量 hex 编码的字符串数组,调用方式类似
_0x3f2a[0x1]; - 一个自执行函数作为字符串解密器,传入一个数组,返回一个解密方法,后续通过这个方法索引真实字符串;
- 整个主逻辑被拆成很多层嵌套的函数,函数名全部是
_0x开头的十六进制风格; - 部分代码块被改写成
while(switch-case)结构,也就是控制流平坦化。
不同混淆手法,对应的还原策略不完全一样。字符串数组化解密相对容易,控制流平坦化需要花更多心思。
3.2 用 Babel 写 AST 还原脚本:字符串解密
字符串解密是所有还原工作的基础。混淆 JS 里几乎所有有意义的字符串(属性名、错误信息、常量值)都被提取到一个大数组中,真正运行的时候通过解密函数动态获取。不把这段逻辑还原,后面的代码根本没法读。
Babel 可以把 JS 源码解析成 AST,翻译后重新生成代码。我在本地用@babel/core和@babel/parser写了个小脚本,思路如下:
- 找到字符串解密函数(通常是接收索引参数,返回解密后的字符串);
- 将其执行方式从
_0xXXXX(index)替换为直接量字符串; - 递归遍历 AST,把函数调用替换为对应的字符串值。
以一个简化的例子来说,原始代码长这样:
(function(arr, key) { var result = []; for (var i = 0; i < arr.length; i++) { result[i] = arr[i]['toString']()['charCodeAt'](0) ^ key; } return result; })(['a', 'b', 'c'], 0x3f)对应的调用点就是_0x2f8b(1)。用 AST 解析后,把调用点直接替换成字符串'b'即可。实操中解密函数往往更复杂,包含字符位移、base64 解码等,但思路是一致的。
做完这一步,你会发现脚本的可读性提升了一大截。常用的属性名、函数名都变成有意义的字符串了,代码逻辑开始浮现。
3.3 控制流平坦化:把 switch-case 分发器拉直
控制流平坦化是让我头疼了一阵子的点。它的表现形式是:原代码中一段顺序执行的逻辑,被拆成许多基本块,所有块都放在一个while(true)循环里,用一个 state 变量标记当前执行到哪个块,每个块末尾通过 switch-case 跳到下一个块,分发器本质是一个状态机。
还原策略有两种:一种是手动梳理状态转移关系,把基本块按顺序拼回去;另一种是用现成的 AST 工具库deobfuscator。我的建议是能跑工具先跑工具,跑不完再手动。
实际操作中,我用的是deobfuscator这个 npm 库处理控制流平坦化:
npm install -g javascript-deobfuscator javascript-deobfuscator input.js -o output.js处理完后再用之前的字符串解密逻辑过一遍,代码基本就能读了。需要注意的是,工具也不是万能的,某些嵌套过深、状态转移复杂的情况它会漏掉,此时需要自己从关键位置入手手动还原。
3.4 还原后的代码轮廓:找主函数、找入口点
完成字符串解密和控制流还原后,脚本的主干逻辑基本能看清了。接下来的任务是找到「加密主函数」和「cookie 写入函数」。
我的做法是在还原后的代码里搜索几个关键词:cookie、reese84、setCookie、location、refresh等。一旦找到对应位置,就可以顺着调用链往上追溯,找到生成 reese84 值的核心函数。
实际代码中,reese84 会先调用一个类似 getToken() 的方法,传入一堆环境信息,返回值是一个字符串;之后通过 document.cookie 写入。从这个函数往内部深入,就是加密逻辑的全貌。
4. 追踪动态 Token 的生成细节:指纹采集与加密算法
反混淆做完只是第一步,真正要复现动态 Token,还得把核心函数的每一步看明白。这个阶段我花了最多时间,也踩了不少坑,尤其是那些藏在不起眼角落里的环境检测逻辑。
4.1 定位关键函数:用 hook 替代瞎猜
在静态分析的基础上,我用了一个很实用的技巧:hook document.cookie 的 setter,在浏览器里记录 cookie 写入时的调用栈。这样能直接看到是哪个函数、哪一行代码写入了 reese84,以及传入的值是什么。
在控制台执行:
var cookieSetter = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie').set; Object.defineProperty(document, 'cookie', { get: function() { return cookieGetter.call(document); }, set: function(val) { if (val.indexOf('reese84') !== -1) { console.trace('reese84 cookie set:', val); } return cookieSetter.call(document, val); } });这么一搞,reese84 在哪个执行上下文、哪个函数里被写入,控制台直接打印调用栈。配合 Chrome 调试器的 step into,就能一步一步看到核心函数的执行过程。
我用这种方式定位到了generateToken相关的函数。它的参数里有浏览器的 object 引用,里面塞了 UA、平台、语言、屏幕分辨率等一堆字段。
4.2 理清 Token 生成的数据流
从定位到的函数入手,我把 Token 生成的数据流梳理了一下:
- 首先收集一组环境数据,形成一个对象;
- 然后把对象里的字段按固定顺序拼接成一个字符串;
- 对字符串做一次编码(有些版本是先 base64,有些是直接拼接原始值);
- 再经过一个自定义的混淆算法处理,生成最终的
reese84字符串。
这里每个站点的具体实现细节可能不同,但大体思路是一样的:把浏览器指纹和某些定时随机值绑在一起,做一次对称加密。加密后的结果不单是个 token,更像一个自包含的「身份凭证」。
值得特别注意的是,脚本里很多看似没用的代码其实是检测点。比如navigator.webdriver的值会被读取并拼接到加密内容里。如果你在模拟时漏掉了这个字段,服务端验签时发现 hash 对不上,会直接拒绝。
4.3 识别加密算法:不是每个加密都叫 AES
看了几个版本的 reese84 脚本后发现,不同站点、不同配置下选用的算法可能不同。有的版本用的是自研的异或混淆加 base64,有的版本用了类似 RC4 的流加密,也有版本会掺杂 HMAC 签名。
识别方法很简单:在核心函数处打上断点,观察它操作字符串的形式。如果看到大量 parseInt、toString(16) 这种十六进制转换,多半是自定义算法;如果直接调用了crypto.subtle或引用了subtle.encrypt,那大概率是基于 WebCrypto 的标准算法,比如 AES-GCM、AES-CBC。
我在那个项目里遇到的版本更偏向自定义混淆,它在字符串拼接后经历了两轮 base64 编码,中间还插入了几个固定盐值。这个盐值藏在脚本开头的数组里,如果不细心还原,几乎不可能发现。
4.4 别忘了环境信息的采集顺序
环境数据换算的顺序也很关键。服务端验签时,是按照固定的字段顺序重新拼接数据再解密的。你生成 Token 时字段顺序必须与脚本一致,否则结果完全不同。
以我处理的这个项目为例,token 生成前的数据对象类似这样:
{ userAgent: navigator.userAgent, language: navigator.language, platform: navigator.platform, vendor: navigator.vendor, screenWidth: screen.width, screenHeight: screen.height, colorDepth: screen.colorDepth, timezone: new Date().getTimezoneOffset(), canvas: fingerprintCanvas(), webgl: fingerprintWebGL() }这些字段的顺序、取值方式都要对照脚本一一复现。比如 screen.width 和 window.outerWidth 在某些环境里是一样的,但有些环境可能有差异,而这种差异会导致 Token 不一致,最终被风控拦下。
5. 在 Node.js 里生成合法 Token:模拟浏览器环境的完整实现
静态分析做完后,终于到了动手写代码的阶段。这一步的核心目标是:在 Node.js 环境中执行还原后的 JS 逻辑,生成一个和真实浏览器里一致的 reese84 cookie。这里有几个关键决策和实现细节需要展开。
5.1 环境兼容性处理的两种路径
要在 Node.js 里跑浏览器 JS,通常有两条路线:
路线 A:用 jsdom 模拟完整 DOM 环境。jsdom 会把 window、document、navigator 这些对象全部模拟出来,代码改动最小。但 jsdom 对 canvas、WebGL 这类底层 API 的支持非常有限,而且运行速度慢,容易在环境检测上被卡住。
路线 B:自己 mock 关键的全局对象。只实现脚本运行时需要的那几个对象和方法,其他一律不管。代码量虽多一些,但可控性强、性能好,这也是我在多个逆向项目中总结出的最优解。
我选择的是路线 B,因为 reese84 的脚本运行并不依赖完整的 DOM 树,它主要就是在读取全局对象、做字符串运算,然后写 cookie。自己 mock 足矣。
5.2 Node.js 侧 mock 的核心对象
mock 不是无脑地给每个对象塞几个属性,而是要精确还原真实验证时涉及到的所有字段。我按脚本的读取顺序整理了一下。
navigator对象至少需要这些字段:
global.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36', language: 'zh-CN', platform: 'Win32', vendor: 'Google Inc.', webdriver: false, languages: ['zh-CN', 'zh'], hardwareConcurrency: 8, deviceMemory: 8, maxTouchPoints: 0 };screen对象:
global.screen = { width: 1920, height: 1080, colorDepth: 24, availWidth: 1920, availHeight: 1040 };window对象在 Node.js 里相对好处理,直接挂到 global 上就行。讽刺的是,Node.js 本身没有 window 这个概念,所以global.window = global这个操作几乎是跑所有浏览器 JS 的前提。
document.cookie的读写逻辑也要 mock。我写了一个简单的存取数组:
let cookieStore = {}; global.document = { get cookie() { return Object.entries(cookieStore).map(([k, v]) => `${k}=${v}`).join('; '); }, set cookie(value) { const [pair] = value.split(';'); const eqIdx = pair.indexOf('='); cookieStore[pair.slice(0, eqIdx).trim()] = pair.slice(eqIdx + 1).trim(); } };这样脚本里执行document.cookie = "...reese84=xxx..."时,我们就能在本地 cookieStore 里拿到结果。
窗口对象、定时器、随机数这些也比较关键。脚本里如果用了Math.random()或类似函数生成随机盐值,必须确保随机数的生成逻辑一致。部分版本的 reese84 会读performance.now()来测量执行耗时,作为人机检测的一个信号,如果缺失或异常,也会被识别。
5.3 把还原后的逻辑封装成可复用函数
当所有 mock 完成后,剩下的就是把还原出的核心函数搬到 Node.js 环境里执行。我的项目里最终形成了一个generateToken.js文件,结构大致如下:
const vm = require('vm'); // 初始化环境对象 function createContext(ua) { // ... 上面的 mock 逻辑 } // 从还原后的脚本中提取核心生成函数 function getToken(context) { const sandbox = { window: {}, navigator: {}, screen: {}, document: {}, console: console, setTimeout: setTimeout }; sandbox.window = sandbox; vm.createContext(sandbox); // 把还原后的脚本内容塞进去执行 vm.runInContext(scriptContent, sandbox); // 调用脚本暴露出来的 generateToken 方法 return sandbox.window.generateToken ? sandbox.window.generateToken() : null; } module.exports = function generateReese84(ua) { const context = createContext(ua); return getToken(context); };由于在原脚本里,核心函数通常是挂在 window 上的,或者通过自执行函数回调传出去的,我在还原时做了一点小改动:在脚本最底部加一行显式导出。这个操作是在分析阶段做的,不会影响 token 生成结果。
5.4 第一次拿到合法 Token 的验证方法
代码跑通后,第一时间别急着全量采集,先拿一个 Token 试试水。建议分三步验证:
第一步,模拟第一次请求,不带 cookie 访问目标站,确认返回 403 挑战页。
第二步,调用 generateToken 获取 reese84,打印出来看看长度、结构是否和浏览器里抓到的 cookie 一致。正常的 reese84 一般是一个几百字符的字符串,如果明显短了或者格式不对,说明还原逻辑有问题。
第三步,带 cookie 发起第二次请求,带上同样的 UA 和 header 集合,看是否返回 200。这一步还需要确保请求头里的 user-agent 和你生成 Token 时 mock 的完全一致。
我在这步踩过一个坑:虽然 mock 的 navigator.userAgent 是 Chrome 的 UA,但 Node.js 里 fetch 默认带的 UA 是node-fetch之类的,如果不手动覆盖,服务端一比对就发现不一致,直接拒绝。所以每次请求都要显式加上user-agentheader,并且和生成 Token 时保持一致。
6. 实际绕坑过程中那些容易卡住人的隐蔽细节
在从「能跑」到「稳定跑」的过程中,我踩了好几个坑。这里集中整理一下,给后面动手的人提个醒。
6.1 Canvas 和 WebGL 指纹:最隐蔽的检测点
在初始版本里,我只 mock 了 navigator、screen 这些基础对象,直接用脚本跑了,结果生成出来的 Token 拿去请求,服务端一直返回 403,连一次 200 都没出过。后来在调试时发现,脚本在某个分支里调用了document.createElement('canvas')并画了个图,然后通过 toDataURL 取了一段摘要。
这个摘要被拼接进了加密内容。如果我没模拟 canvas 的生成逻辑,这段值就和真实浏览器对不上,服务端验签直接失败。
没办法,只能把这个函数一并还原。我找了一个 headless 浏览器的截图工具,在本地预先生成同样的 canvas 指纹字符串,然后在 mock document.createElement 时直接返回预设的值。
WebGL 指纹也类似,脚本里会创建一个 canvas 上下文,读取 WebGL 的扩展信息、渲染器名称等。这类值就算在真实浏览器之间也有差异,服务端主要对比的是格式是否合法、是否和 UA 匹配。所以我的建议是:在 mock 字段时,尽量用一套和自己 UA 匹配的常规值,不要背负侥幸心理去随机生成。
6.2 debugger 陷阱与时间检测
reese84 脚本里还有几个 debugger 陷阱,在浏览器里调试时,遇到debugger语句会直接卡住。这是为了防逆向做的干扰手段。处理方法是在 DevTools 里右键选择「Never pause here」,或者全局禁用断点。脚本里还有一种时间检测:记录某个函数执行前后的时间差,如果发现耗时过短,会认为不是真实浏览器。
这类时间检测在 Node.js 环境下影响不大,因为我们的执行速度和真实浏览器差不多。但如果你在 mock 时用了过重的代理逻辑,导致某一段执行时间明显较长,也可能触发异常。我的建议是,尽量保持脚本逻辑原样,不要为了图快乱改执行流程。
6.3 请求频率与 IP 质量:算法还原之后的大山
算法还原只是第一步。你可能发现,即使 Token 完全正确,高频率请求依旧会触发风控,被强制跳转回挑战页。这其实是风控体系的第二个层面:频率维度的防护。
我实际测试下来,单个 IP 每分钟请求超过 20 次左右,触发概率就明显增加。所以一定要做好限速和代理 IP 的轮换。值得注意的是,reese84 生成的 Token 和 IP 是绑定的,换 IP 后必须重新生成 Token,不能复用旧 IP 下的 cookie。
6.4 合规边界:写在技术方案之外
这个话题必须说清楚。我演示的整个过程属于「安全研究与授权测试」的范畴。如果你是做爬虫采集,一定要确保目标网站允许这种形式的数据获取,或者已经获取了书面授权。没有任何授权的情况下,绕过防护去抓数据,既不合规也不安全。我之前接的项目都是由客户提供书面授权,大家千万别把技术用错地方。
这个技术本身的研究价值在于:理解商业风控系统如何工作,以及如何在合理的应用场景中进行数据获取和自动化操作。它不应该成为攻击目标系统的工具。
7. 把方案落地成可维护的模块
绕坑过程走完后,最终的产出应当是一个可长期维护的模块,而不是一次性脚本。我在项目里把它封装成了一个独立的 npm 包结构,里面包含三个部分。
7.1 模块拆分:环境 mock、Token 生成、请求封装
| 模块 | 职责 | 说明 |
|---|---|---|
env.js | 提供 mock 的 navigator、screen、document 等全局对象 | 每次生成新 Token 前重置状态 |
token.js | 加载还原后的 JS,执行并返回 reese84 | 核心函数入口 |
client.js | 封装请求逻辑,自动完成挑战流程 | 先请求页面,发现 403 就生成 Token 再重试 |
这三个模块各司其职。env.js相对独立,如果后续发现脚本读取了新的环境字段,只需要在env.js里补上即可。token.js里放着还原后的 JS 脚本,如果目标站点把脚本更新成新版,也只需要替换脚本内容并重新过一遍还原流程。client.js则是一个对上层完全屏蔽细节的接口。
7.2 Token 失效与自动重试机制
实际使用中发现,reese84 cookie 的有效期大概在几分钟到十几分钟之间,具体取决于服务端配置。过期后继续使用,服务端会重新返回挑战页。因此,需要在请求层实现自动识别和重试:
async function fetchWithRetry(url, options = {}) { let response = await axios.get(url, options); if (response.status === 403 && response.data.includes('challenge')) { const token = generateToken(options.headers['User-Agent']); options.headers['Cookie'] = `reese84=${token}`; response = await axios.get(url, options); } return response; }这里还要考虑到并发场景。如果是多个请求同时发现 403,不要每个都重新生成 Token,而是做一个缓存锁,只让一个请求负责生成,其余请求 etc. 等它完成后再复用同一个 Token。这样可以避免频繁执行 JS 造成的性能浪费。
7.3 监测脚本更新:维护成本要放在心上
Incapsula 会不定期升级脚本。最直观的体现是挑战页 HTML 里内联 JS 的内容变了,或者外链脚本的文件名变了。我在项目里加了一个简单的监测函数:定时请求一次目标页面,检查返回的 HTML 里是否包含 reese84 相关的关键特征,如果特征变了就触发告警。
这种维护成本无法避免,但通过模块化设计,升级时只需要重新跑一遍反混淆流程,替换脚本内容即可,不需要改业务代码。考虑到 reese84 涉及的算法复杂度,整体维护成本还在可控范围内。
我在这个项目里最大的收获,是对「JS 逆向」这件事有了更系统的方法论:先抓时序,再反混淆,然后定位核心函数,最后环境模拟与复现。最难的不是某一环节的技术深度,而是把每个环节串联起来形成闭环。希望这篇记录对你处理 Incapsula 的防护问题有帮助,如果你在实操中遇到了不一样的环境检测点,也欢迎交流讨论。