动态Cookie逆向实战:突破JS混淆与WASM保护

📅 2026/7/4 15:40:16 👁️ 阅读次数 📝 编程学习
动态Cookie逆向实战:突破JS混淆与WASM保护

1. 项目概述:当Cookie不再“静止”

在Web安全与数据采集领域,动态Cookie的生成机制一直是开发者与逆向分析者之间一场没有硝烟的“攻防战”。传统的静态Cookie,其内容在服务器设置后便固定不变,容易被识别和封禁。而动态Cookie,其核心价值在于“变化”——它往往由前端JavaScript(JS)或更底层的WebAssembly(WASM)模块,根据时间戳、用户行为、设备指纹等多种因子实时计算生成一个加密或编码后的字符串。这个字符串作为请求的“门票”,每次访问都可能不同,极大地增加了自动化脚本直接复用的难度。

我最近在分析一个数据接口时,就遇到了典型的动态Cookie挑战。打开浏览器的开发者工具(F12),在Network面板中能看到每个请求都携带了一个名为acw_sc__v3或类似的长字符串Cookie,其值毫无规律,且每次刷新页面都会变化。直接复制这个值去模拟请求,第二次就会失效。这背后,正是前端利用JS混淆甚至WASM技术保护的签名生成逻辑在起作用。面对这种场景,单纯地“查看网页Cookie”或“谷歌浏览器获取Cookie的方法”已经无能为力,我们必须深入前端代码的“心脏”,去逆向分析其生成算法。这个过程,不仅涉及对高度混淆的JavaScript代码的耐心梳理与动态调试,更可能触及性能更高、更接近底层的WASM模块,需要一套组合技巧才能攻克。

本文旨在分享我在实战中总结出的,针对“JS混淆 + WASM”双重保护的动态Cookie生成逻辑的逆向分析与调试全流程。我们将从最基础的抓包定位开始,逐步深入到反调试绕过、JS代码动态Hook、WASM模块提取与逆向分析,最终实现算法的复现。无论你是从事安全研究、数据爬取,还是单纯对Web前端技术深度感兴趣,这套方法论都能为你打开一扇新的窗户。

2. 核心思路与逆向环境准备

逆向分析动态Cookie,本质上是一个“定位 -> 理解 -> 复现”的过程。我们的目标不是破解某个具体网站,而是掌握一套通用的、可复用的分析方法论。

2.1 逆向分析的核心路径

面对一个带有动态Cookie的请求,标准的逆向分析路径可以概括为以下四步:

  1. 请求定位与关键词搜索:在开发者工具的Network面板中,找到携带目标动态Cookie的请求(通常是XHR或Fetch请求)。然后,在Sources面板或使用全局搜索(Ctrl+Shift+F),搜索该Cookie的名称(如acw_sc__v3)或其特征值片段。这一步的目的是找到设置或生成该Cookie的JavaScript代码入口。
  2. 反调试绕过与代码定位:现代前端保护方案常包含反调试代码,例如在检测到开发者工具打开时无限debugger、死循环或直接跳转。我们需要先识别并绕过这些保护,才能稳定地进行断点调试。然后,在疑似生成Cookie的代码行设置断点。
  3. 动态调试与逻辑追踪:通过断点让代码在关键位置暂停,利用Scope、Call Stack、Watch等面板观察变量的值、函数的调用栈和参数传递。通过单步执行(F10, F11),一步步跟踪Cookie值是如何从原始参数(如时间戳、页面信息)经过一系列函数调用计算出来的。
  4. 算法提取与复现:在理清核心计算逻辑后,将相关的JavaScript函数或WASM模块的算法提取出来。对于JS代码,可能需要手动去混淆或直接扣取关键函数;对于WASM,则需要导出模块进行分析,甚至用其他语言(如Python)重新实现其计算逻辑。

2.2 工具链准备:你的“数字手术刀”

