目录
- 案例一二三
- 四、验证码的逆向(⭐⭐⭐)
- 1、案例11_普通的滑块验证码
- 2、案例插曲_滑块验证码识别3种方法
- (1)滑块缺口颜色为单一颜色时用rgb判断
- (2)滑块缺口颜色非单一值用模板匹配
- (3)滑块带有缺口的图与原始图进行比较
- 3、案例12_jy的滑块验证码
- (1)大概的流程了解
- (2)乱序图片的还原逆向
- (3)案例分析思路
案例一二三
四、验证码的逆向(⭐⭐⭐)
1、案例11_普通的滑块验证码
- (1)案例网址:点击网址
- (2)案例反爬点:验证码缺口的识别,以及识别偏移量的js加密,cookie反爬
- (3)案例分析:cookie只需要首次请求时把set-cookie拿到即可,slidevalue是滑块缺口加密,由于校验图片时是xhr请求,所以我们这里直接通过监听xhr断点,然后追溯堆栈找加密位置
- 通过堆栈很快找到加密位置,和
encode64()
这个js函数相关,加密参数就是滑块距离,至此js逆向已完成,接下来我们只要识别滑块缺口就行
2、案例插曲_滑块验证码识别3种方法
- 滑块识别分析的3种方法:
滑块缺口是单一颜色用连续rgb像素点判断
通过模板匹配cv2.matchTemplate()来确定缺口位置
带有缺口的背景图和原始图进行对比
(1)滑块缺口颜色为单一颜色时用rgb判断
- 滑块缺口是单一的颜色,此处的白色rgb为固定值,所以我们只需要判断当前像素点连续39个点和对角线连续39个点的rgb为固定值,选择39个像素点是因为滑块长宽差不多是40
- 此处rgb固定值白色[245, 250, 255],即可定位到缺口x的位置 (
凸出部分朝向会影响实际x的识别距离
);当滑块凸出部分朝向左的时候,实际的缺口距离应当减去凸出来的部分,此处缺口应该是x-10 - 当然有的其它滑块,
初始距离可能有个6~10的间隙
,应当具体案例具体分析,此处初始距离为0,如果有间隙,我们识别出来的距离还应当减去初始间隙,如某验滑块 - 滑块识别代码截图
- 使用
PIL.Image.open
读取出来是RGB格式 - 通过
np.array(slider_image)
以3通道按行展开读取rgb数组 - 通过
np.all()
判断给定轴向上的所有元素是否都为True,如是否都为固定的rgb值 bulge_offset
为凸出的宽度, 默认凸出只有上,左,下 3个方向,如果有朝向为左,实际距离应当减去凸出部分
- 滑块识别代码截图,通过灰度后的判断,rgb不完全统一,但是灰度后的单一通道大于一个值
def get_slider_offset_method1(pic_content, fixed_rgb_l=250):
"""
返回缺口距离
:param pic_content: 滑块
:param fixed_rgb_l: 单一固定的rgb,转灰度后只有一个通道,需要观察,此种缺口非单一固定rgb,但是rgb转灰度后都大于250
:return:
"""
slider_image = Image.open(BytesIO(pic_content))
slider_image = slider_image.convert('L')
array_img = np.array(slider_image)
height, width = array_img.shape
for y in range(height - 40): # 从上往下
for x in range(40, width - 40): # 从左往右
if np.all(array_img[y:y + 1, x:x + 39] >= fixed_rgb_l): # 横向的连续39个元素点的rgb值大于250
list_side = [array_img[y, x]]
for side in range(1, 39):
list_side.append(array_img[y + side][x + side])
return x
return 0
(2)滑块缺口颜色非单一值用模板匹配
当缺口部分颜色非单一值时,我们可以用cv2.matchTemplate()模板匹配方法来进行缺口识别
,模板匹配的意思就是拿小图到大图里面去寻找和它差不多的地方,然后确定缺口- 通过cv2.matchTemplate()最后的效果如下,cv2.matchTemplata(img_big,img_temp,cv2.method): img_big:在该图上查找图像, img_temp:待查找的图像,模板图像, method: 模板匹配的方法
- 滑块识别代码截图
彩色图像
是三通道(RGB),也就是Red,Green,Blue 三个通道灰度图
是单通道,单通道中的每个像素的值介于0~255之间,代表黑色(0)到白色(255)之间的灰度程度黑白图像
指的是二值图,每个像素非黑即白,黑是0,白是1,通常会用灰度图来表示二值图,黑是0,白是255- 使用
cv2.imread(path, flag):读进来直接是BGR格式
,flag=0(8位深度,1通道,灰度图),flag不填默认3通道 - 使用
cv2.cvtColor(slider_img, cv2.COLOR_BGR2GRAY)
:是颜色空间转换函数,slider_img是需要转换的图片,cv2.COLOR_BGR2GRAY转成灰度格式 cv2.Canny(img, minVal, maxVal, apertureSize=3)
:第一个参数是原图像,第二个和第三个参数分别是两个阈值,minVal 和 maxVal,来确定真实和潜在的边缘,apertureSize是sobel算子(卷积核)大小;调整minVal,检测出的边缘会增多,扩大apertureSize算子,会获得更多的细节
,参考文档理解- 模板匹配
cv2.matchTemplate(background_pic, slider_pic, cv2.TM_CCOEFF_NORMED)
:此处通过归一化相关系数法进行匹配
(3)滑块带有缺口的图与原始图进行比较
- 如果你同时有带缺口的图片,和不带缺口的背景图,此时可以通过判断两张图片的像素点rgb之差,设定一个阈值差,如果在阈值差内认为图片像素点一致,如果超过这个阈值差则认为是缺口位置
- 具体代码实现:这里设置了阈值60为限制来判断缺口点,具体案例具体操作
from PIL import Image
def get_slider_offset_method3(pic_path, cut_pic_path, threshold=60):
"""比较两张图片的像素点RGB的绝对值是否小于阈值60,如果在阈值内则相同,反之不同"""
pic_img = Image.open(pic_path)
cut_img = Image.open(cut_pic_path)
width, height = pic_img.size
for x in range(40, width - 40): # 从左往右
for y in range(5, height - 10): # 从上往下
pixel1 = pic_img.load()[x, y]
pixel2 = cut_img.load()[x, y]
if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(pixel1[2] - pixel2[2]) < threshold:
continue
else:
return x
3、案例12_jy的滑块验证码
- 案例网址 :aHR0cHM6Ly93d3cuZ2VldGVzdC5jb20vc2hvdw==
(1)大概的流程了解
- 一般是两次get+ajax,第一次get+ajax获得challenge使其有效,第二次get+ajax即获取验证码与校验验证码
- 每个应用网站都会对应一个唯一的id值即gt,在获取验证码之前会初始化并校验获得第一个有效challenge,携带有效的challenge后可以获得验证码
- 验证码校验的过程会涉及到w参数,
w参数逆向是关键
,校验通过则返回validate值 - 整个流程主要涉及如下4个请求包,具体网站具体分析
- 第一个链接初始化获得challenge,gt值
- 第二个链接ajax.php校验challenge,此时出现一次w值(可置空,可固定,可能要js生成),具体网站具体分析,本次案例此处w值不研究,直接置空
- 第三个链接get.php携带有效challenge请求获得图片链接,该图片是个乱序图片,还需要还原;如果有返回challenge则取代前面的第一个challenge,同时请求图片的响应会返回c和s参数,在后面研究w参数加密会用到这两个
- 第四个链接ajax.php校验图片,携带challenge,w值,w值的加密会涉及到轨迹信息,轨迹是一难点,随意生成的轨迹并不可用,需尽量趋向人为的轨迹曲线,以及前面的c和s参数校验通过则返回validate
- 距离识别错误返回{“success”: 0, “message”: “fail”}
- 距离正确但轨迹异常则返回{“success”: 0, “message”: “forbidden”}
- 校验通过则返回{success: 1, message: “success”, validate: “f98af504ec3208dc19911b0de0b083c7”, score: “3”}
(2)乱序图片的还原逆向
- 从网页拿到的图片是乱序的,还原后如下
>>>>>>>> - 逆向分析:由于验证码图片是通过Canvas画布画出来的,所以我们这里直接通过监听Canvas下断点,然后就可以调试即可发现还原乱序代码的关键部分
-
getImageData(x,y,width,height)
:开始复制的左上角位置的 x 坐标、左上角位置的 y 坐标,width要复制的矩形区域的宽度、height要复制的矩形区域的高度 -
putImageData(imgData,x,y,dirtyX)
:将图像数据(从指定的 ImageData 对象)放回画布上,左上角位置的 x 坐标、左上角位置的 y 坐标, - 原图是260×160的图片,由乱序的2×26份的10×80小图片拼接而成,根据数组依次判断去乱序的图片里面去取这52份小图片然后依次拼接则形成原始图片
- 还原代码:按照如上还原逻辑,代码如下
-
Image.new(mode, (width, height))
:创建新图片 -
image.crop(x1, y1, x2, y2)
:左上角点到右下角点进行切割图片 image.paste(im, box)
:box为要粘贴到的区域,box类型为(x1, y1):将im左上角对齐(x1,y1)点,其余部分粘贴,超出部分抛弃
from PIL import Image
image = Image.open('./luanxu.png')
standard_img = Image.new("RGBA", (260, 160))
position = [39, 38, 48, 49, 41, 40, 46, 47, 35, 34, 50, 51, 33, 32, 28, 29, 27, 26, 36, 37, 31, 30, 44, 45, 43, 42, 12, 13, 23, 22, 14, 15, 21, 20, 8, 9, 25, 24, 6, 7, 3, 2, 0, 1, 11, 10, 4, 5, 19, 18, 16, 17]
s, u = 80, 10
for c in range(52):
a = position[c] % 26 * 12 + 1
b = s if position[c] > 25 else 0
im = image.crop(box=(a, b, a + 10, b + 80))
standard_img.paste(im, box=(c % 26 * 10, 80 if c > 25 else 0))
standard_img.save("./restore_bg.png")
(3)案例分析思路
- 目标研究参数,第4个链接的w参数,即校验图片的那个w参数,主要会涉及rsa+aes加密,aes的key参数是随机生成的,会通过rsa加密传给服务器
- 确定了目标研究参数w参数,可以采用调用栈回溯的方法,这里由于js文件做了一个unicode混淆字符串加密,所以我们直接通过搜索w的unicode码
"\u0077":
来寻找加密位置并打断点,w=_+s
,所以只要研究s和_
即可 - 我们先研究
s
参数,打入断点进入psgR这个函数 s
参数:发现最后返回的是r参数,而这个r参数的加密方式本质是个rsa加密,rsa加密需要两个参数:公钥+待加密文本,那只需要调试进去找到公钥即可(此处省略,自行调试)s
参数:那待加密文本即aes_key是怎么生成的,我们可以进入t[MIDa(753)]这个函数逐步看一下,一下就发现了加密逻辑,就是4个S4()函数组成s参数
:用python还原rsa加密,传入公钥和待加密文本aes_key即可
import rsa
from binascii import b2a_hex
def rsa_encrypt_text(key, _text: str):
"""
RSA加密
:param key: 公钥的参数
:param _text: 待加密的明文
:return: 加密后的数据
"""
e = int('010001', 16)
n = int(key, 16)
pub_key = rsa.PublicKey(e=e, n=n)
return b2a_hex(rsa.encrypt(_text.encode(), pub_key)).decode()
_
参数:_
参数和u
参数有关,先看u
参数,这个u参数分解下,传入了带加密文本o参数(传入前进行了字典转字符串操作即JSON.stringify())和前面随机生成的aes_key,其中这个o参数我们后面再分析_
参数的u
参数:深入u参数加密函数ee[rndd(365)]再具体分析,对aes的加密结果进行了一个转数组的操作_
参数的u
参数:继续深入看aes加密是CBC加密模式,所以只要3个参数:key,iv,待加密文本,key就是前面所说的aes_key,待加密文本是传进来的(后面具体分析),iv参数调试找到为"0000000000000000",js代码实现,那u参数已研究完_参数
:是对前面的u参数又做了一层加密,这个加密也比较好扣,省略w参数:w=_+s
,前面_和s参数合成即可,至此w参数加密已完成研究,细节部分继续往后看w参数>_参数>u参数>o参数分析
:可变参数分析,我们着重看下aa参数和userresponse参数
- aa:对轨迹和响应的c和s参数进行了加密
- ep:涉及v和tm,v是js文件的版本,tm和window[“performance”][“timing”]相关,可以根据Performance.timing 时间产生的先后顺序以及时间间隔,用当前的时间戳减去相应的值来模拟
- imgload:图片加载时间,随机生成
- passtime:轨迹滑动总时长
- rp:对gt、challenge、passtime进行了md5加密
- userresponse:对滑块距离和challenge进行了加密
- its5:随机参数影响不大
aa参数
:aa参数unicode码"\u0061\u0061"
,我们发现是上一个函数传过来的,也就是现在的t值
aa参数
:往前回溯看上一个函数,发现对应的是f值,也就是aa现在就是f值,只要看f值是怎么生成的,此处打断点,重新调试,可见f是一个函数的返回结果,传入的三个参数分别对应着:轨迹加密、响应的c、s参数aa参数
:r[LsRU(1063)][LsRU(1047)]()
就是对轨迹加密的,其中r[LsRU(1063)]就是传入的数组轨迹,进入SOQd就可以看具体的轨迹加密逻辑,扣完SOQd()这个函数,轨迹加密就有了-
aa参数的轨迹
: 轨迹生成传入,此处需注意你的轨迹是叠加传入的值,还是已经做了减法的值,这涉及到你要不要再加个e函数进行差减;轨迹生成可网上自行查找 - 如我传的xyt_list是这种格式(x和t已经做了差):[[28,34,0],[1,0,45],[3,0,63],[4,-1,8],[4,0,9],[3,0,7],[4,0,9]…
- 而非这种格式(x和t都是累加的):[[-31,-19,0],[0,0,0],[1,0,36],[2,0,44],[2,0,52],[4,0,61],[6,0,69],[8,0,77],[14,0,85],[17,0,92],[21,1,99],[25,2,106],…
userresponse
参数:U函数加密结果返回而得,传入滑块距离和challenge,去掉冗余代码,U函数可以直接扣出来rp
参数:对gt、challenge、passtime进行了md5加密- 最终效果