Python逆向极验四代滑块验证码:AES加密与轨迹模拟实战

📅 2026/7/16 9:16:05 👁️ 阅读次数 📝 编程学习
Python逆向极验四代滑块验证码:AES加密与轨迹模拟实战

1. 项目概述:当滑块遇上逆向工程

最近在做一个数据采集项目时,又双叒叕遇到了极验四代滑块验证码。这玩意儿就像个尽职的门卫,死死挡在我和目标数据之间。传统的打码平台成本高、速度慢,自己写个脚本绕过它,就成了性价比最高的选择。这个项目,就是记录我从零开始,完整拆解极验四代滑块验证码协议,并最终用Python实现自动化通过验证的全过程。核心目标不是教你“作弊”,而是通过逆向其通信流程,理解现代验证码背后的安全设计逻辑,这对于从事安全研究、爬虫开发或者单纯对Web协议感兴趣的朋友来说,是一次绝佳的实战演练。

整个流程可以概括为“观察-分析-模拟”三部曲。首先,我们需要像侦探一样,利用浏览器开发者工具,完整地抓取一次滑块验证的“对话”记录,搞清楚客户端(浏览器)和服务器(极验)之间到底说了些什么。然后,我们会发现其中一些关键参数被加密了,尤其是那个看起来像乱码的w参数,这就是本次实战的硬骨头——AES加密。最后,我们需要在Python环境中,复现出生成这个合法w参数的整个计算过程,从而让我们的脚本能够“伪装”成一个正常的浏览器,成功通过验证。

你会发现,这不仅仅是一个爬虫技巧,更涉及对JavaScript执行逻辑的分析、加密算法的识别与还原,以及网络请求的精确模拟。下面,我就带你一步步走通这个流程,我会把踩过的坑、关键的判断依据和最终的代码都分享出来。

2. 逆向前的环境准备与抓包分析

工欲善其事,必先利其器。在开始逆向之前,搭建一个便于观察和分析的环境至关重要。我选择的是Chrome浏览器,因为它自带的开发者工具(DevTools)功能强大且直观。

2.1 关键工具链配置

首先,确保你的Chrome开发者工具网络面板(Network)是开启状态,并勾选上“Preserve log”(保留日志)和“Disable cache”(禁用缓存)。这能保证页面跳转或提交后,之前的请求记录不会消失,方便我们追踪完整的交互链条。

接下来是一个非常重要的技巧:寻找并进入极验的演示页面。极验官方通常会提供一些测试页面,这些页面结构清晰,没有其他业务逻辑干扰,是理想的逆向起点。你可以通过搜索“极验demo”或“geetest demo”来找到它们。找到后,专门为这个测试页面创建一个新的浏览器书签或标签页,后续所有分析都基于此页面进行,避免被目标业务网站的其他复杂请求干扰。

在Python端,我们需要准备好几个核心库:

  • requests: 用于发送HTTP请求。
  • PyExecJSjs2py: 用于执行JavaScript代码。这里我强烈推荐PyExecJS,因为它对现代JS环境的兼容性更好,尤其是在处理一些浏览器特有的对象时。
  • cryptographypycryptodome: 用于AES等加密算法的实现。pycryptodome功能更全,API也更友好。

你可以通过pip一键安装:pip install requests PyExecJS pycryptodome

2.2 首次滑动与关键请求链解析

打开极验测试页,不要急着滑动。先清空Network面板的记录,然后进行一次完整的手动滑动操作,直到验证成功。这时,Network面板会刷出一系列请求,我们需要从中找出最关键的几个。

通常,极验四代的流程会包含以下核心请求:

  1. 获取验证参数(gt/挑战):一个向极验服务器发起的请求,返回包含gt(网站标识)、challenge(本次验证流水号)等信息的JSON。这个challenge是后续所有计算的种子。
  2. 加载验证码资源:请求一个包含滑块图片、背景图片和核心JavaScript逻辑的包。
  3. 提交验证(ajax.php):这是最核心的请求。在你松开鼠标,完成滑动后,浏览器会向一个类似ajax.php的接口发送POST请求,其表单数据中包含了加密后的轨迹信息w