工欲善其事,必先利其器。以下是进行此类逆向分析的必备工具链,它们构成了从抓包到调试的完整闭环。

  • 浏览器与开发者工具Google ChromeMicrosoft Edge(基于Chromium)是首选。其内置的开发者工具(DevTools)功能最为强大,特别是Sources面板的调试器、Network面板的请求重放(Replay XHR)以及Overrides功能(用于本地替换线上JS文件进行调试)。
  • 抓包与调试代理Fiddler ClassicCharles。它们可以拦截、查看和修改所有HTTP/HTTPS流量,对于需要观察完整请求/响应链、测试Cookie生效条件、或进行断点映射时非常有用。有时,浏览器DevTools无法断下的脚本,通过代理工具设置断点可能成功。
  • Node.js环境:用于在本地执行扣取出来的JavaScript代码,验证算法是否正确。很多动态生成算法依赖于浏览器环境中的某些对象(如windowdocument),在Node.js中需要模拟这些环境,可以使用jsdom库。
  • WASM分析工具
    • Chrome DevTools:自带WASM调试支持,可以单步执行WASM指令,查看线性内存(Memory)。
    • wasm2wat / wat2wasm:WebAssembly Binary Toolkit(WABT)中的工具,可以将WASM二进制文件(.wasm)转换为可读的文本格式(.wat, WebAssembly Text Format),也可以反向转换。这是静态分析WASM的基石。
    • JEBGhidra:专业的反编译工具,对WASM有较好的支持,可以将其反编译为更易读的伪代码(如C语言格式),极大提升分析效率。
  • 代码编辑与整理Visual Studio Code, 配合Prettier等格式化插件,用于阅读和整理混乱的混淆后代码。

注意:在整个分析过程中,请务必遵守相关法律法规和网站的服务条款。本文所有技术讨论仅用于安全研究、学习交流和个人授权的自动化测试目的,严禁用于任何非法爬取、攻击或侵犯他人权益的行为。

3. 突破第一道防线:JS混淆代码的定位与调试

绝大多数动态Cookie的生成逻辑仍然由JavaScript实现,但会被各种混淆工具(如obfuscator.io、javascript-obfuscator)处理,变得难以阅读。

3.1 识别混淆与定位入口点

混淆后的JS代码通常具有以下特征:变量名被替换为_0x1a2b3c这样的十六进制字符串;字符串被编码(如Base64、十六进制);代码结构被平坦化(控制流扁平化),加入大量无用的条件判断和跳转;函数调用被封装。

定位入口的实战技巧

  1. Network面板筛选:在发生关键请求(如点击按钮触发数据加载)前后,仔细对比Network中的请求。找到那个携带了动态Cookie的请求,查看其Initiator列,它能告诉你这个请求是由哪个脚本文件发起的,点击可以跳转到源码位置。
  2. 全局搜索Cookie名:在Sources面板打开所有JS文件,使用Ctrl+Shift+F进行全局搜索。搜索目标Cookie的名称(如setCookieacw_sc__v3)。如果Cookie名也被混淆了,可以尝试搜索其值的一部分(例如值中固定的前缀或特征字符),或者搜索document.cookie这个API。
  3. Hook关键函数:在Console面板中,可以直接注入代码来Hookdocument.cookie的setter和getter。这能帮你快速定位到设置Cookie的精确位置。
    // 在Console中执行,Hook cookie的设置 var cookie_cache = document.cookie; Object.defineProperty(document, 'cookie', { set: function(val) { console.trace('Cookie being set:', val); debugger; // 自动触发断点 cookie_cache = val; return val; }, get: function() { return cookie_cache; } });
    执行上述代码后,任何试图设置Cookie的操作都会在控制台打印堆栈信息并触发断点,让你直接“跳”到关键代码行。

3.2 应对反调试与稳定调试环境

当你找到疑似代码并设置断点后,可能会发现刷新页面时,代码根本执行不到断点处,或者页面自动暂停在某个莫名的debugger;语句上。这就是反调试。

