说实话,第一次刷到猿人学第18题的时候,我压根没想着能在半天内把它啃下来。题名看着就俩关键字:AI、逆向。但等你真正把页面打开,看到那串每次刷新都会变、还带着一堆混淆逻辑的__jsl_clearance_sCookie 时,才会意识到这道题真正考的不是你会不会看 JS,而是你能不能在一个“被虚拟化保护”的代码迷宫里,用最短路径找到出口。
这篇文章我打算完全按我实际操作的过程来写,不绕弯子,不整虚的。我会从最开始的抓包、定位入口讲起,到怎么把混淆代码喂给 AI 辅助分析,再到最后补环境、跑通数据接口。整个过程里踩了不少坑,尤其是 AI 因为幻觉给我编了几个不存在的函数名,差点把我带沟里去。这些我都会在文末整理成速查表,给大家省点时间。
1. 这题到底在考什么:先说清楚目标
1.1 题目链路和核心机关
猿人学平台上的题目分类很明确,Web 方向的题目主要练的是“动态身份验证”和“反爬对抗思路”。第18题从页面形态上看并不复杂:你需要访问某个分页数据接口,比如api/match/18/getData?page=1,拿到 JSON 数据就算过关。但问题在于,你直接在浏览器里访问这个接口,返回的并不是数据,而是一段 HTML,里面塞着一段被高度混淆的 JS,这段 JS 会重新生成一个新的 Cookie,然后你需要带着这个 Cookie 再去请求,才能真正拿到数据。
顺带一提,如果你用删 Cookie 的“裸请求”去撞,服务器会直接给你一个 412 状态码。这就是题目里最核心的关卡:动态 Cookie 校验。
我在初判阶段就确定了几件必须做的事:
- 找到生成
__jsl_clearance_s的那段 JS 文件或内联脚本。 - 理解它的生成逻辑,包括种子值、时间戳、编码方式、循环次数。
- 找到一种稳定的方式复现这个 Cookie,可以是纯 Python 算法还原,也可以是补环境执行。
但难点在于,18题里的 JS 并不是普通的 ob 混淆,而是上了一个“虚拟机”级别的保护。代码里有一堆固定的操作码(opcode)、一个解释器循环、以及真正逻辑被打散的字节码数组。手动一行行看下去,基本等于在迷宫里找一根线头,非常痛苦。所以我当时决定换条路:先用 AI 做初步的“语义翻译”,再结合补环境的方式动态执行,跳过完全还原。
1.2 为什么选 AI 辅助而不是纯手撕
早几年做逆向,遇到混淆代码基本只能靠硬看加console.log插桩,再不行就动态调试断点。现在不一样了,像 GPT-4 这类模型对代码语义的理解已经很强,尤其是处理“一段代码到底干了什么”这种粗粒度任务时,效率远超人工。
不过我要先说清楚一个观点:AI 不是用来替你逆向的,它是用来帮你把“问题域”缩小的。比如面对 1000 行混淆代码,你直接问 AI“请逐行解释”,它大概率会给你一堆正确的废话,反而浪费时间。但如果你问“这段代码的输入是什么、输出是什么、主流程能不能概括”,它能很快给出一个可验证的方向。
我做了一个比较朴素的分工:
- 我负责:抓包、定位关键 JS、控制补环境时的浏览器指纹、验证结果。
- AI 负责:解释混淆代码的行为、给出可能的主流程伪代码、在报错时提醒我漏掉了哪个全局对象。
这个分工非常有用。特别是在第18题里,核心代码被虚拟化之后,纯逻辑看不懂,但行为却能通过“输入输出”的黑盒方式观察出来。AI 在这时候相当于一个经验丰富的翻译官,它不直接给你钥匙,但能告诉你钥匙可能藏在哪几个抽屉里。
2. 抓包与定位:所有逆向的第一步都是网络面板
2.1 从 Preview 到 Request Headers,锁定动态 Cookie
不管什么题目,逆向的第一步永远是打开 DevTools 的 Network 面板,老老实实看请求。我当时的操作路径是这样:
- 打开开发者工具,切到
Network选项卡,勾选Preserve log。 - 在页面上正常翻页,找到数据接口的请求。
- 点开这个请求,先看
Headers,最下面找到Request Headers里的Cookie字段。 - 然后清掉
__jsl_clearance_s,刷新页面,直接改请求再发一次。
重点就在第4步。删掉 Cookie 后再请求数据接口,返回内容会变成一段带 script 标签的 HTML。这就是整个过程的关键入口。我当时看到响应内容里有一个动态生成的脚本片段,脚本指向了一个 JS 文件,文件路径还带了一段类似时间戳的随机参数。也就是说,服务器在你没有合法 Cookie 的时候,会把这个“生成 Cookie 的 JS”当作挑战下发给你。
这个套路非常常见,在业界通常被称为“JS 挑战”,核心思路就是:服务器不是不让你访问,而是先丢给你一个需要执行特定 JS 才能解出的 Cookie,只有带着 Cookie 回来的人,才被认作“真实浏览器”。
我在这一步其实没花太多时间,真正的小坑在于:脚本加载路径不是固定的,每次刷新都会变,如果你以为它是一个静态 JS 然后直接保存,那必然失败。正确做法是先把当前请求返回的完整 HTML 保存下来,再从里面提取脚本地址,保持“一次性使用”的思路。
2.2 用 AI 辅助快速梳理脚本加载链
当我拿到这个动态 JS 文件后,第一感觉就是代码很“碎”,变量名全是_0xabc这种,而且里边还套了数组位移函数,最外层是一个大 IIFE。说实话,这种情况下直接硬看代码容易看吐。
我的策略是先把 JS 文件保存成独立文件,然后分段喂给 AI,但注意不是全量喂,而是有选择性地喂。AI 上下文窗口虽然大,但现在挤进去太多无效代码,它容易抓不住重点。我先做了这几步预处理:
- 用工具格式化混淆代码(网上现成的 JS Beautifier 就行)。
- 先找最外层的 IIFE,看它调用了哪些函数。
- 把“看起来像入口”的那几个函数截出来发给 AI。
我这里发的提示词大概长这样:
下面是一段前端 JS 混淆代码,我需要了解它的核心功能。请忽略反调试和格式化函数,只告诉我:1)这段代码最终会在浏览器环境里设置哪个 Cookie;2)Cookie 的 value 是怎么生成的;3)有没有使用固定的盐值或时间戳;4)有没有使用标准加密库。
AI 给的答案很直接:它指出代码里存在一个document.cookie赋值语句,value 的核心由一个函数生成,这个函数接受时间戳和固定字符串作为参数,返回结果会拼接成xxx|xxx|xxx的格式。虽然它没直接告诉我每一步的算法细节,但我已经知道下一步该去断点验证哪些位置了。
所以我的经验是:给 AI 的任务优先级,永远是“概括行为”大于“逐行翻译”。逐行翻译你早晚会做,但那应该是你自己为了写算法还原才去做的,而不是让 AI 在一开始就铺开所有细节。
3. 核心逻辑拆解:当我在逆向一个“虚拟化”的 JS 时,我在看什么
3.1 把混淆代码丢给 AI,先定主流程
拿到 AI 的“主流程预测”后,我并没有立刻相信。我不知道你有没有遇到过 AI 一本正经地说“这里是一个 AES 加密,密钥是 xxx”,结果你继续往下查发现密钥本身也是动态生成的。所以我的做法是:用 AI 的结论当线索,再去代码里做关键点验证。
在代码里找关键词,通常不是靠肉眼一行行扫,而是靠“结构化搜索”。我一般会用Ctrl+Shift+F全局搜索下面几个特征:
document.cookienew DategetTimecharCodeAtfromCharCode0x开头的十六进制数组eval或者Function构造函数
第18题里,这些关键词几乎都能命中。尤其是charCodeAt和fromCharCode同时出现时,往往说明核心逻辑在做字符串编码或者数字运算。我把这些关键代码段再丢给 AI,让它输出一个“类似行为伪代码”的版本,AI 给出的结果类似这样:
seed = 固定字符串 + 当前时间戳 循环1:对 seed 做某种变换,生成临时数组 循环2:按 opcode 指令表逐条解释执行,更新寄存器和内存 最终值 = 寄存器中某个位置的结果 cookie = seed + "." + 最终值我没有把 AI 的这版伪代码当终极答案,但它清楚地告诉我,这个 VM 的核心在于解释器循环,而我根本不需要完全看懂每条 opcode 的含义,只需要让这段 JS 在 Node 环境里跑起来,让它自己把最终值算出来。这部分思路,基本就是“补环境”的雏形。
3.2 字节码、opcode 与“VM”的还原
很多初学者听到“JSVMP”这个词就头皮发麻,觉得一定是超级难的算法。实际上它的本质很简单:把原本可以直接执行的逻辑,转换成一张指令表和一个执行引擎。指令表里的每个数字代表一种操作,比如加、减、跳转、取属性;执行引擎就是个大的 switch-case,读到什么 opcode 就执行什么操作。
第18题里的虚拟化代码,本质上就是这样。它把真正要执行的代码,先“编译”成一系列数字数组,然后由一个解释器逐条读取。这意味着你看到的代码根本不是最终执行逻辑,只是一台“虚拟机”的骨架。
那怎么破?行业里其实有几种共识性的思路:
- 硬还原 opcode:即把指令表里的每个数字对应的操作都梳理出来,最后用 Python 或 JavaScript 重写整个 VM。工作量非常大,适合拿来做研究,不适合快速刷题。
- Hook 执行环境:在浏览器里给
document.cookie的 setter 挂上钩子,或者直接在本地用 jsdom 模拟环境执行,然后等它算出结果。 - 补环境直跑:把混淆 JS 原封不动放到 Node.js 里,遇到报错就逐个补齐缺失的浏览器对象和函数,让代码认为自己在浏览器里,正常执行。
我选的是第3种。理由也很简单:这道题的核心目标是拿到合法的 cookie 值,而不是成为 VMP 逆向专家。如果直接执行比完整还原成本低一个数量级,就应该选择更经济的路线。
3.3 从算法还原到“行为对齐”的选择
我承认,在刷题的时候如果遇到算法不复杂、但是代码做了多层混淆的情况,直接还原算法是更优雅的方案。但在第18题里,VM 的指令数组是动态生成的,而且会跟随时间戳和会话种子变化。你要是想把 opcode 完全静态还原,等于给自己升级了一下难度。
所以我在决策时做了一个取舍:
- 保留 JS 原始执行能力,用补环境的方式,让 VM 自己计算出 cookie。
- 在 Python 侧只做“调度”——先请求拿 JS,再用 JS 引擎执行,最后带着生成的 cookie 请求真实数据接口。
- 对 AI 的使用重点放在“帮助我认识缺失的浏览器环境对象”,而不是“帮我翻译所有 opcode”。
事实证明这条路走得通。最终我甚至没有写一行算法还原代码,却拿到了全部页面数据。这不丢人,逆向的本质就是用最小成本拿到目标结果,能跑通比“看起来很硬核”重要得多。
4. 动手实现:从 JS 到 Python 调用链
4.1 环境准备与工具选型
整个项目我用的主力语言是 Python,JS 执行引擎走的是 Node.js 方案。原因很简单:Node.js 对 ES6+ 语法支持完整,而且第18题里的 JS 只有浏览器环境依赖,没有用到 req 之类的 Node 模块,所以只要能补上浏览器全局变量,跑起来几乎零障碍。
Python 这一侧,我用了最经典的execjs库来调用 Node。虽然它有一些性能损耗,但对于这种低频次生成 Cookie 的场景完全够用。如果你要高频调用,建议直接用subprocess起长驻 Node 进程,或者用js2py(但兼容性会差一些)。
安装其实就两条命令:
pip install PyExecJS npm install -g jsdom为什么提前装 jsdom?因为第18题的代码里会用到document对象,尤其是document.cookie。没有这个对象,脚本直接报ReferenceError: document is not defined。虽然我也可以手写一个很简化的 stub,但 jsdom 会更接近真实环境,能减少很多手指头。
另外提醒一句,PyExecJS 默认运行时在 Windows 上可能是JScript,这会带来一堆莫名其妙的兼容问题。建议在代码开头先显式指定 Node 运行时:
import execjs import subprocess node_path = r"C:\Program Files\nodejs\node.exe" # 改成你自己的路径 ctx = execjs.compile(js_code, cwd=r"C:\Program Files\nodejs")这个细节不处理的话,后续所有 ES6 语法都可能解析失败。
4.2 补环境实操:把缺失的对象和函数补齐
把 JS 第一次丢进 Node 跑的时候,我碰到了连续四五个报错,基本都属于同一个大类:环境缺失。具体报错顺序我记不清了,但大体有这几个:
document is not definednavigator is not definedwindow is not definedlocation is not defined
处理方式不复杂,直接在 JS 文件的最顶部,也就是所有代码执行之前,注入一段“环境准备代码”。我这里用的不完全是最简 stub,而是带一点真实性的伪对象,因为有些混淆代码会读取对象的属性,如果属性不存在同样会报错。
const jsdom = require("jsdom"); const { JSDOM } = jsdom; const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", { url: "https://match.yuanrenxue.cn/", referrer: "https://match.yuanrenxue.cn/", 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 = dom.window; global.document = dom.window.document; global.navigator = dom.window.navigator; global.location = dom.window.location;这里有一个坑,而且是我实际踩过的:直接const document = dom.window.document不行,因为在 Node 的 CommonJS 模块作用域里,const声明的变量不会挂到global上,JS 解释器执行到document.cookie时依然找不到它。必须用global.document = ...,让对象成为全局属性。
还有一个细微的点,jsdom 默认的userAgent是node.js,很多反爬代码会检测这个。所以我创建JSDOM的时候,显式把userAgent设置成了 Chrome 的 UA 串,避免后端通过 UA 识别出这是个假浏览器。这一步真的挺重要,我在初版跑通后发现有些请求返回值异常,排查来排查去,最后怀疑就是 UA 导致的行为不一致。
4.3 跑通第一个请求:校验顺序和时间戳
环境补齐后,我再跑一次脚本,document.cookie的位置终于不再报错。但问题又来了:生成的 cookie 只用一次,过几分钟再试就失效。这说明生成逻辑里带了时间戳,服务器在校验时会比对 Cookie 里的时间字段与当前时间的差值。
所以完整的请求链路必须是这样,顺序不能乱:
import requests import execjs import time 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://match.yuanrenxue.cn/" }) # 1. 先访问任意一个需要挑战的页面,拿到包含生成逻辑的 JS resp = session.get("https://match.yuanrenxue.cn/api/match/18/getData?page=1") html_text = resp.text # 2. 从 HTML 里提取 JS 代码(这里的提取逻辑要看你拿到的实际页面) js_code = extract_js_from_html(html_text) # 3. 用 Node 执行 JS 生成 cookie node_code = """ const jsdom = require("jsdom"); const { JSDOM } = jsdom; const dom = new JSDOM("<!DOCTYPE html>...", {...}); // ... 补环境代码 // ... 这里把题目 JS 塞进来 """ ctx = execjs.compile(node_code) cookie = ctx.eval("generateCookie()") # 4. 带上新 Cookie 请求真实数据接口 session.cookies.set("__jsl_clearance_s", cookie, domain="match.yuanrenxue.cn") data_resp = session.get("https://match.yuanrenxue.cn/api/match/18/getData?page=1") print(data_resp.json())有一点必须提醒:不同时刻获取到的主 JS 内容,可能因为挑战参数不同而略有变化。稳妥的做法是“每次需要 Cookie 时,都现做一次完整的 请求-提取-执行 流程”,而不是把 JS 缓存下来反复用。我第一次就是把 JS 当静态文件缓存了,结果过了十几分钟再跑,服务器直接给我返回一个全新的挑战脚本,旧的不管怎么执行都算不出合法 Cookie,白白排查了半天。
5. 踩坑记录与排查技巧实录
5.1 AI 给错代码时的“抠底”方法
我前面提到,AI 在分析混淆代码时给过我错误线索,这里展开说一下,因为这是 AI 辅助编程绕不开的坎。
有一次我问 AI“这段代码的入口函数是什么”,AI 回复了一个函数名,比如getClearance()。我去代码里全局搜索,根本找不到这个函数。原因很简单:代码里的函数是动态注册到window对象上的,函数名在每次请求时都可能变化,AI 是根据它见过的训练数据推测出来的,并不是代码里真实存在。
这种时候不要慌,也不要继续跟 AI 纠缠“你错了”。正确的做法是换个角度提问,比如:
请搜索代码片段中出现的
document.cookie赋值位置,并告诉我这个赋值语句位于哪个函数的作用域内,以及这个函数何时被执行。
把 AI 的注意力从“函数名”转移到“位置关系”上,它能更快给出可用信息。如果还是不对,那就老老实实自己在浏览器里下断点,看调用栈,这一步在什么时候都不能省。
5.2 时间戳不一致导致的 412
我在第一次跑通流程后,连续踩了几次 412。后来发现原因并不在 Cookie 格式,而是时间偏差。
Cookie 的值里包含了一段Date.now()生成的时间戳,而服务器会校验这个时间戳跟它本地接收请求的时间差。如果差太多,直接拒绝。我本地电脑的系统时间做了同步矫正,按理说不会差很多,但由于脚本执行有时间开销,从生成 Cookie 到带着 Cookie 发请求之间,隔着几秒甚至更久,一旦超过了服务器的允许范围,就会失败。
解决办法是把“生成 Cookie”和“请求数据”的间隔尽量压缩,最好在同一个函数里完成:
cookie = ctx.eval("generateCookie()") # 生成后立即使用 session.cookies.set("__jsl_clearance_s", cookie, domain="match.yuanrenxue.cn") resp = session.get(target_url)不需要每次调用时都重新请求挑战页,但生成完 Cookie 后不能有太多网络延迟和调试耗时。如果你是手动在断点里看结果,手里复制粘贴之后再去请求,大概率已经过期了。
5.3 常见报错速查表
我把实际过程中遇到的报错整理成了一张表,方便你排查时直接对照:
| 报错信息 | 可能原因 | 解决思路 |
|---|---|---|
document is not defined | 没有注入浏览器环境对象 | 用 jsdom 初始化并挂到全局 |
navigator is not defined | 只注入了 document 没注入 navigator | 把window的属性能挂的尽量都挂上 |
Cannot read properties of undefined (reading 'xxx') | JS 读取了某个缺失的属性 | 查看调用栈,给对应对象补 stub 属性 |
ReferenceError: window is not defined | Node 全局中没有 window | global.window = dom.window |
| 生成了 Cookie 但请求返回 412 | 时间戳偏差或 Cookie 过期 | 压缩生成-请求间隔,统一使用服务器时间 |
| 直接用 JS 文件缓存执行失败 | 挑战脚本每次动态变化 | 每次请求现拉取再执行,不要长期缓存 |
| execjs 报语法错误 ES6 | 跑了默认 JScript 运行时 | 显式指定 node.exe 路径 |
这张表看起来简单,但背后都是实打实的教训。尤其是最后一条,很多人在 execjs 上栽跟头就是没注意运行时切换,导致明明代码没问题,却一直报语法错误。
6. 如果用 AI 辅助逆向,我的工作流是什么
6.1 一个“题-人-AI”的分工模型
做完第18题之后,我把自己这套“AI 辅助逆向”的打法总结成了几个固定步骤:
- 出题阶段:先抓包,确定 Cookie 名、校验方式、JS 入口,这一步绝对不交给 AI,因为 AI 没有网络请求上下文。
- 解题阶段:把定位到的混淆代码丢给 AI,让它输出行为层面的伪代码,帮我快速理解“输入-输出”的关系。
- 验证阶段:把 AI 给的提示作为线索,自己在代码里搜索关键词验证,不能盲信。
- 执行阶段:优先用补环境直接跑 JS,遇到报错时把报错信息发给 AI,让它猜可能缺了哪些浏览器对象。
- 固化阶段:跑通后,把经验整理成可用脚本和速查表,方便以后复用。
这个模型的好处是,AI 被限制在“辅助分析”的边界内,不会出现让它自由发挥、结果带着我们南辕北辙的情况。而且即使 AI 给的信息是碎片化的,组合起来也能让我们少走很多弯路。
6.2 关于 AI 辅助,我最想提醒的一个点
我自己用过 AI 写各种各样的代码,但在逆向这个场景里,它的作用是“放大你的判断力”,而不是“替代你的判断力”。比如 AI 能快速告诉你这段代码可能用了 MD5、AES、Base64,但它不能替你决定该用“动态执行”还是“算法还原”,也无法替你做反调试绕过。最后做决策的,还是得靠人对整个请求链路的理解。
所以我特别建议刚开始接触 JS 逆向的人,不要一上来就想着“AI 能帮我搞定一切”。你应该自己做一遍定位、抓包、断点调试、补环境,把基本功打得差不多之后,再引入 AI 来提速。否则当 AI 给出一个错误判断时,你连“它在胡说”的感觉都没有,那就只能被动浪费时间了。
第18题给我最大的收获,不是“我会算这个 Cookie 了”,而是让我找到了一种在复杂混淆环境下快速逼近真相的方法:抓网络请求、看行为输入输出、让 AI 缩小范围、用环境直跑绕过算法细节。这套流程学完之后,我再去抓别的站的动态 Cookie,明显比以前快很多。
最后再分享一个小技巧:如果你卡在某个断点调不出来,别只顾着看代码逻辑,先试试点一下页面上的翻页按钮,看 Network 面板里有没有新的脚本被加载进来。很多挑战脚本是异步加载的,你不操作页面,脚本就不会出现,自然也就找不到入口。这个习惯,真的能帮你省掉很多白费功夫的时间。