做逆向实战时,最磨人的往往不是某个加密算法,而是像 ssxmod_dyna 这样每次刷新都会变化的动态Cookie。这个字段看起来不起眼,却是目标站点风控体系的第一道门。最近在一次授权范围内的接口分析中,我遇到一个站点:首页加载时会通过内联脚本种下一个动态Cookie,不带它,后续所有接口请求都会被拒绝。我花了两个晚上把这个Cookie的生成链路完整拆开,并还原出了一个可运行的生成函数。这篇文章会从抓包、断点、算法分析到验证,把完整过程记录下来,适合正在做JS逆向、反爬研究,或者想理解Cookie风控设计的朋友参考。
说明:为保护目标,下文中的站点信息与Cookie字段均已脱敏,我把它统一记为 ssxmod_dyna。整体技术原理与实际目标保持一致。
1. 项目背景与前期调研:确认动态Cookie在前端还是服务端
1.1 动态Cookie是什么,站点为什么需要它
先给不熟悉的朋友同步个背景。普通Cookie一般由服务端通过Set-Cookie下发,登录后固定不变;动态Cookie则多半由页面里的JavaScript在本地计算并写入document.cookie,每次访问都可能不同。目标站点把它作为“你是真实浏览器环境”的凭证,本质上是一种轻量级的人机校验。
站点为什么非要把凭证放在Cookie里?两点原因。一是实现成本低,不需要单独维护一套Token接口,只要在HTML里塞一段脚本就行。二是能挡住大量只拉HTML但不执行JS的采集脚本。服务端收到请求后,先看Cookie里有没有这个动态字段,字段格式对不对,在不在有效时间窗口内,只要有一项不对,就直接在网关层拦截,业务接口根本不会进入处理逻辑。
我这次遇到的情况正是这样:首页HTML里有一段内联JS,页面加载时先拼出类似 ssxmod_dyna 的字段并写入Cookie,之后所有业务接口请求自动带上这个Cookie。如果用普通的HTTP客户端直接去请求首页,虽然能拿到HTML,但由于没有执行里面的JS,自然也不会生成这个Cookie,后续请求全被拦。所以还原它的生成算法,等于解掉了自动化场景里的第一道锁。
1.2 动手前的边界判断与工具清单
开始打断点之前,我没有急着去读那一堆混淆代码,而是先判断两件事:第一,这个Cookie是纯前端生成的,还是前端从某个接口拿到的?如果是后者,应该去找接口,而不是在JS里死磕。第二,它是只跟时间戳和随机数有关,还是强依赖浏览器环境指纹?这决定了后面要补多少环境。
从抓包结果看,服务端只在首次返回HTML时带了一段内联JS,后续没有任何下发Cookie值的接口,所以可以判定为纯前端生成。工具准备上,我用了三样东西:
- Chrome DevTools,用来抓包、打断点、看调用栈;
- Node.js 环境,用来验证还原出来的算法逻辑;
- 一个干净的浏览器用户配置,避免历史 Cookie 和缓存干扰判断。
这套流程里最重要的一步,其实是把搜索范围收窄。通过 Network 面板确认动态Cookie只在首页内联脚本和两个业务JS文件里出现,后面所有断点都集中在这几个文件里做,效率会高很多。假如一开始就全站搜索“Cookie”,很容易在无关代码里浪费一晚上。
2. 动态Cookie注入链路与核心函数定位
2.1 抓包确认Cookie注入时机:两步定位入口
实操时我建议用无痕窗口打开目标首页,打开DevTools的Network面板,勾选Preserve log,然后刷新。这样第一条HTML请求的Response里就能看到完整的页面源码。在源码里搜 Cookie 字段名,比如搜 ssxmod_dyna,出现的位置就是生成逻辑的入口。
这里有个很常见的坑:很多站点写入Cookie时会用字符串拼接或模板字符串,直接搜完整赋值语句经常搜不到,因为中间被+拆开了。我习惯搜字段名片段,比如只搜dyna或ssxmod,命中后再去附近的上下文里找拼接逻辑。定位到赋值行之后,在那一行打断点,刷新页面,JS执行到这儿会停下,然后通过右侧的Call Stack逐层往回找,就能找到真正调用的生成函数。
还有一个辅助判断方法:看首页返回后的第一个业务请求,它的Request Headers里是否已经带上了Cookie。如果第一次业务请求就有,说明生成逻辑是同步执行的;如果第一次没有、第二次才有,那可能走了定时器或者二次异步请求。我这次遇到的是同步执行,所以打断点之后很快就定位到了。
2.2 使用Hook与断点定位加密函数
如果目标JS里加了不少反调试逻辑,直接在Sources里打断点会很痛苦。我的做法是先用Hook把document.cookie的赋值过程拦下来,在Console里提前注入一段代理代码:
Object.defineProperty(document, 'cookie', { get() { return this._cookie || ''; }, set(val) { if (val.indexOf('ssxmod_dyna') !== -1) { debugger; } this._cookie = val; return val; } });这样只要目标代码写Cookie,就会自动停在 debugger 处,同时Call Stack里能直接看到整条调用链。注入时机很关键,最好在页面脚本执行之前,可以用DevTools的“脚本注入”功能,或者配合浏览器扩展提前加载。
跑完Hook之后,我注意到一个细节:在执行流进入核心生成函数之前,跳过了三个类似环境检测的函数。它们会检查当前页面是否被自动化工具控制、是否修改过navigator或window上的关键属性。这说明目标站点的JS里也做了运行环境合理性校验。遇到这种情况,不需要硬刚整个检测链,直接把这几处检测分支改掉,让执行流尽快走到核心函数,反而省事。
2.3 从闭包变量拆解参数联动关系
定位到核心函数后,先别急着逐行读混淆代码。我习惯先看Scope面板里的闭包变量和输入源,把函数到底用了哪些外部值记下来。常见输入源包括Date.now()、Math.random()、navigator.userAgent、screen、document以及某些隐藏表单字段。
在这次的函数闭包里,我发现了四个关键依赖:时间戳、随机数、UA版本号,还有一个页面隐藏字段的值。前三者比较常规,隐藏字段才是这个算法里容易被忽略的部分。也就是说,生成结果不是只靠一个随机数,而是把多个因子拼到一起做变换。如果只按时间戳加随机数去还原,生成的结果格式可能对,但服务端校验时依然会失败,因为少了页面上下文里的那个字段。所以记录闭包变量时,一定要把每个依赖都记清楚。
3. 算法因子拆解与伪代码还原
3.1 算法结构拆解:从因子到变换流程
把核心函数读完之后,我把整个算法拆成了四个阶段:
- 因子收集:取时间戳、随机数、UA版本、页面隐藏字段;
- 字符串拼接:把这些因子按固定顺序用分隔符连接;
- 变换加密:做一轮哈希、十六进制转换、截断、反转或大小写处理;
- 写入Cookie:拼成
ssxmod_dyna=值; path=/;并写入。
这个“变换加密”其实不是真正的加密,而是一种混淆。它的作用不是让信息不可读,而是让最终生成的字符串无法被轻易预测。只要找对因子和变换顺序,还原并不困难。难点在于变换顺序经常被展开成多步嵌套写法,看着非常绕,从右往左读代码才能理清。
我这次还原出来的结构可以简化为这样:先取随机串,再取处理后的时间戳,加上UA版本号,拼出一个原始字符串;然后对这个原始字符串做一次SHA-256,取前18个字符,再反转;最后把反转结果和经过36进制转换的时间戳拼在一起,得到最终Cookie值。整体链路不复杂,但中间每一步都容易出错。
3.2 关键参数的计算过程还原
还原过程中,我从Call Stack的变量快照里抓到了几个关键参数。时间戳用的是毫秒级,但没直接用,而是先对某个固定值取模,再转成36进制字符串;随机数用的是Math.random().toString(36).substring(2, 10),这是很常见的8位随机串写法;UA版本号取的是 Chrome 主版本号,比如Chrome/124.0.0.0里的124。
把这些因子组合成一个示例:
const seed = Math.random().toString(36).substring(2, 10); const ts = Date.now() % 100000000; const uaVersion = '124'; const raw = `${seed}#${ts}#${uaVersion}#${pageToken}`;这里pageToken就是页面隐藏字段。不同站点对这些因子的取舍不一样,有的会用screen.width + screen.height,有的会用Canvas指纹。重要的是找到目标站点实际依赖了哪些因子,而不是照搬通用模板。
3.3 同构Demo实现:脱敏后的可运行代码
为了不伤及真实目标站点,下面这段代码里所有因子名称、常量、变换顺序我都做了脱敏替换,只保留结构一致性。它的产物可以用来理解算法思路,跑出来的结果不能直接用于任何真实环境。
const crypto = require('crypto'); // 模拟目标站点中读取到的页面隐藏字段 const pageToken = 'demo_page_token_2024'; function generateSsxmod() { const randomPart = Math.random().toString(36).substring(2, 10); const timePart = (Date.now() % 1000000).toString(36); // 模拟UA版本号提取 const uaVersion = '124'; const raw = `${randomPart}#${timePart}#${uaVersion}#${pageToken}`; const sha = crypto.createHash('sha256').update(raw).digest('hex'); // 模拟截取与反转 let result = sha.substring(0, 18).split('').reverse().join(''); const finalCookie = `${result}_${timePart}`; console.log('生成的动态Cookie值:', finalCookie); return finalCookie; } generateSsxmod();这种做法的本质,是把逆向结论转成一个可重复执行的计算函数。写完这个Demo之后,我参照它的结构去对比真实浏览器里生成的Cookie值,很快确认了哪些部分对应时间戳,哪些部分对应随机数。下一步就是做更细致的双端验证。
4. 反调试绕过与常见问题排查
4.1 断点断不住、Hook失效的排查方案
逆向实战中最容易卡住的情况,就是断点根本断不住。原因通常有三类:第一,JS被打包成一行,单步执行时根本看不清节奏;第二,代码里有setInterval轮询检测,只要检测到调试状态就自动清空Cookie或跳走;第三,debugger指令循环触发,让人没法聚焦核心代码。
如果Hook没生效,先检查是不是document.cookie的代理注入被页面覆盖了。可以在注入代码里加上防覆盖逻辑,或者干脆改成在Object.defineProperty里循环尝试。如果是轮询检测,可以在Sources面板里搜索所有debugger关键字,把命中结果批量删除,再用本地替换功能加载去混淆后的版本。很多浏览器调试工具都支持本地脚本替换,不用依赖第三方抓包工具。
还有一个习惯很重要:每操作一步就截图保存变量值。动态Cookie生成依赖时间戳,如果断点停留时间太长,变量快照可能已经和真实环境不一致。把Scope面板里的值截下来,后面写算法时可以直接对照,至少能少走两次弯路。
4.2 时间戳偏差与Cookie有效期处理
还原后第一次跑通,并不代表能直接用于实战。动态Cookie一般都有有效期,可能是30秒,也可能是10分钟。如果生成过程里时间戳取了毫秒级,而验证环境的系统时间和服务器时间有几秒偏差,服务端验签就会失败。
我在这次还原后专门做了一轮时间窗口测试:用脚本连续生成100个Cookie值,每隔1秒记录一次,然后观察服务端对每个值的接受率。最终发现只有距离生成时间5秒内的请求是稳定的,超过之后虽然格式正确,但会被判定为过期。这个现象说明服务端对时间戳有严格窗口校验。
另一个值得注意的点是,Cookie串里如果真的包含时间戳,服务端可能只校验时间戳差值,而不校验它是否来自当次页面返回。也就是说,提前生成备用值是可行的,但数量不能太多,否则风控规则会通过请求频率识别出非浏览器行为。这个度需要自己在真实环境里测试,不能盲目批量生成。
4.3 环境指纹缺失导致校验失败的补全思路
部分动态Cookie会绑定浏览器指纹,比如Canvas指纹、WebGL信息、屏幕分辨率、字体列表。这类指纹在无头浏览器或普通脚本环境里很常见地会缺失,导致生成出来的值虽然能算出来,但服务端核对不上。遇到这种情况不要急着改算法,先看看目标代码里到底取了哪些环境属性。
一个快速做法是:在核心生成函数入口打断点,查看所有函数参数和闭包变量,里面会有调用环境的引用,比如navigator.userAgent、screen.width、canvas.toDataURL()。把这些值按顺序打印出来,与真实浏览器里的值对比,缺什么就补什么。补环境时不要无脑用固定值,尽量用跟真实浏览器一致的值,否则后台一旦更新校验规则,脚本又要重新改一遍。
我在实际分析里就遇到过canvas.toDataURL()返回内容不一致的问题。原因是没有加载字体或者显卡驱动信息不同。后来通过模拟真实浏览器环境变量,把Canvas输出稳定下来之后,Cookie生成结果才与浏览器端匹配。这个排查过程比较繁琐,但价值很大,因为很多动态Cookie算法的“防自动化”能力恰恰藏在这些环境指纹里。
5. 还原结果验证与合规落地经验
5.1 浏览器与脚本双端结果对照验证
还原算法不是写完就完事的,必须用真实浏览器和脚本做双端对照。我的流程是:先在浏览器无痕窗口里刷新三次,复制下三次生成的Cookie值;然后分析这些值,找出哪些部分是时间戳、哪些是随机数、哪些是固定指纹。再回到脚本里,把同一时刻的时间戳和随机数传入,计算出一个结果,与浏览器中同一次刷新生成的结果比对。
如果完全一致,说明算法还原成功。如果不一致,优先检查因子收集顺序和字符串拼接的编码是否一致。比如分隔符是竖线还是逗号,拼接前是否做过URI编码,大小写是否统一,这些细微差异都会导致结果完全不同。
我自己调试时遇到过一个问题:浏览器里生成的Cookie有小写字母,脚本算出来却是大写。排查了很久,发现是目标代码在拼接前对时间戳做了一次toString()转换,而脚本里是数字直接参与字符串拼接,导致隐式类型转换规则不一样。所以做双端对照时,一定要把每一段的中间值都打出来,逐段比对,才能快速定位差异。
5.2 安全边界:逆向成果如何合规落地
写到这里,我必须认真提醒一句:动态Cookie逆向的用途,必须限定在你自己拥有或已获得授权的系统。做自动化测试、分析自家网站的风控策略、研究反爬设计的合理性,都是合理范围。但如果是未经授权去绕过别人网站的风控,不仅技术不可持续,还可能涉及法律风险。我分享这套分析方法,是为了帮助大家理解Cookie风控的设计逻辑,而不是提供一个“通杀”脚本。
这类研究放到实际业务中,最有价值的落地场景有两个。一是帮助自己网站做风控验证:你可以模拟攻击者的视角,测试动态Cookie防护是否足够健壮。二是给内部测试环境做自动登录器:当手工点击登录已经被动态Cookie干扰时,还原它能大大提升测试效率。如果你是想突破别人站点的限制,我建议换个方向,把思路用在正向防御上。
最后分享一个我自己的调试习惯:拿到一段混淆代码后,永远先做“最小验证”。把核心函数单独抽出来,喂一组假参数,看能不能跑出一个固定格式的结果;跑通了再接入真实参数。这样能避免被大量环境检测分支干扰,也能更快判断算法里隐藏的依赖。动态Cookie的还原从来不是一场记忆力比拼,而是一轮轮“找因子、对拼接、验格式”的循环。多试几次,你也能在几分钟内把它拆干净。