常见的反调试手段及绕过方法

  1. 无限Debugger:代码中包含循环或条件触发的debugger;语句。在Chrome DevTools中,你可以右键点击行号,选择“Never pause here”来禁用这个特定位置的debugger。更彻底的方法是,在Sources面板找到这段反调试代码,通常是一个函数调用或一个setInterval,在其内部逻辑中设置条件断点,让其在关键时刻不执行。
  2. 检测DevTools:通过检测window.outerHeightwindow.innerHeight的差值(开发者工具打开会改变窗口内嵌尺寸)或navigator.userAgent中的特定字段。绕过方法是在打开DevTools之前先打开页面,或者使用开发者工具的“Drawer”中的“Sensor”功能覆盖这些属性值。更高级的方法是使用--auto-open-devtools-for-tabs启动参数打开浏览器,让页面一开始就“适应”DevTools的存在。
  3. 时间差检测:在代码开始和结束处用Date.now()计时,如果中间因为断点调试导致时间过长,则判定为调试状态。对付这个比较麻烦,需要你熟悉代码逻辑,找到检测点并修改其判断条件,或者使用“跳过所有断点”快速执行过检测代码段后再开启调试。

创建稳定的调试环境: 最有效的方法是使用DevTools的**本地覆盖(Local Overrides)**功能。首先在Sources面板的Overrides标签页设置一个本地文件夹。然后,在网络中找到被混淆和加了反调试的主JS文件,在文件内容上右键,选择“Save for overrides”。这样,这个文件就被保存到本地,并且所有修改都会直接作用于这个本地副本,刷新页面也不会丢失。你可以从容地在这个本地文件里删除反调试代码、格式化混乱的代码,然后进行调试。

3.3 动态调试技巧:从混沌中理清逻辑

在可以稳定断下后,面对混淆的代码,如何理解其逻辑?

  1. 善用“Watch”和“Scope”:在断点停下后,不要急于单步。先查看“Scope”面板中的局部变量、闭包变量和全局变量。将疑似关键的变量(如时间戳、用户ID、待签名的原始字符串)添加到“Watch”面板进行持续观察。
  2. 控制单步节奏
    • F10 (Step Over):执行当前行,如果该行是函数调用,则直接执行完这个函数,不进入其内部。用于快速跳过已知的、不重要的工具函数。
    • F11 (Step Into):执行当前行,如果该行是函数调用,则进入该函数内部。用于深入分析核心计算逻辑。
    • Shift+F11 (Step Out):快速执行完当前函数,返回到调用它的地方。当不小心进入一个复杂无关的函数时,用它快速跳出。
    • F8 (Continue):继续执行,直到下一个断点。
  3. 追踪调用栈(Call Stack):“Call Stack”面板显示了当前断点位置是如何被一步步调用过来的。点击调用栈中的上一级,可以跳转到对应的源代码位置,并查看当时的变量状态。这对于理解整个生成函数的调用链路至关重要。
  4. 条件断点与日志点:如果某个函数被频繁调用,但你只关心特定参数下的执行情况,可以右键行号设置“条件断点”。例如,条件设置为param1.indexOf('sign') > -1。你还可以设置“日志点”,在不暂停的情况下打印变量值,非常适合追踪数据流。

实操心得:面对高度扁平化的混淆代码,不要试图去理解每一行。我们的目标是找到“数据转换的节点”。关注那些对疑似原始数据(如Date.now()的结果、window.location.href、某个固定字符串)进行操作的函数,特别是出现位运算(&,|,<<,>>>)、charCodeAtfromCharCodeencodeURIComponent、或者调用CryptoJSbtoaatob等地方。这些往往是加密或编码的关键步骤。

4. 深入二进制领域:WASM模块的逆向分析

当JS代码中的核心计算部分变得异常简洁,可能只是一个函数调用,传入几个参数,返回一个结果,而你在全局搜索中找不到这个函数的实现时,就要警惕了——计算逻辑可能被编译成了WebAssembly(WASM)模块。WASM以其接近原生的性能和二进制格式,提供了更强的代码保护。

