登录
原创

旋转图片验证码防御能力到底有多高、人机校验现巨大漏洞?

发布于 2020-11-01 阅读 90
  • 前端
  • 算法
  • 黑客
  • 安全
原创

旋转图片验证码,一个为防止爬虫攻击的行为验证产品。它是由最初的字符验证码演变而来,与其相似的产品还有滑动拼图,文字点选,以及刮涂层等产品。

这些产品有很多共同点,它们的目标都是为了防止爬虫攻击,防止一些恶意的程序来爬取网站数据,或者恶意注册等等,它们都统称为人机校验程序。

当然这些所谓的人机校验程序也就是反爬攻城狮们为了对抗爬虫攻城狮迫于无奈而想出的这些招数。对于爬虫攻城狮来讲也破解就是时间问题,而对于用户来将,这些产品反而是降低了用户体验。

那么问题来了,旋转图片验证码到底如何破解呢?

20200916173335112.png

且先看一下破解后的结果

20200916182900118.gif

实现原理分析:

首先说下图片库,经过不断抓取图片发现,这里的图片库基本上由几十张样本图,各自旋转360°的部分结果。那我们就 按照他们生成图片库的方法来生成样本数据,首先我们要通过某种方式来得到正确的样本数据,就是的到一张图和它对 应滑动的距离。这样就可以将这张图的360°样本图全部计算得出,从而得到模型、匹配库。当样本库与他们的图片库数量 趋于一致时,基本上就可以达到95%以上的识别率了。

那么就有了以下几个问题:

  1. 如何得到正确的样本数据?
  2. 如何生成360°各个角度对应的模型图?
  3. 有了模型库如何与遇到的图片进行匹配?

1.第一个问题:如何得到正确的样本数据?

目前还没有找到能够直接识别图片旋转角度的方法。所以我们就先通过计较笨的办法来得到样本数据。当然可以直接手动去滑动来得到样本数据和他的结果。这里比较懒,就写了一段程序去抓,毕竟手动去找样本数据还是很累的。既然来破解页面滑动那大家对selenium应该都很熟悉了,大多数人都用python,但是我这里用的是Java,其实大同小异了。这里就不放代码的,说下具体思路吧:

先给一个初始的距离,然后每次都从那个位置开始尝试,成功则记录结果,失败则先记下失败,下一次给一个偏动距离再次尝试,直达所有距 离都尝试过基本就有答案了,再没有就是这张图的问题了。本来想从左到右依次往上加的,但是 发现答案最多的地方基本都在正中间左右一点的位置,所以就改为从中间开始,向两边依次加减,左右轮回,这样更容 易得到正确结果。

20200921152321944.jpg?x-oss-process=style/thumb

这里补充一段计算图片唯一标识(Checksum)的Demo:

/**
	 * 计算Checksum
	 * 
	 * @param imgUrl
	 * @return imgChecksum
	 * @throws IOException
	 */
	public static byte[] getPic(String bgUrl) throws IOException {
		URL url = new URL(bgUrl);
		DataInputStream dataInputStream = null;
		try {
			dataInputStream = new DataInputStream(url.openStream());
			ByteArrayOutputStream output = new ByteArrayOutputStream();
			byte[] buffer = new byte[1024];
			int length;
			while ((length = dataInputStream.read(buffer)) > 0) {
				output.write(buffer, 0, length);
			}
			return (output != null && output.size() > 0) ? output.toByteArray() : null;
		} catch (SocketException | SocketTimeoutException | ConnectTimeoutException | NoHttpResponseException | UnknownHostException e) {
			System.out.println("getPic() Network exception ! ");
			return null;
		} catch (MalformedURLException e) {
			System.out.println("getPic() MalformedURLException  bgUrl= " + bgUrl);
			return null;
		} catch (IOException e) {
			System.out.println("getPic() IOException  ! ");
			return null;
		} catch (Exception e) {
			System.out.println("getPic() Exception  ! " + e.toString());
			return null;
		} finally {
			dataInputStream.close();
		}
	}

	public static String genChecksum(byte[] input) throws NoSuchAlgorithmException, IOException {
		MessageDigest messageDigest = MessageDigest.getInstance("MD5");
		messageDigest.update(input);
		byte[] digestBytes = messageDigest.digest();
		String ret = String.format(DatatypeConverter.printHexBinary(digestBytes).toLowerCase());
		return ret;
	}

2.第二个问题:如何生成360°各个角度对应的模型图?

这里我们对样本图片进行处理,得到旋转360°的结果。

样本图:
20200921161744799.jpg

生成模型图:

20200921161643652.png

