尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

JS逆向实战:极验滑块验证码首个w参数生成算法深度解析

JS逆向实战:极验滑块验证码首个w参数生成算法深度解析 1. 项目概述从“黑盒”到“白盒”的挑战最近在搞一个数据采集的项目目标站点用了极验的验证码。这玩意儿大家应该都不陌生登录、注册、防刷的时候经常能碰到。我这次遇到的是它那个经典的滑块验证。流程很简单页面加载后会请求一个初始化的接口返回一堆参数其中最关键的就是一个叫w的值。这个w值就是后续提交验证时的核心凭证服务器会用它来校验你这次滑动行为是不是“真人”完成的。听起来好像没什么不就是个参数嘛。但问题在于这个w值不是明文传输的它是一长串看起来毫无规律的、由字母数字组成的密文字符串。前端的 JavaScript 代码会用一系列复杂的算法收集用户鼠标移动的轨迹、时间、滑块位置等信息然后加密生成这个w。我们的目标就是把这个生成w的算法给“逆向”出来让我们写的程序也能模拟出这个加密过程从而构造出能被服务器认可的合法请求。这其实就是典型的 JS 逆向场景。前端把核心逻辑和加密算法都写在 JS 里对用户来说是透明的“黑盒”但对我们来说需要把它变成可理解、可复现的代码“白盒”。今天要拆解的这个“第一个w值”通常指的是验证码初始化时生成的那个它可能包含了浏览器指纹、环境参数等固定信息是后续动态轨迹加密的基础。搞定它就等于拿到了入场券。2. 逆向环境与工具准备工欲善其事必先利其器。逆向分析不是靠肉眼硬看混淆后的代码得有合适的工具来辅助我们理清逻辑。2.1 浏览器开发者工具我们的主战场Chrome DevTools 是核心中的核心。以下几个功能键必须熟练掌握Sources 面板用于查看、调试 JavaScript 文件。可以设置断点单步执行观察变量变化。Network 面板记录所有网络请求。我们要在这里找到生成或提交w值的那个关键请求通常是ajax或fetch类型然后查看它的调用栈Initiator一步步回溯到是哪个 JS 文件、哪行代码发起的。Console 面板可以执行任意 JS 代码用于测试我们还原的函数或者查看某些对象的值。Application 面板查看本地存储LocalStorage, SessionStorage、Cookie。有时候关键参数会存在这里。一个关键技巧是使用“XHR/Fetch Breakpoints”。在 Network 面板找到那个携带w的请求右键选择 “Breakpoint” - “Add XHR breakpoint”并填入请求 URL 中包含的关键词如validate。这样当浏览器下次发起这个请求时代码执行就会自动暂停在发起请求的那一行极大地方便了我们定位加密入口。2.2 反混淆与代码格式化工具极验的 JS 代码肯定是经过混淆的变量名都是a, b, c, _0xabc123这种可读性极差。首先用格式化在 Sources 面板找到疑似加密的 JS 文件点击左下角的{}Pretty-print按钮将压缩成一行的代码格式化使其有缩进和换行。使用本地反混淆工具谨慎对于简单的字符串混淆如\x68\x65\x6c\x6c\x6f转成hello可以尝试用一些在线或本地的 JS 反混淆工具。但要注意复杂的控制流平坦化混淆这些工具可能力不从心甚至可能破坏代码逻辑。我个人的经验是优先依靠调试和逻辑分析工具辅助。2.3 本地调试环境搭建为了能反复、离线地测试我们还原的算法最好能搭建一个本地环境。保存关键资源从 Network 面板将那个核心的、混淆过的 JS 文件比如叫gt.js或带有一串版本号的.js文件保存到本地。创建测试 HTML写一个简单的 HTML 文件引用这个本地的 JS 文件。模拟缺失环境极验的代码通常会检测window、document、navigator等浏览器对象。在 Node.js 或纯 JS 环境下运行可能会报错。我们需要用jsdom库或者在浏览器控制台里手动补全这些环境对象。更常见的做法是直接在我们保存的 HTML 页面里通过 Chrome 的 Console 来调用和测试我们关注的函数这样环境是最真实的。注意直接修改并替换线上 JS 文件可能会违反网站的使用条款。我们的所有分析应仅限于学习和技术研究目的并在本地环境进行。3. 核心加密逻辑定位与追踪这是整个逆向过程中最考验耐心和技巧的环节。目标是在茫茫混淆代码中找到生成那个w字符串的“罪魁祸首”。3.1 确定入口点从请求发起处回溯我采用的方法是“由外向内”追踪在 Network 面板找到提交验证的请求比如api/validate。它的请求体Payload里一定有一个w参数。在这个请求上右键选择 “Initiator” 标签页查看它的调用栈Call Stack。调用栈会显示这个请求是从哪一行代码发起的一层层往上。从调用栈的最顶层通常是send或fetch往下看找到属于我们自己目标网站域名下的 JS 文件而不是jquery.min.js这类库文件。点击进去就能直接定位到发起网络请求的那行代码。通常是$.ajax、axios.post或者fetch。3.2 关键函数断点调试定位到发起请求的代码行后往前看几行一定能找到w参数被赋值的地方。比如data: { w: xxxxx }。这个xxxxx就是一个变量或者一个函数的返回值。下断点在这个赋值语句的行号上点击设置一个断点。触发验证在网页上手动滑动一次验证码代码执行会自动暂停在断点处。追踪变量在右侧的 Scope 面板或把鼠标悬停在变量上查看这个w的值是什么。更重要的是看它是怎么来的。如果它是一个函数返回值比如w: get_w_value()那么我们就要进入这个get_w_value函数内部。步进Step Into使用调试工具栏的步进功能进入这个函数。然后重复这个过程观察输入、输出分析逻辑继续步进更深层的函数。3.3 识别加密特征在追踪过程中要留意一些 JS 加密的常见特征这能帮你快速判断是否找对了地方数组混淆大量使用[‘slice‘, ‘concat‘, ‘charCodeAt‘]这样的数组然后通过下标来调用方法。16进制/Unicode字符串字符串被写成\x41\x42或\u0041\u0042的形式。位操作频繁出现与、|或、^异或、左移、右移等运算符。常量数组定义了一个很大的、看似随机的数字或字符串数组后续操作频繁引用这个数组。CryptoJS或类似库如果看到CryptoJS.MD5、CryptoJS.AES.encrypt等那很可能就是标准的加密算法。btoa/atobBase64 编码解码。JSON.stringify将对象转为字符串通常是加密前的最后一步。在极验的场景中第一个w的生成往往不涉及用户轨迹所以可能更侧重于环境信息收集 固定算法加密。算法可能是自定义的也可能是标准算法如 AES、RSA但使用了特定的密钥和模式。4.w值生成算法深度拆解假设通过上面的追踪我们找到了一个名为_0x12ab3c的函数它返回了最终的w值。现在需要把它拆开揉碎。4.1 输入参数分析首先看这个函数接收哪些参数。参数可能包括gt验证码ID从初始化接口返回。challenge挑战码从初始化接口返回每次不同。lang语言。一个包含各种浏览器指纹信息的对象比如屏幕宽高、用户代理User-Agent、插件列表、字体列表、Canvas指纹等。这个对象可能由另一个函数比如叫collect_env()生成。我们需要在调试状态下记录下这些参数在一次正常请求中的具体值。这些值是我们后续还原算法时验证结果是否正确的基础。4.2 核心加密流程还原进入函数内部面对混淆的代码可以尝试以下方法理清逻辑重命名在 DevTools 里可以对变量和函数进行临时的重命名。把那个返回最终结果的变量改名为final_w把那个处理加密的函数改名为encrypt_func。这能极大提升代码的可读性。控制台打印在关键步骤前后使用console.log()打印中间变量的值注意需要临时修改代码刷新页面后失效。观察数据是如何一步步变化的。逻辑分段根据代码结构如if判断、for循环、明显的赋值操作将整个函数在心理上或笔记上分成几个阶段阶段一参数预处理。可能将多个参数拼接成一个字符串或者转换成特定格式如数组。阶段二核心变换。这里可能是自定义的循环位移、替换或者是调用某个加密库。如果是自定义算法通常能看到对某个常量数组的查表操作。阶段三编码输出。将加密后的二进制数据可能是数组进行 Base64 编码或者转换成16进制字符串。极验的w常常是 Base64 字符串。4.3 算法复现与验证当我们自认为理解了算法后就要开始动手复现。剥离代码将那个核心的加密函数及其所有依赖的函数从混淆的 JS 文件中单独拷贝出来。注意要沿着调用链把所有用到的函数都拿全。创建测试用例在浏览器 Console 或 Node.js 环境中用我们之前记录的那一套真实的输入参数gt,challenge, 环境对象调用我们剥离出来的函数。比对结果看输出的w值是否和 Network 里抓到的那个w值完全一致。这里必须完全一致一个字符都不能差。处理环境依赖如果复现失败最常见的原因是函数内部依赖了某些浏览器特有的全局变量或对象如window.screen,navigator.plugins。我们需要在测试环境中模拟这些对象。一个取巧的办法是在浏览器 Console 调试时直接让函数运行在原网页上下文中这样环境是完全真实的。实操心得极验的算法可能会把环境信息如 Canvas 指纹的哈希值也作为加密的一部分。在本地复现时必须保证模拟的环境信息与真实浏览器环境产生的信息一致否则就算算法正确生成的w也会因为输入不同而不同。有时候它可能对某些信息做了“标准化”处理比如把屏幕分辨率取整到某个倍数需要仔细比对。5. 本地化实现与参数模拟成功在 Console 里复现算法只是第一步。我们的目标是在 Python或其他后端语言中实现这个算法以便集成到爬虫程序中。5.1 语言转换要点将 JS 代码翻译成 Python 时要特别注意以下几点位运算JS 和 Python 的位运算逻辑基本一致可以直接转换。字符编码JS 的字符串是 UTF-16而 Python 3 的字符串是 Unicode。在处理charCodeAt()获取字符的 Unicode 码点时Python 中可以用ord()。对于fromCharCode()Python 可以用chr()。但要注意JS 的charCodeAt返回的是 0-65535 的值而 Python 的ord()对于基本多文种平面BMP外的字符码点65535也能正确处理通常问题不大但涉及二进制操作时要小心。数组操作JS 数组的slice,concat,splice等方法在 Python 列表中有对应的操作但语法不同需要仔细转换。自定义算法中的常量数组那个巨大的、看似随机的数组必须原封不动地拷贝到 Python 代码中。它是算法的一部分。5.2 环境参数模拟这是本地化最大的难点。第一个w值依赖的环境信息可能包括userAgent: 直接从你的爬虫请求头里复制一个真实的浏览器 UA 即可。screen.width/height: 模拟一个常见的分辨率如1920x1080。navigator.plugins: 可以模拟一个空数组或固定的插件列表。极验的代码可能会遍历这个数组生成一个特征字符串。Canvas 指纹这是重灾区。JS 可以通过在 Canvas 上绘制文字和图形然后调用toDataURL()获取图像数据的哈希值作为指纹。在 Python 中我们需要使用PILPillow库来完全模拟这个过程创建同样大小的画布用同样的字体、字号、颜色绘制同样的文字如“Hello, GeeTest!”然后计算图像的哈希如 MD5。字体是关键必须确保系统中有对应的字体或者使用字体文件。有时极验会使用一些系统常见字体如 Arial可以尝试固定使用一种字体。一个简化策略是直接使用第一次成功验证时抓取到的、完整的浏览器环境对象。将这个对象序列化保存下来以后每次生成w时都使用这同一套数据。只要服务器不升级验证策略这套固定指纹在短时间内通常是有效的。5.3 代码封装与测试将翻译好的 Python 算法封装成一个函数例如generate_first_w(gt, challenge)。这个函数内部固定使用我们模拟好的一套环境参数。 然后编写测试脚本从真实的网页请求中抓取一次有效的gt和challenge。调用generate_first_w函数。将生成的w与真实请求中的w进行比对。甚至可以尝试用这个w构造一个验证请求看服务器是否返回成功。6. 逆向过程中的常见陷阱与解决方案这条路坑很多我踩过不少这里总结一下。6.1 反调试与代码动态执行现代的反爬手段会部署反调试。无限 Debugger代码中会插入debugger;语句或者通过Function(‘debugger‘)()动态生成。导致代码一执行就暂停。解决方案在 Sources 面板找到对应的行右键选择 “Never pause here”或者通过条件断点将其禁用。更彻底的方法是在 DevTools 的设置中勾选 “Disable JavaScript” 不推荐可能影响页面功能或者使用 Fiddler/Charles 等代理工具将包含debugger的 JS 文件映射到一个本地修改过的、删除了 debugger 语句的文件。代码动态加载与混淆核心加密函数可能是通过eval、Function构造函数或者setTimeout动态生成的字符串。你在 Sources 里看到的静态代码并不是最终执行的。解决方案在动态代码执行后下断点。或者在 Console 中 Hookeval和Function构造函数将它们替换成可以输出其参数即要执行的代码字符串的函数从而捕获动态生成的代码。例如eval_ eval; eval function(s) { console.log(Eval:, s); return eval_(s); };6.2 算法依赖浏览器特性浮点数精度JS 和 Python 的浮点数计算可能存在细微差异如果算法中涉及浮点数运算比如在生成轨迹时计算加速度可能导致最终结果不同。解决方案尽量将算法中的数学运算转换为整数运算或者确保在 Python 中使用Decimal高精度库并模拟 JS 的精度。随机数种子如果算法中使用了Math.random()并且其种子与环境或时间相关那么在 Python 中需要用random库模拟相同的随机序列。但通常第一个w的生成不依赖真随机数更多是确定性算法。6.3 结果不一致的排查思路如果本地生成的w和线上的对不上按以下顺序排查输入一致性确保gt、challenge、所有环境参数与线上那次请求完全一致。一个空格、一个大小写都不能差。算法还原完整性是否漏掉了某个细微的步骤比如字符串拼接时是否在末尾多了一个空格数组join时用的连接符是什么编码问题加密前的字符串是什么编码UTF-8 还是 UTF-16在 Python 中进行哈希或加密前字符串是否需要先.encode(‘utf-8‘)依赖函数你拷贝的加密函数是否内部又调用了其他全局函数这些函数是否都正确还原了时间戳或计数器算法里是否混入了毫秒级时间戳或一个自增的计数器你需要模拟相同的时间点或计数器值。避坑技巧在逆向初期可以采用“最小化验证法”。不要试图一次性还原整个w的生成。而是先找到生成w的最外层函数然后在 Python 中通过网络请求直接调用原网站的 JS 文件在一个无头浏览器环境中如 Puppeteer让浏览器算好w返回给你。这样能确保 100% 正确。虽然效率低且依赖浏览器但可以为你后续的纯算法复现提供一个绝对正确的“参考答案”用于比对每一步的输出。7. 进阶动态密钥与算法变异我们上面分析的是“第一个w”相对静态。但极验的验证是连续的后续提交滑动轨迹的w可能是第二个、第三个会更加复杂。密钥动态化第一个w解密后可能会包含一个一次性的密钥key或向量iv用于加密后续的轨迹数据。这意味着每次验证的加密密钥都不同。算法组合轨迹数据可能先经过自定义的编码/混淆然后再用 AES 等标准算法加密最后再 Base64。代码热更新极验的 JS 文件可能会频繁更新每次更新可能改变混淆方式甚至微调算法。需要有一套机制来检测这种变化。应对策略是建立一套可持续的逆向框架特征码定位不要依赖固定的函数名或行号。而是寻找算法中不变的代码特征片段例如一段特殊的常量数组或一个独特的位操作序列编写脚本在 JS 文件中自动搜索定位核心函数。自动化提取定位到核心函数后自动将其依赖的函数树提取出来。差分比对当 JS 文件更新后自动比对新旧版本的核心函数代码判断是单纯混淆变了还是逻辑也变了。逆向工作就像一场攻防战没有一劳永逸的解决方案。理解原理、掌握工具、耐心分析和不断实践才是应对变化的最好方法。这次对极验第一个w的分析就是一个完整的“破门”过程掌握了这套方法面对更复杂的验证逻辑你也有了拆解它的基础能力。
返回列表