我们的首要目标就是第三个请求。点击这个ajax.php请求,查看它的“Headers”和“Payload”。在“Payload”标签页下,你会看到form data,里面通常有gtchallengew等字段。其中,w是一长串看似随机的字符,这就是被AES加密后的核心数据。

注意:不同网站集成极验的方式可能有细微差别,接口名称不一定是ajax.php,也可能是validate之类的,但提交加密参数w的这个行为是统一的。关键在于找到那个携带了长串w参数的POST请求。

2.3 定位加密入口:搜索与断点

现在我们知道w是加密结果,下一步就是找到生成它的JavaScript代码在哪里。这里用到一个DevTools的“杀手锏”功能:全局搜索

在Sources面板,按Ctrl+Shift+F(Windows) 或Cmd+Opt+F(Mac),打开全局搜索框。因为w是作为参数名被提交的,所以我们可以尝试搜索"w"w:w =或者w:。更直接的方法是,搜索ajax.php这个接口URL,或者搜索challengegt这些我们知道一定会出现的参数名。

很快,你可能会找到一段压缩过的、变量名都是单字母的JavaScript代码。别慌,点击代码区域左下角的{}(美化代码)按钮,让它变得可读。

美化后,搜索w,你会找到类似这样的代码块:

var w = get_w(param1, param2); data["w"] = w;

或者直接是data: {gt: ..., challenge: ..., w: ...}的组装过程。这个get_w或者组装w的地方,就是加密发生的函数。在它所在的行号上点击,设置一个JavaScript断点

设置断点后,回到网页,再次触发一次滑动。浏览器的执行会在断点处暂停。这时,你可以将鼠标悬停在变量上查看其值,也可以在Console面板手动执行console.log(...)来打印变量。我们的目标是进入get_w函数内部。在Sources面板,找到这个函数定义,同样进行美化,然后仔细分析它的实现。

3. 核心加密逻辑的逆向与解析

断点调试进入get_w函数后,真正的挑战开始了。你会发现,w的值并非简单的加密,而是一个多层嵌套的结构,通常是一个JSON字符串经过AES加密后再进行Base64编码的结果。

3.1 拆解W参数的数据结构

通过断点调试,打印出get_w函数的返回值,或者直接查看即将被赋值给data[“w”]的那个变量。你可能会得到一个这样的字符串:“1A2B3C...(很长)==”。这明显是Base64编码的特征(可能以===结尾)。

在Console里尝试用atob()(浏览器内置的Base64解码函数)解码它:

console.log(atob(“你的w参数值”))

解码后,你看到的很可能还不是明文,而是一串乱码。这说明在Base64编码之前,数据已经过加密。常见的加密就是AES。

那么,加密的原文是什么?继续在get_w函数里向上追溯,找到被加密的那个原始对象。它通常是一个包含了滑动轨迹、时间戳、挑战码等信息的庞大JSON对象。结构可能类似于:

var raw_data = { ‘gt’: ‘网站标识’, ‘challenge’: ‘本次挑战码’, ‘lang’: ‘zh-cn’, ‘pt’: 0, ‘userresponse’: distance + challenge, // 关键:用户滑动距离与challenge的某种计算 ‘passtime’: slide_time, // 滑动耗时 ‘imgload’: img_load_time, // 图片加载时间 ‘aa’: track_string, // 加密后的移动轨迹 ‘ep’: {‘v’: ‘4.0’, ‘...’: ‘...’} // 环境参数,可能包含浏览器指纹 };

这个raw_data对象,就是需要被加密的明文。userresponseaa(轨迹)是其中最关键且动态的计算部分。

3.2 追踪AES加密的关键要素

找到了明文,下一步就是看它如何被加密成AES密文。在JS代码中搜索AESCryptoJS(一个常用的JS加密库)、encryptmodepadding等关键词。

你可能会发现类似这样的代码:

var encrypted = CryptoJS.AES.encrypt(JSON.stringify(raw_data), key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }).toString();

这里揭露了AES加密的三个核心要素:

  1. 密钥(Key):用于加密和解密的密码。
  2. 初始化向量(IV):用于CBC模式,增加加密随机性。
  3. 加密模式与填充:通常是CBC模式和PKCS7填充。

