我如何为我的大学网站开发验证码破解器

2025-05-26

我如何为我的大学网站开发验证码破解器

再次问好!

这篇文章算是我原文章的衍生作品。一些读者请求我解释一下我是如何开发这个解析器的,因此我决定与大家分享我的第一个(意义重大的?)项目的故事。
代码库链接

开始吧!

当我开发这套脚本时,我对图像处理及其使用的算法一无所知。我是在大一的时候开始做这件事的。

我开始时想到的基本想法是:

  • 图像基本上是一个矩阵,其中像素作为单独的单元。
  • 彩色图像的每个像素都有一个三元组(红色、绿色、蓝色)值,而灰度图像只有一个值,并且一般图像中每个像素值的范围是(0,255)

我所在大学的学生登录门户如下所示:
验证码

首先,我对该图像做了一些非常有用的观察。
验证码图片

  • 验证码的字符数始终为6个,并且是一张灰度图像。
  • 字符之间的间距看起来非常恒定
  • 每个角色都有完整的定义。
  • 图像中有许多散落的暗像素,并且有线条穿过图像。

所以我最终下载了一张这样的图像,并使用此工具以二进制形式(0 表示黑色,1 表示白色像素)对图像进行可视化。
验证码二进制

我的观察是正确的——图片尺寸为 45x180,每个字符占用 30 像素的空间,因此它们间距均匀。
于是,我得到了第一步,也就是

  • 将任何图像裁剪成 6 个不同的部分,每个部分的宽度为 30 像素。

我选择 Python 作为我的原型设计语言,因为它的库最容易使用和实现。
经过一番简单的搜索,我找到了PIL库。我决定使用Image模块,因为我的操作仅限于裁剪图像并将其加载为矩阵。
因此,根据文档,裁剪图像的语法是

from PIL import Image
image = Image.open("filename.xyz")
cropped_image = image.crop((left, upper, right, lower))
Enter fullscreen mode Exit fullscreen mode

就我的情况而言,如果你只想裁剪第一个字符,

from PIL import Image
image = Image.open("captcha.png").convert("L") # Grayscale conversion
cropped_image = image.crop((0, 0, 30, 45))
cropped_image.save("cropped_image.png")
Enter fullscreen mode Exit fullscreen mode

已保存的图像:
裁剪图像

我将其包装在一个循环中,编写了一个简单的脚本,从网站获取 500 个验证码图像,并将所有裁剪的字符保存到一个文件夹中。

第三个观察结果是:每个字符都清晰可见。
为了“清理”图像中被裁剪掉的字符(去除不必要的线条和点),我使用了以下方法。

  • 字符中的所有像素都是纯黑色(0)。我使用了一个简单的逻辑——如果不是全黑,就是白色。因此,对于每个值大于 0 的像素,将其重新赋值为 255。使用 load() 函数将图像转换为 45x180 的矩阵,然后进行处理。
pixel_matrix = cropped_image.load()
for col in range(0, cropped_image.height):
    for row in range(0, cropped_image.width):
        if pixel_matrix[row, col] != 0:
            pixel_matrix[row, col] = 255
image.save("thresholded_image.png")
Enter fullscreen mode Exit fullscreen mode

为了清晰起见,我将代码应用于原始图像。
原文: 修改后: 因此您可以看到,所有非全黑的像素都被移除了。这包括穿过图像的线条。 直到项目完成后,我才知道上述方法在图像处理中被称为阈值处理。
原来的

阈值满

继续第四个观察——图像中有很多杂散像素。
循环遍历图像矩阵,如果相邻像素为白色,且与该相邻像素相对的像素也为白色,且中心像素为暗色,则将中心像素设为白色。

for column in range(1, image.height - 1):
    for row in range(1, image.width - 1):
        if pixel_matrix[row, column] == 0 \
            and pixel_matrix[row, column - 1] == 255 and pixel_matrix[row, column + 1] == 255 :
            pixel_matrix[row, column] = 255
        if pixel_matrix[row, column] == 0 \
            and pixel_matrix[row - 1, column] == 255 and pixel_matrix[row + 1, column] == 255:
            pixel_matrix[row, column] = 255
Enter fullscreen mode Exit fullscreen mode

输出:
nostray_image

所以你看,图像已经被精简到只剩下单个字符了!虽然有些字符看起来好像丢失了基本像素,但它们仍然可以作为其他图像的良好骨架进行比较。毕竟,我们进行这么多修改的主要原因是为了为每个可能的字符生成合适的图像。

我将上述算法应用于所有裁剪后的字符,并将它们存储在一个新文件夹中。下一个任务是为每个属于“ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789”的字符至少命名一个样本。此步骤类似于“训练”步骤,我手动为每个字符选择一个字符图像并重命名。

完成此步骤后,我便获得了每个角色的骨架图像!
骨骼

我运行了其他几个脚本,以便在所有字符图像中找出最佳图像。例如,如果有 20 张“A”字符图像,那么暗像素数量最少的图像显然是噪点最少的图像,因此最适合用作骨架图像。因此,有两个脚本:

  • 一个用于按字符排序的相似图像(约束:暗像素数量,相似度> = 90-95%)
  • 一个可以从每个分组角色中获得最佳图像

至此,库图像已生成。将它们转换为像素矩阵,并将“位图”存储为 JSON 文件。

最后,这是解决任何新验证码图像的算法

  • 使用相同的算法减少新图像中不必要的噪音
  • 对于新验证码图像中的每个字符,我都会对其进行暴力破解,强制执行我生成的 JSON 位图。相似度是根据相应的暗像素匹配来计算的。
    • 这意味着,如果要破解验证码的图像中某个像素是暗的并且位于位置 (4, 8),并且如果该像素在我们的骨架图像/位图中的相同位置是暗的,则计数增加 1。
    • 该计数与骨架图像中的暗像素数量进行比较,用于计算匹配百分比。百分比以及计算该百分比的字符被推送到字典中。
  • 选择匹配率最高的字符。
    import json
    characters = "123456789abcdefghijklmnpqrstuvwxyz"
    captcha = ""
    with open("bitmaps.json", "r") as f:
        bitmap = json.load(f)

    for j in range(image.width/6, image.width + 1, image.width/6):
        character_image = image.crop((j - 30, 12, j, 44))
        character_matrix = character_image.load()
        matches = {}
        for char in characters:
            match = 0
            black = 0
            bitmap_matrix = bitmap[char]
            for y in range(0, 32):
                for x in range(0, 30):
                    if character_matrix[x, y] == bitmap_matrix[y][x] and bitmap_matrix[y][x] == 0:
                        match += 1
                    if bitmap_matrix[y][x] == 0:
                        black += 1
            perc = float(match) / float(black)
            matches.update({perc: char[0].upper()})
        try:
            captcha += matches[max(matches.keys())]
        except ValueError:
            print("failed captcha")
            captcha += "0"
    print captcha
Enter fullscreen mode Exit fullscreen mode

我们得到的最终结果是:
最终的

即 Z5M3MQ - 验证码已成功解决!

基本上就是这样。这是一次很棒的学习经历,我还用这个算法开发了一个Chrome 扩展程序,现在它有 1800 多个用户了!

期待您的意见和建议!
以上代码托管于此处

文章来源:https://dev.to/presto412/how-i-cracked-the-captcha-on-my-universitys-website-237j
PREV
全栈式、类型安全的 GraphQL 完整介绍(功能包括 Next.js、Nexus、Prisma) 全栈式、类型安全的 GraphQL 完整介绍(功能包括 Next.js、Nexus、Prisma)
NEXT
25 个使用 CSS Grid 的极其现代的布局✨