V5验证码逆向全解析:WebSocket加密、图像识别与轨迹模拟实战

📅 2026/7/4 23:49:44
V5验证码逆向全解析:WebSocket加密、图像识别与轨迹模拟实战
1. 项目概述最近在分析一些网站的登录或高频操作防护时V5验证码出现的频率相当高。这玩意儿说简单也简单就是一个滑块拼图说复杂也复杂它的加密和校验逻辑层层嵌套尤其是那个WebSocketWS协议传输加密让不少刚接触逆向的朋友直挠头。我花了几天时间把一个典型的V5验证码Demo从头到尾扒了一遍核心目标就一个搞清楚它从加载、交互到最终验证通过的完整链条特别是那个“密文”图片和轨迹是怎么生成和校验的。这篇文章我会把整个逆向分析的思路、关键点、踩过的坑以及最终的Python复现过程掰开揉碎了讲给你听。无论你是做安全研究、爬虫开发还是单纯对验证码机制感兴趣相信都能从中获得可以直接“抄作业”的实操经验。简单来说V5验证码常见于各类需要防止机器批量操作的场景比如注册、登录、发帖、抢票等。它的前端展示是一个缺失一块的拼图背景图以及一个需要拖动的滑块用户需要将滑块拖到正确位置完成拼图。但背后它并非简单比较位置而是涉及图片加密传输、滑动轨迹加密生成、以及通过WebSocket进行实时加密交互等一系列操作。逆向它意味着我们要能模拟出这一整套人类操作行为并且让服务端“认为”这就是一个真实用户。下面我们就从最核心的通信协议开始拆解。2. 核心思路与协议层逆向逆向任何验证码第一步永远是抓包看清数据是怎么来的、怎么去的。对于V5用浏览器的开发者工具F12打开网络Network选项卡然后触发一次验证码加载和滑动操作你会看到除了常见的图片资源请求还有一个非常显眼的WebSocket连接。2.1 WebSocket连接与消息结构分析这个WebSocket连接是整个验证码交互的生命线。所有关键数据包括验证码初始化参数、加密后的图片、以及最终的验证结果都通过它进行双向通信。连接地址通常包含ws或wss协议以及一个唯一的token标识。抓取到的WebSocket消息看起来是一堆乱码或者很长的字符串这明显是经过加密的。我们的首要任务就是找到加密和解密的逻辑。通常这些逻辑会放在前端JavaScript代码中。在开发者工具的“源代码”Sources选项卡中搜索关键词如WebSocket、send、onmessage、encrypt、decode等可以快速定位到相关的JS文件。关键点一消息的加密模式逆向分析后发现V5验证码的WS消息体并非整体加密而是采用了一种“信封”式结构。一个典型的发送消息send可能如下结构仅为逻辑示意{ cmd: command_name, // 指令类型如get用于获取验证码图片 data: Base64Encoded_EncryptedData, // 实际加密的业务数据 other_params: ... }而接收的消息onmessage也类似data字段里是加密的响应数据。加解密的密钥和算法往往在WebSocket连接建立之初通过最初的几次握手消息协商或固定下来。实操心得不要一上来就试图去硬啃混淆后的JS。先通过抓包理清关键的操作步骤对应的WS指令序列。比如通常的流程是1. 连接WS - 2. 发送get命令获取验证码挑战 - 3. 服务端返回加密的图片数据 - 4. 前端解密并渲染 - 5. 用户滑动 - 6. 前端生成加密轨迹并发送check命令 - 7. 服务端返回验证结果。先把握住这个主干再针对每个环节的data字段去逆向加解密。2.2 加密算法定位与还原找到处理WS消息的JavaScript函数后接下来就是最核心的加解密算法逆向。V5验证码常见的加密算法包括AES、DES或者一些自定义的位运算、Base64变种等。在JS代码中可能会看到CryptoJS库的调用或者一些包含encrypt、decrypt、Cipher、mode、pad等字眼的函数。关键点二密钥的获取方式密钥的来源至关重要。通常有两种情况固定密钥密钥硬编码在JS文件中。虽然安全性较差但仍有使用。搜索字符串常量特别是十六进制字符串或Base64字符串可能是密钥。动态密钥密钥在WS连接建立后由服务端下发给前端。这就需要你仔细分析连接建立后的前几条WS消息很可能其中一条就包含了加密所需的key、iv初始化向量等信息。以AES-CBC模式为例如果你在JS中看到类似CryptoJS.AES.encrypt(data, key, {iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7})的代码那么你就确定了算法是AES-CBC填充方式是Pkcs7。接下来就是要找到key和iv的值从哪里来。操作技巧在开发者工具的“控制台”Console中你可以下断点或者直接Hook关键函数。例如重写CryptoJS.AES.encrypt方法让它先打印出参数再调用原方法。这样可以在滑动验证码时实时捕获到加密前的明文、使用的密钥和IV。// 示例Hook CryptoJS.AES.encrypt (控制台直接执行) let originalEncrypt CryptoJS.AES.encrypt; CryptoJS.AES.encrypt function(data, key, cfg){ console.log([Hook AES.encrypt] Plaintext:, data); console.log([Hook AES.encrypt] Key:, key); console.log([Hook AES.encrypt] Config:, cfg); return originalEncrypt.call(this, data, key, cfg); };通过这种方式你可以直接拿到关键的密钥信息而无需完全理解密钥的生成逻辑。这对于快速验证和复现至关重要。3. 图片密文解密与缺口识别成功逆向WS通信的加解密后我们拿到了获取验证码图片的get命令的响应。响应中的data字段解密后通常是一个JSON对象里面包含了拼图背景图bg和滑块缺口图tp的密文。3.1 图片数据的解密与还原这里有一个非常重要的细节bg和tp字段的值看起来像Base64但直接解码得到的二进制数据无法被图片查看器识别。这是因为它们并非标准的PNG或JPEG文件而是经过了额外的、自定义的混淆或加密。关键点三图片的二次混淆逆向JS代码会发现解密WS消息得到的bg/tp字符串还需要经过一个特定的“解码”函数处理才能变成真正的图片二进制数据。这个函数可能进行了一些操作比如按字节进行异或XOR运算。字节顺序的调换swap。去除或添加特定的文件头尾。例如你可能在JS中找到这样的函数function decodeImage(cipherText) { let rawData atob(cipherText); // Base64解码 let arr new Uint8Array(rawData.length); for(let i0; irawData.length; i) { arr[i] rawData.charCodeAt(i) ^ 0xAA; // 假设是简单的XOR混淆 } return arr.buffer; // 返回ArrayBuffer }实操步骤在JS中找到处理bg/tp的函数通常函数名包含decode、parse、getImage等。分析其具体算法。如果是简单的XOR找出密钥如果是复杂的置换理解其规则。在Python中复现这个解码函数。将解密WS得到的Base64字符串经过同样的算法处理。处理完成后将得到的二进制数据保存为文件如bg.png检查是否能正常打开。注意有时图片数据可能直接是PNG格式但被分割成了多个部分或者需要根据额外的参数如图片宽度w、高度h在内存中重新组装。务必对照JS实现确保每一步都完全一致。3.2 缺口位置的计算得到清晰的背景图(bg)和滑块图(tp)后下一步就是计算出滑块需要移动到的目标位置缺口位置。这里就是传统的图像识别领域了。关键点四识别算法的选择与优化最直接有效的方法是使用OpenCV库进行模板匹配。滑块图(tp)就是模板在背景图(bg)上滑动匹配找到最相似的位置。import cv2 import numpy as np def get_gap_position(bg_path, tp_path): # 读取图片 bg_img cv2.imread(bg_path, 0) # 灰度图 tp_img cv2.imread(tp_path, 0) # 执行模板匹配使用归一化相关系数匹配法 result cv2.matchTemplate(bg_img, tp_img, cv2.TM_CCOEFF_NORMED) # 获取最大匹配值和其位置 min_val, max_val, min_loc, max_loc cv2.minMaxLoc(result) # 缺口左上角x坐标 gap_x max_loc[0] # 注意通常滑块图是带有阴影或透明度的匹配到的位置可能需要一个偏移量 # 这个偏移量需要根据实际UI测量比如滑块图左边空白部分的宽度 offset 5 # 示例偏移量需实测 target_x gap_x offset return target_x常见问题与调优匹配不准确确保两张图片解码后是完全“干净”的没有多余的水印或干扰线。可以尝试不同的匹配方法TM_CCOEFF_NORMED,TM_SQDIFF_NORMED等。多尺度问题如果验证码图片会缩放可能需要多尺度模板匹配。性能对于高精度需求可以只对背景图的可能区域如下半部分进行匹配减少计算量。偏移量template matching找到的是模板左上角在背景中的位置。而实际滑块的拖动终点是这个位置加上滑块图片本身左侧的空白区域宽度。这个偏移量必须通过实际测量确定用截图工具测量滑块图左侧透明/空白部分到缺口实际边缘的像素距离。4. 轨迹生成与加密模拟计算出目标位置target_x后我们不能直接把这个值发给服务端。服务端会检测滑动轨迹包括移动路径、速度、加速度等来判断是真人还是机器。4.1 人类轨迹模拟人类拖动滑块不是匀速直线运动而是“先快后慢略有抖动”的。我们需要用算法模拟出一条以假乱真的轨迹。关键点五轨迹生成算法一个经典的模拟算法是根据匀加速/变加速运动模型来生成位移序列。这里分享一个我经过多次调整后效果不错的方案import random import math def generate_track(distance): 生成滑动轨迹 :param distance: 需要滑动的总距离像素 :return: 位移列表每个元素是每一步的位移增量 track [] current 0 mid distance * 3 / 5 # 减速阈值点前3/5距离加速后2/5减速 t 0.2 # 模拟的时间间隔 v 0 # 初速度 while current distance: if current mid: a 2 random.random() # 加速阶段的加速度 else: a - (3 random.random() * 2) # 减速阶段的减速度 v0 v v v0 a * t # 当前速度 move v0 * t 0.5 * a * t * t # 当前间隔内的位移 current move track.append(round(move)) # 四舍五入取整 # 处理可能超出或不足的情况 over current - distance if over 0: # 最后几步微调模拟对准时的抖动 track[-1] - round(over) # 可以再最后加几个1像素内的微小移动 track.extend([0, 1, -1, 0][:random.randint(0, 3)]) elif over 0: track.append(round(-over)) # 确保总和等于目标距离 if sum(track) ! distance: track[-1] (distance - sum(track)) return track这个算法生成了一个位移增量列表。你需要记录下每个位移发生的时间戳或间隔最终轨迹数据通常包含x水平位移、y垂直位移通常为0或有微小抖动、t时间戳三个维度。4.2 轨迹数据的加密与提交生成原始轨迹数组后需要按照前端相同的加密逻辑将其加密并封装成WS的check命令消息体。关键点六轨迹加密的细节数据格式原始轨迹可能是一个对象数组如[{x: 10, y: 0, t: 100}, ...]。在加密前需要将其序列化成字符串如JSON.stringify。加密过程使用之前逆向出来的加密算法如AES-CBC和密钥对序列化后的轨迹字符串进行加密得到密文。消息封装构建一个符合check命令格式的WS消息。例如check_message { cmd: check, data: encrypted_track_data, # 加密后的轨迹密文 token: ws_token, # WebSocket连接令牌 other_field: value }发送与验证通过WebSocket连接发送此消息。服务端解密、校验轨迹后会返回一个结果消息。解密该消息的data字段通常能得到一个包含success布尔值的JSON对象。注意事项时间戳的基准轨迹中的时间t通常是相对于轨迹开始时刻的毫秒数。确保你的时间基准与前端一致。垂直抖动有些验证码会检测垂直方向是否有微小移动。可以在轨迹生成时给y坐标添加一个很小的随机扰动如 -1, 0, 1。加密一致性务必确保Python端的加密结果与前端JavaScript的加密结果完全一致。可以通过写测试用例用相同的明文、密钥、IV分别在JS和Python中加密对比输出是否相同来验证。5. 完整Python复现流程与核心代码将上述所有步骤串联起来就是一个完整的自动化通过V5验证码的流程。这里给出一个高度概括的、以代码逻辑为主的复现框架。5.1 环境准备与依赖安装你需要一个Python环境并安装以下关键库pip install requests websocket-client opencv-python-headless pycryptodome numpywebsocket-client: 用于建立WebSocket连接。opencv-python-headless: 用于图像处理和缺口识别。pycryptodome: 一个功能强大的加密算法库兼容CryptoJS常用的AES等算法。numpy: OpenCV的依赖也用于数值计算。5.2 核心类设计与实现下面是一个简化版的核心类结构展示了主要的方法import json import base64 import time from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad import cv2 import numpy as np from websocket import create_connection class V5CaptchaSolver: def __init__(self, ws_url): self.ws_url ws_url self.ws None self.aes_key None self.aes_iv None self.token None def connect_and_handshake(self): 建立WS连接并完成密钥协商等握手过程 self.ws create_connection(self.ws_url) # 接收第一条消息可能包含token或初始化信息 init_msg self.ws.recv() init_data json.loads(init_msg) self.token init_data.get(token) # 假设下一条消息包含AES密钥和IV (需根据实际逆向调整) key_msg self.ws.recv() key_data self._decrypt_initial_msg(key_msg) # 初始消息可能有另一层加密 self.aes_key base64.b64decode(key_data[key]) self.aes_iv base64.b64decode(key_data[iv]) def _aes_encrypt(self, plaintext): 模拟前端AES-CBC加密 cipher AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv) ciphertext cipher.encrypt(pad(plaintext.encode(utf-8), AES.block_size)) return base64.b64encode(ciphertext).decode(utf-8) def _aes_decrypt(self, ciphertext_b64): 模拟前端AES-CBC解密 cipher AES.new(self.aes_key, AES.MODE_CBC, self.aes_iv) ciphertext base64.b64decode(ciphertext_b64) plaintext unpad(cipher.decrypt(ciphertext), AES.block_size) return plaintext.decode(utf-8) def _decode_image_data(self, encrypted_b64): 还原图片的二次混淆例如XOR解密 encrypted_bytes base64.b64decode(encrypted_b64) # 假设逆向得知是每个字节与0xAA异或 decoded_bytes bytes([b ^ 0xAA for b in encrypted_bytes]) return decoded_bytes def get_captcha_image(self): 发送get命令获取并解密验证码图片 cmd_msg json.dumps({cmd: get, token: self.token}) self.ws.send(cmd_msg) resp self.ws.recv() resp_data json.loads(resp) # 解密data字段 decrypted_data self._aes_decrypt(resp_data[data]) image_info json.loads(decrypted_data) # 包含bg和tp的密文 # 解码图片 bg_decoded self._decode_image_data(image_info[bg]) tp_decoded self._decode_image_data(image_info[tp]) # 保存为文件 with open(bg.png, wb) as f: f.write(bg_decoded) with open(tp.png, wb) as f: f.write(tp_decoded) return bg.png, tp.png def calculate_gap(self, bg_path, tp_path): 计算缺口位置返回目标x坐标 # 使用OpenCV模板匹配代码见3.2节 target_x get_gap_position(bg_path, tp_path) # 调用前面定义的函数 return target_x def generate_and_send_track(self, distance): 生成轨迹加密并发送check命令 track generate_track(distance) # 调用4.1节的轨迹生成函数 # 构建轨迹对象数组 track_data [] start_time int(time.time() * 1000) x 0 for dx in track: x dx track_data.append({ x: x, y: random.choice([-1, 0, 1]), # 添加微小垂直抖动 t: int(time.time() * 1000) - start_time }) time.sleep(0.02) # 模拟每步间隔实际生成时不需要sleep这里仅为构造时间戳 track_json json.dumps(track_data) encrypted_track self._aes_encrypt(track_json) check_msg json.dumps({ cmd: check, data: encrypted_track, token: self.token }) self.ws.send(check_msg) # 接收验证结果 result_msg self.ws.recv() result_data json.loads(result_msg) decrypted_result self._aes_decrypt(result_data[data]) return json.loads(decrypted_result) def solve(self): 主解决流程 self.connect_and_handshake() bg, tp self.get_captcha_image() target_x self.calculate_gap(bg, tp) result self.generate_and_send_track(target_x) self.ws.close() return result.get(success, False)5.3 关键参数与调试技巧在复现过程中以下几个参数必须与目标网站完全一致否则必定失败WebSocket URL包含的token或path参数可能动态变化需要从首次加载验证码的页面响应中提取。AES加解密参数key和iv长度如16字节 for AES-128, 24字节 for AES-192, 32字节 for AES-256和值必须准确。mode通常是CBC。padding最常见的是PKCS7在CryptoJS中叫Pkcs7。图片解码算法XOR的密钥、字节交换的顺序等必须分毫不差。轨迹数据格式属性名x,y,t、时间单位毫秒、坐标原点通常是滑块初始位置为0必须一致。WS消息结构cmd字段的值、其他必须的字段如token,scene等必须齐全。调试技巧日志记录在每个关键步骤发送/接收WS消息、加解密前后、图片保存前后打印出关键数据便于比对。与浏览器行为对比在浏览器中执行一次成功验证用开发者工具的网络和Console面板记录下所有关键数据明文、密文、密钥、轨迹数组。然后用你的Python脚本执行对比中间产出的数据是否一致。使用mitmproxy或Fiddler这些中间人代理工具可以拦截和修改HTTPS/WebSocket流量方便你将浏览器端的请求参数复制到脚本中或者将脚本生成的参数替换到浏览器请求中进行双向验证。6. 常见问题排查与进阶思考即使按照上述流程也可能遇到各种问题。这里记录一些我踩过的坑和解决方案。6.1 问题排查清单问题现象可能原因排查方向WebSocket连接立即关闭连接URL不对或缺少必要参数Origin、User-Agent等请求头不正确。检查浏览器建立WS连接时的完整URL和请求头在Python中模拟一致。收到get命令响应后解密失败AES密钥或IV错误加密模式或填充方式不对密文在传输中被额外编码/解码。1. 确认密钥/IV获取正确。2. 确认mode和padding。3. 检查密文是否经过URL编码或额外Base64。解密出的图片数据无法打开图片二次混淆算法还原错误文件头损坏。用十六进制编辑器对比浏览器端解密后的二进制数据与你Python解密的数据从第一个不同字节开始分析。缺口识别位置永远不对图片解码正确但模板匹配参数有误滑块图(tp)包含多余空白或阴影偏移量计算错误。1. 人工目测缺口位置。2. 测量滑块图左侧空白像素宽度调整target_x计算。3. 尝试不同的cv2.matchTemplate方法。发送check命令后返回验证失败轨迹加密错误轨迹数据格式不符轨迹行为特征被识别为机器如匀速、无抖动。1. 对比浏览器生成的轨迹密文与你生成的密文。2. 检查轨迹数组的JSON结构、字段名、时间戳单位。3. 优化轨迹生成算法增加随机性和“人性化”抖动。偶尔成功经常失败验证码有随机性更强的策略如轨迹校验加入加速度变化阈值、鼠标移动路径非线性检测。1. 收集更多成功轨迹样本分析其统计特征速度曲线、加速度分布。2. 引入更复杂的人类行为模型如“慢启动-快移动-微调整”模式。6.2 进阶安全策略对抗一些更高级的V5验证码或它的变种可能会引入以下机制增加逆向难度动态密钥与一次一密每次验证的加密密钥都不同且由服务端动态生成并通过首次WS消息下发甚至结合前端环境指纹生成最终密钥。前端代码强混淆与反调试JS代码被严重混淆并添加了反调试逻辑如检测开发者工具、代码执行流完整性校验等。轨迹行为深度建模不仅校验轨迹点还可能校验鼠标按下到释放的整个事件序列、移动过程中的加速度变化率加加速度甚至结合浏览器指纹、鼠标移动的贝塞尔曲线拟合度。图片干扰项增强背景图和滑块图加入更多噪点、干扰线、局部变形增加图像识别难度。应对思路动态密钥必须完整逆向密钥的生成和协商流程通常需要模拟整个握手过程。反调试可以使用无头浏览器如puppeteer、selenium直接运行原站JS绕过静态分析。或者使用Node.js环境配合jsdom等库来执行关键JS函数。深度行为模型需要采集大量真人滑动数据使用机器学习如LSTM来学习并生成难以区分的轨迹或者使用强化学习让模型在与验证码的对抗中优化策略。复杂图像识别可能需要更鲁棒的图像识别算法如深度学习目标检测模型YOLO、SSD来定位缺口或者使用图像分割技术。6.3 关于“明文参数可伪造”的深度理解在逆向过程中我们发现很多校验参数如图片bg/tp的密文、滑动轨迹data虽然以密文形式传输但其生成逻辑完全在前端。这意味着只要我们完整复现了前端的逻辑就可以在本地生成任何我们想要的、但能被服务端正确解密的“合法”数据。这就是所谓的“明文参数可伪造”。这带来了一个重要的思路并非所有步骤都需要按部就班模拟。例如如果我们已经知道缺口位置的计算是确定性的通过图像识别那么我们可以直接生成一条从起点滑动到该终点的“完美”轨迹而无需真的去模拟拖动滑块、监听鼠标事件的过程。这大大简化了自动化流程。关键在于伪造的数据必须严格遵循前端定义的加密和格式规范。最后逆向分析是一个需要耐心和细致观察的过程。每一个验证码的实现都有其细微的差别。最好的老师就是浏览器开发者工具和对比测试。从抓包开始理清数据流定位关键函数逐步还原最终用代码实现自动化。这个过程本身就是对前端安全、加密通信和反自动化技术的一次深刻学习。