关键来了:keyiv从哪里来?它们绝不会是硬编码在代码里的(那样太不安全)。极验四代通常采用一种动态生成的方式:利用challenge(挑战码)通过某种算法生成

你需要在代码中寻找keyiv的定义。它们很可能通过一个函数从challenge计算得来。例如:

var key = generate_key(challenge); var iv = generate_iv(challenge);

或者更隐蔽一些,keyiv可能就是challenge本身,或者其MD5值的前16位/后16位。这需要你通过多次抓包对比来验证:更换不同的challenge,观察生成的w是否完全不同,并尝试找出challengekey/iv的映射关系

实操心得:一个非常有效的方法是,在断点处,将challenge、计算出的keyiv以及最终的w都打印出来。然后,清空记录,刷新页面获取一个新的challenge,再重复一次滑动,记录第二组数据。对比两组数据,分析key/ivchallenge的变化关系。很多时候,keyiv就是challenge的MD5哈希值的不同部分。

3.3 轨迹生成算法的模拟

raw_data中的aa(轨迹)字段也是模拟的重点。它并不是你鼠标原始的(x, y)坐标数组,而是经过加密、压缩或特定编码处理的字符串。

在JS代码中搜索aatracktrajectory等词,找到生成轨迹数组的函数。这个函数通常会接收鼠标移动事件,生成一个包含时间戳和偏移量的数组。然后,这个数组会被另一个函数处理,转换成aa字符串。

你需要做的是:

  1. 理解原始轨迹数据的格式(例如,[[时间差1, x偏移1, y偏移1], [时间差2, x偏移2, y偏移2], ...])。
  2. 找到将原始轨迹转换成aa字符串的转换函数。这个函数可能包含自定义的编码、或使用JSON.stringify后再进行简单的位移/替换加密。
  3. 在Python中复现这个转换过程。有时这个转换很简单,可能就是JSON.stringify后做一次Base64编码;有时会复杂一些,比如每个数字与一个随机数进行异或。

模拟轨迹的核心在于“拟人化”。完全复制鼠标的物理轨迹是不现实的,服务器也不要求100%还原。关键在于生成一段在总时间、总距离、加速度变化上符合人类行为的轨迹数据。通常,一个先加速后减速的匀变速运动模型就能满足大部分情况。

4. Python代码实现与关键步骤还原

分析清楚后,我们就可以用Python来复现整个流程了。代码结构将清晰地对应我们分析的每一步。

4.1 请求流程的Python封装

首先,我们封装核心的HTTP请求流程,模拟浏览器行为。

import requests import time import json import execjs from Crypto.Cipher import AES from Crypto.Util.Padding import pad import base64 import hashlib class GeeTestV4Cracker: def __init__(self): self.session = requests.Session() # 设置合理的请求头,模拟浏览器 self.headers = { ‘User-Agent‘: ‘Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36‘, ‘Referer‘: ‘https://你的测试页地址/‘, ‘Content-Type‘: ‘application/x-www-form-urlencoded; charset=UTF-8‘ } self.session.headers.update(self.headers) def get_gt_challenge(self, page_url): ‘‘‘第一步:从页面或接口获取gt和challenge参数‘‘‘ # 方法1:如果gt和challenge直接嵌入在页面HTML的JS变量中 # 我们可以用正则表达式提取 resp = self.session.get(page_url) # 这里需要根据目标网站实际情况编写提取逻辑,例如: # import re # gt_match = re.search(r‘gt\s*[:=]\s*["\']([a-f0-9]+)["\']‘, resp.text) # challenge_match = re.search(r‘challenge\s*[:=]\s*["\']([a-f0-9]+)["\']‘, resp.text) # return gt_match.group(1), challenge_match.group(1) # 方法2:更常见的是,有一个独立的API接口返回这些参数 # 你需要从抓包中找到这个接口,例如: # api_url = ‘https://api.geetest.com/load.php?…‘ # resp = self.session.get(api_url) # data = resp.json() # return data[‘gt‘], data[‘challenge‘] # 此处为演示,返回假数据,实战中替换为上述逻辑 print(‘请实现从目标页面获取gt和challenge的逻辑‘) return ‘demo_gt_code‘, ‘demo_challenge_code_‘ + str(int(time.time()*1000))