4.1 定位与提取WASM模块

  1. Network面板筛选:在Network面板中,使用wasm作为过滤器,可以快速找到页面加载的所有.wasm文件。通常,负责核心加密/签名的WASM模块会在页面初始化或首次调用相关功能时加载。
  2. Sources面板查找:在Sources面板的“WebAssembly”目录下,可以看到当前页面加载并实例化的所有WASM模块。Chrome会将WASM二进制代码实时反编译为可读的文本格式(.wat)并进行展示。
  3. 从内存中提取:如果WASM模块是动态创建或通过fetch加载的,可能不会在Network中留下明显的.wasm文件记录。此时,可以在Console中执行以下代码来查找已实例化的模块:
    // 遍历所有可能包含WebAssembly实例的对象 for (let key in window) { try { if (window[key] && window[key].exports) { console.log('Potential WASM instance:', key, window[key]); } } catch(e) {} }
    找到实例后,可以通过其exports属性调用函数。但要获取原始的.wasm二进制文件,更直接的方法是在DevTools的Sources面板找到对应的WASM模块,在代码区域右键,选择“Save as...”即可保存到本地。

4.2 静态分析:从WAT到理解逻辑

保存下来的.wasm文件是二进制格式,无法直接阅读。我们需要使用wasm2wat工具(来自WABT工具包)将其转换为文本格式(WAT)。

wasm2wat module.wasm -o module.wat

打开生成的.wat文件,你会看到基于S-表达式的汇编指令。对于简单的算法,有经验的开发者可以直接阅读WAT来理解逻辑,比如识别出它是一个MD5或SHA256的哈希计算。但对于复杂逻辑,这非常困难。

提升效率:使用反编译工具此时,专业的反编译工具如GhidraJEB就派上用场了。它们可以将WASM二进制文件反编译成更高级的、类似C语言的伪代码。

  1. 使用Ghidra分析WASM

    • 安装Ghidra并打开。
    • 新建项目,导入.wasm文件。
    • 使用默认的“WebAssembly”语言加载器进行分析。
    • 分析完成后,在“Symbol Tree”中可以看到导出的函数(通常以export开头,如export.mainexport._Z10calculate_somethingii)。
    • 双击函数名,Ghidra会生成反编译的伪代码。虽然伪代码可能仍有些晦涩,但相比原始的WAT,可读性有质的飞跃。你可以看到清晰的函数参数、局部变量、循环和条件判断结构。
  2. 分析要点

    • 关注导出函数:WASM模块通过export语句暴露给JavaScript调用的函数,就是我们的主要分析目标。在Ghidra的伪代码视图中,找到这些函数。
    • 理解内存模型:WASM通过线性内存(Memory)与JS交换数据。JS将字符串或数组的指针(内存地址)和长度传递给WASM函数,WASM函数在内存中进行读写操作。在伪代码中,你会看到大量的内存加载(如*(char*)(param_1 + 0x10))和存储操作。需要理清哪块内存区域存放了输入,哪块存放了输出。
    • 识别标准算法:很多动态Cookie的生成基于标准加密哈希函数(如MD5, SHA-1, SHA-256)或HMAC。在伪代码中寻找大的常量数组(初始化向量、轮常数)和特定的循环结构(如64次循环),这有助于快速识别算法。

4.3 动态调试:在浏览器中单步执行WASM

静态分析有时不足以理清所有细节,特别是当算法中掺杂了自定义的混淆或变换时。Chrome DevTools提供了强大的WASM动态调试功能。

  1. 启用调试:在Sources面板找到WASM文件,确保其已加载。如果显示的是二进制代码(一堆十六进制数字),点击左下角的“{}”格式化按钮,将其转换为可读的WAT文本。
  2. 设置断点:在WAT文本的任意行号上点击,即可设置断点。你可以直接在疑似核心计算的指令序列开始处(例如,在函数入口func $export.main处)下断。
  3. 触发执行:回到网页,执行会触发该WASM函数的操作(如点击按钮)。浏览器会在WASM断点处暂停。
  4. 观察状态
    • 作用域(Scope):可以看到WASM函数的参数、局部变量(以i32, i64, f32, f64类型显示)的值。
    • 内存(Memory):在调试器右侧的“Memory”面板,可以查看WASM模块的线性内存。你需要知道输入/输出数据的内存地址(通常来自函数参数或全局变量),然后在该地址查看原始字节数据。这对于验证字符串的传入和传出至关重要。
    • 单步执行:使用F10/F11在WASM指令间单步,观察寄存器和内存的变化。