实现代码:


	public static Color bgColor = new Color(255, 255, 255);

	/**
	 * 创建任意角度的旋转图像
	 * 
	 * @param image
	 * @param theta
	 * @param backgroundColor
	 * @return
	 */
	public BufferedImage rotateImage(BufferedImage image, double theta, Color backgroundColor) {
		int width = image.getWidth();
		int height = image.getHeight();
		double angle = theta * Math.PI / 180; // 度转弧度
		double[] xCoords = getX(width / 2, height / 2, angle);
		double[] yCoords = getY(width / 2, height / 2, angle);
		int WIDTH = (int) (xCoords[3] - xCoords[0]);
		int HEIGHT = (int) (yCoords[3] - yCoords[0]);
		BufferedImage resultImage = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
		for (int i = 0; i < WIDTH; i++) {
			for (int j = 0; j < HEIGHT; j++) {
				int x = i - WIDTH / 2;
				int y = HEIGHT / 2 - j;
				double radius = Math.sqrt(x * x + y * y);
				double angle1;
				if (y > 0) {
					angle1 = Math.acos(x / radius);
				} else {
					angle1 = 2 * Math.PI - Math.acos(x / radius);
				}
				x = (int) Math.round(radius * Math.cos(angle1 - angle));
				y = (int) Math.round(radius * Math.sin(angle1 - angle));
				if (x < (width / 2) & x > -(width / 2) & y < (height / 2) & y > -(height / 2)) {
					int rgb = image.getRGB((int) Math.round(x + width / 2), (int) Math.round(height / 2 - y));
					resultImage.setRGB(i, j, rgb);
				} else {
					resultImage.setRGB(i, j, -1);
				}
			}
		}
		return resultImage;
	}

	// 获取四个角点旋转后Y方向坐标
	private double[] getY(int i, int j, double angle) {
		double results[] = new double[4];
		double radius = Math.sqrt(i * i + j * j);
		double angle1 = Math.asin(j / radius);
		results[0] = radius * Math.sin(angle1 + angle);
		results[1] = radius * Math.sin(Math.PI - angle1 + angle);
		results[2] = -results[0];
		results[3] = -results[1];
		Arrays.sort(results);
		return results;
	}

	// 获取四个角点旋转后X方向坐标
	private double[] getX(int i, int j, double angle) {
		double results[] = new double[4];
		double radius = Math.sqrt(i * i + j * j);
		double angle1 = Math.acos(i / radius);
		results[0] = radius * Math.cos(angle1 + angle);
		results[1] = radius * Math.cos(Math.PI - angle1 + angle);
		results[2] = -results[0];
		results[3] = -results[1];
		Arrays.sort(results);
		return results;
	}

	public BufferedImage writeCyclePic(BufferedImage image) {
		BufferedImage newImage = new BufferedImage(350, 350, BufferedImage.TYPE_INT_BGR);
		try {
			int width = image.getWidth();
			int heigth = image.getHeight();
			double x0 = width / 2;
			double y0 = heigth / 2;
			int woffset = (width - 350) / 2;
			int hoffset = (heigth - 350) / 2;
			for (int i = woffset; i < 350 + woffset; i++) {
				for (int j = hoffset; j < 350 + hoffset; j++) {
					double r = Math.sqrt(Math.pow(Math.abs(i - x0), 2.0) + Math.pow(Math.abs(j - y0), 2.0));
					if (r > (x0 - woffset)) {
						newImage.setRGB(i - woffset, j - hoffset, -1);
					} else {
						newImage.setRGB(i - woffset, j - hoffset, image.getRGB(i, j));
					}
				}
			}
			return newImage;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}

	// 旋转生成图片
	public Map<String, Object[]> rotate360(File input, String outPath) {
		Map<String, Object[]> binMap = new LinkedHashMap<String, Object[]>();
		System.out.println("rotate360() start");
		System.out.print("distance list:");
		try {
			BufferedImage image = ImageIO.read(input);
			BufferedImage mid, result;
			File output = null;
			for (int i = 0; i < 360; i++) {
				mid = rotateImage(image, i, bgColor);
				result = writeCyclePic(mid);
				output = new File(outPath + i + ".jpg");
				if (!output.exists()) {
					output.mkdirs();
				}
				ImageIO.write(result, "jpg", output);
				System.out.print(output.getName() + "\n");
			}
			System.out.println("\n------> rotate() finish ");
			return binMap;
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}

3.第三个问题:有了模型库如何与遇到的图片进行匹配?

这里用到了相似度匹配的算法:

public final class PicFinger {
	/**
	 * 图像指纹的尺寸,将图像resize到指定的尺寸,来计算哈希数组
	 */
	private static final int HASH_SIZE = 16;
	/**
	 * 保存图像指纹的二值化矩阵
	 */
	private final byte[] binaryzationMatrix;

	public PicFinger(byte[] hashValue) {
		if (hashValue.length != HASH_SIZE * HASH_SIZE) {
			throw new IllegalArgumentException(String.format("length of hashValue must be %d", HASH_SIZE * HASH_SIZE));
		}
		this.binaryzationMatrix = hashValue;
	}

	public PicFinger(String hashValue) {
		this(toBytes(hashValue));
	}

	public PicFinger(BufferedImage src) {
		this(hashValue(src));
	}

	public byte[] getBinaryzationMatrix() {
		return binaryzationMatrix;
	}

	private static byte[] hashValue(BufferedImage src) {
		BufferedImage hashImage = resize(src, HASH_SIZE, HASH_SIZE);
		byte[] matrixGray = (byte[]) toGray(hashImage).getData().getDataElements(0, 0, HASH_SIZE, HASH_SIZE, null);
		return binaryzation(matrixGray);
	}

	/**
	 * 从压缩格式指纹创建{@link PicFinger}对象
	 *
	 * @param compactValue
	 * @return
	 */
	public static PicFinger createFromCompact(byte[] compactValue) {
		return new PicFinger(uncompact(compactValue));
	}

	public static boolean validHashValue(byte[] hashValue) {
		if (hashValue.length != HASH_SIZE) {
			return false;
		}
		for (byte b : hashValue) {
			{
				if (0 != b && 1 != b) {
					return false;
				}
			}
		}
		return true;
	}

	public static boolean validHashValue(String hashValue) {
		if (hashValue.length() != HASH_SIZE) {
			return false;
		}
		for (int i = 0; i < hashValue.length(); ++i) {
			if ('0' != hashValue.charAt(i) && '1' != hashValue.charAt(i)) {
				return false;
			}
		}
		return true;
	}

	public byte[] compact() {
		return compact(binaryzationMatrix);
	}

	/**
	 * 指纹数据按位压缩
	 *
	 * @param hashValue
	 * @return
	 */
	private static byte[] compact(byte[] hashValue) {
		byte[] result = new byte[(hashValue.length + 7) >> 3];
		byte b = 0;
		for (int i = 0; i < hashValue.length; ++i) {
			if (0 == (i & 7)) {
				b = 0;
			}
			if (1 == hashValue[i]) {
				b |= 1 << (i & 7);
			} else if (hashValue[i] != 0) {
				throw new IllegalArgumentException("invalid hashValue,every element must be 0 or 1");
			}
			if (7 == (i & 7) || i == hashValue.length - 1) {
				result[i >> 3] = b;
			}
		}
		return result;
	}

	/**
	 * 压缩格式的指纹解压缩
	 *
	 * @param compactValue
	 * @return
	 */
	private static byte[] uncompact(byte[] compactValue) {
		byte[] result = new byte[compactValue.length << 3];
		for (int i = 0; i < result.length; ++i) {
			if ((compactValue[i >> 3] & (1 << (i & 7))) == 0) {
				result[i] = 0;
			} else {
				result[i] = 1;
			}
		}
		return result;
	}

	/**
	 * 字符串类型的指纹数据转为字节数组
	 *
	 * @param hashValue
	 * @return
	 */
	private static byte[] toBytes(String hashValue) {
		hashValue = hashValue.replaceAll("\\s", "");
		byte[] result = new byte[hashValue.length()];
		for (int i = 0; i < result.length; ++i) {
			char c = hashValue.charAt(i);
			if ('0' == c) {
				result[i] = 0;
			} else if ('1' == c) {
				result[i] = 1;
			} else {
				throw new IllegalArgumentException("invalid hashValue String");
			}
		}
		return result;
	}

	/**
	 * 缩放图像到指定尺寸
	 *
	 * @param src
	 * @param width
	 * @param height
	 * @return
	 */
	private static BufferedImage resize(Image src, int width, int height) {
		BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
		Graphics g = result.getGraphics();
		try {
			g.drawImage(src.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
		} finally {
			g.dispose();
		}
		return result;
	}

	/**
	 * 计算均值
	 *
	 * @param src
	 * @return
	 */
	private static int mean(byte[] src) {
		long sum = 0;
		// 将数组元素转为无符号整数
		for (byte b : src) {
			sum += (long) b & 0xff;
		}
		return (int) (Math.round((float) sum / src.length));
	}

	/**
	 * 二值化处理
	 *
	 * @param src
	 * @return
	 */
	private static byte[] binaryzation(byte[] src) {
		byte[] dst = src.clone();
		int mean = mean(src);
		for (int i = 0; i < dst.length; ++i) {
			// 将数组元素转为无符号整数再比较
			dst[i] = (byte) (((int) dst[i] & 0xff) >= mean ? 1 : 0);
		}
		return dst;

	}

	/**
	 * 转灰度图像
	 *
	 * @param src
	 * @return
	 */
	private static BufferedImage toGray(BufferedImage src) {
		if (src.getType() == BufferedImage.TYPE_BYTE_GRAY) {
			return src;
		} else {
			// 图像转灰
			BufferedImage grayImage = new BufferedImage(src.getWidth(), src.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
			new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null).filter(src, grayImage);
			return grayImage;
		}
	}

	@Override
	public String toString() {
		return toString(true);
	}

	/**
	 * @param multiLine
	 *            是否分行
	 * @return
	 */
	public String toString(boolean multiLine) {
		StringBuffer buffer = new StringBuffer();
		int count = 0;
		for (byte b : this.binaryzationMatrix) {
			buffer.append(0 == b ? '0' : '1');
			if (multiLine && ++count % HASH_SIZE == 0) {
				buffer.append('\n');
			}
		}
		return buffer.toString();
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof PicFinger) {
			return Arrays.equals(this.binaryzationMatrix, ((PicFinger) obj).binaryzationMatrix);
		} else {
			return super.equals(obj);
		}
	}

	/**
	 * 与指定的压缩格式指纹比较相似度
	 *
	 * @param compactValue
	 * @return
	 * @see #compare(PicFinger)
	 */
	public float compareCompact(byte[] compactValue) {
		return compare(createFromCompact(compactValue));
	}

	/**
	 * @param hashValue
	 * @return
	 * @see #compare(PicFinger)
	 */
	public float compare(String hashValue) {
		return compare(new PicFinger(hashValue));
	}

	/**
	 * 与指定的指纹比较相似度
	 *
	 * @param hashValue
	 * @return
	 * @see #compare(PicFinger)
	 */
	public float compare(byte[] hashValue) {
		return compare(new PicFinger(hashValue));
	}

	/**
	 * 与指定图像比较相似度
	 *
	 * @param image2
	 * @return
	 * @see #compare(PicFinger)
	 */
	public float compare(BufferedImage image2) {
		return compare(new PicFinger(image2));
	}

	/**
	 * 比较指纹相似度
	 *
	 * @param src
	 * @return
	 * @see #compare(byte[], byte[])
	 */
	public float compare(PicFinger src) {
		if (src.binaryzationMatrix.length != this.binaryzationMatrix.length) {
			throw new IllegalArgumentException("length of hashValue is mismatch");
		}
		return compare(binaryzationMatrix, src.binaryzationMatrix);
	}

	/**
	 * 判断两个数组相似度,数组长度必须一致否则抛出异常
	 *
	 * @param f1
	 * @param f2
	 * @return 返回相似度(0.0 ~ 1.0)
	 */
	private static float compare(byte[] f1, byte[] f2) {
		if (f1.length != f2.length) {
			throw new IllegalArgumentException("mismatch FingerPrint length");
		}
		int sameCount = 0;
		for (int i = 0; i < f1.length; ++i) {
			{
				if (f1[i] == f2[i]) {
					++sameCount;
				}
			}
		}
		return (float) sameCount / f1.length;
	}

	public static float compareCompact(byte[] f1, byte[] f2) {
		return compare(uncompact(f1), uncompact(f2));
	}

	public static float compare(BufferedImage image1, BufferedImage image2) {
		return new PicFinger(image1).compare(new PicFinger(image2));
	}

	public static Map<String, byte[]> getLibMatrix(List<File> imgListLib) {
		Map<String, byte[]> binMap = new ConcurrentHashMap<String, byte[]>();
		BufferedImage libBuf = null;
		PicFinger fpLib = null;
		// 初始化Lib库
		String fileName = null;
		System.out.print("getLibMatrix() imgListLib size=" + imgListLib.size());
		int c = 0;
		for (File imgfileLib : imgListLib) {
			if (imgfileLib.exists()) {
				fileName = imgfileLib.getName();
				fileName = fileName.substring(0, fileName.indexOf("."));
				try {
					libBuf = ImageIO.read(imgfileLib);
				} catch (IOException e) {
					e.printStackTrace();
				}
				if (libBuf != null) {
					fpLib = new PicFinger(libBuf);
					binMap.put(fileName, fpLib.getBinaryzationMatrix());
					if (c % 100 == 0) {
						System.out.print("\ngetLib() list c=" + c + " ");
					}
					System.out.print(fileName + ",");
					c++;
				} else {
					System.out.println("libBuf=" + libBuf + "|imgfileLib=" + imgfileLib.getName());
				}
			} else {
				continue;
			}
		}
		System.out.println("\ngetLibMatrix() size=" + binMap.size());
		return binMap;
	}
}

好了,相信你看完以后都会觉得,旋转图片验证码防御能力其实也不是太高,那么人机校验到底能不能防御住爬虫攻城狮呢,相信你都已经有了一个答案。
本文内容仅供学习使用,如有侵权,请联系本人处理,谢谢。
—康康:1956507329

评论区

励志做一条安静的咸鱼,从此走上人生巅峰。

0

0

0