4.2 AES密钥推导与加密函数实现

这是整个破解的核心,对应我们分析的keyiv生成逻辑。

def _generate_aes_params(self, challenge): ‘‘‘根据challenge推导AES加密所需的key和iv‘‘‘ # 这是最常见的模式之一:对challenge进行MD5,然后分割 # 具体算法需要根据你的逆向结果调整!!! m = hashlib.md5() m.update(challenge.encode(‘utf-8‘)) md5_str = m.hexdigest() # 假设逆向发现key是md5前16位,iv是md5后16位(需验证!) key = md5_str[:16].encode(‘utf-8‘) # 16字节 = 128位 AES密钥 iv = md5_str[16:32].encode(‘utf-8‘) # 16字节 IV # 另一种可能是key和iv都直接是challenge的某部分 # key = challenge[:16].ljust(16, ‘0‘).encode(‘utf-8‘) # iv = challenge[16:32].ljust(16, ‘0‘).encode(‘utf-8‘) return key, iv def _aes_encrypt(self, plaintext, key, iv): ‘‘‘AES-CBC-PKCS7加密,模拟CryptoJS的行为‘‘‘ # 确保明文是bytes if isinstance(plaintext, str): plaintext = plaintext.encode(‘utf-8‘) # PKCS7填充 padded_plaintext = pad(plaintext, AES.block_size) # 创建加密器 cipher = AES.new(key, AES.MODE_CBC, iv) # 加密 ciphertext = cipher.encrypt(padded_plaintext) # 返回Base64编码的字符串,这是极验w参数需要的格式 return base64.b64encode(ciphertext).decode(‘utf-8‘)

4.3 轨迹生成与核心参数组装

接下来,模拟生成滑动轨迹和构造待加密的原始数据。

def _generate_track(self, distance, total_time=2000): ‘‘‘生成拟人的滑动轨迹 distance: 需要滑动的总距离(像素) total_time: 滑动总耗时(毫秒) 返回: 轨迹列表,格式可能为 [[t, x, y], ...] 或自定义格式 ‘‘‘ tracks = [] current_x = 0 current_time = 0 # 简单的匀变速模型:前半段加速,后半段减速 mid_time = total_time / 2 mid_distance = distance / 2 # 生成轨迹点,大约每10-30毫秒一个点 while current_x < distance: t = len(tracks) * 18 # 模拟时间间隔,有一定随机性更好 if t > total_time: t = total_time # 根据时间计算理论位移(匀加速公式) if t <= mid_time: # 加速阶段 s = 0.5 * (2 * mid_distance / (mid_time ** 2)) * (t ** 2) else: # 减速阶段 s = distance - 0.5 * (2 * mid_distance / (mid_time ** 2)) * ((total_time - t) ** 2) current_x = min(s, distance) # 确保不超过总距离 # y轴可以有一些细微的随机抖动,模拟人手不稳 current_y = 0 # 极验通常只关心x轴位移,y轴可以固定或轻微随机 tracks.append([t, int(current_x), int(current_y)]) if current_x >= distance: break # 根据逆向结果,可能需要将轨迹转换成特定的字符串格式 # 例如:aa = json.dumps(tracks) 或进行自定义编码 # 这里假设aa就是轨迹JSON的字符串 aa_string = json.dumps(tracks, separators=(‘,‘, ‘:‘)) # 去除空格,与JS的JSON.stringify接近 return aa_string def _construct_raw_w(self, gt, challenge, track_string): ‘‘‘构造待加密的原始数据对象‘‘‘ # 注意:这里的参数名和结构必须与逆向分析结果完全一致! raw_data = { ‘gt‘: gt, ‘challenge‘: challenge, ‘lang‘: ‘zh-cn‘, ‘pt‘: 0, ‘userresponse‘: self._calc_user_response(challenge, track_string), # 需要实现 ‘passtime‘: self._calc_passtime(track_string), # 需要实现 ‘imgload‘: random.randint(80, 120), # 模拟图片加载时间 ‘aa‘: track_string, ‘ep‘: {‘v‘: ‘4.0‘, ‘...‘: ‘...‘} # 环境参数,可能需要更复杂的生成 } return json.dumps(raw_data, separators=(‘,‘, ‘:‘)) def _calc_user_response(self, challenge, track_string): ‘‘‘计算userresponse,通常是滑动距离与challenge的某种组合‘‘‘ # 从轨迹字符串中解析出最终滑动距离(最后一个点的x坐标) # 这里简化处理,假设我们已经知道滑动距离 slide_distance = 125 # 示例距离,实际需要计算 # 极验常见算法:将滑动距离与challenge进行某种运算,例如MD5 # 也可能是简单的字符串拼接后取MD5的一部分 # 具体算法需逆向确定! # 示例(可能不正确!): combined = str(slide_distance) + challenge m = hashlib.md5(combined.encode(‘utf-8‘)) return m.hexdigest()[:32] def _calc_passtime(self, track_string): ‘‘‘计算滑动总耗时,从轨迹中提取‘‘‘ # 解析轨迹,最后一个时间点就是总耗时 tracks = json.loads(track_string) if tracks: return tracks[-1][0] # 假设轨迹格式为[[时间, x, y], ...] return 2000 # 默认值

