验证码逆向工程实战:从加密参数到风控对抗的完整解析

📅 2026/7/27 18:37:45
验证码逆向工程实战:从加密参数到风控对抗的完整解析
1. 项目概述从“黑盒”到“白盒”的验证码攻防实战在当前的互联网安全攻防体系中验证码作为区分人机行为的第一道防线其复杂度和对抗强度与日俱增。特别是以某讯为代表的互联网大厂其旗下的滑块、云验证码、天御、防水墙等产品早已不是简单的图片识别而是集成了行为分析、环境指纹、加密混淆、云端决策于一体的复杂系统。对于安全研究人员、爬虫工程师或是逆向爱好者而言理解并突破这些验证码不仅是为了“绕过”更是一个深入理解现代前端安全、密码学应用和风控逻辑的绝佳窗口。本次实战我们将聚焦于这些验证码体系中最核心、也最令人头疼的部分那些看似随机、实则蕴含丰富信息的加密参数如collect、eks、ans等。我们的目标不是提供一个“一键破解”的脚本而是带你走完一次完整的逆向工程旅程解密其生成逻辑理解其防御意图最终掌握自主分析与应对的能力。2. 核心思路与逆向工程方法论逆向验证码尤其是大厂的验证码切忌一上来就埋头抠代码。一个系统性的方法论能让你事半功倍避免在无尽的混淆代码中迷失方向。2.1 逆向分析的核心目标与路径我们的终极目标是模拟出一次完整的、能被验证码服务器认可的验证请求。这通常需要构造一个HTTP请求包其中包含多个关键参数。以常见的滑块验证为例一个成功的验证请求可能包含以下参数collect: 环境指纹数据经过加密。eks: 加密密钥种子用于后续解密或签名。ans: 滑动轨迹数据经过加密。captcha_id: 验证码场景ID。token: 一次验证会话的令牌。逆向工程就是要找出这些参数是如何生成的。基本路径遵循“由外向内由果溯因”的原则网络抓包定位首先使用浏览器开发者工具F12的Network面板录制一次完整的失败或成功的验证码交互过程。筛选XHR/Fetch请求找到最终提交验证的那个请求通常包含verify、validate等关键字。这个请求的Form Data或Payload就是我们需要的参数集合。关键参数溯源在找到的提交请求上点击右键选择“Copy - Copy as cURL”或使用工具重放确认其必要性。然后在开发者工具的Sources面板或使用调试工具对生成这些参数的JavaScript代码进行断点调试。逻辑分析与算法还原跟踪代码执行流理清参数构造、加密、编码的完整链条。这一步最考验耐心和技术涉及反混淆、AST抽象语法树解析、算法识别等。2.2 工具链准备工欲善其事必先利其器现代验证码的JavaScript代码普遍经过高强度混淆如Obfuscator、JScrambler等直接阅读几乎是天书。因此一套强大的工具链至关重要浏览器与调试器Chrome/Edge开发者工具是基础。重点关注Sources面板代码调试、Network面板网络抓包、Console面板执行上下文。反混淆与格式化工具对于简单混淆浏览器自带的“Pretty print”功能{}按钮可能有效。对于更复杂的场景需要专门的工具例如AST解析库如babel、espree用于程序化地解析、修改和还原JavaScript代码结构。你可以编写自己的脚本将十六进制字符串还原、将复杂的控制流扁平化。浏览器自动化Puppeteer或Playwright。它们不仅能驱动浏览器完成验证码交互以供抓包更强大的功能在于可以注入代码在页面上下文中直接修改或Hook关键函数动态地观察输入输出极大降低静态分析的难度。密码学辅助工具CyberChef在线、Python的Crypto库。用于识别和验证加密算法AES, RSA, DES、哈希算法MD5, SHA系列、编码方式Base64, Hex等。编程环境Node.js用于执行和测试还原出的JavaScript算法Python用于整合整个验证流程编写最终的破解脚本。注意逆向工程的法律和道德边界必须清晰。所有分析应仅用于学习、安全研究或在拥有明确授权的范围内进行。未经授权对生产环境系统进行攻击性测试是违法的。3. 关键参数深度解析与逆向实战接下来我们深入解剖collect、eks、ans这三个最具代表性的参数还原其背后的故事。3.1collect参数你的“数字指纹”收集器collect参数通常是经过加密的一长串字符串或二进制数据解密后是一个庞大的JSON对象它负责将你的浏览器和环境信息“出卖”给验证码服务器。3.1.1collect包含哪些信息解密后的collect数据可能包含数十甚至上百个字段主要类别如下浏览器特征userAgent、language、platform、hardwareConcurrencyCPU核心数、deviceMemory内存大小。屏幕与渲染信息screen分辨率、色深、webgl显卡渲染器信息、canvasCanvas指纹。时区与本地化timezone、locale。插件与字体plugins浏览器插件列表、fonts系统字体列表通过Canvas测量。行为与性能基线performance页面性能数据、某些操作的响应时间差。其他高级指纹audioContext音频指纹、webRTC本地IP泄露。3.1.2collect的生成与加密流程信息收集验证码的JS代码会执行一系列“探针”函数同步或异步地收集上述信息。数据序列化将收集到的信息组装成一个大的JavaScript对象。数据标准化与排序为了确保同一环境生成的指纹一致对象键值对可能会被按字母顺序排序或进行其他标准化处理。字符串化将对象转换为JSON字符串。压缩可能会使用pako等库进行gzip或deflate压缩以减少数据量。加密这是最关键的一步。常见的加密方式是AES加密。密钥eks在其中扮演核心角色。模式通常是CBC模式。填充PKCS7。IV初始化向量可能是固定的或从eks中派生或随请求变化。编码输出将加密后的二进制数据进行Base64编码最终生成collect参数。3.1.3 逆向collect的实操技巧搜索关键函数在混淆代码中搜索get、collect、fingerprint、data等关键词的变量或函数名。Hook 关键API使用Puppeteer的page.evaluateOnNewDocument方法在页面加载前注入代码覆盖navigator、screen、CanvasRenderingContext2D等对象的属性或方法记录其被调用的时机和参数。定位加密函数在最终提交请求前打XHR断点然后回溯调用栈Call Stack。寻找类似encrypt、AES、CryptoJS、mode、padding的字符串。有时密钥eks会作为参数传入加密函数。验证还原算法将你还原的JavaScript加密函数在Node.js环境中运行输入已知的原始数据看输出是否与浏览器生成的collect一致。这是检验逆向是否成功的金标准。3.2eks参数加密密钥的“种子”eks看起来可能像一串随机字符串或数字它是生成加密密钥的关键。3.2.1eks的常见生成方式随机数衍生通过Math.random()、crypto.getRandomValues()生成一个随机数然后经过一个固定的算法如哈希、简单变换生成最终eks。服务器端持有相同算法可以还原出密钥。时间戳衍生结合当前时间戳和某个固定盐值salt进行哈希如MD5生成。服务器通过时间窗口进行校验。服务器下发在验证码初始化时服务器返回一个初始的seed客户端JS用这个seed经过复杂运算衍生出eks。这种方式逆向难度最大需要完整还原整个派生算法。作为AES密钥本身在某些简单实现中eks可能直接就是AES加密的密钥或密钥的一部分。3.2.2 逆向eks的策略搜索与追踪在代码中全局搜索eks这个变量名。查看它在哪里被赋值它的值来源于哪个函数调用。算法还原如果eks是衍生出来的需要仔细分析其衍生算法。常见的模式是eks md5(timestamp ‘固定字符串’)或eks btoa(一些操作后的随机数)。动态调试在生成eks的代码行设置断点观察其输入和输出。对比多次请求的eks值寻找规律是否与时间相关是否每次变化。3.3ans参数行为轨迹的“密文”ans代表了用户的行为对于滑块就是滑动轨迹对于点选验证码就是点击坐标序列。3.3.1 轨迹数据的采集与加工原始轨迹采集监听鼠标的mousedown、mousemove、mouseup事件记录每个事件点的(x, y)坐标和对应的时间戳(t)。轨迹是一个数组[{x, y, t}, {x, y, t}, ...]。轨迹加工与特征提取原始轨迹不会直接加密通常会提取特征使其既能反映人的操作又能压缩数据。特征可能包括总滑动距离、总耗时。移动路径的x方向序列去除了y轴的抖动。速度曲线加速度、减速度特征。在缺口处的停留或抖动模拟人的犹豫。轨迹的平滑度通过贝塞尔曲线拟合或滤波算法。数据序列化将特征数据转换为一个数组或特定格式的字符串。3.3.2ans的加密与编码加工后的轨迹数据会经过加密流程与collect类似压缩可能使用简单的自定义编码或标准压缩。加密使用由eks参与生成的密钥进行加密常见为AES。注意加密ans的密钥可能与加密collect的密钥相同也可能是用eks衍生的另一个密钥。编码Base64或Hex编码生成最终的ans参数。3.3.3 逆向ans的突破口事件监听器在滑块按钮或验证码图片的DOM元素上查找addEventListener绑定的mousedown事件。从这里入手跟踪事件处理函数。轨迹模拟与对比自己用程序生成一条模拟轨迹例如匀速滑动然后和一条人工滑动的轨迹分别提交对比生成的ans密文。观察加密后的长度、模式差异辅助判断加密强度。加密函数复用通常一个验证码系统会使用同一套加密工具函数。一旦你成功还原了collect的加密函数例如一个叫encryptData的函数那么ans很可能也是调用同一个函数进行加密的。只需找到传入的数据不同即可。4. 完整逆向流程与代码还原实战让我们以一个虚构但典型的某讯滑块验证码为例串联起整个逆向过程。4.1 环境搭建与抓包首先使用Puppeteer打开目标验证码页面。const puppeteer require(puppeteer); (async () { const browser await puppeteer.launch({headless: false}); // 非无头模式以便观察 const page await browser.newPage(); // 监听所有网络请求 await page.setRequestInterception(true); page.on(request, request { // 可以在这里过滤或记录请求 request.continue(); }); page.on(response, response { const url response.url(); if (url.includes(captcha) || url.includes(verify)) { console.log(捕获相关请求: ${url}); // 可以进一步处理响应 } }); await page.goto(https://目标网站.com/login); // 替换为目标网址 // 等待验证码出现这里需要根据实际页面调整选择器 await page.waitForSelector(.captcha-container); })();手动完成一次滑块验证同时从Puppeteer的控制台输出或浏览器开发者工具的Network面板中找到最终的验证提交请求例如https://captcha.qq.com/cap_union_verify记录下完整的请求体和响应。4.2 核心算法定位与Hook假设我们通过抓包得到提交数据如下{ aid: 123456, captcha_id: abcdefg, token: xyz789, collect: U2FsdGVkX1/...长Base64字符串..., eks: a1b2c3d4e5, ans: U2FsdGVkX1/...另一长Base64字符串... }现在需要在页面中定位生成这些参数的代码。我们通过注入代码来Hook关键函数。// 在page.evaluateOnNewDocument中注入 // 1. Hook JSON.stringify 和 console.log 因为collect数据很可能在加密前被stringify const originalStringify JSON.stringify; JSON.stringify function(...args) { const result originalStringify.apply(this, args); if (args[0] typeof args[0] object args[0].veryLongProperty) { // 假设我们通过属性猜测这是collect对象 console.trace(捕获到可能的collect原始数据:, args[0]); window.__collectRaw args[0]; // 存到全局变量供外部读取 } return result; }; // 2. Hook 加密函数 (假设是CryptoJS) if (window.CryptoJS window.CryptoJS.AES) { const originalEncrypt window.CryptoJS.AES.encrypt; window.CryptoJS.AES.encrypt function(message, key, cfg) { console.trace(AES.encrypt被调用:, { message, key, cfg }); const result originalEncrypt.apply(this, arguments); console.log(加密结果 (CipherParams):, result); console.log(加密结果 (Base64):, result.toString()); return result; }; } // 3. Hook 特定的提交函数 (通过抓包请求的调用栈可以找到函数名假设叫submitVerify) if (window.submitVerify) { const originalSubmit window.submitVerify; window.submitVerify function(data) { console.trace(提交数据被调用原始数据:, data); return originalSubmit.apply(this, arguments); }; }通过这样的Hook我们可以在控制台看到清晰的调用链和数据流。4.3 算法还原与Node.js实现经过动态调试和静态分析我们假设还原出以下核心算法示例eks生成算法eks md5(‘固定前缀’ Date.now().toString()).substr(0, 16)。这是一个基于时间戳的简单MD5哈希。加密函数使用CryptoJS的 AESCBC模式PKCS7填充。密钥为eksIV是一个固定值‘0123456789abcdef’。collect生成流程收集数据 - 排序键名 - JSON.stringify -使用上述加密函数加密- Base64输出。ans生成流程生成轨迹数组 - 计算特征如x轴位移序列- JSON.stringify -使用同一个加密函数加密但密钥可能是eks加盐后派生- Base64输出。基于此我们可以用Node.js编写还原算法const CryptoJS require(crypto-js); const md5 require(md5); function generateEks() { const timestamp Date.now(); const fixedPrefix tx_captcha_; return md5(fixedPrefix timestamp).substr(0, 16); } function encryptData(data, key, iv 0123456789abcdef) { const keyHex CryptoJS.enc.Utf8.parse(key); const ivHex CryptoJS.enc.Utf8.parse(iv); const dataStr typeof data object ? JSON.stringify(data) : data; const encrypted CryptoJS.AES.encrypt(dataStr, keyHex, { iv: ivHex, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }); return encrypted.toString(); // 返回Base64密文 } function simulateCollectData() { // 模拟收集的浏览器指纹这里需要尽可能真实 return { userAgent: navigator.userAgent, platform: navigator.platform, // ... 数十个其他字段 webglVendor: 模拟的显卡信息, canvasFp: 模拟的Canvas指纹 }; } function simulateTrajectory(dragDistance) { // 模拟生成一条滑动轨迹包含x, y, t const points []; const startTime Date.now(); const duration 2000 Math.random() * 1000; // 2-3秒 const steps 30; for (let i 0; i steps; i) { const t startTime (i / steps) * duration; const x (i / steps) * dragDistance; // y轴添加一些符合人手抖动的随机偏移 const y Math.sin(i * 0.3) * 2 Math.random() * 1; points.push({ x, y, t }); } // 提取特征例如只取x位移序列和耗时 return { track: points.map(p Math.round(p.x)), time: duration }; } // 主流程 const eks generateEks(); console.log(生成的 eks:, eks); const rawCollect simulateCollectData(); const encryptedCollect encryptData(rawCollect, eks); console.log(模拟的 collect 密文:, encryptedCollect); const dragDistance 300; // 假设缺口距离300像素 const rawAns simulateTrajectory(dragDistance); const encryptedAns encryptData(rawAns, eks _ans_salt); // 假设ans密钥不同 console.log(模拟的 ans 密文:, encryptedAns);这个Node.js脚本模拟了核心参数的生成。在真实场景中simulateCollectData函数需要极其精细地模拟真实浏览器的所有指纹这是对抗验证码风控最困难的部分之一。5. 常见问题排查与高级对抗技巧即使还原了算法在实际应用中仍然会遇到各种问题。5.1 参数有效性校验失败问题服务器返回“验证失败”或“参数错误”。排查collect指纹不完整/不真实这是最常见的原因。检查你模拟的指纹字段是否齐全值是否合理例如webgl渲染信息是否是一串有效的哈希。解决方案是使用更真实的浏览器环境如puppeteer-extra-plugin-stealth或直接复用真实浏览器采集的指纹数据。eks生成算法有误或有时效性检查服务器时间与本地时间是否同步。eks可能有时效窗口如5分钟过期即失效。确保你的eks生成逻辑与JS代码完全一致包括所有细微的字符串处理大小写、编码。ans轨迹过于“机器化”匀速滑动、直线滑动、过于完美的贝塞尔曲线都会被识别。需要注入人性化随机因素随机加速度、在缺口附近轻微抖动或停顿、移动路径略带弧度。缺少其他隐藏参数仔细检查提交请求是否还有如csessionid、sig、rnd等参数。这些可能在其他接口返回需要关联获取。5.2 代码混淆与反调试问题代码被严重混淆无法下断点或一打开开发者工具页面就卡死/跳转。对策本地替换将关键的、混淆的JS文件保存到本地使用AST工具进行反混淆和格式化然后通过Fiddler/Charles等代理工具将线上请求映射到本地清晰的脚本文件。绕过反调试有些代码会检测debugger语句或开发者工具状态。可以尝试在无头模式下用Puppeteer调试或者使用Debugger原型链劫持等方法来绕过检测。Hook 构造函数直接HookFunction构造函数或eval在代码执行前就捕获并美化它。5.3 风控升级与动态变化问题今天还能用的方案明天就失效了。collect的字段增加了加密算法变了。应对策略建立监控机制定期如每天运行你的测试脚本监控成功率。一旦下降立即触发警报。模块化设计将指纹收集、加密算法、轨迹模拟等模块解耦。当某个部分失效时可以单独替换该模块。参数化配置将所有可能变化的参数如加密的IV、密钥派生盐值、指纹字段列表放在配置文件中便于快速调整。拥抱“真实”最根本的对抗方式是无限逼近真实用户。考虑使用浏览器指纹管理工具如browser-fingerprint-sdk的逆向思路来生成稳定且唯一的指纹或者直接使用自动化框架驱动真实浏览器来完成验证虽然效率低但成功率最高。这演变成了一场资源计算资源、IP资源、浏览器环境资源与风控策略的对抗。5.4 实战心得与避坑指南不要试图完全“破解”加密我们的目标不是破解AES而是找出密钥eks的生成规律和加密的调用方式。重点在流程不在算法本身。善用“比较”同时抓取多次成功和失败的请求对比它们的参数差异。差异点往往就是关键所在。关注初始化请求验证码页面加载时通常有一个init或get请求它会返回captcha_id、token以及可能的初始seed。这个请求的响应数据是后续所有计算的基石。环境一致性确保你模拟请求的User-Agent、Cookies、Referer等HTTP头与浏览器行为一致。不一致的头部信息可能导致请求在到达业务逻辑前就被网关拦截。成本权衡对于快速变化的验证码维护逆向方案的成本可能很高。在商业项目中需要权衡自研逆向、购买商业打码平台API、或采用更高级的模拟浏览器方案之间的成本与收益。逆向验证码是一个持续对抗的过程没有一劳永逸的解决方案。它考验的不仅是技术深度更是耐心、细心和对细节的掌控力。通过本次对collect、eks、ans等核心参数的拆解希望为你打开一扇门让你在下次面对类似挑战时能够有条不紊地进行分析直击要害。记住理解防御者的思路是成为更出色的进攻者或建设者的第一步。