一个关键技巧:为了将WASM中的内存数据与JS中的字符串对应起来,你可以在JS调用WASM函数的地方下断点,查看传入的指针和长度。然后,在WASM函数内部,通过Memory面板查看该指针地址开始、指定长度的内存内容,验证其是否为预期的字符串。

5. 算法复现与本地化验证

无论是从混淆的JS中扣出代码,还是逆向出WASM的逻辑,最终目的都是要在本地环境(如Node.js)中复现这个生成算法。

5.1 扣取JS代码并补环境

对于JS实现的算法,你可能会扣出一大段包含许多依赖函数的代码。直接放在Node.js中运行通常会报错,因为缺少浏览器环境(如windowdocumentnavigator)或某些Web API。

补环境的核心方法

  1. 使用jsdomjsdom库可以模拟一个完整的浏览器DOM环境。
    const { JSDOM } = require('jsdom'); const dom = new JSDOM(`<!DOCTYPE html><html><body></body></html>`); global.window = dom.window; global.document = window.document; global.navigator = window.navigator; // 可能需要补全其他对象,如 location, screen等 global.location = window.location;
  2. 手动定义缺失对象:如果只是缺少少数几个对象或函数,可以直接在全局定义。
    // 例如,扣取的代码使用了 window.btoa if (typeof global.btoa === 'undefined') { global.btoa = (str) => Buffer.from(str, 'binary').toString('base64'); } // 例如,代码使用了 performance.now() if (typeof global.performance === 'undefined') { global.performance = { now: () => Date.now() }; }
  3. 导出目标函数:将你扣取的、包含所有依赖的代码封装在一个IIFE(立即执行函数表达式)中,并只将最终生成Cookie的核心函数暴露出来。
    // 扣取的代码,可能很庞大 var bigObfuscatedCode = (function() { // ... 大量混淆代码 ... function generateSignature(param1, param2) { // 核心算法 return signature; } // ... 更多代码 ... return generateSignature; // 或挂载到window })(); // 在Node.js中,将其赋值给一个模块导出 module.exports = bigObfuscatedCode;

5.2 复现WASM算法