4.4 整合:生成最终W参数并提交验证

最后,将所有步骤串联起来,生成可提交的w参数。

def generate_w_param(self, gt, challenge, slide_distance): ‘‘‘主函数:生成完整的w参数‘‘‘ # 1. 生成轨迹 track_str = self._generate_track(slide_distance) # 2. 构造待加密的原始JSON字符串 plaintext_json = self._construct_raw_w(gt, challenge, track_str) print(‘待加密明文:‘, plaintext_json) # 3. 生成AES Key和IV key, iv = self._generate_aes_params(challenge) print(f‘Key: {key}, IV: {iv}‘) # 4. AES加密 encrypted_b64 = self._aes_encrypt(plaintext_json, key, iv) # 5. 注意:有时w参数就是加密后的Base64,有时可能还需要额外包装 # 根据抓包结果确定 final_w = encrypted_b64 return final_w def crack_and_submit(self, target_page_url, submit_api_url): ‘‘‘完整的破解与提交流程‘‘‘ # 1. 获取gt和challenge gt, challenge = self.get_gt_challenge(target_page_url) print(f‘获取到参数 - gt: {gt}, challenge: {challenge}‘) # 2. 假设我们通过图像识别或已知缺口位置,得到了需要滑动的距离 # 这里简化,使用一个固定距离。实战中需要结合图像识别。 slide_distance = 125 # 像素 # 3. 生成w参数 w_param = self.generate_w_param(gt, challenge, slide_distance) print(f‘生成的w参数长度: {len(w_param)}‘) # 4. 组装提交数据 post_data = { ‘gt‘: gt, ‘challenge‘: challenge, ‘w‘: w_param, ‘client_type‘: ‘web‘, # 可能需要的其他参数 ‘lang‘: ‘zh-cn‘, } # 5. 提交验证 resp = self.session.post(submit_api_url, data=post_data) result = resp.json() print(‘服务器响应:‘, result) # 6. 验证结果 if result.get(‘status‘) == ‘success‘ or ‘validate‘ in result: print(‘验证成功!‘) # 通常成功后会返回一个validate字段,用于后续业务请求 return result.get(‘validate‘, ‘‘) else: print(‘验证失败!‘) return None # 使用示例 if __name__ == ‘__main__‘: cracker = GeeTestV4Cracker() # 替换成实际的URL test_page = ‘https://www.example.com/geetest-demo‘ submit_api = ‘https://api.geetest.com/ajax.php‘ validate = cracker.crack_and_submit(test_page, submit_api) if validate: print(f‘获得的validate令牌: {validate}‘) # 你可以将这个validate用于后续需要验证的请求中

5. 常见问题排查与实战调试技巧

即使代码按照分析写出来了,第一次运行很可能失败。因为逆向过程中任何一个细节的偏差都会导致最终的w参数不合法。下面是我在实战中总结的排查清单。

5.1 参数不匹配导致的失败排查

服务器返回错误时,首先需要定位问题出在哪个环节。

