前言
戳这里→康康你手机号在过多少网站注册过!!!
友情推荐:新一代安全短信
本文详细介绍了破解极验滑动验证码的所有过程,包括破解思路,实现步骤以及测试结果,相信你看完之后也能轻松破解滑动验证码;#短信防洪轰炸#
一丶解析验证码组成
从上面三张图来看,极验滑动拼图验证码是由一个小的拼图和一个大的背景图组成,拼图的形状各式各样,背景图中有一个阴影缺口,与拼图形状一致。
这里我们使用F12大法打开浏览器控制台,观察一下验证码的页面结构。
通过观察可以看到,验证码所包含的图片均以<canves>
画布的形式呈现在页面中,且有三张图片,且第三张图片被加上了属性style=“display: none;”,即为隐藏不显示。那么我们修改下页面代码,看下这张图究竟是什么。
修改完代码发现,这不就是完整的背景图嘛。那么根据上面的命名来看,基本可以确定这三张图分别是什么了。
-
第一张class为geetest_canvas_bg geetest_absolute,可以确定为带缺口的背景图。
-
第二张class为geetest_canvas_slice geetest_absolute,可以确定为拼图。
-
第三张便是完整的图片。
二丶分析出破解思路
- 首先根据这个验证码的组成,来分析一下我们人要做的事情:
按照正常的手动操作流程来看,我们需要看出背景图中与拼图对应的阴影缺口的位置,然后鼠标按住下方滑块来把拼图对正到缺口位置来完成验证。
- 然后根据人要做的事情,来分析一下程序要做的事情:
根据分析得出下面几个步骤:
1.获取到两张图片(带缺口背景图、完整背景图)
2.处理图片,得到阴影位置并计算滑动距离
3.根据滑动距离模拟滑动
三丶具体操作步骤
1丶获取到两张图片
由于这里的图片都是通过canvas
画布呈现的,我们可以通过执行js代码来生成图片。
可以参考《如何抓取canvas画布中的图片》。
2丶处理图片,计算滑动距离
通过第一步得到的两张图片可以看出,两张图有两处不同的地方,一处差异不大,一处差异较大,我们可以通过比较每一个像素点的差异度来确定阴影缺口的位置。缺口的位置横坐标减去小图距离边框的距离即为滑动距离。
以下是关键部分代码:
private final String INDEX_URL = "https://www.geetest.com/Register";
// 延时加载
private static WebElement waitWebElement(WebDriver driver, By by, int count) throws Exception {
WebElement webElement = null;
boolean isWait = false;
for (int k = 0; k < count; k++) {
try {
webElement = driver.findElement(by);
if (isWait)
System.out.println(" ok!");
return webElement;
} catch (org.openqa.selenium.NoSuchElementException ex) {
isWait = true;
if (k == 0)
System.out.print("waitWebElement(" + by.toString() + ")");
else
System.out.print(".");
Thread.sleep(50);
}
}
if (isWait)
System.out.println(" outTime!");
return null;
}
/**
* 计算需要平移的距离
*
* @param driver
* @param fullImgPath完整背景图片文件名
* @param bgImgPath含有缺口背景图片文件名
* @return
* @throws IOException
*/
public static int getMoveDistance(WebDriver driver, String fullImgPath, String bgImgPath) throws IOException {
File fullFile = new File(fullImgPath);
File bgFile = new File(bgImgPath);
try {
BufferedImage fullBI = ImageIO.read(fullFile);
BufferedImage bgBI = ImageIO.read(bgFile);
for (int i = 0; i < bgBI.getWidth(); i++) {
for (int j = 0; j < bgBI.getHeight(); j++) {
int[] fullRgb = new int[3];
fullRgb[0] = (fullBI.getRGB(i, j) & 0xff0000) >> 16;
fullRgb[1] = (fullBI.getRGB(i, j) & 0xff00) >> 8;
fullRgb[2] = (fullBI.getRGB(i, j) & 0xff);
int[] bgRgb = new int[3];
bgRgb[0] = (bgBI.getRGB(i, j) & 0xff0000) >> 16;
bgRgb[1] = (bgBI.getRGB(i, j) & 0xff00) >> 8;
bgRgb[2] = (bgBI.getRGB(i, j) & 0xff);
if (difference(fullRgb, bgRgb) > 255) {
return i;
}
}
}
} catch (Exception e) {
return 0;
} finally {
fullFile.delete();
bgFile.delete();
}
return 0;
}
private static int difference(int[] a, int[] b) {
return Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
}
/**
* // 执行 JS 代码并生成图片
*
* @param driver
* @param jsString
* @param input
* @return图片路径
*/
public static String getImgByJs(WebDriver driver, String jsString) {
try {
String imgFilePath = "c://GeeTest_" + System.currentTimeMillis() + "_" + (Math.random() * 9 + 1) * 100000 + ".jpg";
String imgInfo = ((JavascriptExecutor) driver).executeScript(jsString).toString();
if (imgInfo != null && imgInfo.contains("data")) {
imgInfo = imgInfo.substring(imgInfo.indexOf(",") + 1);
ByteArrayOutputStream outputStream = imgStrToFile(imgInfo);
if (outputStream != null) {
byte[] picBytes = outputStream.toByteArray();
outPicToFile(picBytes, imgFilePath);
return imgFilePath;
}
}
return null;
} catch (Exception e) {
return null;
}
}
/**
* 将base64字节码转byte输出流
*
* @param imgBase64Str
* @return
*/
private static ByteArrayOutputStream imgStrToFile(String imgBase64Str) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
if (imgBase64Str != null) {
BASE64Decoder decoder = new BASE64Decoder();
byte[] data = decoder.decodeBuffer(imgBase64Str);
outputStream.write(data);
outputStream.flush();
}
return outputStream;
} catch (Exception e) {
return null;
}
}
/**
* 图片流转图片
*
* @param o
* @param imgFilePath
*/
private static void outPicToFile(Object o, String imgFilePath) {
if (o == null)
return;
try {
if (o instanceof byte[]) { // 转为图片
if (((byte[]) o).length == 0)
return;
File imgFile = new File(imgFilePath);
// byte数组到图片
FileImageOutputStream imageOutput = new FileImageOutputStream(imgFile);
imageOutput.write((byte[]) o, 0, ((byte[]) o).length);
imageOutput.close();
} else {
return;
}
} catch (Exception e) {
}
}
/**
* 模拟人工移动
*
* @param driver
* @param element页面滑块
* @param distance需要移动距离
* @throws InterruptedException
*/
public static void move(WebDriver driver, WebElement element, int distance) throws InterruptedException {
int randomTime = 0;
if (distance > 90) {
randomTime = 250;
} else if (distance > 80 && distance <= 90) {
randomTime = 150;
}
List<Integer> track = getMoveTrack(distance - 2);
int moveY = 1;
try {
Actions actions = new Actions(driver);
actions.clickAndHold(element).perform();
Thread.sleep(200);
for (int i = 0; i < track.size(); i++) {
actions.moveByOffset(track.get(i), moveY).perform();
Thread.sleep(new Random().nextInt(300) + randomTime);
}
Thread.sleep(200);
actions.release(element).perform();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据距离获取滑动轨迹
*
* @param distance需要移动的距离
* @return
*/
public static List<Integer> getMoveTrack(int distance) {
List<Integer> track = new ArrayList<>();// 移动轨迹
Random random = new Random();
int current = 0;// 已经移动的距离
int mid = (int) distance * 4 / 5;// 减速阈值
int a = 0;
int move = 0;// 每次循环移动的距离
while (true) {
a = random.nextInt(10);
if (current <= mid) {
move += a;// 不断加速
} else {
move -= a;
}
if ((current + move) < distance) {
track.add(move);
} else {
track.add(distance - current);
break;
}
current += move;
}
return track;
}
private void seleniumTest() {
ChromeDriverManager manager = ChromeDriverManager.getInstance();
int status = -1;
String phone = "13814389438";
try {
WebDriver driver = manager.getDriver();
driver.get(INDEX_URL);
driver.manage().window().maximize(); // 设置浏览器窗口最大化
Thread.sleep(2000);
// 输入手机号
WebElement phoneElemet = waitWebElement(driver, By.xpath("//input[@placeholder='手机号码']"), 20);
phoneElemet.clear();
for (int i = 0; i < phone.length(); i++) {
char c = phone.charAt(i);
phoneElemet.sendKeys(c + "");
phoneElemet.click();
}
sleep(50);
// 点击获取验证码
waitWebElement(driver, By.className("sendCode"), 20).click();
sleep(2000);
// 完整背景图geetest_canvas_fullbg geetest_fade geetest_absolute
String fullImgJs = "return document.getElementsByClassName(\"geetest_canvas_fullbg geetest_fade geetest_absolute\")[0].toDataURL(\"image/png\");";
String fullImgPath = getImgByJs(driver, fullImgJs);
// 含有缺口背景图geetest_canvas_bg geetest_absolute
String bgImgJs = "return document.getElementsByClassName(\"geetest_canvas_bg geetest_absolute\")[0].toDataURL(\"image/png\");";
String bgImgPath = getImgByJs(driver, bgImgJs);
// 获取滑动按钮
WebElement moveElemet = waitWebElement(driver, By.className("geetest_slider_button"), 20);
// 获取滑动距离并删除图片
int distance = getMoveDistance(driver, fullImgPath, bgImgPath);
if (distance == 0) {
}
// 滑动
move(driver, moveElemet, distance - 6);
// 滑动结果
sleep(2 * 1000);
String gtInfo = waitWebElement(driver, By.className("sendCode"), 20).getAttribute("innerHTML");
System.out.println(gtInfo);
} catch (Exception e) {
e.printStackTrace();
} finally {
manager.closeDriver(status);
}
}
protected static void sleep(long time) {
try {
Thread.sleep(time);
} catch (InterruptedException e) {
}
}
3丶根据滑动距离模拟滑动
得到滑动距离之后,我们再来看下滑动轨迹,如果滑动轨迹过于规律,则很容易被识别。所以我们就将滑动轨迹贴近人类的正常操作轨迹即可。
比如:先快后慢,慢慢对准缺口。在缺口处左右晃动。在缺口处停留,欣赏成果等。
四丶结果展示
五丶结果分析
目标:
识别阴影位置,推算出对应滑动距离,模拟滑动。
实现思路:
1.获取到两张图片(完整图、缺口图)
2.处理图片,得到阴影位置并计算滑动距离
3.根据滑动距离模拟滑动
识别耗时:
15 - 50毫秒
通过率:
>95%
六丶结语
这篇文章到这里就结束了,感谢大佬们驻足观看,大佬们点个关注、点个赞呗~
谢谢大佬~
戳这里→康康你手机号在过多少网站注册过!!!
谷歌已经宣布退出图形验证码服务,为何国内各种奇葩验证方式层出不穷,安全性到底如何?
《腾讯防水墙滑动拼图验证码》
《百度旋转图片验证码》
《网易易盾滑动拼图验证码》
《顶象区域面积点选验证码》
《顶象滑动拼图验证码》
《极验滑动拼图验证码》
《使用深度学习来破解 captcha 验证码》