对于WASM算法,你有几种选择:

  1. 使用原WASM模块:如果你能成功提取.wasm文件,并理清了其导出函数的调用方式,可以直接在Node.js中使用wasm模块加载并调用它。这需要你编写对应的JS胶水代码来分配内存、传递参数。

    const fs = require('fs'); const wasmBuffer = fs.readFileSync('module.wasm'); WebAssembly.instantiate(wasmBuffer, { env: { // 提供WASM可能需要的导入函数,如内存分配、打印等 memory: new WebAssembly.Memory({ initial: 256 }), // ... 其他导入 } }).then(instance => { const result = instance.exports.calculate_signature(param1_ptr, param2_len); console.log(result); });

    这种方法最准确,但需要处理内存管理,复杂度较高。

  2. 用JS/其他语言重写:基于对WASM伪代码的分析,用JavaScript或Python等高级语言重新实现算法。这是最彻底、最可控的方式,也是逆向的终极目标。你需要仔细对照伪代码中的每一步操作,特别是位运算和内存访问逻辑,确保完全一致。

验证正确性: 无论用哪种方式复现,都必须进行验证。方法是在浏览器中,通过调试器截取多组“输入-输出”对(即生成Cookie的原始参数和最终的Cookie值)。然后在你的本地复现程序中,输入相同的参数,对比输出结果是否完全一致。至少验证5-10组不同的数据,确保算法在所有边界条件下都正确。

6. 实战中的疑难杂症与排查技巧

即使掌握了上述流程,实战中依然会踩坑。下面记录一些常见问题及解决思路。

6.1 问题排查速查表

问题现象可能原因排查思路与解决方案
全局搜索不到Cookie名或特征值1. 字符串被编码或加密。
2. Cookie在WASM中生成。
3. 代码被动态加载或eval执行。
1. 尝试搜索编码后的字符串(如Base64、Hex)。
2. 在Network面板查找.wasm文件,或HookWebAssembly.instantiate
3. 在Network面板查看JS文件加载顺序,关注动态创建的<script>标签或eval调用。
断点无法命中或瞬间跳过1. 反调试代码导致执行流改变。
2. 代码被包裹在setTimeout/Promise等异步中。
3. 断点位置在函数内部,但函数未被调用。
1. 使用“Never pause here”禁用干扰debugger,或使用本地覆盖删除反调试代码。
2. 在异步任务发起处(如setTimeout回调、Promise.then)下断。
3. 在函数被调用的上一级栈帧下断。
扣出的代码在Node.js中运行报错XXX is not defined缺少浏览器环境对象或API。1. 使用jsdom补全基础环境。
2. 根据报错信息,在全局手动定义缺失的变量或函数,模拟其行为。
WASM反编译后伪代码难以理解1. 算法本身复杂。
2. 反编译器优化导致代码变形。
3. 存在自定义的混淆或虚拟化。
1. 结合动态调试,观察输入输出,猜测算法类型(如哈希、AES)。
2. 尝试用不同工具(JEB)反编译对比。
3. 重点分析内存读写模式,画出数据流图。
本地生成的签名与浏览器不一致1. 算法依赖的环境变量未捕获全(如window.performance.timing.navigationStart)。
2. 代码中存在随机数或时间戳,且精度不一致。
3. 重写算法时存在细微错误(如位运算优先级)。
1. 在浏览器中调试时,将所有疑似用到的环境变量值都打印出来,在本地复现时硬编码这些值进行测试。
2. HookMath.randomDate.now等函数,固定其返回值进行测试。
3. 逐行对比本地代码与调试时观察到的执行逻辑,特别注意+号用于字符串连接还是数字相加。

6.2 独家避坑技巧

  1. 从结果倒推:如果正向跟踪算法非常复杂,可以尝试从最终生成的Cookie值入手。在代码中搜索这个最终值被使用或返回的地方,然后反向设置条件断点,观察是哪个函数生成了它。
  2. Hook一切:除了document.cookie,还可以HookXMLHttpRequest.prototype.sendfetchwindow.localStorage等,观察数据流动。对于加密库,可以尝试HookCryptoJS.MD5window.btoa等标准函数的输入输出。
  3. 简化输入:在调试时,尽量构造最简单、可预测的输入参数。例如,如果算法与页面URL有关,可以先在固定URL的页面上测试;如果与用户交互有关,则先模拟最简单的点击事件。这能减少干扰变量,让你更专注于核心计算逻辑。
  4. 善用“忽略列表”:在DevTools的Settings -> Ignore List中,可以添加你不想调试的脚本文件(如jQuery、React等大型库)。这样在单步执行时,调试器会自动跳过这些库的代码,让你聚焦于业务逻辑。
  5. 保持耐心与记录:逆向分析是一个极其需要耐心的工作。务必随时使用截图、录屏或详细的文字记录关键断点处的变量状态、调用栈和内存数据。这些记录是你在复杂逻辑中不至于迷失的“地图”。

逆向分析动态Cookie的生成,是一场对耐心、细心和技术广度的综合考验。从看似混乱的JS混淆代码,到冰冷的WASM二进制指令,每一步都需要你像侦探一样寻找线索、构建逻辑。掌握这套从定位、调试到复现的完整方法论,不仅能帮你解决眼前的Cookie问题,更能深刻理解现代Web前端安全机制的实现方式,提升你的底层调试和代码分析能力。记住,工具和技巧是辅助,最重要的永远是清晰的思路和坚持不懈的尝试。当你成功复现出算法,看到本地生成的Cookie与浏览器完全一致时,那种成就感,便是对这份努力最好的回报。