极验滑块验证码破解与研究(二):缺口图片还原
声明
原创文章,请勿转载!
本文内容仅限于安全研究,不公开具体源码。维护网络安全,人人有责。
本文关联文章超链接:
- 极验滑块验证码破解与研究(一):AST还原混淆JS
- 极验滑块验证码破解与研究(二):缺口图片还原
- 极验滑块验证码破解与研究(三):滑块缺口识别
- 极验滑块验证码破解与研究(四):滑块轨迹构造
- 极验滑块验证码破解与研究(五):请求分析及加密参数破解
一、缺口图片还原js分析
1. 为什么需要还原
网页打开的图片都是裁剪后重新拼接的乱序图片,所以需要将乱序图片还原
2. 本篇文章需要用到的小工具
2.1. reres插件
这个chrome插件超级好用哦,网页加载资源文件时,可以将其替换成本地的资源文件,js代码调试时非常方便。
3. 找到图片还原js入口函数
3.1. 通过观察html标签,发现图片是由canvas标签绘制的
3.2. 打开 极验滑块官方测试网页 分析js
因为图片是由canvas标签绘制的,首先想到在浏览器的开发者工具中,监听Canvas事件,当浏览器使用canvas绘制图片时,触发Canvas事件。刷新时,有没有发现断点处的上下文很熟悉的感觉
3.3. 背景图还原js源码分析
下面是小编从源码(slide.7.8.4.js)中抠出来的背景图还原函数:
function $e(e, t) {
var ylK = AaWgt.EeS()[28][34];
for (; ylK !== AaWgt.EeS()[28][32]; ) {
switch (ylK) {
case AaWgt.EeS()[4][34]:
e = e[XxZM(150)];
t = t[XxZM(150)];
var r = e[YcZy(509)];
var n = e[YcZy(549)];
var i = h[XxZM(124)](YcZy(106));
i[YcZy(509)] = r;
i[YcZy(549)] = n;
var o = i[YcZy(167)](YcZy(187));
ylK = AaWgt.EeS()[8][33];
break;
case AaWgt.EeS()[0][33]:
o[XxZM(684)](e, 0, 0);
var a = t[YcZy(167)](XxZM(187));
t[YcZy(549)] = n;
t[XxZM(509)] = Ne;
var s = n / 2;
var u = 10;
for (var c = 0; c < 52; c = c + 1) {
var _ = Ge[c] % 26 * 12 + 1;
var f = Ge[c] > 25 ? s : 0;
var l = o[XxZM(697)](_, f, u, s);
a[XxZM(621)](l, c % 26 * 10, c > 25 ? s : 0);
}
ylK = AaWgt.EeS()[0][32];
break;
}
}
}
为了下面分析方便,将混淆的js还原下,配置chrome插件reres,将网页加载的js文件替换成对应的反混淆后的js文件,还原方式请看上一篇文章 极验滑块验证码破解与研究(一):AST还原混淆JS ,配置如下图:
下面是还原后的js代码,自己加上了一些注释,有错误的地方还望指出:
function $e(e, t) {
// e为待还原的背景图对象
e = e["lKRC"];
// t为canvas图形容器对象
t = t["lKRC"];
// 将原图的宽度赋值给r,312
var r = e["width"];
// 将原图的高度赋值给n,160
var n = e["height"];
// h为document对象,i为canvas图形容器对象
var i = h["createElement"]("canvas");
// 定义canvas图形容器i的宽度为312
i["width"] = r;
// 定义canvas图形容器i的高度为160
i["height"] = n;
// o为图形容器i的二维绘图环境
var o = i["getContext"]("2d");
// 画布o上定位图像e
o["drawImage"](e, 0, 0);
// a为图形容器t的二维绘图环境
var a = t["getContext"]("2d");
// 定义canvas图形容器t的高度为160
t["height"] = n;
// 定义canvas图形容器t的宽度为260
t["width"] = Ne;
// s = 80
var s = n / 2;
var u = 10;
// Ge为数组对象,定值,其中为原图的还原顺序,共52个值,为0-51的整数。因此原图被切割成52块拼图,每块拼图大小为12*80。
// Ge = [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]
for (var c = 0; c < 52; c = c + 1) {
// 每个小块在原图宽度上的相对位置, 注意每个小块的间隔是12
var _ = Ge[c] % 26 * 12 + 1;
// 每个小块在原图高度上的相对位置
var f = Ge[c] > 25 ? s : 0;
// 从原图中裁剪出对应的小块, 注意裁剪图片的宽度是10
var l = o["getImageData"](_, f, u, s);
// 将裁剪的小块l, 画入二维绘图环境a
a["putImageData"](l, c % 26 * 10, c > 25 ? s : 0);
}
// 注意原图为312*160的图片分割成52个大小为12*80的小块,最后由52个10*80的小块拼接成260*160完整图
}
4. 背景图还原js改写成python语法
这个js逻辑比较简单,所以改写成python代码啦
# -*- coding: utf-8 -*-
import io
from pathlib import Path
from PIL import Image
def parse_bg_captcha(img, im_show=False, save_path=None):
"""
滑块乱序背景图还原
:param img: 图片路径str/图片路径Path对象/图片二进制
eg: 'assets/bg.webp'
Path('assets/bg.webp')
:param im_show: 是否显示还原结果, <type 'bool'>; default: False
:param save_path: 保存路径, <type 'str'>/<type 'Path'>; default: None
:return: 还原后背景图 RGB图片格式
"""
if isinstance(img, (str, Path)):
_img = Image.open(img)
elif isinstance(img, bytes):
_img = Image.open(io.BytesIO(img))
else:
raise ValueError(f'输入图片类型错误, 必须是<type str>/<type Path>/<type bytes>: {type(img)}')
# 图片还原顺序, 定值
_Ge = [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]
w_sep, h_sep = 10, 80
# 还原后的背景图
new_img = Image.new('RGB', (260, 160))
for idx in range(len(_Ge)):
x = _Ge[idx] % 26 * 12 + 1
y = h_sep if _Ge[idx] > 25 else 0
# 从背景图中裁剪出对应位置的小块
img_cut = _img.crop((x, y, x + w_sep, y + h_sep))
# 将小块拼接到新图中
new_x = idx % 26 * 10
new_y = h_sep if idx > 25 else 0
new_img.paste(img_cut, (new_x, new_y))
if im_show:
new_img.show()
if save_path is not None:
save_path = Path(save_path).resolve().__str__()
new_img.save(save_path)
return new_img
if __name__ == '__main__':
parse_bg_captcha("bg.webp", im_show=True, save_path='bg.jpg')