ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

腾讯滑块验证码collect参数逆向与JSVMP对抗实战解析

腾讯滑块验证码collect参数逆向与JSVMP对抗实战解析 “腾讯滑块验证码collect参数逆向与JSVMP对抗实战”这个项目我做了大概两周期间踩了不少坑也把JSVMP的几层保护从黑盒翻到了白盒。最近有同行问到collect参数到底怎么定位、怎么还原我干脆把整条路线重新梳理一遍。这篇内容适合有一定JS逆向基础、想系统性理解JSVMP保护机制的人也适合刚拿到腾讯滑块一脸懵、不知道从哪下手的初学者。我不打算只贴结论重点讲清楚每一步的选择逻辑以及我在实战中碰到的真实问题和解法。1. 项目基础先弄清楚collect参数是什么1.1 腾讯滑块验证码的工作机制腾讯滑块验证码本质上是前端先做一轮环境采集和风险判定再把结果提交到后端风控由风控决定是否弹出验证码、滑块能否通过。用户在页面上看到的滑块只是冰山一角隐藏在后面的是一整套浏览器指纹、环境检测、行为轨迹、时序分析等逻辑。整套流程可以拆成四段页面加载时前端脚本启动自动采集运行环境的各类信息。采集完成后脚本生成一个加密的字符串随验证请求提交给服务端。服务端解出这段字符串中的环境指标和风险标签再结合当前用户的行为特征打分。分数不够或环境可疑时服务端下发滑块验证动作用户拖动后把轨迹再次提交。其中第二步提到的“加密字符串”在腾讯验证码体系里就是collect参数。它几乎携带了整台设备的指纹信息服务端校验条款也是围绕它展开。所以逆向的重点就是想明白collect是怎么生成的、里面有哪些字段、用什么算法组织最后能不能在脱离真实浏览器的环境下稳定复现。1.2 collect参数在请求中的位置与形态先看一次正常的验证码请求网络面板里搜索collect这个词结果通常长这样collect: {cd:-178,ee:{}, fa:{...}, ga:..., ia:false, ja:{...}, ka:{...}, la:... }表面上是一个JSON结构嵌套了几层对象里面的字段名都是缩写没有注释没有类型声明。字段的命名规律看起来像是压缩混淆之后的产物直接猜含义会非常浪费时间。我实际处理过的一个请求中collect里不同字段加起来超过二十个其中既有字符串、数字、布尔值也有嵌套对象和数组。更麻烦的是同一字段在不同请求里可能会变化——有的字段只在特定条件下出现有的字段值会随着运行环境不同而不同。比如某些字段只在WebView环境下才有某些字段在移动端和桌面端返回的格式都不一样。所以第一步不是看到什么抓什么而是先抓一批不同环境下的请求做横向对比把稳定字段和动态字段区分开。只有把基线梳理清楚后面做还原和模拟才知道哪些地方要动态计算。1.3 为什么说collect是风控的关键入口如果服务端只靠一张滑块图片做背景切割校验那很简单模拟个拖拽轨迹就能过。但腾讯这套体系明显不是collect几乎可以理解为一封“环境自白书”。风控服务端拿到collect以后会做几件事校验参数结构是否完整字段类型和取值范围是否符合前端脚本的预期。校验指纹信息是否自洽比如canvas指纹、WebGL渲染器、字体列表、时区、语言、硬件并发数之间有没有矛盾。校验行为特征是否合理比如鼠标轨迹、触摸事件时间间隔、页面停留时间。校验加密内容能否被正确解析解析结果中是否存在明显的模拟器特征或异常标签。所以只关注滑块轨迹远远不够collect生成这一关过不了后面一切免谈。而我之所以选这个项目做实战就是因为它的保护强度比一般JS加密高一个层级——JSVMP正好可以顺带把这类保护方案的通用对抗思路摸透。2. 深度拆解JSVMP为什么collect保护这么硬2.1 JSVMP的运作原理JSVMP全称是JavaScript Virtual Machine Protection核心思路是把JavaScript代码先编译成一套自定义的字节码然后在运行时由一个解释器逐条执行这份字节码。开发者看到的代码只剩下面目全非的解释器逻辑真正的业务逻辑全部藏在了字节码里。从外部看它的执行过程大概长这样原始JS代码经过自定义编译器转成指令序列。指令序列按一定规则编码存放于数组、字符串或对象属性中。解释器从指令流中逐条读取操作码查表分发到对应处理逻辑。每条指令执行时改变虚拟机的栈、变量表、上下文等状态。最终输出在外部看就是一次普通函数调用返回一个结果。换句话说平时我们追函数逻辑的堆栈、断点、调用关系在这套机制里大部分都失效了。你看到的是循环里套循环、switch里套switch真实的运算逻辑被切碎后藏在一大批无意义的分支和跳转里。2.2 怎么判断一个脚本是否用了JSVMP网上关于JSVMP识别的资料不少但很多都停留在“有很多case的就是JSVMP”这种粗糙判断。实际分辨可以从这几个维度入手代码中大量存在switch-case分支且case数量非常多几十上百个起跳。主流程中存在多层while(true)循环循环内部有变量自增或跳转标记从阅读上很难找到退出条件。出现几万行甚至十几万行的“扁平化”代码而你很清楚原始功能不可能有那么复杂。变量名和函数名几乎全部被替换成无意义的短名称比如a、b、c或者下划线加数字组合。函数调用频繁通过数组下标或属性名间接完成直接看不到函数声明与调用点之间的对应关系。腾讯那个滑块脚本我拿到手后第一眼就看出了两个特征一个是文件体积比想象的大另一个是核心逻辑区域里有大量操作码分发表。常规混淆最多做到变量名替换但JSVMP是连控制流都改成虚拟机执行了两者完全不在一个等级。2.3 为什么AST还原在JSVMP面前容易“翻车”网上大量文章都说“用AST还原控制流平坦化”这招对普通混淆有效但对JSVMP帮助有限。原因在于控制流平坦化只是把代码结构拍平了但代码逻辑本身还在还保留着函数调用、表达式运算、变量赋值这些可读元素。而JSVMP把这些元素全拆成了字节码你在AST里看到的只是一堆解释器的状态更新操作业务逻辑根本没有以可读的形式存在。所以那段时间我踩过很多次坑拿AST插件去还原JSVMP的代码结果运行完以后确实漂亮了一点但这个“漂亮”只是把解释器内部的分支理顺了collect参数的核心算法依然没有暴露出来。真正有用的做法是绕过解释器本身从两个方向下手动态方向不去还原代码而是直接Hook解释器执行前后的关键状态等真正的字符串拼装、加密运算发生在内存里时把它拦截下来。静态方向想办法找出原始操作码表和解释器的指令处理函数映射把字节码翻译回可读的中间表示再结合AST做结构重建。我最终采用的方式是把两者结合先用动态分析确认collect生成的入口和输出时机再用静态分析还原其中真正关键的运算逻辑。这里我建议如果你也是第一次接触JSVMP别一开始就扎进AST先试着从动态黑盒的角度理解整个执行链路否则容易在无关代码上浪费大把时间。3. 实操记录collect参数的逆向与对抗全流程3.1 前置准备与工具选型工欲善其事必先利其器。这次我用到的工具如下Chrome DevTools用于抓包、断点调试、动态修改JS代码。Babel用于对JS代码做AST解析、遍历、修改和生成重点是处理还原后的代码再编译。jsdom在Node.js里模拟浏览器环境便于脱离浏览器运行完整JS脚本。Puppeteer做动态执行和补环境验证特别是在某些接口需要真实浏览器行为时才用。Fiddler或Charles抓取移动端或其它进程发起的请求这主要用于对比不同环境下的collect差异。工具不需要多关键是思路。整个过程我依次经历了四个阶段网络抓包定位、脚本代码定位、JSVMP动态分析、补环境和算法复现。每个阶段的侧重点不同下面分别展开。3.2 第一阶段从网络请求中锁定collect位置打开目标页面触发验证码然后在DevTools的Network面板里搜索所有包含collect的请求参数。这一步你会看到两种可能一类是GET请求的query参数里带collect另一类是POST请求的请求体里带collect。我遇到的场景是POST请求collect藏在一个更大的JSON对象里。先别急着看生成逻辑先把请求参数完整的复制下来多做几组正常浏览器打开不做任何额外操作直接触发验证码。浏览器里清掉一些关键指纹信息后再触发。修改系统时区或语言后再触发。无头浏览器环境下触发一遍。这样对比下来很快就能把collect里哪些字段是环境自适应的、哪些字段是每次都会变的、哪些字段是固定写死的区分开。这也决定了后面写模拟代码时哪些参数需要动态计算哪些可以用常量代替。3.3 第二阶段定位collect参数的生成入口请求参数出现在网络上它的来源一定在某个JS文件里。在DevTools里全局搜索collect这个关键词搜索范围覆盖所有JS文件通常能搜到多处匹配。有些匹配是赋值语句有些是字符串拼接有些是对象键名真正有用的匹配是那些把collect作为属性名写入请求对象的代码。我建议按以下顺序缩小范围搜索collect先找赋值点不是正常变量赋值而是“某个对象.某个属性 某块内容”的形式。找到赋值点后在那一行下断点刷新页面触发验证码。命中断点后向上查看函数调用栈找到包含collect生成的最近一个函数。从那个函数开始单步调试观察collect对象里每个字段的来源。需要注意的是腾讯这套脚本里collect名字本身可能被混淆过不一定每个版本都叫collect。更为稳妥的办法是从请求构造的位置往回找也就是在发送请求的xhr或fetch位置下断点看请求参数对象是在哪个作用域里被组装出来的。找到这一段代码后再回溯整个组装过程中引用的变量就能精确定位到collect对象本体。3.4 第三阶段JSVMP动态分析与关键状态捕获这是整个项目里最费时间的一步。腾讯滑块脚本里collect的核心处理过程被包在JSVMP的“壳”中。从外部看函数的真实逻辑没办法直接阅读但有一个规律是可用的——它最终一定会把结果赋值给某个变量并作为对象属性返回。我的做法是在collect生成函数的返回值处设断点等函数return时查看返回值对象。在该函数内部查找所有写对象属性的地方给写入操作打上日志断点观察每个属性的值及写入时机。找到可疑的加密操作比如调用了一些高密度计算函数通过修改输入值看输出变化判断它们各自承担什么职责。日志断点在DevTools里可以直接通过右键断点选择“Add log point”实现不用修改源码也不用重新加载整个脚本。我当时的做法是把整个对象写入过程全部打了日志然后拉取日志文件对每个字段值做对比分析。这个阶段收获巨大既搞清了字段之间的依赖关系也找到了几个关键算法的内存入口。为了把某个字段的计算过程看得更清楚我还用过一种取巧的方式直接在DevTools的Console里重新执行一小段脚本先把之前的JSVMP解释器实例拿到手再手工调用解释器内部的某些处理函数。只要能拿到虚拟机实例的引用绕开外部封装直插内部是完全可行的前提是你对它的对象结构已经有一定了解。3.5 第四阶段AST还原与算法重建动态分析找到了关键路径后静态分析来做算法重建。JSVMP的字节码解析器再怎么复杂它执行的最小单元一定是“读一条指令、查操作码、执行对应处理函数”的循环。只要能把操作码表和每个操作码对应的处理逻辑理出来就能把字节码翻译成可读的中间指令。实际操作时我用Babel把JSVMP脚本解析成AST然后按以下步骤处理找到解释器的主循环函数这个函数特征非常明显一个while/switch结构内部核心逻辑是按照某个指针从数组中读取数字再从分发表里查函数地址。提取分发表把case分支和对应处理函数一一对应整理成表格。对每个处理函数单独还原函数内如果只是简单的入栈、出栈、变量读取写入就做好标记不必展开细读。定位到真正复杂的处理函数比如字符串拼接、数组操作、位运算、加密算法相关的部分结合动态Hook结果做针对性还原。有一点要特别提醒AST还原在JSVMP里只能帮你看到解释器本身的结构不代表能看到业务逻辑。真正决定collect参数生成的还是那段字节码。所以我的实操路线是把动态Hook到的状态变化和静态还原出来的指令执行顺序对照起来逐步推断出每个操作码的语义进而重建核心算法。以collect里的某个加密字段为例我在动态分析时发现它经历了一次类Base64编码加一次自定义的字典替换。顺着这个线索回到静态代码里搜索编码相关的操作函数很快就找到了对应那几条指令。把这几条指令从虚拟机的逻辑中单独抽出来再用Python重写一遍就完成对这一步算法的复现。3.6 补环境让脚本在Node.js里稳定跑起来算法复现以后还要解决“完整运行整个脚本”的问题。腾讯滑块脚本在运行时依赖大量浏览器API比如document、navigator、screen、canvas、WebGL等。直接在Node.js里执行肯定是会报错的必须补环境。补环境的本质不是让所有API都和浏览器一模一样而是让脚本在这个环境下“认为是它熟悉的浏览器”从而顺利走到生成collect那一步。我补环境时的经验如下先用一个简易的浏览器环境对象框架例如jsdom搭底。把脚本运行过程中报错缺失的属性一个个补上。每次补完跑一遍记录新的报错继续补直到没有报错。重点关注navigator属性、screen对象、window下的各种事件相关属性、CanvasRenderingContext2D上的方法。某些方法内部会读取底层渲染结果比如canvas.toDataURL如果不返回合理值后续算法会算出异常指纹。补环境这块很容易陷入“补了报错、报错再补”的循环里。建议给脚本运行的入口包一个try-catch把完整的调用堆栈打印出来这样能快速定位到底哪个对象没有被定义或者哪个属性的类型不对。我实际补了两三轮就稳定通过了真正费时间的反而是后续对生成结果一致性的校验。3.7 验证与结果对比脚本能在Node.js里跑通并输出collect参数后还要做一组对照实验。我当时的做法是用Puppeteer打开真实浏览器环境加载同一套JS代码生成一份collect。同一份JS代码在Node.js补环境后运行生成另一份collect。对比两份collect的结构和关键字段值找出差异。实测下来大部分字段可以做到一致少数环境相关性强的字段比如canvas指纹、字体列表会有差异。针对这些差异要么直接在补环境阶段就把相关API的返回值固定成和真实浏览器一致要么理解它的算法后自行构造。这里的选择要看你的业务场景如果只是做数据分析和研究字段值差异不影响整体理解如果是要完整模拟请求那必须逐字段对齐。4. 实战中的高频问题与排查技巧4.1 collect参数收集阶段最容易踩的坑收集参数时大家最容易犯的错误是“只跑了一遍就开始逆向”。同一份collect在不同页面、不同时间、不同浏览器版本下可能都不完全一样。如果只抓一条请求就急着定位很容易被偶然出现的字段干扰判断。我建议至少构造三种环境各抓五条请求本机普通浏览器环境。移动端模拟器环境。无头浏览器环境。然后把这十几条请求里的collect参数汇总做一个字段矩阵。某个字段如果只在移动端出现不需要在PC端模拟里关心某个字段如果每次值都不同就要注意它是不是随机数某个字段如果只在某一条里特别长要警惕是不是加了额外条件导致的数据分支。这个过程虽然繁琐但能省掉后面大量试错时间。我从头到尾做了一遍大概多花了半小时但后面定位逻辑时几乎一次命中。4.2 JSVMP断点调试失灵怎么办JSVMP在动态调试时有一个最恶心的特性你刚在某一行下好断点刷新一次后发现断点位置跑偏了或者原来那个函数根本不执行了。这是因为JSVMP的指令分发循环会让同一段代码反复被不同的业务逻辑使用你在某一个case上下断点它可能在处理别的指令时也路过这里。我常用的对策有三个只在业务逻辑的边界下断点不深入解释器内部。比如collect对象组装完成、即将返回时在return语句处下断点。使用条件断点。给断点加上条件只有某个特定值出现时才暂停。比如某个寄存器值等于某个特殊标记时再断。改用日志断点。不打暂停而是打印关键变量的值这样即使代码被反复执行也不会因为断点中断影响执行流程。还有一个办法是直接改写脚本把JSVMP解释器某个函数的开头插入一句复制参数值的代码然后通过console.log输出。这种方法需要对构建流程有基本了解但非常有效。4.3 补完环境后生成结果对不上怎么排查补环境完成后最常见的现象是“能跑但结果不正确”。这种问题排查起来特别头疼因为错误可能出在任何一环。我的排查顺序是先确认是否成功进入collect生成逻辑。在入口和出口分别打印日志看看执行流程是否走完。对比字段差异。把真实浏览器生成的collect与Node.js生成的collect逐字段做diff锁定差异集中在哪个子对象里。定位差异的来源是环境API返回值不对还是算法还原不对。如果差异字段恰好对应canvas指纹、Audio指纹、字体检测结果那问题大概率在环境API返回值。针对环境API返回值去真实浏览器里把对应结果打出来再在Node.js里手动设置成相同值继续跑。这套排查流程说起来简单实际操作中可能会翻来覆去好几轮。但每次把差异范围缩小一层离最终解决就不远了。不要试图一口气把二十多个字段全部对齐先挑影响最大的三五个核心字段确定无误后再处理边缘字段。5. 新手避坑与小技巧分享5.1 先从黑盒入手别一头扎进AST我在前面反复强调过JSVMP的AST还原容易让人陷进无止境的代码阅读里。新手最容易犯的错误是拿到脚本就开始全文分析试图把每一行都看懂。这种思路在普通JS里可行在JSVMP里行不通。正确姿势是用黑盒视角先找出输入输出。用动态手段定位关键路径。用差异分析缩小待还原的代码范围。最后才针对关键路径做静态还原。换句话说先把“从哪来、到哪去”搞清楚再深入“中间怎么算”。这样能把静态分析的工作量降低好几个量级。5.2 每个版本都在变要保留“快照”习惯腾讯前端的验证码脚本更新很频繁可能每隔几周就换一版。今天写的还原代码下次版本更新后可能连入口函数名称都对不上。我建议每次分析时把脚本文件、请求参数、运行日志都存档并标记好版本号。另外不要过度依赖写死的函数名或变量名最好是根据结构特征定位而不是根据名字定位。这样即使对方下次换个混淆名你的定位思路依然有效。5.3 一个容易被忽略的隐蔽环境检测点腾讯这套脚本里有些环境检测藏在很隐蔽的位置比如通过判断某个数组的长度来决定是否进入异常分支或者通过某个对象是否有某个方法来判断是不是真实浏览器。补环境时如果不是特别细心很容易漏掉。一个实用的做法是在运行脚本之前先用一个干净的浏览器内核把脚本需要的所有API枚举一遍把类型、返回值、属性名都记录下来再在Node.js里做一个自动映射。虽然手工核对仍然不可避免但至少能避免“明明属性存在却没定义”这类低级问题。最后再说两句做腾讯滑块collect参数逆向和JSVMP对抗这段时间我最大的体会是这类验证码风控系统的核心难点不在于单点算法有多复杂而在于整个链路环环相扣任何一环缺失都会导致整体失败。JSVMP只是其中比较显眼的一个技术点真正决定成败的反而是你对执行链路、环境差异、数据流关系的理解深度。如果你也在摸这套体系建议按“黑盒定位 - 动态Hook - 静态还原 - 补环境验证”的顺序推进一步一个脚印别指望看几篇文章就能一晚上搞定。遇到卡住的地方不妨把脚本放一放换个角度重新观察请求和运行日志很多答案其实早就藏在数据里了。
返回列表