1. 先说结论动态 Cookie 到底动在哪做 JS 逆向分析和接口对接的朋友大概率都被一类每次刷新都不一样的 Cookie 折磨过。这种 Cookie 不是服务端通过 Set-Cookie 下发固定的 session id而是由页面里的 JavaScript 在运行时动态计算出来的。它的名字可以五花八门今天我们要拆的这个原形就叫ssxmod_itna。为了表述方便下文我统一叫它盼之动态 Cookie这是我自己起的代称不代表任何真实站点。这类 Cookie 的核心特点是你刷新一次页面它的值就会变化你换一个浏览器、改一次 User-Agent它的值也变化甚至你在同一页面停留几秒再发送请求它可能又是另一个值。很多人第一反应是是不是加密了固定的东西加了个时间戳实际拆完你会发现它通常等于时间因子 随机数因子 环境指纹签名三件套的组合编码。拆这个 Cookie 的思路跟拆绝大多数前端反爬签名机制是通用的先定位生成入口再还原算法最后用工程化方式在 Node 或 Python 里复现。这篇东西适合谁适合做爬虫工程、前端安全研究、接口自动化、需要进行已授权平台兼容开发的开发者。我会从定位讲起把 Cookie 的生成入口、原理拆解、两种还原路径和常见异常都过一遍。整个分析过程强调一个原则所有操作均限于自建演示环境、开源项目或已获得授权的测试目标反对用这些技术去突破未授权系统的访问控制。要理解这类动态 Cookie首先要跟传统的静态 Cookie 区分开。静态 Cookie 是服务端分配好、浏览器只负责保存和携带。动态 Cookie 则完全由前端 JS 在内存里算出来再通过document.cookie或请求头直接带上。服务端收到以后会用同样的算法反向校验这段字符串里的时间戳新鲜度、随机因子是否合法、环境指纹是否像真实浏览器。换句话说它不是一个账号标识更像一个环境签名。2. 定位入口找到 Cookie 是在哪一行代码里生成的2.1 全局搜索不是万能但永远值得先试拿到一个页面第一步当然是全局搜索 Cookie 名。打开开发者工具在 Source 面板里按 CtrlShiftF搜ssxmod_itna如果有结果最好直接跳到赋值那一行往下读就行。但这里有个坑很多站点会做字符串离散化明明代码里生成了几个段最后拼接时用a b c中间可能还夹着Array.prototype.join和String.fromCharCode导致ssxmod_itna这个字符串本身不会完整出现。这种情况怎么办有两种对策。第一种是对document.cookie做 Hook用Object.defineProperty替换掉 document 对象的 cookie 属性 setter在注入点打印完整的调用栈。Object.defineProperty(document, cookie, { get() { return documentCookie; }, set(cookieString) { if (cookieString.includes(ssxmod_itna)) { console.trace([cookie hook] →, cookieString); debugger; } documentCookie cookieString; } });这段代码建议在页面加载的最早期注入用 Override 或浏览器插件的方式都行。命中以后调用栈会直接把你带到赋值处后面的工作就顺了。第二种是监听网络请求的 Initiator。你可以在 Network 面板里找到第一个带上ssxmod_itna的请求点开 Initiator 列的调用链通常就能看到是哪个 JS 文件里的哪一行发起的请求。这个方法用来定位哪个模块发起的非常有效但它不会带你到 cookie 字符串生成的那个函数因为发起请求的地方和生成 cookie 的地方不一定是同一段代码。2.2 格式化代码后先读结构不要急着找加密函数现在假设你已经通过 Hook 跳到了生成相关的位置。但不少站点为了防分析会用混淆器把代码压缩成几行甚至一行。我见过的典型输出大概长这样var _0xabc [\x73\x73\x78\x6d\x6f, \x64\x5f\x69\x74\x6e\x61, ...]; function _0xdef(a, b) { ... }这种情况下不要立刻一头扎进加密函数开始逐行读先做三件事把代码格式化搞清楚整体控制流是先算时间、再取随机数、最后拼接吗把所有字符串数组找出来还原字符串你会看到很多navigator.userAgent、canvas、toDataURL、charCodeAt之类的关键词这些关键词本身就是成分表。找到最外层的调用顺序给它画一条粗略的数据流路径输入 → 中间变换 → 拼接 → 写入 cookie。我强烈建议用一个小技巧在格式化后的代码里搜索.length、substring、charCodeAt、fromCharCode、Date.now、Math.random这些 API 是绝大多数前端 Cookie 签名算法的积木。搜索一遍你基本就能猜到整个算法用的是哪些原材料。2.3 一个容易忽略的细节Cookie 可能不是一次生成的有些动态 Cookie 并不是在一段代码里一把梭算出来的。它可能先由服务端返回一个种子值比如某个接口响应里的一个字段前端拿着这个种子值再做一轮运算最后在某个后续请求前拼装完成。所以如果你 Hook 到document.cookie被设置了初始值但后续又不见了别急着下结论多刷新几次页面记录一下 Cookie 值变化的顺序。我的习惯是做一个值快照表每次刷新之前先记录当前 Cookie 值刷新后再记录新值对比两次的差异。如果只有后半段变化说明包含时间因子如果整体都变了说明初始种子也在变。这个简单的观测动作能帮你省下后面很多无意义的逆向时间。3. 生成原理拆解ssxmod_itna 内部的三段结构3.1 从形态上先猜结构当你拿到了生成代码并读懂大概逻辑以后我建议先不要在算法细节里纠结而是把最终的 Cookie 字符串拿来切段。我这里把一个典型值脱敏一下拆给你看ak3Jd9f2WqFv8Hc0XmN1 ... 7sBvQz2KpLwR4YtUaD |--- 第 1 段A 因子---|--- 第 2 段B 因子---|很多这种形态的 Cookie 都遵循一个规律后 8 到 16 位往往是前面所有内容的校验尾类似哈希摘要或者 CRC 值中间位通常是可以解码出来的环境字符串最前面几个字符则直接反映了当前时间。拆段之后你再去代码里找各个段的生成边界会非常轻松。3.2 核心算法RC4 变体和字符串异或在我拆过的众多前端 Cookie 生成实现中ssxmod_itna这类形态的生成算法使用最多的一类是 RC4 变体加字符串异或。所谓异或就是把两个字符的 charCode 按位异或得到新的字符编码再通过自定义字符集映射输出。这个算法简单、高效、易写服务端还原也容易因此前端加密里特别常见。举一个非常精简的伪代码结构function encrypt(input, key) { let result ; for (let i 0; i input.length; i) { let code input.charCodeAt(i) ^ key.charCodeAt(i % key.length); result charset[code % charset.length]; } return result; }实际实现里还会混入循环位移、字符集映射表和随机干扰位。你不需要一开始就恢复得一模一样第一步是验证是不是异或/RC4类算法。验证方法很简单改一下输入里的一位字符看输出变化范围是局部还是全局。如果只改一个字符输出只有对应位置变说明是逐位运算大概率涉及异或或查表映射。如果输出整个都变了那可能是带模式扩散的算法比如哈希加盐。3.3 环境指纹为什么换 UA 后 Cookie 就变了拆到后面你会发现ssxmod_itna并不只是时间和随机数在参与它还混入了浏览器的环境特征。最常见的特征源包括navigator.userAgent、navigator.platform、navigator.languagewindow.chrome对象是否存在及其属性数量Canvas指纹通过绘制特定图形后取toDataURL()的哈希AudioContext指纹通过特定音频处理链路拿到输出特征为什么签名里要混环境指纹因为服务端只有确认这条 Cookie 来自真实浏览器环境才会认为请求可信。如果某个脚本只是用 Node 直接发请求没有这些环境特征生成的 Cookie 服务端就能判定异常。这给我们的还原工作带来了一个很重要的提示想要稳定复现这个 Cookie光知道算法不够还需要把环境信息喂进去而且喂进去的环境信息必须跟最终发请求时 Headers 里呈现的 User-Agent 保持一致。我之前在实际分析中踩过这个坑算法已经完全还原了Python 里生成了同样的 Cookie但发过去还是返回校验失败。后来仔细对比才发现生成 Cookie 时用的 UA 是 Chrome 120但请求头里带的是 Chrome 125。环境和签名不一致服务端一验就露馅。这个问题后面会专门讲。4. 还原落地两条路线怎么选才是真·工程实践4.1 方案 A在 Node 里搭一个环境沙箱跑原版 JS这个思路说白了就是我不改算法我造环境。你把这个加密 JS 从原页面里抠出来丢进 Node 的一个 vm 沙箱里然后把浏览器特有的一些对象一个一个补进去比如window、navigator、document、canvas等。补全之后直接调用里面的window.generateCookie()或者类似函数让它自己算出一个 Cookie 值。这个方案的优点是还原成本低不需要逐行翻译算法只要代码能跑就行。缺点也明显代码里可能有非常多的环境检测点某个属性不存在或者取值不对就直接抛异常或返回空值。补环境本质上是个拆东墙补西墙的活常见套路是在沙箱外面用一个 Proxy 代理访问记录所有被访问的属性然后逐个补齐const sandbox { navigator: { userAgent: ..., platform: Linux }, window: {}, document: { cookie: , createElement: function() { ... } }, screen: { width: 1920, height: 1080 } }; const context vm.createContext(sandbox); vm.runInContext(jsCode, context);如果你观察记录显示代码还访问了history、location、localStorage、Performance这些 API通常也要一并补上。但补环境有个副作用代码访问的属性越多你需要伪造的浏览器行为也越多工作量会直线上升。所以这里我的建议是能拿到就先用原版 JS 跑通跑不通再看缺什么补什么绝大多数情况下你不需要把整个浏览器实现出来你只需要让加密函数计算时刚好不报错。4.2 方案 B纯算法还原用 Python 或 Node 直接算方案 B 是真正意义上的逆向后写一份等价实现。你需要把 JS 里的加密/编码逻辑完整翻译成目标语言。这一步对 JS 逆向基础有一定要求但做熟了一次以后后面遇到类似的签名算法会快很多。标准流程是这样的把生成的输入参数确定下来。比如输入字符串 时间戳 固定前缀 环境变量拼接值 随机数。把 JS 里的转换函数逐步还原遇到字符集映射表就导出成一个数组。用一个This is a test string的已知输入分别跑原版 JS 和你的 Python 实现对比输出。如果结果不一致定位到差距常见原因是字符编码不一致、索引越界、常量值抄错或 JS 的数字与 Python 的数字在取整上出现差异。批量验证构造 50~100 组样本对齐到 100% 一致再接入正式逻辑。如果你的核心场景只需要在服务端生成 Cookie不需要在浏览器里执行任何东西方案 B 通常更稳。但前提是你的逆向足够准确。站在工程稳定性角度我比较倾向方案 B因为原版 JS 的运行环境重依赖 Web API一旦目标站点算法更新你维护的成本高。而方案 B 只要保证算法等价问题就能缩小到新算法多改了哪个常量、哪个字符集这个级别。4.3 两个方案的取舍表对比项方案 ANode 沙箱跑原版 JS方案 B算法翻译为 Python/Node还原工作量小主要补环境大需要逐函数翻译运行稳定性依赖环境补全程度高逻辑可控执行速度偏慢需启动沙箱环境快纯计算站点升级后的维护可能要重新补新特性直接对比新 JS 中变化的部分适用场景算法复杂度极高、临时验证长期、稳定、批量使用的场景我个人的实践建议是第一次拆一个动态 Cookie 时先用方案 A 把整条链路跑通确保你对生成逻辑的理解是准确的。然后再花点时间做方案 B哪怕只是把核心核心函数翻译过去。两套互相验证比其中任何一套单独放在生产环境里都可靠。5. 高频异常排查Cookie 生成了却不被认可5.1 请求被拒生成值和请求头环境不一致这是最最常见的问题。你已经在代码里生成了漂亮的 Cookie放到请求里却发现服务端返回校验失败或者干脆没有返回业务数据。前面提到过很多签名算法把 UA、平台信息作为输入因子如果生成 Cookie 时使用的环境与最终请求头中的 User-Agent 不一致服务端就会判定不合法。即使你算法完全还原对了只要设备指纹对不上一样白搭。处理方式是把生成 Cookie 时的环境参数和真正发请求时带的请求头参数做成同一份配置。比如你定义了当前伪装成 Chrome 125 / Windows那么生成 Cookie 时的 navigator 模拟数据必须是 Chrome 125 / Windows请求头里的 User-Agent 也必须是 Chrome 125 / Windows。放到项目里建议把 UA、平台、语言、时区写一个公共配置类生成和请求共用。5.2 时好时坏可能是时间窗口和随机数机制有些动态 Cookie 生成以后只在一小段时间内有效过了就失效。这时候你需要注意生成时取的时间戳与服务器处理时接收到的时间戳之间存在偏差。如果 Cookie 里的时间因子要求客户端与服务器时间误差小于一定阈值那本地时间是否标准就非常关键。还有一个容易忽略的点有些实现会记录生成时的页面性能时间甚至是performance.now()的调用顺序这类值没法人为精确模拟。不过通常服务端并不会校验得太严格它更关注的是时间差值是否在一个滑动窗口内。你可以从生成值的字符长度和变化规律来反推时间窗口。我推荐一个验证方法连续生成三个 Cookie分别相隔 1 秒、5 分钟、30 分钟然后逐一发送请求看看到底哪个时间点开始失效。这个实验能快速帮你确认时间窗口的大小也能帮你判断随机数部分是否真的参与校验。5.3 线上失效站点了更新算法如何快速定位差异如果你维护的动态 Cookie 方案上线一段时间后突然失灵不需要重新从头逆向一般只需要做两件事重新拉取最新 JS 文件对比生成的 Cookie 长度和字符集是否变化。如果长度变了说明新的输出结构变了定位到新的拼接代码即可。用 diff 工具对比新旧两份 JS 里加密函数的部分。很多站点更新算法只是换了几个常量、字符集或者替换了某个环境变量取值不会从零重写。再补充一个实用小技巧每次做完还原以后把 JS 源码、字符集映射表、关键常量、算法核心逻辑整理成文档放在项目里。下次站点升级时你能在半小时内完成对比而不是又从头开始。5.4 异常排查速查表异常表现可能原因排查方向生成了但服务端拒收环境指纹与请求头不一致核对 UA、平台、语言等参数是否统一有时成功有时失败时间窗口不足或随机数验证严格同步标准时间缩短生成与请求的间隔代码执行报错缺少属性沙箱补环境不完整用 Proxy 记录缺失属性并逐个补齐Cookie 长度与本地生成不一致算法版本更新或映射表变了重新抓取新 JS对比长度和字符集请求频率稍高就被拒绝服务端频控策略而非签名错误检查 Cookie 本身再考虑限速和代理池6. 写在最后动态 Cookie 逆向的几个稳定心法从我实际拆这些前端 Cookie 的经验来看这类签名的设计思路万变不离其宗时间戳保证时效、随机数保证不可预测、环境指纹保证来源合法最后再用一层自定义编码挡住初级的字符串搜索。当你面对一个全新的动态 Cookie先不要急着去找什么加密库而是把这三个成分挨个定位出来后面一切都会顺很多。在开发环节有两点建议一是优先把调试环境配好比如浏览器的 Override 功能能让你在刷新页面时直接替换加载的 JS 文件在任意位置断点调试这比反复修改源代码重新刷新效率高得多。二是做好样本留存。我自己每次做这类还原都会先留存 20 组真实的 Cookie 快照和对应的生成时间作为后续验证的基准数据这也让我在调整算法参数时有了明确的坐标系。最后还是要强调这类技术只建议用于自己拥有或获授权的系统安全研究的边界就是对边界本身保持敬畏。