逆向工程实战:破解AES加密滑块验证码的完整方案
1. 项目概述:当滑块验证码遇上AES加密
最近在做一个自动化项目时,遇到了一个相当“顽固”的对手——一个采用了AES加密的滑块验证码。这玩意儿不像那些简单的图片缺口识别,它的核心验证逻辑,尤其是用户滑动轨迹的校验数据,被一层AES加密牢牢包裹,直接提交原始的滑动距离和轨迹数据是行不通的。这本质上是一场攻防演练:防御方(验证码服务)试图通过加密来保护其核心验证算法,而作为进攻方(自动化脚本),我们的目标就是逆向分析出它的加密密钥和模式,从而能够模拟出合法的请求。这个过程,就是一次典型的Web逆向工程实战。
这个项目非常适合那些已经接触过基础爬虫和自动化,但在遇到加密参数时感到无从下手的朋友。它不涉及任何灰色或非法操作,其核心价值在于学习如何系统性地分析一个前端加密逻辑,理解现代Web应用如何保护其接口,并掌握一套可复用的逆向工程方法论。通过拆解这个案例,你将能学到如何从纷繁的浏览器网络请求中定位关键接口,如何一步步追踪JavaScript代码找到加密函数,以及如何最终在Python环境中复现整个加密流程,实现验证码的自动化破解。接下来,我就把自己趟过的路、踩过的坑,以及最终成功的方案,毫无保留地分享给你。
2. 逆向工程的核心思路与准备工作
2.1 目标分析与技术栈选型
我们的终极目标是让程序能自动完成滑块验证码的识别、滑动和结果提交。拆解开来,需要解决三个子问题:
- 图像识别:获取带缺口的背景图和滑块图,计算出需要滑动的距离。
- 轨迹模拟:生成一个模拟人类滑动行为的鼠标移动轨迹。
- 加密破解:将滑动距离和轨迹数据,按照目标网站的加密方式(本例是AES)进行加密,生成能被服务端验证的
token或validate参数。
对于问题1和2,已有非常成熟的方案。图像识别可以用OpenCV的模板匹配;轨迹模拟可以使用符合正态分布的加速度模型来生成坐标点序列。真正的难点和本项目的核心在于问题3:如何找到并复现那个AES加密函数。
因此,我们的技术栈很明确:
- 分析工具:Chrome/Edge开发者工具(特别是Sources和Network面板)、可能用到的浏览器插件(如
EditThisCookie用于查看Cookie)。 - 调试与追踪:依赖于浏览器内置的JavaScript调试器,设置断点、单步执行、查看调用栈是基本功。
- 实现语言:Python。因为它有丰富的库支持后续的自动化操作(如
requests、selenium、opencv-python)以及加密解密(pycryptodome)。
注意:整个逆向过程必须在法律和网站服务条款允许的范围内进行,仅用于学习与研究目的,切勿对目标网站造成负载压力。
2.2 关键接口的抓取与链路分析
一切分析始于网络请求。打开浏览器的开发者工具(F12),切换到Network(网络)面板,并勾选“Preserve log”(保留日志)。然后,手动完成一次滑块验证码操作。
操作完成后,仔细审视网络请求列表。通常,一个完整的滑块验证码流程会包含以下几个关键接口,这与我们搜索到的信息是吻合的:
- 初始化/获取验证码接口:通常以
captcha、verify或auth为路径关键词。它负责初始化一个验证会话,返回本次验证的唯一标识(如session_id或captcha_id)、背景图、滑块图的URL,有时还会包含一些用于后续加密的初始参数(一个key或iv)。 - 提交验证接口:通常以
validate、verify或user_verify为路径关键词。这是最核心的接口,我们生成的滑动距离和轨迹数据,经过加密后,就通过这个接口提交。我们的主要目标就是分析这个接口的请求参数是如何生成的。
你需要找到那个提交验证结果的POST请求。点击它,查看它的Headers(尤其是Form Data或Payload)和Response。你可能会看到类似这样的提交数据:
{ "captcha_id": "xxxx", "validate": "aes_encrypted_long_string_here", "轨迹数据或其他信息": "..." }这个validate参数,就是一串长长的、看似随机的密文,它就是经过AES加密后的验证信息。我们的任务就是搞清楚,明文是什么,以及加密的细节(密钥、模式、填充方式)。
3. 深入JavaScript:定位与解析加密函数
3.1 全局搜索与断点调试
找到提交接口后,下一步是找出生成validate参数的JavaScript代码。有以下几种常用方法:
- 搜索关键词:在Sources面板下,按
Ctrl+Shift+F进行全局文件搜索。可以尝试搜索validate、encrypt、AES、CryptoJS(一个常用的前端加密库)、enc等关键词。 - XHR/Fetch断点:在Sources面板的XHR/Fetch Breakpoints中,可以添加一个断点,URL包含提交接口的关键部分(如
user_verify)。这样,当JavaScript代码发起这个请求时,执行会自动暂停。 - 堆栈追踪:在Network面板中,点击那个提交请求,在右侧标签页选择
Initiator(发起者),它会显示是哪个函数发起了这个请求,点击对应的行号可以直接跳转到源代码。
一旦成功在加密或提交相关代码处暂停,你就进入了调试环节。这时要充分利用:
- Call Stack(调用堆栈):查看函数调用链,向上追溯,找到加密函数的入口。
- Scope(作用域):查看当前函数局部变量、闭包变量、全局变量的值。密钥(key)、偏移量(iv)等关键参数很可能就在这里!
- 单步执行(F10/F11):一步步执行代码,观察变量值的变化,理解数据流转过程。
3.2 逆向常见的AES实现
在前端,AES加密通常通过以下方式实现:
- 使用CryptoJS库:非常普遍。你可能会看到
CryptoJS.AES.encrypt(plainText, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 })这样的代码。这几乎给了你所有信息:明文、密钥、IV、模式(CBC)、填充(Pkcs7)。 - 使用Web Crypto API:较新的标准。代码会涉及
window.crypto.subtle.encrypt,参数配置在algorithm对象中。 - 自定义或混淆的实现:最棘手的情况。开发者可能自己实现了AES算法,或者对CryptoJS进行了深度混淆。这时需要更大的耐心去分析。
实操心得:在调试时,重点关注一个明文字符串(可能是滑动距离和轨迹的JSON字符串)是如何一步步变成最终的密文的。记录下转换过程中的每一个关键变量值。一个非常有效的方法是:在加密函数执行前,通过控制台(Console)手动修改传入的明文为你自己构造的已知字符串(例如{"distance": 100}),然后执行加密,对比结果。这能帮你确认加密函数是否正确,以及后续在Python中复现时是否一致。
4. 在Python中复现加密流程
4.1 环境搭建与库的选择
分析完成后,我们就需要在Python中还原这个加密过程。首先安装必要的库:
pip install requests opencv-python numpy pycryptodomepycryptodome:这是Python下功能强大的加密库,完美支持AES。注意,它通常以Crypto模块导入,与旧的pycrypto冲突,请确保正确安装和导入(from Crypto.Cipher import AES)。
4.2 复现AES加密的关键步骤
假设我们通过逆向分析,得知了以下信息:
- 明文(plain_text):是一个JSON字符串,例如
{"distance": 185, "track": [[0,0],[10,5],...], "timestamp": 1648886400000}。 - 密钥(key):可能是从初始化接口返回的一个字符串,长度为16、24或32字节(对应AES-128, AES-192, AES-256)。有时密钥需要经过某种编码(如Base64解码)或哈希处理才能使用。
- 偏移量(iv):可能是一个固定值,也可能是动态生成的。长度必须为16字节。
- 模式(mode):最常见的是
CBC模式。 - 填充(padding):最常见的是
PKCS7填充。
下面是在Python中使用pycryptodome进行AES-CBC加密的示例代码:
import json from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 def aes_encrypt(plain_text, key, iv): """ 模拟前端AES-CBC加密 :param plain_text: 明文字符串 :param key: 密钥字节串 :param iv: 偏移量字节串 :return: Base64编码后的密文字符串 """ # 确保明文是bytes data = plain_text.encode('utf-8') # 进行PKCS7填充 padded_data = pad(data, AES.block_size) # 创建加密器 cipher = AES.new(key, AES.MODE_CBC, iv) # 加密 encrypted_bytes = cipher.encrypt(padded_data) # 通常前端输出是Base64字符串 encrypted_b64 = base64.b64encode(encrypted_bytes).decode('utf-8') return encrypted_b64 # 示例用法 if __name__ == '__main__': # 这些值需要从逆向分析中获取 mock_plain_text = json.dumps({"distance": 185, "ts": 1648886400000}) # 密钥和IV必须是16/24/32字节的bytes对象 # 如果分析得到的是Base64字符串,需要先解码 key_from_analysis = "thisis16byteskey".encode('utf-8') # 或 base64.b64decode(key_b64) iv_from_analysis = "1234567890123456".encode('utf-8') # 或 base64.b64decode(iv_b64) validate = aes_encrypt(mock_plain_text, key_from_analysis, iv_from_analysis) print(f"生成的validate: {validate}")注意事项:
- 编码问题:前端JavaScript的字符串到字节的转换可能与Python默认的UTF-8不同。如果遇到问题,可以尝试其他编码(如
latin-1),或者检查密钥/IV是否经过了特殊的处理(如CryptoJS.enc.Utf8.parse)。 - 动态参数:密钥和IV不一定是固定的。它们很可能在“初始化接口”的响应中返回,或者由前端JavaScript根据时间戳、会话ID等动态计算生成。务必确保你在Python中使用的key/iv与单次验证会话中前端使用的完全一致。
- 完整流程集成:这个加密函数需要嵌入到你的自动化流程中:先调用初始化接口获取
captcha_id和可能的key/iv;然后用OpenCV计算滑动距离;生成轨迹;组装明文;加密;最后提交。
5. 图像识别与轨迹生成实战
5.1 使用OpenCV计算滑动距离
获取到背景图和滑块图后,我们使用OpenCV的模板匹配功能。这里有一个技巧:滑块图通常是带透明通道的PNG,而背景图是JPG。直接匹配效果可能不好。通常我们需要对滑块图进行一些预处理,比如提取其不透明部分(alpha通道)作为模板。
import cv2 import numpy as np def calculate_slide_distance(bg_path, slice_path): """ 计算滑块需要滑动的距离 :param bg_path: 背景图路径或numpy数组 :param slice_path: 滑块图路径或numpy数组 :return: 滑动距离(像素) """ # 读取图片 bg_img = cv2.imread(bg_path) if isinstance(bg_path, str) else bg_path slice_img = cv2.imread(slice_path, cv2.IMREAD_UNCHANGED) if isinstance(slice_path, str) else slice_path # 处理滑块图的透明背景 # 如果滑块图有4个通道(RGBA),则分离出alpha通道作为掩码 if slice_img.shape[2] == 4: b, g, r, a = cv2.split(slice_img) slice_rgb = cv2.merge([b, g, r]) # 前三个通道是颜色 mask = a # 第四个通道是透明度 # 将模板匹配限制在非透明区域 # 但TM_CCOEFF_NORMED方法本身对均匀区域不敏感,有时直接用RGB效果更好 template = slice_rgb else: template = slice_img # 执行模板匹配,使用归一化相关系数匹配法,效果较好 result = cv2.matchTemplate(bg_img, template, cv2.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) # max_loc是匹配位置左上角的点,对于缺口在右侧的滑块,这就是缺口左边缘的位置 # 注意:有些验证码的滑块图是缺口本身,匹配到的是缺口位置;有些滑块图是凸起部分,匹配到的是滑块位置。 # 需要根据实际情况判断。通常匹配到的x坐标就是需要滑动的距离。 slide_distance = max_loc[0] # 可视化(调试用) # h, w = template.shape[:2] # top_left = max_loc # bottom_right = (top_left[0] + w, top_left[1] + h) # cv2.rectangle(bg_img, top_left, bottom_right, (0, 255, 0), 2) # cv2.imshow('Match', bg_img) # cv2.waitKey(0) return slide_distance避坑技巧:
- 匹配方法:
cv2.TM_CCOEFF_NORMED是最常用的,它对光照变化有一定鲁棒性。 - 多尺度与旋转:极少数验证码会对背景图进行轻微缩放或扭曲。如果常规匹配失败,可以考虑多尺度模板匹配。
- 二次校验:计算出的距离可以尝试加减几个像素,模拟人类误差,并分别加密提交,看哪个能成功。
5.2 模拟人类滑动轨迹
直接以匀速将滑块拖到目标位置太“机器”了,容易被识别。我们需要生成一个包含加速、减速、轻微抖动和过冲回拉的轨迹。
import random import time def generate_track(distance): """ 生成模拟人类的滑动轨迹 :param distance: 目标滑动距离(像素) :return: 轨迹列表,每个元素为[时间偏移量, x坐标, y坐标] """ track = [] current_x = 0 current_y = random.randint(5, 15) # 初始y坐标有一些随机偏移 t = 0 # 总时间在0.5秒到2秒之间随机 total_t = random.uniform(0.8, 1.5) # 将滑动过程分为三段:加速、匀速、减速 mid_distance = distance * random.uniform(0.6, 0.8) # 第一阶段:加速 v = 0 a = random.uniform(1.5, 3.0) while current_x < mid_distance: t += random.uniform(0.01, 0.02) # 时间步长 v += a * 0.02 delta_x = v * 0.02 + random.uniform(-1, 1) # 加入微小抖动 current_x += delta_x current_y += random.uniform(-0.5, 0.5) # y轴轻微晃动 track.append([round(t, 3), round(current_x, 2), round(current_y, 2)]) # 第二阶段:匀速或微减速 v = v * random.uniform(0.8, 1.0) while current_x < distance: t += random.uniform(0.01, 0.02) delta_x = v * 0.02 + random.uniform(-0.5, 0.5) current_x += delta_x current_y += random.uniform(-0.3, 0.3) track.append([round(t, 3), round(current_x, 2), round(current_y, 2)]) # 第三阶段:过冲与回拉(模拟人手抖) overshoot = random.uniform(0, distance * 0.03) current_x += overshoot track.append([round(t+0.05, 3), round(current_x, 2), round(current_y, 2)]) # 回拉到精确位置 current_x = distance track.append([round(t+0.1, 3), round(current_x, 2), round(current_y, 2)]) # 确保最后一个点是精确的目标距离 if track[-1][1] != distance: track.append([round(t+0.12, 3), distance, track[-1][2]]) return track这个轨迹生成函数模拟了基本的物理运动,并加入了随机性,使其更接近真人操作。生成的轨迹数据需要被整合到上一步的明文JSON中。
6. 全流程集成与自动化脚本
现在,我们将所有模块组合起来,形成一个完整的自动化破解脚本。这里以使用requests和session为例,假设目标网站接口清晰,没有强反爬。
import requests import json import time import cv2 from io import BytesIO from PIL import Image import numpy as np # ... 导入前面定义的 aes_encrypt, calculate_slide_distance, generate_track 函数 ... class SliderCaptchaCracker: def __init__(self, base_url): self.session = requests.Session() self.base_url = base_url self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Referer': base_url, }) self.captcha_id = None self.encrypt_key = None self.encrypt_iv = None def init_captcha(self): """步骤1:初始化验证码,获取图片和加密参数""" init_url = f"{self.base_url}/api/captcha/init" resp = self.session.post(init_url).json() # 假设返回格式:{“code”:0, “data”:{“id”:”abc123”, “bg_img”:”url1", “slice_img”:”url2", “key”:”base64key”, “iv”:”base64iv”}} self.captcha_id = resp['data']['id'] bg_img_url = resp['data']['bg_img'] slice_img_url = resp['data']['slice_img'] self.encrypt_key = base64.b64decode(resp['data']['key']) # 解码得到bytes self.encrypt_iv = base64.b64decode(resp['data']['iv']) # 下载图片 bg_content = self.session.get(bg_img_url).content slice_content = self.session.get(slice_img_url).content # 转换为OpenCV格式 bg_img = cv2.imdecode(np.frombuffer(bg_content, np.uint8), cv2.IMREAD_COLOR) slice_img = cv2.imdecode(np.frombuffer(slice_content, np.uint8), cv2.IMREAD_UNCHANGED) return bg_img, slice_img def crack_and_submit(self): """主流程:破解并提交""" # 1. 初始化 print("正在初始化验证码...") bg_img, slice_img = self.init_captcha() # 2. 计算滑动距离 print("正在计算滑动距离...") distance = calculate_slide_distance(bg_img, slice_img) print(f"计算出的滑动距离: {distance} 像素") # 3. 生成轨迹 print("正在生成模拟轨迹...") track = generate_track(distance) # 4. 构造明文并加密 print("正在构造验证数据...") timestamp = int(time.time() * 1000) plain_data = { "captchaId": self.captcha_id, "distance": distance, "track": track, "timestamp": timestamp, # 可能还有其他固定或动态参数,需根据逆向分析补充 } plain_text = json.dumps(plain_data, separators=(',', ':')) # 紧凑格式,与前端一致 validate = aes_encrypt(plain_text, self.encrypt_key, self.encrypt_iv) # 5. 提交验证 print("正在提交验证...") submit_url = f"{self.base_url}/api/captcha/verify" payload = { "captcha_id": self.captcha_id, "validate": validate, } resp = self.session.post(submit_url, json=payload).json() return resp if __name__ == '__main__': # 替换成目标网站的基地址 cracker = SliderCaptchaCracker("https://target-website.com") result = cracker.crack_and_submit() print(f"验证结果: {result}")这个类提供了一个完整的框架。在实际使用中,你需要根据目标网站的具体接口地址、参数名和响应格式进行适配。
7. 常见问题排查与进阶技巧
即使按照上述流程,你也可能会遇到各种问题。下面是一些常见坑点及解决方案:
7.1 加密结果不一致
这是最常遇到的问题,表现为Python加密出的validate和服务端不认可。
- 检查密钥和IV:确认从初始化接口获取的key/iv是否经过了额外的处理(如字符串截取、哈希)。用浏览器调试工具在加密函数处打印出原始的key和iv(可能是CryptoJS的WordArray对象),并查看其十六进制或Base64表示,与你的Python代码获取的进行逐字节对比。
- 检查明文格式:对比你的明文JSON字符串和前端的明文字符串是否完全一致。包括字段顺序、空格、缩进(通常前端会使用
JSON.stringify的紧凑模式)。可以尝试在浏览器控制台执行JSON.stringify(your_data)来获取标准格式。 - 检查编码和填充:确认AES的模式(CBC/ECB等)和填充方式(PKCS7/ZeroPadding等)是否正确。
pycryptodome的默认填充是PKCS7。 - 动态密钥生成:key/iv可能不是直接来自接口,而是由接口返回的某个种子参数,经过前端JavaScript计算得出。你需要逆向这个计算过程。
7.2 轨迹验证与行为检测
除了加密参数,服务端可能还会验证轨迹数据本身。
- 轨迹合理性:确保生成的轨迹在总时间、速度变化、移动路径上符合人类行为。过于完美或随机的轨迹都可能被拒绝。
- 轨迹加密:有些网站会将轨迹数据也作为明文的一部分进行整体加密。有些则可能单独加密或签名轨迹。需要仔细分析提交的数据结构。
- 鼠标事件监听:高级的验证码可能会监听真实的鼠标移动事件(
mousemove)。如果使用requests纯发包模拟,缺少这些事件,可能导致失败。此时可能需要使用selenium或playwright这类浏览器自动化工具来真实地拖动滑块,并在拖动过程中注入JavaScript来获取所需的加密参数。
7.3 应对代码混淆与反调试
现代网站会使用Webpack打包、变量名混淆、代码压缩,甚至加入反调试技巧(如无限debugger循环)。
- 格式化代码:在Sources面板,点击
{}按钮可以美化压缩的代码。 - Hook关键函数:在加密函数可能被调用的地方(如
Function.prototype.call,CryptoJS.AES.encrypt)设置条件断点,或者使用浏览器插件(如Tampermonkey)注入脚本,直接Hook这些函数,打印出输入输出。 - 绕过反调试:对于无限debugger,可以通过在debugger语句上右键选择“Never pause here”,或者使用
setInterval钩子来绕过。
7.4 提升识别成功率
OpenCV模板匹配不是万能的。
- 图像预处理:对背景图和滑块图进行灰度化、高斯模糊、边缘检测(如Canny)有时能提升匹配精度,尤其是当图片有噪声或颜色干扰时。
- 深度学习方案:对于极其复杂或动态的验证码,可以考虑使用深度学习模型(如YOLO做目标检测)来识别缺口位置。但这需要收集和标注数据,成本较高。
- 第三方打码平台:作为备选方案,可以接入商业打码平台,将图片发送给他们,返回识别结果。这适用于项目稳定性和成功率要求高,且愿意支付一定费用的场景。
逆向工程破解加密验证码是一个需要耐心、细心和系统方法的过程。它没有一成不变的公式,每个网站都可能有自己的“花样”。核心思路永远是:观察 -> 假设 -> 验证 -> 实现。从网络请求出发,定位关键代码,理解其数据流和加密逻辑,最后在自动化环境中复现。成功破解的那一刻,带来的不仅是项目上的进展,更是对Web前端安全机制深入理解的成就感。希望这份详细的实战指南,能为你打开逆向工程的大门。