逆向工程实战:数美滑块验证码行为加密与Python自动化破解

发布时间:2026/7/2 22:08:02
逆向工程实战:数美滑块验证码行为加密与Python自动化破解 1. 项目概述与核心挑战最近在分析一个需要登录的网站时遇到了数美科技的滑块验证码。这玩意儿在不少金融、电商、社交类App的后台或网页端都很常见主要目的就是拦住自动化脚本确保操作的是真人。对于做数据采集、自动化测试或者单纯想研究一下安全机制的朋友来说绕过它是个绕不开的坎。所谓的“扣代码”其实就是指从混淆、压缩、加密过的前端JavaScript代码中把生成关键参数比如滑块轨迹、加密token的核心逻辑剥离出来用Python或其他语言重新实现。这个过程远比简单地调用一个打码平台要硬核也更考验对JavaScript运行机制和加密原理的理解。数美的滑块业内口碑算是“有点东西”的。它不像一些简单的滑块只校验一下拖拽的位移距离。数美会采集你整个拖拽过程中的行为数据包括移动轨迹、速度、加速度、甚至鼠标的微小抖动然后通过一套复杂的算法生成一个加密的token提交到后端进行验证。如果你直接用自动化工具模拟一个“完美”的匀速直线运动百分之百会被识别出来。所以逆向的目标非常明确第一搞清楚前端是如何采集和加密这些行为数据的第二把这个加密过程用Python复现出来让我们的脚本能生成一个足以“骗过”服务器的合法token。整个逆向过程可以概括为“定位、分析、扣取、还原”四步。听起来简单但每一步都可能遇到各种“坑”比如令人眼花缭乱的代码混淆、层层嵌套的函数调用、以及为了反调试而设置的各种陷阱。接下来我就结合这次逆向数美滑块的实际经历把这其中的门道和踩过的坑掰开揉碎了跟大家聊聊。2. 逆向环境准备与关键工具链工欲善其事必先利其器。在开始“扣代码”之前搭建一个顺手的逆向分析环境至关重要。这个环境的核心目标有两个一是能清晰地看到前端JavaScript的执行逻辑和数据流二是能方便地对我们感兴趣的代码片段进行调试和导出。2.1 浏览器开发者工具深度配置Chrome DevTools 是我们的主战场。除了常用的Sources和Network面板有几个关键设置和功能必须打开。首先在Settings-Preferences中确保勾选了“Disable JavaScript”可以随时禁用JS和“Enable local overrides”允许本地覆盖源码。后者极其有用当你在Sources面板里找到关键JS文件后可以直接右键Save for overrides之后你对这个文件所做的任何修改比如下断点、修改变量值都会在刷新页面后生效而无需每次都去线上找。其次针对混淆代码Sources面板下的“Pretty-print”按钮那个花括号{}图标是救命稻草。它能将压缩成一行的代码重新格式化变得可读。但要注意对于经过特殊混淆比如变量名替换为_0x1a2b3c这种十六进制字符串的代码美化后逻辑依然绕但至少结构清晰了。最关键的是断点策略。不要一上来就在可疑函数入口下断点那样很容易被海量的调用淹没。我的习惯是事件监听器断点在Sources面板右侧的Event Listener Breakpoints里展开Mouse事件勾选mousedownmousemovemouseup。因为滑块操作必然触发这些事件从这里切入能最快定位到事件处理函数。XHR/Fetch断点在滑块拖动完成后前端一定会发送一个验证请求。在Sources面板右侧的XHR/Fetch Breakpoints里点击号输入包含验证关键字如verifyvalidatetoken的URL片段。当请求发出时代码执行就会自动暂停在发起请求的那一行逆向查找加密逻辑的入口就变得非常直接。DOM断点有时候加密参数会直接写入到某个隐藏的input元素中。右键点击该DOM元素选择Break on-Attribute modifications当其值被修改时执行流也会中断。2.2 辅助逆向工具的选择与搭配光靠浏览器还不够一些专业工具能极大提升效率。Fiddler Classic / Charles这类抓包工具的重要性不亚于DevTools。它们能记录所有HTTP/HTTPS请求和响应并且具备强大的断点和修改功能。在逆向初期通过抓包可以快速确定验证请求的端点Endpoint、请求参数的结构尤其是那个关键的token或sig字段。我们可以用Fiddler的AutoResponder功能将线上的JS文件替换为本地的、经过我们解混淆或修改的版本方便调试。Node.js环境这是“扣代码”后进行本地化验证和模拟执行的沙盒。很多时候浏览器里能跑通的代码片段直接复制到Node.js里会因为缺少浏览器环境如windowdocument对象或某些Web API而报错。我们需要用vm2这类沙盒模块或者更简单地使用puppeteer无头浏览器来执行这些代码确保逻辑正确。Python环境及相关库最终我们要用Python来实现整个流程。除了基本的requests库用于发请求execjs或PyExecJS库常用于直接执行我们“扣”出来的JavaScript代码片段。对于更复杂的、需要模拟浏览器完整环境的情况playwright或selenium是更好的选择它们能直接驱动真实浏览器执行JS。注意使用execjs时要注意其调用的JavaScript引擎如Node.js或系统安装的JavaScriptCore。不同引擎对ES6语法的支持度可能不同可能导致扣出来的代码无法运行。稳妥起见在扣代码时尽量将ES6语法如let/const、箭头函数、模板字符串转换为ES5语法。2.3 核心思路由外而内顺藤摸瓜逆向分析最忌讳一头扎进几万行的混淆代码里。正确的思路是从网络请求这个最外层的“结果”出发反向推导。抓包定位验证请求正常操作一次滑块在Fiddler或Chrome Network中找到最后那个状态码为200的验证请求通常是POST方法。重点关注它的Form Data或Payload里面一定有一个长得像乱码的长字符串字段这就是加密后的token。记下这个请求的URL和所有参数。搜索关键参数在Sources面板按CtrlShiftF进行全局搜索。搜索这个token的字段名如tokensigw或者搜索验证请求的URL片段。通常能直接定位到组装这个请求参数的JavaScript代码位置。下断点追踪数据流在找到的位置下断点重新操作滑块。当断点触发时观察调用栈Call Stack看看这个参数是从哪个函数返回的。然后一步步跟进去关注这个参数的生成过程。它很可能是一个复杂函数调用的返回值这个函数就是我们最终要扣取的目标。3. 数美滑块核心逻辑分析与代码定位按照上面的思路我们开始针对数美滑块进行实战分析。数美的前端代码混淆程度属于中等偏上变量名和函数名都被替换了但函数结构和控制流通常保持得相对完整。3.1 网络请求分析与加密入口定位首先操作滑块完成验证在开发者工具的Network面板中过滤XHR请求。很快就能找到一个指向类似https://captcha-api.fengkongcloud.com/api/v2/slider/verify的POST请求。查看其请求负载Payload会发现一个非常关键的字段比如叫riskToken、sig或一个无意义的键名其值是一长串由字母数字和符号组成的字符串这就是加密后的核心凭证。同时请求里通常还会包含一些其他信息比如organization机构标识、captchaUuid本次验证会话ID、model滑块类型如slide、以及一个data字段里面可能包含了滑块背景图、缺口位置的加密信息等。我们的核心目标就是逆向生成那个最长的、加密的token字段。在Sources面板全局搜索这个token的字段名例如riskToken。搜索结果可能会指向一个被压缩的JS文件。点击进入格式化代码后继续在该文件内搜索这个字段名。通常会找到类似这样的代码块var _0x123456 { riskToken: _0xabcdefg[generateToken](_0xparam1, _0xparam2), captchaUuid: _0xuuid, // ... 其他参数 };这里_0xabcdefg[generateToken]很可能就是加密函数。在这个函数名上点击尝试跳转到定义处。如果无法直接跳转可以搜索generateToken这个字符串注意可能是被赋值给一个变量或者更直接地在包含riskToken的这一行代码左侧行号处点击设置一个行断点。3.2 关键函数追踪与行为数据采集逻辑设置断点后刷新页面并重新拖动滑块。当代码执行到断点处暂停时重点观察两个地方调用栈Call Stack和作用域变量Scope。调用栈可以看到当前函数是被谁调用的。一层层往上查看你就能理清从鼠标事件触发到轨迹采集再到加密生成token的完整调用链。这个链条里每一个节点都可能是我们需要分析的函数。作用域在Local或Closure作用域中查看传入generateToken函数的参数_0xparam1, _0xparam2到底是什么。通常其中一个参数就是包含了完整用户行为数据的对象。通过单步调试F10步入F11这些函数我们可以发现数美滑块行为数据采集的大致逻辑事件绑定在滑块按钮mousedown时开始记录。轨迹记录在mousemove事件中以高频率如每10-20毫秒采集当前鼠标相对于滑块容器的位置offsetX,offsetY以及一个时间戳。数据封装在mouseup事件触发时停止记录。将收集到的所有轨迹点可能包含上百个点封装成一个数组。每个点的数据结构可能类似{t: 时间戳, x: 横坐标, y: 纵坐标}。注意这里的y坐标可能变化很小主要是x坐标在变化。数据加工原始的轨迹数据可能会被加工。例如计算每个点的瞬时速度、加速度或者对轨迹进行平滑处理滤波以消除录制时手抖产生的噪声。加工后的数据才是最终被送入加密函数的数据。实操心得在调试轨迹采集时可以在控制台Console里打印出轨迹数组。复制出来用Python的matplotlib画个图直观地看看自己拖动生成的轨迹是什么样的。你会发现人工拖动的轨迹是有起伏、有变速的而脚本生成的匀速轨迹在图上就是一条直线这很可能就是被识别的特征之一。3.3 加密算法识别与代码扣取策略当我们跟随调用链最终进入那个生成riskToken的核心函数时就进入了最关键的环节。这个函数内部可能非常复杂但通常由几个部分组成数据序列化将行为数据对象转换成字符串可能是JSON.stringify。加密/编码这是核心。常见的有几种可能AES/ DES 对称加密可能会看到CryptoJS.AES.encrypt或类似的调用。需要找到key和iv初始化向量。这两个值可能是硬编码在JS里的也可能是通过其他接口动态获取的。RSA 非对称加密可能会看到JSEncrypt库或类似setPublicKey的调用。公钥通常直接写在JS里或从接口返回。自定义算法最麻烦的情况。开发者自己写了一套混淆的字符变换、位移、哈希混合的算法。这需要耐心地一步步跟理解其每一步操作的目的。Base64编码通常作为最后一步将加密后的二进制数据转换为可传输的字符串。可能会看到btoa或CryptoJS.enc.Base64.stringify。扣取代码的策略最小化扣取我们的目标不是把整个几万行的JS文件都搬过来而是只扣取生成token所必需的最小依赖集。从核心加密函数开始沿着它的函数调用关系向上追溯把用到的函数、变量、对象定义都复制出来。补环境扣出来的代码片段很可能依赖浏览器环境windownavigatordocument或某个特定的JS库如CryptoJS。我们需要在Node.js或Python的execjs环境中模拟出这些依赖。对于window、document等可以定义一个空对象{}来占位如果代码只是判断其是否存在而不调用具体属性通常就能通过。对于CryptoJS这样的库最佳方案是在Node.js环境中通过npm install crypto-js安装然后在扣取的代码开头通过require引入。如果要在execjs中用可能需要将整个crypto-js.js源码合并进你的执行上下文。构造入口函数将扣取的所有代码包裹在一个自定义的JavaScript函数中这个函数接收行为数据轨迹数组作为输入输出最终的token字符串。这样在Python中就可以通过execjs调用这个函数了。// 扣取代码后构造的示例入口函数 function getRiskToken(trackData, captchaUuid, otherParams) { // 这里粘贴所有扣取出来的依赖函数和变量定义 // ... // 假设核心加密函数是 window._0xabc.encrypt var encryptedData window._0xabc.encrypt(JSON.stringify(trackData), window._0xdef.key); var finalToken window._0xghi.format(encryptedData, captchaUuid); return finalToken; }4. 轨迹模拟与Python代码复现有了生成token的JavaScript代码我们还需要用Python模拟出“像人一样”的拖动轨迹然后将轨迹数据传给扣出来的JS逻辑才能得到有效的验证参数。4.1 人类行为轨迹建模与生成算法完全随机的轨迹不行匀速直线的轨迹更不行。一个相对靠谱的人类轨迹模型应该包含以下几个阶段启动加速手指按下瞬间从静止开始移动初始加速度较大速度快速上升。中途调整速度达到一个峰值后由于意识到要对准缺口会有轻微的减速、停顿甚至小幅回拉overshoot速度曲线呈波浪形。减速贴合在接近目标缺口时开始明显减速以便精确对齐。微小抖动在整个过程中由于肌肉震颤轨迹会在主方向上叠加非常微小的随机垂直偏移。我们可以用物理学中的匀加速/匀减速运动模型来模拟并加入随机因子。下面是一个Python的轨迹生成函数示例import random import time import math def generate_track(distance): 生成模拟人类拖动的轨迹 :param distance: 需要拖动的总距离像素 :return: 轨迹列表每个元素为 [时间差, x偏移, y偏移] track [] current_x 0 current_time 0 # 1. 启动阶段 (约占总距离30%) stage1_distance distance * 0.3 v0 0 # 初速度 a random.uniform(0.8, 1.2) # 加速度 t1 math.sqrt(2 * stage1_distance / a) # 加速阶段时间 for i in range(int(t1 * 10)): # 每0.1秒记录一个点 delta_t 0.1 current_time delta_t s v0 * delta_t 0.5 * a * delta_t * delta_t current_x s v0 a * delta_t # 加入微小垂直抖动 y_offset random.randint(-2, 2) track.append([round(delta_t*1000), round(current_x), y_offset]) # 2. 匀速微调阶段 (约占总距离50%) stage2_distance distance * 0.5 avg_v v0 # 从加速阶段末速度开始 t2 stage2_distance / avg_v for i in range(int(t2 * 10)): delta_t 0.1 current_time delta_t # 速度有轻微波动模拟调整 current_v avg_v * random.uniform(0.9, 1.1) current_x current_v * delta_t y_offset random.randint(-3, 3) track.append([round(delta_t*1000), round(current_x), y_offset]) # 3. 减速贴合阶段 (剩余距离) stage3_distance distance - current_x v_end 0 # 末速度 # 使用匀减速公式 s (v0^2 - v_end^2) / (2*a_dec)求减速度 a_dec (v0 ** 2) / (2 * stage3_distance) if stage3_distance 0 else 1 t3 v0 / a_dec for i in range(int(t3 * 10)): delta_t 0.1 current_time delta_t current_v v0 - a_dec * delta_t * (i1) current_x current_v * delta_t y_offset random.randint(-1, 1) track.append([round(delta_t*1000), round(current_x), y_offset]) v0 current_v # 确保最终位置精确等于目标距离并可能有一个“贴合”的微小回拉 if abs(current_x - distance) 1: track.append([50, distance, 0]) # 一个快速的修正 else: track.append([random.randint(30, 100), distance, 0]) # 最后的停顿 return track4.2 扣取JS代码的Python集成与调用轨迹生成后我们需要用Python调用之前扣取的JavaScript代码来加密。这里主要使用execjs库。首先确保已安装PyExecJS和Node.js环境pip install PyExecJS然后将我们扣取并封装好的JavaScript代码保存为一个字符串或者放在一个.js文件中。这里假设我们保存为sm_slider.js其中暴露了一个名为get_sm_token的全局函数。import execjs import json # 1. 读取扣取的JS代码 with open(sm_slider.js, r, encodingutf-8) as f: js_code f.read() # 2. 创建JS执行上下文 ctx execjs.compile(js_code) # 3. 生成轨迹 (假设缺口位置距离为200像素) track_data generate_track(200) # 4. 准备其他参数 (这些参数需要从首次加载滑块的接口响应中获取) captcha_uuid 从第一次请求获取的session id organization 从网页源码或请求中提取的机构标识 model slide # 5. 调用JS函数生成token # 注意根据扣取的代码确定函数名和参数顺序 risk_token ctx.call(get_sm_token, track_data, captcha_uuid, organization, model) print(f生成的riskToken: {risk_token})4.3 完整请求流程组装与验证生成risk_token后我们还需要组装完整的验证请求。这需要参考第一次加载滑块页面时的网络请求。初始化请求通常访问页面时会有一个GET请求返回滑块图片、缺口位置可能是加密的、以及一个重要的captchaUuid。我们需要解析这个响应获取后续验证必需的参数。缺口识别数美的缺口位置信息很可能不是明文的而是经过加密或编码的。可能需要用扣取的JS代码中的另一个函数来解密或者是一个固定的算法比如Base64解码后是JSON。这一步也可能需要简单的图像识别但数美通常会把缺口距离信息直接给出来只是被编码了。组装验证请求将risk_token、captcha_uuid、organization、model以及可能有的data包含缺口信息等参数按照抓包看到的格式组装成字典。发送请求并验证使用requests.post发送验证请求。如果返回的JSON数据中code字段为1100或success字段为true通常表示验证通过。import requests # 假设从初始化接口获得了以下数据 init_data { captchaUuid: xxxx-xxxx-xxxx, organization: xxxxxx, data: {bgImgWidth: 300, pieceImgWidth: 50, encryptedDistance: AbCdEfG...} # 加密的缺口距离 } # 解密缺口距离 (假设有对应的JS解密函数) distance_encrypted init_data[data][encryptedDistance] real_distance ctx.call(decode_distance, distance_encrypted) # 另一个扣出来的函数 # 生成轨迹和token track generate_track(real_distance) risk_token ctx.call(get_sm_token, track, init_data[captchaUuid], init_data[organization], slide) # 组装验证请求 verify_url https://captcha-api.fengkongcloud.com/api/v2/slider/verify payload { captchaUuid: init_data[captchaUuid], organization: init_data[organization], model: slide, riskToken: risk_token, # 可能还有其他固定参数或从data中提取的参数 data: json.dumps(init_data[data]) } headers { User-Agent: 你的浏览器UA, Content-Type: application/x-www-form-urlencoded, # 注意Content-Type可能是form也可能是json Referer: 页面来源, } resp requests.post(verify_url, datapayload, headersheaders) result resp.json() if result.get(code) 1100: print(滑块验证通过) # 通常这里会返回一个一次性的token用于后续的登录或业务请求 business_token result.get(token) else: print(f验证失败: {result})5. 常见问题排查与实战避坑指南逆向过程中几乎一定会遇到各种报错和意外情况。这里记录几个最典型的问题和我的解决思路。5.1 环境缺失导致的JS执行错误问题在Node.js或execjs中运行扣取的代码报错ReferenceError: window is not defined或CryptoJS is not defined。解决window/document未定义在JS代码的最顶部添加模拟代码。// 在扣取的JS代码开头添加 if (typeof window undefined) { global.window {}; global.document {}; // 如果代码用了navigator也模拟一下 global.navigator {userAgent: Mozilla/5.0 ...}; }CryptoJS未定义如果扣取的代码直接用了CryptoJS对象你需要将完整的crypto-js库源码合并进来或者更优雅地在Node.js环境中通过require引入。// 方式一合并源码 (适用于execjs) // 将crypto-js.min.js的内容复制粘贴到你的扣取代码前面 // 方式二Node.js环境 (更推荐) // 确保已 npm install crypto-js // 在扣取代码开头添加 if (typeof require ! undefined) { var CryptoJS require(crypto-js); }核心技巧在浏览器的Console中在断点处将你扣取的函数整体复制然后手动补上模拟的window等对象直接执行测试可以快速定位环境问题。5.2 加密参数动态变化与应对问题第一次逆向成功了但过段时间发现token生成失败或者生成的token被服务器拒绝。解决检查密钥/公钥是否动态获取加密用的key或RSA公钥可能不是硬编码在JS里的而是通过另一个初始化接口获取的。你需要分析滑块加载时更早的网络请求找到返回这些密钥的接口并在Python脚本中先请求它。检查是否有时间戳或随机数因子加密算法可能引入了服务器时间戳或一个随机数nonce。你需要从第一次请求的响应中或者从当前页面的全局变量里找到这个值并作为参数传入加密函数。在扣代码时要留意函数里是否有Date.now()或Math.random()的调用。代码更新数美可能会不定期更新前端JS代码和加密逻辑。你需要建立一种机制定期或失败时重新抓取和分析最新的JS文件。可以将关键的JS文件URL和加密函数特征码记录下来写一个简单的校验脚本。5.3 轨迹模拟被识别与优化策略问题加密逻辑正确但验证通过率很低怀疑是轨迹模拟得不够“真人”。解决增加轨迹随机性上面提供的轨迹模型只是一个基础。可以增加更多随机因素加速段和减速段的长度比例随机、中途加入1-2次极短的停顿delta_t突然变大、在垂直方向y加入更有规律的“拟人抖动”比如正弦波叠加随机噪声。采集真人轨迹学习最有效的方法是用浏览器插件或脚本录制几十次自己手动滑动的真实轨迹将这些轨迹数据保存下来。分析这些真实轨迹的统计特征平均速度、最大速度、加速度分布、停顿点等。然后用算法如随机森林学习这些特征或者直接从录制的轨迹库中随机选取一条进行复用。验证失败反馈分析如果服务器返回了具体的错误码比如RISK_001可能代表了不同的识别原因轨迹异常、操作过快、环境异常等。针对不同的错误码调整模拟策略。5.4 调试技巧与日志记录问题在Python中调用JS生成token结果不对但不知道中间哪一步出了问题。解决在扣取的JS代码中加入日志在关键的步骤比如数据序列化后、加密前、加密后用console.log或return中间变量。在Node.js中运行可以看到输出在execjs中可以通过修改代码将日志收集到一个数组并最终返回。function getRiskToken(trackData) { var logs []; logs.push(输入轨迹长度: trackData.length); var strData JSON.stringify(trackData); logs.push(序列化后: strData.substring(0, 100)); // ... 加密过程 logs.push(最终token: finalToken); return { token: finalToken, logs: logs // 同时返回日志 }; }对比调试在浏览器中执行一次成功的滑块操作在加密函数的入口和出口打上断点记录下输入的轨迹数据和输出的token。然后在Python脚本中使用完全相同的轨迹数据直接复制浏览器里的数组调用扣取的JS函数看生成的token是否一致。如果不一致说明扣取的代码或环境有问题如果一致说明问题出在轨迹生成上。分步验证将加密过程拆解。先确保能正确生成序列化字符串再确保每一步加密/编码的中间结果都和浏览器里的一致。逆向分析是一个需要极大耐心和细致观察力的过程。每一个滑块验证码的实现都有其特点没有一成不变的解决方案。核心思路——从网络请求反推、定位关键函数、补全执行环境、模拟人类行为——是通用的。面对数美这样不断升级对抗策略的服务保持对代码变化的敏感建立一套自己的分析工具链和调试方法论比掌握某一个具体的破解技巧更为重要。