爬虫逆向之滑块验证码加密分析(轨迹和坐标)
本文章中所有内容仅供学习交流使用,不用于其他任何目的。否则由此产生的一切后果均与作者无关!
在爬虫开发过程中,滑块验证码常常成为我们获取数据的一大阻碍。而滑块验证码的加密方式多种多样,其中轨迹加密和坐标加密是比较常见的两种方式。本文将详细介绍这两种加密方式的原理以及如何进行逆向分析。
验证码逆向过程分析
第一步,找生成图片的接口(接口可能有加密参数)获取图片url或者图片base64编码,可能还有id,token等值。
第二步,用识别工具识别图片,获取缺口坐标(用函数模拟轨迹)
第三步,获取缺口坐标(轨迹)(可能会进行加密)和第一次接口获取id、token等(可能会进行加密),可能直接携带加密坐标(轨迹)和token、id直接请求页面。也可能作为请求体或者cookie、请求头进行另一个接口请求(验证接口可能不止一个,甚至多级),请求成功返回一个成功的token(可能有时间效期,token可能只能用一次,也可能用多次)。携带token请求你要请求的页面,成功请求。
最好用seesion = requests.session()进行请求
接口不一定一次性返回 图片+id+token
识别后并不一定直接拿“缺口坐标”(轨迹)就能用,可能会进行加密
验证接口可能不止一个,甚至多级
1.坐标加密
目标网址:aHR0cHM6Ly96YnRiLmdkLmdvdi5jbi8jL2p5Z2c=
大于五页后都会有滑块验证码
抓包分析,图片接口
图片接口请求头字段加密字段
找加密位置非常简单,xhr跟栈就行了
回调第一个就是加密的位置
事实上是搜不到的,对字段进行打乱重组,但是跟栈也轻轻松松
这个值是要求的
求出来这种格式像什么
CryptoJS 中 WordArray 对象的内部格式,哈希加密,.toString()
测试,这个就是原型的SHA256加密,直接用原生库即可
内部进行字符串拼接
最后一个参数需要传验证码接口的请求体
输出为请求体的拼接
直接写死即可(这网站请求详情,则不能写死,因为页数变化,时间变化)
简简单单请求出参数。获取两张图片
在验证码接口一共四个参数有用到
两张图片
secretKey为密钥
token为验证接口携带
用ddddcor求出x距离
ocr = ddddocr.DdddOcr()
img_1 = base64.b64decode(response.json()['data']['repData']['jigsawImageBase64'])
img_2 = base64.b64decode(response.json()['data']['repData']['originalImageBase64'])
token = response.json()['data']['repData']['token']
secretKey = response.json()['data']['repData']['secretKey']
print(ocr.slide_match(img_1,img_2)['target'][0])
第二个接口check
直接搜,比较简单
这个X对于是距离。
进去NO函数。非常容易的AES加密
def aes_encrypted(w, L):data = json.dumps(w, separators=(',', ':')).encode('utf-8')key = L.encode('utf-8')[:16].ljust(16, b'\0')cipher = AES.new(key, AES.MODE_ECB)ct = cipher.encrypt(pad(data, AES.block_size))return base64.b64encode(ct).decode('ascii')
对坐标加密
有个非常巨大的坑,表面L已经写好了,其实传参L不是这个值
恶心。实际L值是第一个接口返回secretKey
最后成功获取token
在详情页携带token,就能顺利请求内容了。
事实上以上例子,直接请求验证码接口获得的token就可以请求页面了。不需要在进行下一个接口的请求,纯属练习。
2.轨迹加密
目标网址:aHR0cHM6Ly9janljLmhiYmlkZGluZy5jb20uY24vaHViZWl5dGgvanl4eC90cmFkZV9pbmZvci5odG1s
验证码接口直接请求即可,非常友好(后面你就知道有一个巨大的坑)
图片处理请求距离
img_1 = base64.b64decode(response.json()['captcha']['templateImage'].split('base64,')[1])
img_2 = base64.b64decode(response.json()['captcha']['backgroundImage'].split('base64,')[1])
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
print(x)
非常友好,瞬间出来
看看第二个验证接口
一眼看出,id是第一个接口请求的,data加密很明显是base64加密
响应cookies为第二个接口求这个值
base64解密看一下
解析一下
'bgImageWidth': 260, 'bgImageHeight': 159, 'sliderImageWidth': 49, 'sliderImageHeight': 159,这四个参数为两张验证码大小
"startSlidingTime":"2025-08-14T09:55:48.568Z","endSlidingTime":"2025-08-14T09:55:50.110Z",
startSlidingTime开始时间,endSlidingTime点击到验证码验证时间
trackList就是轨迹
一大串轨迹,跟栈调试一下
x代表移动距离,y代表上下多动,由x的变化看出,是先慢后快,在慢,t则是时间
轨迹生成的函数
def gen_track( gap_x, gap_y=0, seed=None):# ran_x = random.randint(19, 40)"""模拟轨迹生成生成「慢→快→慢」三段式轨迹gap_x : 缺口 x 像素gap_y : y 轴最大抖动像素seed : 随机种子,方便调试"""if seed:random.seed(seed)# 总步数 & 总耗时steps = random.randint(40, 60) # 步数少一点更平滑total_t = random.randint(2800, 3500) # 总耗时 2.8~3.5 strack = []x0, y0 = 0, 0t0 = 2383 # 起始时间戳gap_x = gap_xfor i in range(steps + 1):# 1. 三段式 S 曲线映射t = i / steps# 三次贝塞尔缓动:慢→快→慢ratio = 3 * t ** 2 - 2 * t ** 3if i != 0:# 2. 计算本次坐标x = int(round(x0 + gap_x * ratio)) + 1y = y0 + random.randint(-1, 1)if i == 0:x = 0y = 0# 3. 时间分布也按 S 曲线:开始稀疏、中间密集、末尾稀疏dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)t0 += dt# 4. 事件类型if i == 0:ev_type = "down"elif i == steps:ev_type = "up"else:ev_type = "move"track.append({"x": x, "y": y, "type": ev_type, "t": t0})# 提前到达终点就停if x >= gap_x:track[-1]['x'] = gap_xtrack[-1]['type'] = "up"breakreturn track
y上下抖动,X先慢后快在慢,t时间不能太快,传入x距离即可,t实际为毫秒,随机累加
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
在把时间整理一下
tart_iso = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
end_iso = (datetime.datetime.utcnow() +datetime.timedelta(milliseconds=track[-1]['t'])).isoformat(timespec='milliseconds') + 'Z'
全部求出来,再用base64编码
ef gen_track(gap_x, gap_y=0, seed=None):# ran_x = random.randint(19, 40)"""模拟轨迹生成生成「慢→快→慢」三段式轨迹gap_x : 缺口 x 像素gap_y : y 轴最大抖动像素seed : 随机种子,方便调试"""if seed:random.seed(seed)# 总步数 & 总耗时steps = random.randint(40, 60) # 步数少一点更平滑total_t = random.randint(2800, 3500) # 总耗时 2.8~3.5 strack = []x0, y0 = 0, 0t0 = 2383 # 起始时间戳gap_x = gap_xfor i in range(steps + 1):# 1. 三段式 S 曲线映射t = i / steps# 三次贝塞尔缓动:慢→快→慢ratio = 3 * t ** 2 - 2 * t ** 3if i != 0:# 2. 计算本次坐标x = int(round(x0 + gap_x * ratio)) + 1y = y0 + random.randint(-1, 1)if i == 0:x = 0y = 0# 3. 时间分布也按 S 曲线:开始稀疏、中间密集、末尾稀疏dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)t0 += dt# 4. 事件类型if i == 0:ev_type = "down"elif i == steps:ev_type = "up"else:ev_type = "move"track.append({"x": x, "y": y, "type": ev_type, "t": t0})# 提前到达终点就停if x >= gap_x:track[-1]['x'] = gap_xtrack[-1]['type'] = "up"breakreturn track
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
tart_iso = datetime.datetime.utcnow().isoformat(timespec='milliseconds') + 'Z'
end_iso = (datetime.datetime.utcnow() +datetime.timedelta(milliseconds=track[-1]['t'])).isoformat(timespec='milliseconds') + 'Z'
payload = {'bgImageWidth': 260,'bgImageHeight': 159,'sliderImageWidth': 49,'sliderImageHeight': 159,'startSlidingTime': tart_iso,'endSlidingTime': end_iso,'trackList': track
}
data = base64.b64encode(json.dumps(payload, separators=(',', ':')).encode()).decode()
url = "https://cjyc.hbbidding.com.cn/captcha/check2"
payload = {"id": id ,"data":data
}
response = requests.post(url, headers=headers, cookies=cookies, data=payload)
print(response.text)
最终结果请求失败,请求多次还是失败
我在想,这么完美的请求方式,为什么错了,最后调试好久,对比距离得出
最后x的距离是网页验证码的距离,不是实际下载图片的距离
实际下载图片这么大
最终的大小按页面大小算
所以还得把图片修改一下
用opencv,改一下图片大小,再识别距离
def _resize(b64: str, w: int, h: int) -> str:"""把 base64 图片缩放成指定宽高后再 base64 编码"""img = cv2.imdecode(np.frombuffer(base64.b64decode(b64), np.uint8), cv2.IMREAD_COLOR)img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)return base64.b64encode(cv2.imencode('.jpg', img)[1]).decode()
以下为核心代码。图片大小,轨迹生成
response = requests.get(url, headers=headers, cookies=cookies, params=params)
id = response.json()['id']def _resize(b64: str, w: int, h: int) -> str:"""把 base64 图片缩放成指定宽高后再 base64 编码"""img = cv2.imdecode(np.frombuffer(base64.b64decode(b64), np.uint8), cv2.IMREAD_COLOR)img = cv2.resize(img, (w, h), interpolation=cv2.INTER_AREA)return base64.b64encode(cv2.imencode('.jpg', img)[1]).decode()
slider_b64 = _resize(response.json()['captcha']['templateImage'].split(',', 1)[-1], 49, 159)
bg_b64 = _resize(response.json()['captcha']['backgroundImage'].split(',', 1)[-1], 260, 159)
target_bytes = base64.b64decode(slider_b64)
bg_bytes = base64.b64decode(bg_b64)
img_1 = base64.b64decode(slider_b64)
img_2 = base64.b64decode(bg_b64)
def gen_track(gap_x, gap_y=0, seed=None):# ran_x = random.randint(19, 40)"""模拟轨迹生成生成「慢→快→慢」三段式轨迹gap_x : 缺口 x 像素gap_y : y 轴最大抖动像素seed : 随机种子,方便调试"""if seed:random.seed(seed)# 总步数 & 总耗时steps = random.randint(40, 60) # 步数少一点更平滑total_t = random.randint(2800, 3500) # 总耗时 2.8~3.5 strack = []x0, y0 = 0, 0t0 = 2383 # 起始时间戳gap_x = gap_xfor i in range(steps + 1):# 1. 三段式 S 曲线映射t = i / steps# 三次贝塞尔缓动:慢→快→慢ratio = 3 * t ** 2 - 2 * t ** 3if i != 0:# 2. 计算本次坐标x = int(round(x0 + gap_x * ratio)) + 1y = y0 + random.randint(-1, 1)if i == 0:x = 0y = 0# 3. 时间分布也按 S 曲线:开始稀疏、中间密集、末尾稀疏dt = int(total_t * (0.8 + 1.2 * (1 - math.sin(math.pi * t))) / steps)t0 += dt# 4. 事件类型if i == 0:ev_type = "down"elif i == steps:ev_type = "up"else:ev_type = "move"track.append({"x": x, "y": y, "type": ev_type, "t": t0})# 提前到达终点就停if x >= gap_x:track[-1]['x'] = gap_xtrack[-1]['type'] = "up"breakreturn track
ocr = ddddocr.DdddOcr()
x = ocr.slide_match(img_1,img_2)['target'][0]
track = gen_track(x)
请求失败
多试几次就请求成功了。请求概率挺高的
请求成功