问题现象可能原因排查步骤
服务器直接返回failerror1.gtchallenge错误或已过期。
2. 提交的接口地址或请求方法不对。
3. 缺少必要的请求头(如Referer,X-Requested-With)。
1. 检查获取gt/challenge的代码,确保与抓包看到的一致,且每次验证使用新的。
2. 核对submit_api_url是否完全正确(包括路径和查询参数)。
3. 在session.headers中补全抓包时看到的所有请求头。
返回{"status": "fail", "...": "data error"}w参数根本性错误。加密前的原始数据(raw_data)结构或字段名与服务器预期不符。1.核心步骤:将你代码中生成的plaintext_json(加密前的JSON字符串)打印出来。
2. 在浏览器断点处,将JS生成的raw_data对象通过JSON.stringify转换成字符串并打印。
3.逐字段对比两个字符串,确保键名、键值类型(字符串/数字)、顺序(有时有要求)完全一致。特别注意userresponseaa的格式。
返回{"status": "fail", "...": "sign error"}或类似w参数加密错误。AES的keyivmodepadding或编码方式有误。1. 对比keyiv:确保Python生成的与JS中计算的完全一致(十六进制或字节形式)。
2. 验证加密模式:一定是CBC。
3. 验证填充:一定是PKCS7。
4. 验证输出:JS中CryptoJS.AES.encrypt(...).toString()默认输出的是OpenSSL格式的字符串或Base64,需确认Python输出的是纯Base64。可以尝试用JS加密一个简单字符串,再用Python复现,对比结果。
返回{"status": "fail", "data": {"result": "slide"}}滑动轨迹aauserresponse不合法,服务器判定为机器行为。1. 检查轨迹aa的生成算法是否还原正确。不仅仅是数据,编码方式(如Base64、自定义编码)也要一致。
2. 检查userresponse的计算公式。它通常是滑动距离与challenge的某种函数(如MD5(距离+challenge)),务必与JS逻辑一致。
3.优化轨迹:使用更拟人的轨迹算法,加入随机抖动和更真实的加速度曲线。

5.2 高级调试与验证手段

当基础对比无法解决问题时,需要更深入的调试。

1. 本地执行JavaScript:如果key/ivuserresponse的计算逻辑非常复杂,直接用Python还原困难,可以考虑将关键的JS函数代码抠出来,在Python中用PyExecJS环境执行。

import execjs # 假设你从JS中抠出了计算key的函数,保存为字符串 js_code = ‘‘‘ function generate_key(challenge) { // 这里是复杂的JS计算逻辑 return md5(challenge).substr(0, 16); } ‘‘‘ # 创建JS上下文 ctx = execjs.compile(js_code) # 调用函数 key = ctx.call(‘generate_key‘, ‘你的challenge‘) print(key)

注意PyExecJS需要系统安装有JS运行时(如Node.js)。对于依赖浏览器环境(如windowdocument)的代码,需要做适配或使用jsdom模拟。

2. 中间结果对比法:这是最可靠的方法。在浏览器断点处,将所有中间变量导出。

  • 在JS中,通过console.log(JSON.stringify(raw_data))导出待加密对象。
  • 通过console.log(key, iv)导出密钥和向量。
  • 通过console.log(CryptoJS.enc.Base64.stringify(ciphertext))导出加密后的Base64(注意不是.toString()的结果,可能是.ciphertext属性)。

在Python代码的对应位置,也打印出这些中间结果。进行逐字节对比。任何细微差别(如一个字段是字符串"123"还是数字123,一个空格,一个尾随的逗号)都会导致最终的加密结果天差地别。

3. 轨迹算法的“白盒”测试:不要依赖猜测来生成轨迹。在JS中,找到生成最终aa字符串的函数,输入一个固定的、简单的轨迹数组(如[[0,0,0], [100,50,0]]),记录其输出。然后在Python中,用你还原的算法对同样的输入进行计算,看输出是否一致。这能帮你精准定位轨迹编码环节的问题。

逆向极验四代滑块是一个需要耐心和细致的过程,它没有一成不变的答案。不同网站、甚至同一网站不同时期的集成方式,其加密细节都可能发生变化。核心思路是通用的:抓包定位、断点分析、动态调试、对比还原。成功的那一刻,你获得的不仅仅是一个绕过验证的工具,更是对前端安全、加密协议和协议模拟的深刻理解。