爬虫逆向学习(八):Canvas画图滑块验证码解决思路与绕过骚操作
此分享只用于学习用途,不作商业用途,若有冒犯,请联系处理
逆向站点
aHR0cHM6Ly93d3cuYm9odWF5aWNhaS5jbi8/VTU4Iy9jaGVtaWNhbC9sb2dpbj9yZWRpcmVjdD0lMkZjaGVtaWNhbA==
滑块验证码样式
滑块验证码研究
一般的滑块验证码都是会直接提供滑块和缺口背景图片,而这个站点只能直接拿到背景图,滑块和缺口都是用Canvas绘制出来的
接下来看看它怎么绘制的,我们点击刷新一次得到图片的调用堆栈,进入最顶层的栈,打上断点再刷新一次
这个时候不断执行跳出函数,大概7次,就能看到Canvas绘制的具体位置了
当然如果你对Canvas绘图了解的话,如果站点js没有混淆也是可以直接搜索drawImage
定位到绘制的代码位置的
到这里我们只要破解绘制的距离参数就能得到滑块具体了,不过这里先别急,我们先手动滑成功一次,它验证账号密码的接口是/api/igo-cloud-member/login/loginByCred
,但是看它的请求头和请求体都没有跟滑块验证码有关的地方,经过验证后发现这个站点只是在前端做了滑块验证码验证,实际的登录流程可以直接构造接口即可。
登录破解
/api/igo-cloud-member/login/loginByCred
接口只是对请求体进行加密,并没有其它强校验
这里我们添加xhr断点,看看这个接口发包前都做了啥
看调用堆栈,直接点击loginByUserName
栈,就可以看到入口了
添加断点重新登录一次就能看到加密操作了,也就是c.default.jiami
,这里说一下sequenceCode
,它是在接口/api/igo-cloud-member/login/getCodeByUserCode?userCode=
返回的result
值,然后经过与密码一样的加密操作后得到的
这里提供加密算法参考
def aes_encrypt(plaintext, aes_key):
cipher = Cipher(algorithms.AES(aes_key.encode()), modes.ECB(), backend=default_backend())
encryptor = cipher.encryptor()
padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_data = padder.update(plaintext.encode()) + padder.finalize()
encrypted = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(encrypted).decode()
附加分析
在不考虑绕过前端直接破解接口的方案下,我们怎么处理滑块呢,当然这里的前提使用模拟登录
方案
模拟登录的难点就是如何得到滑动距离,有思考过在打开站点页面前进行代码注入,不过没验证,下面提供的方案是通过cv2
包得到滑动距离
cv2讲解可以参考这篇文章:不想用selenium处理滑块验证码?教你用cv2解决
这里我们需要先拿到滑块图和缺口图,在我们点击登录时它是直接都显示的,如果直接保存是无法使用的,这时可以修改标签样式实现隐藏来截图
只要加上style="display: none;"
就能隐藏不必要的元素的,截完图后去掉它就恢复显示了
以playwright为例,screenshot
方法能实现标签元素截图保存,而page.evaluate('document.getElementsByClassName("block")[0].setAttribute("style","display=none;");')
能实现标签样式修改
得到后调用下方代码就能拿到滑块距离了。
def identify_gap_cut(bg, tp):
"""
三通道 RGB 滑块距离识别
:param bg: 背景图片
:param tp: 缺口图片
:return int
"""
# 读取背景图片和缺口图片
bg_img = get_cv2_img(bg) # 背景图片
tp_img = get_cv2_img(tp) # 缺口图片
if type(bg_img) == str or type(tp_img) == str:
print('图片格式存在问题,无法用cv2读取!')
return
# # 裁剪图片
# tp_height, tp_width, _ = tp_img.shape
# tp_start_row, tp_start_col = int(tp_height * 0.25), 0
# tp_end_row, tp_end_col = int(tp_height * 0.63), int(tp_width * 1)
# tp_img = tp_img[tp_start_row:tp_end_row, tp_start_col:tp_end_col]
# height, width, _ = bg_img.shape
# print(tp_height, tp_width)
# print(height, width)
# start_row, start_col = 0, int(tp_width)
# end_row, end_col = int(height), int(width)
# bg_img = bg_img[start_row:end_row, start_col:end_col]
# cv_show(bg_img)
# 识别图片边缘
bg_edge = cv2.Canny(bg_img, 100, 200)
tp_edge = cv2.Canny(tp_img, 100, 200)
# 转换图片格式
bg_pic = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
tp_pic = cv2.cvtColor(tp_edge, cv2.COLOR_GRAY2RGB)
# 缺口匹配
res = cv2.matchTemplate(bg_pic, tp_pic, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
print(min_val, max_val, min_loc, max_loc)
# 绘制方框
th, tw = tp_pic.shape[:2]
tl = max_loc # 左上角点的坐标
br = (tl[0] + tw, tl[1] + th) # 右下角点的坐标
cv2.rectangle(bg_img, tl, br, (0, 0, 255), 2) # 绘制矩形
cv2.imwrite('check_ground_map.jpeg', bg_img) # 保存在本地
# 返回缺口的X坐标
return max_loc[0]
def get_cv2_img(img_object, cv_type=None):
if type(img_object) == str:
cv_img = cv2.imread(img_object, cv_type) if cv_type else cv2.imread(img_object)
elif type(img_object) == bytes:
cv_img = np.frombuffer(img_object, np.uint8)
cv_img = cv2.imdecode(cv_img, cv_type) if cv_type else cv2.imdecode(cv_img, cv2.IMREAD_ANYCOLOR)
else:
cv_img = None
return cv_img