1. 项目概述当滑块验证码遇上AES加密最近在做一个自动化项目需要处理一个带滑块验证码的登录环节。本以为是个常规操作用图像识别算出缺口位置然后模拟拖动就完事了。结果一脚踩进大坑里——这个验证码的缺口位置坐标竟然不是明晃晃地放在前端代码里而是被一层AES加密给包得严严实实更“贴心”的是它用的还不是最常见的AES-CBC或AES-ECB模式。前端的JavaScript代码里一个decrypt函数静静地躺在那里参数里赫然写着mode: ‘CCM‘。好家伙AES-CCM模式这在国内不少大厂的验证码里还挺常见尤其是当你看到“阿里云滑块验证码”这个关键词时心里就该拉响警报了。这不仅仅是“识别图片计算距离”那么简单了。它变成了一场前端逆向工程与密码学的混合挑战你需要从纷繁复杂的混淆JavaScript中找到那个关键的加密密钥Key和初始化向量IV理解AES-CCM模式的解密流程然后用后端语言比如Python完美复现这个解密过程最终从密文里捞出那个决定性的、以像素为单位的缺口坐标。任何一个环节出错得到的坐标都是错的滑块自然对不上自动化也就卡死在这里。这个项目就是一个完整的“避坑指南”我会带你从加密原理一路走到坐标换算把路上所有可能崴脚的地方都标出来。2. 核心思路拆解逆向、解密与模拟面对这种加密的滑块验证码蛮干是没用的。我们需要一套清晰、可复现的策略。核心思路可以分解为三个层次环环相扣。2.1 逆向定位在前端代码的海洋里捞针第一步也是最基础的一步就是找到加密参数。所有滑块验证码为了验证拖动的真实性都会在前端生成一个待验证的“答案”这个答案往往就是缺口距离。为了不让这个答案被轻易抓取它会经过加密后可能放在某个HTML标签的>CryptoJS.AES.decrypt(ciphertext, key, { iv: iv, mode: CryptoJS.mode.CCM, padding: CryptoJS.pad.NoPadding });你的任务是在Python中使用cryptography或pycryptodome库完全复现这个解密过程。这里有几个绝对会踩的坑编码问题JavaScript和Python对字符串、字节的处理方式不同。JS里的密钥和IV可能是Base64编码的字符串或Hex字符串而Python解密函数通常需要字节bytes。你必须确保在Python中加载的密钥、IV和密文其字节序列与JavaScript中完全一致。一个字符的编码差异比如UTF-8和Latin-1都会导致解密失败。CCM模式参数AES-CCM是一种认证加密模式它除了需要Key和IV通常还需要一个tag认证标签。在有些实现中密文ciphertext本身就包含了tag通常是附加在末尾而在CryptoJS的某些用法里可能需要单独指定。你需要仔细对照JS代码看它是如何构造解密参数的。pycryptodome中的AES.new(key, AES.MODE_CCM, nonceiv).decrypt_and_verify(ciphertext, tag)这个接口就很典型。Padding填充AES块加密需要对数据填充至块长度的整数倍。常见的填充方式有PKCS7。但在CCM模式下通常不使用额外的填充即padding: CryptoJS.pad.NoPadding因为CCM模式本身会处理数据长度。如果你在Python中错误地指定了填充方式解密也会失败。2.3 坐标换算从解密结果到鼠标轨迹成功解密后你得到的数据可能是一个数字字符串比如120.5。但这120.5像素就是你要拖动的距离吗不一定这里还有第二个大坑坐标换算。前端验证码的滑动距离往往不是简单的缺口横坐标 - 滑块起点横坐标。原因在于缩放Scale验证码图片在前端显示时可能被CSS缩放。你通过图像识别计算出的像素距离是基于原始图片尺寸的。而前端JavaScript获取的鼠标事件坐标clientX,clientY是基于屏幕视口的。如果图片被缩放就需要按比例换算。Canvas绘图偏移很多滑块验证码使用Canvas绘制滑块的轨道和背景图可能绘制在Canvas的特定位置而不是从(0,0)开始。你需要计算Canvas元素相对于页面视口viewport的偏移量getBoundingClientRect()。轨迹模拟直接让鼠标“瞬移”到终点是会被检测出来的。你需要生成一个包含加速度变化的人类鼠标移动轨迹通常是一条先加速后减速的贝塞尔曲线或根据物理模型计算的路径然后将轨迹坐标换算到上述考虑偏移和缩放后的坐标系中再通过自动化工具如Selenium的ActionChains模拟。所以完整的流程是获取原始密文 - 逆向得到Key/IV及算法参数 - 在Python中复现解密 - 得到原始像素距离 - 结合前端页面布局计算缩放比例和偏移量 - 生成人类轨迹 - 执行模拟拖动。3. 实操详解以AES-CCM为例的完整解密流程理论说再多不如动手做一遍。我们假设已经通过逆向分析找到了以下信息密文ciphertext一个Base64编码的字符串。密钥Key一个16字节128位的字符串例如“myverystrongkey”。初始化向量IV/Nonce一个13字节的Hex字符串例如“aabbccddeeff001122334455”。算法模式AES-CCM无填充NoPadding。下面我们用Python的cryptography库来走通解密流程。3.1 环境准备与库安装首先确保你安装了必要的库。cryptography是一个功能强大且维护良好的密码学库。pip install cryptography3.2 解密代码实现与逐行解析import base64 from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend from cryptography.exceptions import InvalidTag def decrypt_slide_offset(ciphertext_b64, key_str, iv_hex): 解密滑块验证码的偏移量坐标 :param ciphertext_b64: Base64编码的密文 :param key_str: 密钥字符串UTF-8 :param iv_hex: 初始化向量的十六进制字符串 :return: 解密后的偏移量浮点数或整数 # 1. 将参数转换为字节bytes # 密钥字符串转字节。注意编码必须与JS端完全一致通常为UTF-8。 key key_str.encode(‘utf-8‘) # 初始化向量Nonce从Hex字符串转换。CCM模式的nonce长度有要求常见7-13字节。 iv bytes.fromhex(iv_hex) # 例如 ‘aabbcc...‘ - b‘\xaa\xbb\xcc...‘ # 密文从Base64解码。这是最常用的一种传输编码。 ciphertext_bytes base64.b64decode(ciphertext_b64) # **关键坑点1CCM的Tag分离** # 在AES-CCM中密文通常由“实际密文”“认证标签Tag”组成。 # Tag长度通常是16字节128位认证。但具体实现需要看JS端。 # 假设我们已知Tag是附在密文末尾的16字节。 tag_length 16 if len(ciphertext_bytes) tag_length: raise ValueError(“密文长度异常可能不包含Tag或格式错误”) # 分离出实际密文和Tag actual_ciphertext ciphertext_bytes[:-tag_length] tag ciphertext_bytes[-tag_length:] # 2. 创建AES-CCM解密器 # CCM模式需要nonce即IV和tag。data_len参数在某些旧版本或配置中可能需要用于指定关联数据AAD的长度这里没有AAD。 # 使用 default_backend() 确保兼容性。 cipher Cipher( algorithmalgorithms.AES(key), modemodes.CCM(iv, tag, min_tag_lengthtag_length), # 明确指定tag backenddefault_backend() ) decryptor cipher.decryptor() # 3. 执行解密 try: # 由于没有附加关联数据AAD这里直接解密。 plaintext_bytes decryptor.update(actual_ciphertext) decryptor.finalize() except InvalidTag: # 如果Tag验证失败说明密钥、IV、密文或Tag有任何不匹配解密无效。 raise ValueError(“解密失败认证标签(Tag)验证无效。请检查Key, IV, 密文或Tag分离方式。”) # 4. 解码明文 # 解密出的明文通常是字节需要根据实际情况解码。可能是UTF-8字符串形式的数字。 plaintext plaintext_bytes.decode(‘utf-8‘) # 尝试转换为数值整数或浮点数 try: # 可能是整数 “120”也可能是浮点数 “120.5” offset float(plaintext) if ‘.‘ in plaintext else int(plaintext) except ValueError: # 如果不是纯数字直接返回字符串可能是其他格式的令牌 offset plaintext print(f“解密结果不是纯数字{plaintext}”) return offset # 示例用法 if __name__ “__main__“: # 这些值需要你从实际网站逆向得到 example_ciphertext “SGVsbG8gV29ybGQhAQIDBAUGBwgJCgsMDQ4PEA“ # 示例Base64非真实数据 example_key “myverystrongkey“ # 16字节 example_iv “aabbccddeeff001122334455“ # 12字节Hex对应6个十六进制数对 try: result decrypt_slide_offset(example_ciphertext, example_key, example_iv) print(f“解密出的滑块偏移量是{result} 像素”) except Exception as e: print(f“解密过程出错{e}”)代码关键点解析字节转换encode(‘utf-8‘),bytes.fromhex(),base64.b64decode()这三行是确保数据格式正确的生命线。务必与前端JavaScript中的处理方式核对例如JS里CryptoJS.enc.Utf8.parse(key)对应这里的key.encode(‘utf-8‘)。Tag的处理这是AES-CCM解密最易错的地方。cryptography库的CCM模式要求将tag作为参数传入modes.CCM。你需要明确知道tag在哪里。在我们的示例中我们假设tag附加在密文末尾。但实际情况可能有变变体1Tag单独作为一个参数传递可能在另一个字段。变体2JS的CryptoJS.AES.decrypt在CCM模式下可能自动处理了Tag的分离你只需要传入完整的ciphertext。这时在Python中你可能需要使用decryptor.update(ciphertext_bytes)而不分离Tag但这要求库支持这种“自动”模式cryptography不支持pycryptodome的接口更灵活。所以最可靠的方法是仔细阅读JS代码看它是如何调用解密函数的。异常处理InvalidTag异常非常有用它能立刻告诉你密钥、IV或数据是否被篡改是调试的重要依据。结果解析解密出的明文不一定就是数字。有时可能是一个包含坐标和其他信息的JSON字符串比如{“x”: 120, “y”: 0}。你需要根据实际情况用json.loads()解析。3.3 验证与调试技巧如何验证你的Python解密代码是正确的黄金标准与浏览器结果对比。在浏览器的开发者工具Console中直接调用你找到的那个JS解密函数传入密文、Key、IV得到结果。在你的Python脚本中使用完全相同的输入参数运行解密函数。对比两者输出。必须完全一致字节对字节或字符串完全一致。使用已知明文测试如果可能。如果你能通过其他方式如手动滑动一次抓取提交的数据知道某次滑动的真实坐标那么用该次请求的密文来调试你的解密函数是最直接的方法。分步打印在解密函数的每一步打印出中间结果的Hex或Base64表示如key.hex(),iv.hex(),ciphertext_bytes.hex()与JS端执行到对应步骤时的值进行比对。这是定位编码或数据分离错误的最有效手段。4. 前端坐标换算与轨迹生成假设我们已经成功解密得到了缺口在原始背景图上的像素横坐标gap_x_raw 120。4.1 计算前端实际滑动距离现在我们需要知道在前端页面上需要拖动多少像素。from selenium import webdriver from selenium.webdriver.common.by import By def calculate_actual_distance(driver, gap_x_raw): 计算在前端页面中需要滑动的实际距离。 :param driver: Selenium WebDriver 实例 :param gap_x_raw: 解密得到的原始图片缺口像素横坐标 :return: 实际需要滑动的距离像素 # 1. 找到滑块验证码的容器通常是Canvas或一个包裹图片的div # 假设滑块背景图在一个id为‘slide-bg’的img标签里且可能被缩放 bg_element driver.find_element(By.ID, ‘slide-bg‘) # 请替换为实际选择器 # 2. 获取图片的原始宽度和显示宽度 # 原始宽度可以从图片的自然属性或data-属性获取这里假设存在‘data-original-width‘ original_width int(bg_element.get_attribute(‘data-original-width‘)) # 例如 300 # 显示宽度是图片在页面上渲染后的实际宽度 display_width bg_element.size[‘width‘] # 例如 240 # 3. 计算缩放比例 scale_ratio display_width / original_width # 240 / 300 0.8 # 4. 计算缩放后的缺口位置 gap_x_scaled gap_x_raw * scale_ratio # 120 * 0.8 96 像素 # 5. 找到滑块的起点滑块按钮的初始位置 slide_button driver.find_element(By.ID, ‘slide-button‘) # 请替换为实际选择器 button_start_x slide_button.location[‘x‘] # 滑块按钮左上角相对于页面左上角的x坐标 # 6. 找到轨道或背景图的起点 # 通常滑块的移动范围就是背景图的x坐标开始。我们假设背景图元素就是轨道起点。 track_start_x bg_element.location[‘x‘] # 7. 计算需要滑动的总距离 # 实际滑动距离 缩放后缺口位置 - (滑块起点 - 轨道起点) # 因为gap_x_scaled是相对于轨道起点的而我们需要移动滑块按钮从它的起点到缺口位置。 # 简化理解滑块按钮初始时其左侧边缘通常对齐轨道起点。 # 所以滑动距离就是 gap_x_scaled 减去滑块按钮本身的宽度一半为了对准中心但很多验证码直接按左边缘算。 # 最稳妥的方法计算缺口中心在页面上的绝对X坐标减去滑块按钮中心的初始绝对X坐标。 gap_center_absolute_x track_start_x gap_x_scaled button_center_start_x button_start_x slide_button.size[‘width‘] / 2 slide_distance gap_center_absolute_x - button_center_start_x # 另一种常见情况缺口坐标已经是相对于滑块按钮初始位置的距离。这时直接使用缩放后的值。 # 具体需要根据验证码的前端逻辑判断。这里我们采用更通用的绝对坐标计算法。 return slide_distance # 在Selenium脚本中调用 driver webdriver.Chrome() driver.get(“your_target_url“) # ... 执行其他步骤触发验证码 ... raw_offset decrypt_slide_offset(...) # 从网络请求获取密文并解密 actual_distance calculate_actual_distance(driver, raw_offset) print(f“需要滑动的实际距离是{actual_distance} 像素”)关键坑点元素定位find_element的选择器必须准确找到背景图和滑块按钮。它们可能藏在多层div或iframe里。缩放计算display_width是渲染后的宽度original_width可能来自naturalWidth属性或自定义的>import random import time def generate_move_track(distance): 生成模拟人类拖动的移动轨迹。 :param distance: 需要滑动的总距离像素 :return: 一个列表包含每个时间点移动的位移量像素 track [] current 0 # 设置一个阈值当剩余距离小于这个值时开始减速 mid distance * 4 / 5 # 例如在总距离的80%处开始减速 # 初始速度 v 0 # 当前已移动距离 sum_move 0 # 模拟前段加速过程 while current mid: # 加速度在一定范围内随机更接近人类 a random.uniform(1.5, 3.0) v0 v v v0 a move v0 0.5 * a # 根据匀加速公式 s v0*t 1/2*a*t^2这里简化t1 move round(move) current move sum_move move track.append(move) # 模拟后段减速过程 while sum_move distance: # 剩余距离 left distance - sum_move # 减速过程速度逐渐减小移动距离也减小 dec random.uniform(1.5, 3.0) # 确保最后一步不会超出 move max(1, min(left, round(v - dec))) v max(0, v - dec) # 速度不能为负 track.append(move) sum_move move # 最后可能因为取整总移动距离有1-2像素的误差进行微调 if sum_move ! distance: track.append(distance - sum_move) # 在轨迹末尾有时人类会有一个微小的回拉或抖动 if random.choice([True, False]): track.append(-random.randint(1, 3)) track.append(random.randint(1, 2)) return track def drag_slider(driver, slider_element, track): 按照轨迹拖动滑块。 :param driver: WebDriver :param slider_element: 滑块按钮的WebElement :param track: 位移轨迹列表 from selenium.webdriver.common.action_chains import ActionChains actions ActionChains(driver) actions.click_and_hold(slider_element).perform() time.sleep(random.uniform(0.1, 0.3)) # 按下后停顿一下更像人类 for move in track: actions.move_by_offset(move, 0).perform() # 每次移动后加入一个随机的小延迟时间间隔不均匀 time.sleep(random.uniform(0.02, 0.1)) # 释放鼠标 time.sleep(random.uniform(0.1, 0.5)) # 释放前可能有的犹豫 actions.release().perform() # 整合使用 slide_distance actual_distance # 从上一步计算得来 move_track generate_move_track(slide_distance) slider driver.find_element(By.ID, ‘slide-button‘) drag_slider(driver, slider, move_track)轨迹生成要点随机性加速度、减速值、移动间隔都加入了随机范围避免轨迹过于规律。先加速后减速这是人类拖动滑块最典型的特征。微小抖动最后的回拉或抖动能有效提高模拟的真实性。总距离对齐确保轨迹的总位移等于需要滑动的距离。5. 常见问题排查与实战心得即使按照上述步骤你可能还是会遇到各种问题。下面是我踩过坑后总结的排查清单和心得。5.1 解密失败问题排查表问题现象可能原因排查步骤与解决方案InvalidTag异常1. 密钥(Key)错误。2. 初始化向量(IV)错误。3. 密文格式错误如Tag分离方式不对。4. 加密模式或填充方式不匹配。1.核对Key/IV确保从JS中提取的Key和IV字符串与Python中转换后的字节完全一致。使用print(key.hex())和JS端的Hex表示对比。2.检查Tag处理这是最常见的问题。用浏览器Console单步调试JS解密函数查看它接收的ciphertext参数是什么是完整的Base64吗。然后看JS库内部是如何处理这个ciphertext的。是自动剥离最后16字节作为Tag吗pycryptodome的AES.new(key, AES.MODE_CCM, nonceiv).decrypt_and_verify(ciphertext, tag)要求分开传入而decrypt(ciphertext)可能要求合并传入。必须和JS库的行为保持一致。3.确认算法参数确认JS中CryptoJS.AES.decrypt的选项特别是mode和padding。CCM模式通常对应CryptoJS.mode.CCM和CryptoJS.pad.NoPadding。解密出的明文是乱码1. 解密其实成功了但明文不是UTF-8可解码的文本可能是二进制数据或数字的字节表示。2. Key/IV正确但解密流程仍有细微错误。1.尝试其他解码不要直接.decode(‘utf-8‘)。先打印plaintext_bytes.hex()看看。如果是一串ASCII码范围的十六进制如32333435对应‘2345‘说明它是数字字符的字节。如果像7b226...开头是7b即‘{‘那可能是JSON字符串的字节。尝试plaintext_bytes.decode(‘ascii‘)或直接int.from_bytes(plaintext_bytes, ‘big‘)。2.对比中间值在JS解密函数的关键步骤如调用CryptoJS.enc.Base64.parse后、调用decrypt前打印出变量的Hex值与Python中对应步骤的Hex值逐字节比对。解密结果是一个很大的数字明显不是坐标解密成功但得到的可能是加密前的原始数据而非简单的像素坐标。有些验证码会对坐标进行进一步处理比如乘以一个随机系数或加上一个随机偏移。1.分析JS逻辑找到解密函数被调用后的代码看解密出的数据是如何被使用的。可能有一个后续的JSON.parse或者进行了一些算术运算如result / 2才得到最终坐标。2.动态调试在浏览器中设置断点在解密函数返回后观察返回值被如何处理然后在你Python代码中复现这个后处理过程。5.2 坐标换算与滑动失败排查问题现象可能原因排查步骤与解决方案滑块总是差一点过不去1. 缩放比例计算错误。2. 滑块起点或轨道起点定位不准。3. 轨迹模拟被识别验证码要求更精确的终点或加入了抖动容差。1.手动计算验证在浏览器Console中手动计算一次。获取背景图元素的getBoundingClientRect().width和它的naturalWidth计算比例。再手动计算滑块按钮中心到缺口中心的距离。2.检查元素状态确保在获取元素位置时页面布局是稳定的没有正在进行的动画。有时需要短暂time.sleep等待渲染完成。3.调整轨迹在轨迹的最后几步将步长调得非常小1-2像素进行“微调”。增加轨迹的随机性或者在释放鼠标前加入一个极小的、缓慢的往复移动。直接被判定为机器人没有滑动机会1. 浏览器指纹或WebDriver特征被检测。2. 滑动行为模式过于规律。3. 整个操作流程太快不像人类。1.使用更隐蔽的驱动考虑使用undetected-chromedriver或给Selenium添加实验性选项来隐藏“自动化控制”特征。2.增加随机延迟在每一步操作如点击、拖动前、拖动中、释放后都加入随机、不均匀的等待时间。3.模拟更真实的行为在拖动前让鼠标在滑块上悬停一会儿拖动路径可以加入轻微的垂直方向随机偏移move_by_offset(dx, random.randint(-2, 2))。5.3 个人实战心得逆向是第一生产力也是最大门槛不要一上来就想着写Python代码。花70%的时间在浏览器开发者工具上把JS的加密逻辑、参数来源、坐标计算方式彻底搞清楚。用Console做实验验证你的每一个猜想。保存好关键的JS代码片段。“字节级一致”是解密的铁律在密码学操作中“看起来一样”的字符串没用必须保证字节序列一模一样。善用console.log(btoa(key))JS和print(base64.b64encode(key))Python这类工具进行跨语言比对。AES-CCM的Tag是头号敌人遇到CCM模式第一时间就去找Tag在哪。如果JS代码混淆严重看不清一个笨办法是用错误的Tag长度比如假设Tag是15或17字节去尝试解密如果报错信息不同可能帮你推断出正确长度。或者直接搜索tagLength或authenticationTag等关键词。缩放和偏移是隐藏的坑不要以为解密出坐标就万事大吉。务必写一个测试脚本用你计算出的距离让Selenium移动滑块同时用driver.save_screenshot(‘debug.png‘)截图用画图工具打开看看滑块最终位置和缺口是否对齐。这是最直观的调试方法。备选方案如果逆向难度实在太大或者加密逻辑经常变动可以考虑其他方案比如使用商业验证码识别服务直接返回坐标或者对于不太复杂的验证码尝试基于深度学习的目标检测模型如YOLO来直接识别缺口位置但这需要收集和标注数据成本也不低。这个从AES加密逆向到轨迹模拟的完整链条确实比单纯的图像识别复杂得多但它能帮你攻克那些更坚固的防线。每一步都需要耐心和细致尤其是对齐JS和Python两端的数据处理逻辑。当你最终看到滑块“咔哒”一声完美合上时那种成就感绝对是值得的。