丰巢滑块验证码逆向分析:从JS扣取到轨迹模拟的完整实战

发布时间:2026/7/1 7:44:06

丰巢滑块验证码逆向分析:从JS扣取到轨迹模拟的完整实战 1. 项目概述与背景最近在分析一些主流互联网服务的登录流程时丰巢快递柜的官网登录验证机制引起了我的注意。它采用了一种典型的滑块验证码用户需要将滑块拖动到缺口位置才能完成验证。这种机制在保护账户安全、防止恶意登录方面起到了关键作用。然而对于开发者而言无论是出于自动化测试、数据采集的合法需求还是单纯对技术实现的好奇理解其背后的验证逻辑都极具价值。今天我就来深度拆解一下丰巢官网登录滑块的逆向分析过程并分享一套完整的、可运行的代码实现。整个过程不仅涉及前端JavaScript的逆向还包括轨迹模拟、加密参数破解等核心环节希望能为从事相关领域开发或研究的同行提供一个清晰的思路和实用的参考。这个项目适合对Web安全、JavaScript逆向、自动化测试感兴趣的中高级开发者。即使你是初学者只要具备一定的Python和JavaScript基础也能跟随本文的步骤理解整个逆向工程的脉络。我们将从最基础的页面请求分析开始一步步深入到算法核心最终实现一个能够模拟人类操作、成功通过验证的自动化脚本。在这个过程中你会了解到现代Web验证码的常见防御策略以及如何系统性地进行逆向分析。2. 核心思路与技术选型逆向一个滑块验证本质上是在模拟一个“合法用户”的完整操作。一个真实的用户操作包含几个关键步骤加载页面获取验证素材、识别缺口位置、生成模拟人类的拖动轨迹、在拖动过程中计算并提交必要的加密参数、最后完成验证。我们的自动化脚本也需要完美复现这条链路。2.1 整体逆向流程设计我的分析思路是自顶向下从网络请求入手逐步剥离出核心的验证逻辑请求拦截与静态分析首先使用浏览器开发者工具记录一次完整的、成功的手动滑块验证过程。重点关注Network面板中从页面加载到验证成功期间的所有请求特别是那些携带了加密参数、看起来像是提交验证结果的XHR或Fetch请求。同时在Sources面板中搜索与滑块、验证、轨迹相关的关键词定位核心的JavaScript文件。关键参数定位与追踪找到提交验证结果的请求后分析其请求体Request Payload。里面通常会包含滑块拖动的最终位置、一个由轨迹生成的加密token、时间戳等。我们的核心任务就是找出这些参数尤其是token的生成算法。JavaScript代码逆向与扣取参数生成逻辑必然封装在前端JavaScript代码中并且很可能经过了混淆或压缩。我们需要使用调试工具在关键函数处设置断点跟踪参数的生成过程。对于复杂的混淆代码可能需要使用AST抽象语法树技术进行反混淆和代码还原然后从中“扣取”出生成关键参数如轨迹加密token所必需的函数代码。缺口位置识别获取滑块背景图和带缺口的滑块图。识别算法有多种选择最简单的是通过OpenCV模板匹配或像素比对来计算缺口位置。更复杂的情况可能需要处理图片被切割、旋转或添加干扰噪点的情况。轨迹模拟算法这是模拟人类行为的关键。不能简单地将滑块从起点直线移动到终点。需要设计一个轨迹生成算法模拟人类的拖动行为先加速再匀速最后减速并可能包含微小的随机抖动。轨迹数据通常是一个包含多个(x, y, t)坐标点的数组。加密参数生成与请求模拟将识别出的缺口位置和生成的轨迹数据代入我们扣取出来的JavaScript加密函数中计算出最终的验证token。然后使用Python的requests库或Node.js的axios等工具模拟浏览器发送最终的验证请求。2.2 技术栈与工具选型基于以上流程我选择了以下技术组合这套组合在逆向工程中非常高效和通用分析工具Chrome/Edge DevTools核心分析工具用于网络抓包、JavaScript调试、DOM元素查看。Fiddler/Charles可选作为辅助的HTTP/HTTPS抓包代理有时比浏览器自带的工具更便于查看和修改请求。OverridesChrome DevTools的一个强大功能允许你本地替换网站上的JavaScript文件便于我们修改和调试扣取出来的代码。编程语言与库Python 3.8作为主力的自动化脚本语言生态丰富编写快捷。requests用于发送HTTP请求获取页面、图片和提交验证。Pillow (PIL)/OpenCV (cv2)用于图片处理。Pillow轻量易用适合简单的像素比对OpenCV功能强大模板匹配精度高能应对更复杂的图片情况。本项目将展示两种方式。numpy配合OpenCV进行数值计算。execjs/PyExecJS一个关键的库它允许你在Python环境中执行JavaScript代码。这样我们就可以直接调用从丰巢前端扣取出来的加密函数而无需用Python重写一遍复杂的加密逻辑。re/jsonPython标准库用于处理正则表达式和JSON数据。注意选择execjs而不是用Python重写JS加密逻辑是基于效率和安全性的考虑。很多网站的加密算法复杂且可能频繁变更直接调用原JS函数是最稳定、最不容易出错的方式。重写算法不仅工作量大而且一旦对方微调算法你的脚本就可能失效。3. 关键环节深度解析与实操3.1 网络请求分析与加密参数定位首先打开丰巢官网登录页按F12打开开发者工具切换到Network面板并勾选Preserve log。然后进行一次手动滑块验证。观察请求列表你会发现几个关键请求获取验证码图片的请求通常是一个GET请求返回一个包含滑块背景图和缺口滑块图的JSON数据或图片URL。你需要从中提取出两张图片的Base64编码或直接下载链接。提交验证结果的请求在你松开滑块后触发。这是一个POST请求URL可能包含verify,validate,check等关键词。点击这个请求查看它的Headers和Payload。以我分析时的请求为例实际参数名可能不同但结构类似// Request Payload { token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...很长的一串加密字符串, point: 245, action: login, timestamp: 1687854321000 }point: 这通常就是滑块拖动的最终x坐标即缺口位置。token: 这是核心它是由前端JavaScript根据轨迹、时间、缺口位置等多种因素加密生成的一个凭证。服务器端会解密这个token验证其有效性和是否来自“真人”操作。timestamp: 时间戳。我们的首要目标就是找到生成这个token的JavaScript函数。3.2 JavaScript逆向与核心函数扣取在Sources面板中使用CtrlShiftF进行全局搜索。搜索关键词可以是token,point,encrypt,sign,滑动,verify等中英文组合。找到可疑的JS文件后点击进入点击左下角的{}Pretty-print格式化代码。然后在疑似生成token的代码行附近设置断点。实操技巧一个高效的方法是在提交验证的XHR请求发起处设置“XHR/fetch Breakpoint”。在Sources面板的XHR Breakpoints栏点击输入请求URL的一部分如verify这样当浏览器发起该请求时会自动在发起请求的JavaScript代码处断住。然后顺着调用栈Call Stack向上回溯就能找到生成请求参数的函数。找到核心函数后假设它叫generateToken(track, point, time)接下来的任务是把这个函数以及它所有依赖的函数、变量、加密库如CryptoJS都“扣”出来。扣代码的原则是保持函数执行上下文的最小完整性。你需要把函数定义、它内部调用的其他函数、以及可能用到的全局变量或对象如window.crypto或某个特定的加密对象一并复制出来。常见坑点环境依赖扣出来的JS代码可能依赖浏览器环境下的window,document,navigator等对象。在Node.js或execjs环境中运行时需要模拟这些对象。一个简单的方法是在扣取的代码开头添加var window this; var document {}; var navigator { userAgent: Mozilla/5.0 ... }; // 如果用到location var location { href: https://www.fengchao.com.cn };加密库如果使用了CryptoJS你需要将整个CryptoJS库的源码或至少用到的那部分如MD5,SHA256,HmacSHA1一起扣取或者使用npm安装crypto-js并在execjs中引入。更简单的方法是在网上找到CryptoJS的完整min.js文件将其内容包含在你的扣取代码中。扣取完成后将代码保存为一个独立的.js文件例如fengchao_slider.js。然后写一个简单的test.html或用Node.js测试这个函数输入模拟的轨迹和缺口位置看输出的token是否与浏览器生成的一致。这一步的验证至关重要。3.3 缺口位置识别算法实现拿到背景图和缺口滑块图后我们需要计算出缺口所在的x坐标。这里提供两种主流方法。方法一使用OpenCV进行模板匹配这是最稳健、最准确的方法尤其适合图片有噪点或轻微变形的情况。import cv2 import numpy as np def get_gap_position(bg_path, slice_path): 使用OpenCV模板匹配识别缺口位置 :param bg_path: 背景图路径 :param slice_path: 缺口滑块图路径 :return: 缺口左上角的x坐标 # 读取图片 bg_img cv2.imread(bg_path) # 背景大图 slice_img cv2.imread(slice_path) # 缺口小图 # 转换为灰度图减少计算量 bg_gray cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY) slice_gray cv2.cvtColor(slice_img, cv2.COLOR_BGR2GRAY) # 使用模板匹配方法这里用相关系数匹配法TM_CCOEFF_NORMED效果较好 result cv2.matchTemplate(bg_gray, slice_gray, cv2.TM_CCOEFF_NORMED) # 获取最佳匹配位置 min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result) # TM_CCOEFF_NORMED方法下最大值位置是最佳匹配 top_left max_loc # 缺口位置通常是滑块图的左边缘所以返回top_left的x坐标 gap_x top_left[0] print(f使用OpenCV匹配缺口x坐标约为: {gap_x}, 匹配置信度: {max_val}) return gap_x # 注意实际中丰巢的图片可能需要先进行预处理比如二值化、去噪等。 # 另外缺口滑块图可能不是完整的缺口形状而是缺口的“凸起”部分需要根据实际情况调整。方法二使用PIL进行像素列比对这种方法更简单直接适用于图片清晰、缺口边缘对比明显的情况。原理是背景图在缺口位置那一列的像素与滑块图对应列的像素会有显著差异。from PIL import Image def get_gap_position_pixel(bg_path, slice_path, start_x50): 通过像素列比对识别缺口位置 :param bg_path: 背景图路径 :param slice_path: 缺口滑块图路径 :param start_x: 开始比对的x坐标跳过左侧可能存在的干扰部分 :return: 缺口的大致x坐标 bg_img Image.open(bg_path) slice_img Image.open(slice_path) bg_pixels bg_img.load() slice_pixels slice_img.load() width, height slice_img.size for x in range(start_x, bg_img.width - width): # 假设滑块图高度与背景图匹配区域高度一致 diff_all 0 for y in range(height): # 比较背景图(x, y)和滑块图(0, y)的像素 bg_pixel bg_pixels[x, y] slice_pixel slice_pixels[0, y] # 比较滑块图最左侧一列 # 计算RGB差异的绝对值之和 diff sum([abs(bg_pixel[i] - slice_pixel[i]) for i in range(3)]) diff_all diff # 设置一个阈值当差异总和突然变小时说明找到了匹配的列 # 注意这里逻辑需要根据实际情况反转。当滑块图与背景图缺口处对齐时差异应该最小。 # 更通用的做法是计算滑块图在背景图每个x位置上的整体差异找到差异最小的位置。 pass # 具体实现需要根据图片特点调整阈值和算法 # 更常见的实现将滑块图在背景图上滑动计算每个位置的像素差 bg_width, bg_height bg_img.size slice_width, slice_height slice_img.size threshold 0.95 # 相似度阈值可调 for x in range(start_x, bg_width - slice_width): # 裁剪背景图的一块区域与滑块图比较 bg_region bg_img.crop((x, 0, x slice_width, slice_height)) # 可以计算直方图相似度或转换为数组后计算 # 这里简化处理实际应用OpenCV方法更可靠。实操心得在实际对抗中丰巢可能会对图片进行“挖空”处理即背景图的缺口区域不是简单的平移而是被填充了随机噪声。这时单纯的像素比对或模板匹配可能会失效。需要更高级的图像识别技术如基于深度学习的特征匹配或者分析网页前端生成图片的Canvas操作直接获取缺口位置数据如果前端暴露了的话。通常模板匹配OpenCV是首选它足够应对大多数情况。3.4 人类轨迹模拟算法这是绕过行为检测的核心。服务器会分析你提交的轨迹数据判断是否符合人类拖动特征。一个糟糕的直线匀速轨迹会被立刻识别为机器。一个基本的人类轨迹模型通常包含三个阶段加速启动、匀速滑动、减速停止。轨迹由一系列(x偏移量, y偏移量, 时间戳)的点组成。import random import time def generate_track(distance): 生成模拟人类拖动的轨迹 :param distance: 需要拖动的总距离像素 :return: 轨迹列表每个元素为 [时间差, x偏移, y偏移] track [] current_x 0 current_t 0 # 1. 初始段可能有微小抖动或延迟 start_delay random.uniform(0.1, 0.3) current_t start_delay track.append([round(start_delay, 3), 0, 0]) # 2. 加速阶段 (约占总距离30%) accelerate_distance int(distance * 0.3) v 0 a random.uniform(1.5, 2.5) # 加速度 while current_x accelerate_distance: t random.uniform(0.02, 0.05) # 每步的时间间隔 current_t t s v * t 0.5 * a * t * t # 位移 current_x s v a * t # 更新瞬时速度 # 添加微小的y轴随机抖动 y_offset random.randint(-2, 2) track.append([round(t, 3), round(current_x, 2), y_offset]) # 3. 匀速阶段 (约占总距离40%) uniform_distance int(distance * 0.4) while current_x accelerate_distance uniform_distance: t random.uniform(0.03, 0.08) current_t t s v * t # 匀速运动 current_x s y_offset random.randint(-1, 1) track.append([round(t, 3), round(current_x, 2), y_offset]) # 速度可能有微小波动 v random.uniform(-0.2, 0.2) v max(v, 1) # 保证最小速度 # 4. 减速阶段 (剩余距离) while current_x distance: t random.uniform(0.04, 0.1) current_t t a random.uniform(-3, -1.5) # 减速度 s v * t 0.5 * a * t * t # 防止滑过 if current_x s distance: s distance - current_x current_x s v a * t v max(v, 0) # 速度不为负 y_offset random.randint(-2, 2) track.append([round(t, 3), round(current_x, 2), y_offset]) # 5. 最终可能有过冲和回调模拟人手抖 if abs(current_x - distance) 1: adjust_t random.uniform(0.05, 0.15) current_t adjust_t track.append([round(adjust_t, 3), round(distance, 2), 0]) # 确保最后一个点的x坐标精确等于目标距离 track[-1][1] round(distance, 2) return track # 示例生成拖动245像素的轨迹 distance 245 human_track generate_track(distance) print(f轨迹点数: {len(human_track)}) print(f最后一点坐标: {human_track[-1]}) print(f总耗时: {sum([point[0] for point in human_track]):.3f}秒)轨迹算法的关键点非匀变速一定要有加速和减速过程。随机性时间间隔、加速度、减速度、y轴抖动都要加入随机因子避免规律性。总时间总拖动时间最好在1.5秒到3秒之间太快或太慢都显得可疑。轨迹点密度轨迹点不宜过疏或过密通常每秒生成20-50个点比较合理。4. 完整代码实现与集成将上述所有环节整合形成一个完整的自动化验证脚本。以下是核心代码框架import requests import json import time import execjs from PIL import Image import io import cv2 import numpy as np class FengchaoSliderCracker: def __init__(self): self.session requests.Session() # 设置请求头模拟浏览器 self.headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.fengchao.com.cn/login, Accept-Language: zh-CN,zh;q0.9, } self.session.headers.update(self.headers) # 加载扣取出来的JS加密函数 with open(fengchao_slider.js, r, encodingutf-8) as f: js_code f.read() self.ctx execjs.compile(js_code) def get_slider_images(self): 请求并解析验证码图片数据 # 1. 获取包含图片信息的接口需要从网络请求中分析得出 img_api_url https://www.fengchao.com.cn/api/v1/captcha/slider try: resp self.session.get(img_api_url) data resp.json() # 假设返回数据格式{bg: base64..., slice: base64..., token: ...} bg_base64 data[bg].split(,)[1] # 去掉 data:image/png;base64, 前缀 slice_base64 data[slice].split(,)[1] captcha_token data[token] # 可能是一个会话token用于后续提交 # 将base64转换为PIL Image对象 bg_image Image.open(io.BytesIO(base64.b64decode(bg_base64))) slice_image Image.open(io.BytesIO(base64.b64decode(slice_base64))) # 保存到本地或直接使用 bg_image.save(bg.png) slice_image.save(slice.png) return bg.png, slice.png, captcha_token except Exception as e: print(f获取滑块图片失败: {e}) return None, None, None def calculate_gap(self, bg_path, slice_path): 计算缺口位置使用OpenCV方法 # 这里使用前面定义的 get_gap_position 函数 gap_x get_gap_position(bg_path, slice_path) return gap_x def generate_human_track(self, distance): 生成人类轨迹 return generate_track(distance) # 使用前面定义的函数 def get_encrypted_token(self, track_data, gap_x, captcha_token, timestamp): 调用JS函数生成加密的token # 将轨迹数据转换为JS函数需要的格式 # 假设扣取的JS函数名为 generateSliderToken track_str json.dumps(track_data) token self.ctx.call(generateSliderToken, track_str, gap_x, captcha_token, timestamp) return token def submit_verification(self, encrypted_token, gap_x, captcha_token): 提交验证结果 submit_url https://www.fengchao.com.cn/api/v1/captcha/verify payload { token: encrypted_token, point: gap_x, # 最终拖动到的x坐标 captchaToken: captcha_token, # 会话token timestamp: int(time.time() * 1000) } try: resp self.session.post(submit_url, jsonpayload) result resp.json() print(f验证结果: {result}) # 通常返回格式{success: True, message: 验证成功, validate: ...} return result.get(success, False), result.get(validate) except Exception as e: print(f提交验证失败: {e}) return False, None def crack(self): 主流程 print(1. 获取滑块图片...) bg_path, slice_path, captcha_token self.get_slider_images() if not bg_path: return False print(2. 识别缺口位置...) gap_x self.calculate_gap(bg_path, slice_path) print(f识别到的缺口位置: {gap_x}px) print(3. 生成模拟人类轨迹...) track self.generate_human_track(gap_x) print(f轨迹生成完毕共{len(track)}个点总耗时{sum([p[0] for p in track]):.2f}秒) print(4. 生成加密Token...) timestamp int(time.time() * 1000) encrypted_token self.get_encrypted_token(track, gap_x, captcha_token, timestamp) print(5. 提交验证...) success, validate_token self.submit_verification(encrypted_token, gap_x, captcha_token) if success: print(f✅ 滑块验证成功验证Token: {validate_token}) # 这个validate_token可以用于后续的登录请求 return validate_token else: print(❌ 滑块验证失败) return False if __name__ __main__: cracker FengchaoSliderCracker() result cracker.crack()关于fengchao_slider.js文件 这个文件是你从丰巢前端JavaScript中扣取出来的核心加密代码。它至少应该包含一个主函数例如generateSliderToken这个函数接收轨迹、缺口位置、会话token、时间戳等参数并返回最终的加密字符串。由于涉及具体网站的私有算法这里无法提供真实的扣取代码。你需要按照3.2节的方法自行分析和扣取。5. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到各种问题。下面是我在逆向过程中踩过的坑和解决方案。5.1 图片识别不准问题使用OpenCV模板匹配时max_val匹配度很低例如低于0.7或者识别出的位置明显错误。排查图片预处理丰巢的图片可能有阴影、渐变或噪点。尝试对灰度图进行高斯模糊(cv2.GaussianBlur)或二值化(cv2.threshold)处理突出边缘特征。匹配方法cv2.matchTemplate有多种方法。TM_CCOEFF_NORMED和TM_CCORR_NORMED对光照变化不敏感TM_SQDIFF_NORMED则适用于模板和图像亮度差异大的情况。可以都试试。滑块图裁剪缺口滑块图可能包含多余的透明边或阴影。尝试用PIL或OpenCV裁剪掉四周可能干扰匹配的区域。多尺度匹配如果图片有缩放可以尝试多尺度模板匹配但丰巢一般不会。直接获取坐标最根本的方法是分析前端生成滑块和缺口的JavaScript代码。有时缺口位置point会直接作为一个变量存在于某个全局对象或接口返回的JSON数据中根本不需要识别图片。仔细搜索Network响应和Sources中的变量。5.2 轨迹验证不通过问题token生成和提交都成功了但服务器返回验证失败提示“轨迹异常”或“操作过快”。排查轨迹分析将你生成的轨迹数据打印出来与浏览器真实操作录制的轨迹可以通过在生成token的JS函数里console.log轨迹数据获取进行对比。检查总时间、加速度变化、y轴抖动范围是否在合理区间。轨迹加密确认你的轨迹数据格式是否与前端完全一致。前端可能对轨迹进行了预处理比如过滤掉时间间隔过小的点、对坐标进行取整、或者计算的是相对于上一个点的位移增量而非绝对坐标。你扣取的JS函数输入参数要求什么格式你就必须提供什么格式。时间戳确保提交请求中的timestamp与生成token时使用的时间戳是同一个。通常token的生成会用到当前时间戳提交时也需要带上这个时间戳服务器会校验两者是否在合理的时间窗口内。其他隐藏参数除了轨迹和缺口位置生成token可能还依赖其他参数如鼠标点击的初始坐标、页面URL、用户Agent的某个哈希值等。你需要仔细跟踪JS代码看它是否从window,document,navigator等对象中读取了其他信息。5.3 JavaScript执行环境报错问题在execjs中执行扣取的JS代码时报错ReferenceError: window is not defined或CryptoJS is not defined。解决补全环境在扣取的JS代码开头手动定义缺失的全局变量如window,document,navigator,location如3.2节所述。引入加密库如果用到CryptoJS确保将整个库的源码包含在你的.js文件中。可以从https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js获取并粘贴到你的文件头部。或者如果你的Node.js环境安装了crypto-js可以尝试在execjs中通过require引入但这取决于execjs的后端引擎如Node.js或PyV8是否支持。简化代码有时扣取的代码包含大量无关的函数和变量。尝试只保留最核心的加密函数及其直接依赖移除无关的UI操作、事件监听等代码减少环境依赖。5.4 请求被风控拦截问题脚本运行几次后获取图片或提交验证的请求开始返回403、412或带有“风险验证”的HTML页面。解决会话保持确保使用同一个requests.Session()对象它会自动管理cookies。丰巢很可能通过cookies或session来跟踪一次验证会话。请求头模拟检查你的请求头是否与浏览器完全一致。特别注意Origin,Referer,Accept,Accept-Encoding,Accept-Language,Connection等字段。可以使用Fiddler等工具直接复制浏览器请求的头信息。请求频率在脚本中加入随机延迟time.sleep(random.uniform(1, 3))避免高频请求触发风控。IP问题如果频繁失败可能是服务器端对IP进行了限制。考虑使用代理IP池。注意此处仅讨论技术可能性实际使用需严格遵守相关服务条款和法律法规TLS指纹高级的反爬可能检测客户端的TLS指纹如JA3。requests库的TLS指纹可能与浏览器不同。如果怀疑是此问题可以考虑使用curl_cffi或tls_client等能模拟浏览器TLS指纹的库但这属于更高级的对抗。逆向分析是一个不断对抗和调试的过程。没有一劳永逸的代码丰巢的验证机制也可能随时更新。保持耐心掌握基本的调试和分析方法才是应对变化的关键。这套分析思路和代码框架经过适当调整也可以应用于分析其他类似的滑块验证码系统。

相关新闻