声明
本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!
本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!
前言
之前分析了某盾 Blackbox 的指纹算法, 这次再来做做它的验证码,该指纹算法在验证码参数里面也会有用到,
详细查看往期文章 :
【JS逆向百例】某盾 Blackbox 算法逆向分析:https://mp.weixin.qq.com/s/ueWVmlpLOljOLb1a7vEBag
逆向目标
目标:某盾 v2 滑动验证码
网站:aHR0cHM6Ly9sb2dpbi5kb3NzZW4uY29tL3Nzby9jaGVja0xvZ2lu
抓包分析
抓包分析,发现 图片接口 和 验证接口 是同一个接口只是请求参数不同:
需要分析的参数有 P1 ~ P9
看着多,我们慢慢来。
验证结果:
-
失败:
-
needValidateCode
:true -
继续返回图片接口信息
-
-
成功:
-
needValidateCode
:false -
返回
validateToken
-
逆向分析
我们先来看图片接口的 P1 ~ P9
生成,通过堆栈即可定位目标参数生成位置:
点击进入:
非常明显,我们依次对 P1 ~ P9
进行分析,其中 QQoooQ.blackBox
就是我们前言提到的 某盾 Blackbox 的指纹算法,也可暂时写死:
-
p1 = oQO0Q0
:oOoQQ0
为固定值:
“b37uCyfyme4S7TF/MVDRqSRxP4CB2BjsnDxr4bSxz0vSL/hXNGID9Tr7vzaBmF”
window._fmOpt.token
,搜索定位,由window._fmOpt.partner
、时间戳和随机数拼接而成:
-
window._fmOpt.partner
、window._fmOpt.appName
:不同网站的标识; -
oO0QQo.mfaId
:undefined
不用管。
-
p2 = OoOQ0O
:- 调试分析可知,由
QQoooQ.blackBox + ^^1^^1^^1
生成。
- 调试分析可知,由
-
p3 = oQOoO0(ooQQ0Q, QQQOQO)
:-
ooQQ0Q
,调试分析可知:var ooQQ0Q = QOOO0O(p1 + ‘^^’ + p2) + ‘^||^|’ +QOOO0O(oOOoQO)
其中
oOOoQO
,由固定值161155
拼接时间戳构成:“161155^||^1739762660066”
还差
QOOO0O
函数,我们进到该函数中去:常数特征很明显,我们也可以问下
deepseek
:验证之后为标准的
MD5
哈希算法。 -
QQQOQO
:var OOOQOQ = window._fmOpt.token.split(’-’);
var QoQQo0 = OOOQOQ[OOOQOQ.length - 2] + ‘-’ + OOOQOQ[OOOQOQ.length - 1];
var QQQOQO = QQ00QO(‘stq67pv9’) + QoQQo0.substring(10, 18);window._fmOpt.token
:p1
分析过;
QQ00QO('stq67pv9')
:生成固定值rsp67ou9
。-
oQOoO0
最后的加密函数,我们同样先跟进去观察,然后单步走,就定位到如下return
的位置,发现关键字AES
,iv
=Moa14C2uXpe8AUJ5
:跟我们分析某盾 Blackbox 的
DES3
加密,大差不差,需要自己处理一下:
from Crypto.Cipher import AES
import base64
def swap_characters(input_str):
return input_str.replace(‘q’, ‘tem1’).replace(‘p’, ‘q’).replace(‘tem1’, ‘p’).replace(‘I’, ‘tem2’).replace(‘J’,
‘I’).replace(
‘tem2’, ‘J’)
def encrypt_aes_cbc(data, key):
iv = ‘Mnz14C2tXod8AUJ5’
block_size = AES.block_size
pad = lambda s: s + (block_size - len(s) % block_size) * chr(block_size - len(s) % block_size)
data = pad(data)
cipher = AES.new(key.encode(‘latin-1’), AES.MODE_CBC, iv.encode(‘latin-1’))
encrypted = cipher.encrypt(data.encode(‘latin-1’))
return swap_characters(base64.b64encode(encrypted).decode(‘latin-1’).swapcase().replace(’+’, ‘~’))
print(encrypt_aes_cbc(‘d2f551c596fe634b5d7956250a1d5274^||^|e0315c2431445fccb801d5a1349aa50f’, ‘rsp67ou9626-390c’)) -
-
p4 = oQOoO0(QOo0Oo, QQQOQO)
:-
oQOoO0
:就是上面的AES
加密,iv
不变; -
QOo0Oo
:图片接口直接"|^^|^^|"
写死即可; -
QQQOQO
:同上,key
一致。
-
-
p5 = QoOO00
:QQ00QO('xfc')
:“web”,写死。
-
p6 = Qoo0Q0
:var QOQQO0 = o0QoQQ(8);
var Qoo0Q0 = oQOoO0(QOQQO0 + window.location.href, QQQOQO)o0QoQQ
函数进去调式发现,就是取几位随机数:
-
oQOoO0
:上面的AES
加密,iv
不变; -
window.location.href
:不同网站不同,写死; -
QQQOQO
:同上,key
一致。
-
p7 = QOQ0QQ + o0QoQQ(32)
:-
o0QoQQ(32)
:取 32 位随机数; -
QOQ0QQ = QOOO0O(Qoo0Q0) + QOOO0O(oOOoQO)
:QOOO0O
:标准MD5
加密;Qoo0Q0
:p6
值;oOOoQO
:上面分析过了。
-
-
p8 = QOQQO0
:QOQQO0 = o0QoQQ(8)
:取 8 位随机数,与p6
生成中的要一致。
-
p9 = oOOoQO
:-
oOOoQO = oQOoO0(oOOoQO, QQQOQO)
:oQOoO0
:上面的AES
加密,iv
不变;oOOoQO
:上面分析过了;QQQOQO
:同上,key
一致。
-
组包后,请求图片接口数据,发现大图是乱序的,需要还原:
通过加载的事件断点,即可定位到图片还原的代码:
整体逻辑就是按上下 2
层平均分割成 16
张小图,然后通过图片接口返回的 bgImageSplitSequence
参数,计算新的顺序,再进行排序拼接,转换为 python
代码如下:
from io import BytesIO
from PIL import Image
def reconstruct_image(segment_sequence, image_binary):
"""
重新构建图像,将输入图像拆分为8x2的网格并按照指定的顺序重新排列。
:param segment_sequence: bgImageSplitSequence 参数,16进制字符串列表,表示重新排列的顺序
:param image_binary: 二进制图像数据
:return: 重新排序后的图像二进制数据
"""
加载图像
img_io = BytesIO(image_binary)
original_img = Image.open(img_io)
定义图像尺寸和分割参数
img_width, img_height = 320, 180
segment_width, segment_height = img_width // 8, img_height // 2
拆分图像
image_layers = [{}, {}]
for layer in range(2):
y_start = layer * segment_height
for i in range(8):
x_start = i * segment_width
crop_box = (x_start, y_start, x_start + segment_width, y_start + segment_height)
image_layers[layer][i] = original_img.crop(crop_box)
创建新图像
new_image = Image.new(‘RGB’, (img_width, img_height))
new_image_layers = [{}, {}]
重新排序
for index, hex_value in enumerate(segment_sequence):
position = int(hex_value, 16)
layer, segment = divmod(position, 8)
original_layer = 1 if index >= 8 else 0
new_image_layers[layer][segment] = image_layers[original_layer][index % 8]
拼接图像
for layer in range(2):
for i in range(8):
new_image.paste(new_image_layers[layer][i], (segment_width * i, segment_height * layer))
转换为二进制数据
img_byte_arr = BytesIO()
new_image.save(img_byte_arr, format=‘PNG’)
return img_byte_arr.getvalue()
以及滑块识别代码:
import cv2
import numpy as np
def bytes_to_cv2(img):
""“
将二进制数据转换为 OpenCV 图像。
参数:
img (bytes): 读取的二进制图片数据。
返回:
numpy.ndarray: OpenCV 格式的 BGR 图像。
”""
将二进制数据转换为 NumPy 数组
img_buffer_np = np.frombuffer(img, dtype=np.uint8)
解码为 OpenCV 图像格式
img_np = cv2.imdecode(img_buffer_np, cv2.IMREAD_COLOR)
return img_np
def get_distance(bg, tp, save_path=None):
""“
计算滑块验证码缺口的位置,并在背景图上标记。
参数:
bg (bytes): 背景图片的二进制数据。
tp (bytes): 滑块图片的二进制数据。
save_path (str, 可选): 若提供路径,则保存标记后的图片。
返回:
dict: 缺口位置的坐标 {‘x’: x 坐标, ‘y’: y 坐标},若未找到则返回 None。
”""
将二进制数据转换为 OpenCV 图像
bg_img = bytes_to_cv2(bg)
tp_img = bytes_to_cv2(tp)
转换为灰度图,并进行高斯模糊,减少噪声影响
tp_gray = cv2.GaussianBlur(cv2.cvtColor(tp_img, cv2.COLOR_BGR2GRAY), (5, 5), 0)
bg_gray = cv2.GaussianBlur(cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY), (5, 5), 0)
使用 Canny 边缘检测提取图像特征
lower_threshold = 30 # 低阈值
high_threshold = 100 # 高阈值
tp_edge = cv2.Canny(tp_gray, lower_threshold, high_threshold)
bg_edge = cv2.Canny(bg_gray, lower_threshold, high_threshold)
使用模板匹配算法 (TM_CCORR_NORMED) 计算滑块与背景的最佳匹配位置
result = cv2.matchTemplate(bg_edge, tp_edge, cv2.TM_CCORR_NORMED)
获取匹配位置的最大值(即最匹配的点)
_, _, _, max_loc = cv2.minMaxLoc(result)
寻找滑块图像的轮廓
contours, _ = cv2.findContours(tp_edge, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if contours:
选择面积最大的轮廓
contour = max(contours, key=cv2.contourArea)
获取该轮廓的边界框
x, y, width, height = cv2.boundingRect(contour)
在背景图上绘制矩形标记滑块缺口位置
cv2.rectangle(bg_img,
(max_loc[0] + x, max_loc[1] + y),
(max_loc[0] + x + width, max_loc[1] + y + height),
(0, 255, 0), 2) # 绿色矩形框,线宽 2
如果提供了保存路径,则保存标记后的图片
if save_path:
cv2.imwrite(save_path, bg_img)
返回缺口的 x, y 坐标
return {‘x’: max_loc[0] + x, ‘y’: max_loc[1] + y}
else:
return None # 未找到匹配的缺口
最后再来看验证接口的 P1 ~ P9
生成,我们只讲不同的地方:
-
p2
,由QQoooQ.blackBox
+'^^3^^1^^1'
生成; -
p3
,加密的明文多了:-
validateCodeObj
:图片接口返回的validateCodeObj
对象; -
userAnswer
:userAnswer = Math.round(QoO0Oo / Oo0OOo) + QQ00QO(’|10|’) + new Date().getTime()
Math.round(QoO0Oo / Oo0OOo)
:滑块识别的距离;QQ00QO('|10|')
:固定值"|10|"
。
-
-
p4
:加密的明文多了个mouseInfo
轨迹信息,经过测试写死即可。
纯 Python
算法的源码,会分享到知识星球当中,需要的小伙伴自取,仅供学习交流。
结果验证
共同学习,写下你的评论
评论加载中...
作者其他优质文章