「Machine Learning 」机器学习验证码识别初探
in PENETRATIONPython with 0 comment

「Machine Learning 」机器学习验证码识别初探

in PENETRATIONPython with 0 comment

前言


在渗透测试后台口令猜解或其他应用中,验证码能有效阻断机器行为,给我们的渗透测试带来极大的不便,于是攻击验证码的一种方式:验证码识别技术便逐步有所需求。tesseract是一个流行的字符识别库。然而,其对于字符的标准程度依赖较高,识别率为%50-%70左右。为了提高验证码识别的识别率,采用机器学习的方式对某种特定的验证码进行大量数据训练,识别得到的模型即可满足要求。

环境


安装opencv

brew install opencv
opencv与虚拟环境virtulenv中的python3进行关联链接
sudo ln -s /usr/local/Cellar/opencv/3.4.2/lib/python3.7/site-packages/cv2.cpython-37m-darwin.so cv2.so

其他requirements

numpy
imutils
sklearn
tensorflow
keras

算法


机器学习算法的本质是找到一个目标函数 ( F ) ,使其成为输入变量 ( X ) 到输出变量 ( Y ) 直接的最佳映射:Y = F (X)

可以理解为,我们使用大量数据训练后的模型,就是我们追求的算法。上述的Y可以是一个连续的空间,也可以是一个离散的标签。

算法类型

在验证码识别中,我们可用的机器学习算法有:

  1. CNN卷积神经网络
  2. BP神经网络(负反馈神经网络)
  3. SVM支持向量机

其中卷积神经网络是图像处理领域最热门也是最有效的算法之一,BP神经网络可以看作是卷积神经网络发展的前身。而使用SVM支持向量机进行验证码识别就是将验证码通过一系列转化,成为数学上的表示,将一个识别问题看作为一个分类问题。

验证码识别


前置概念

Keras是一个高层神经网络API,Keras由纯Python编写而成并基于tensorflow,Theano以及CNTK后端。Keras为支持快速实验而生,能够把你的idea迅速转换为结果。
opencv是一个流行的计算机视觉和图像处理框架。它有一个Python的API,可以直接在API中去调用。
tensorflow:google开源的机器学习框架。

验证码切割

一般而言,验证码似乎都由几个字母或数字组成。如果存在某种办法去切分图像,然后对切分的部分进行训练和识别。能有效提高效率。在对图像的切割方式中,有以下几种常用的方式:
验证码切割:

  1. 等距切割法
  2. 垂直投影法
  3. 色块识别法
  4. 流水算法

详细的切割算法在后节进行详细说明。

流程

catpcha-buz.jpg

图像基本概念

图像在计算机中是一堆按顺序排列的数字,数值为0到255。有时候会将图像信息保存为一个很长的向量,但会失去图像的结构信息,为保留图像的结构信息。通常使用选择矩阵的形式:28x28的矩阵。
RGB颜色模型即红R(red),绿G(Green),蓝B(Blue)三原色的色光,三原色以不同的比例相加,形成不同的颜色。
在RGB颜色模型中,单个矩阵扩展为了有序排列的三个矩阵,即三维张量,其中每一个矩阵可称其为这个图片的一个channel.
在电脑中,一张图片存储为数字构成的长方体,可用宽width,高height,深depth描述。

画面不变性

在一个图像中,如果一个物体无论在图像的左右还是上下,都能被识别为一个物体,这个特效被称为不变性invariance.

张量

为了表述统一,张量可以看作是向量,矩阵的自然推广。0阶张量,即标量,是一个数。将一个数有序排列起来,就形成1阶张量,即向量。同样的,将一组向量有序排列起来,就形成2阶张量,即一个矩阵。
张量的阶数可以趁其为维度,或轴。如矩阵有两个维度。

前馈神经网络

前馈神经网络也是图像识别算法的一种,它的缺点是在图像识别中不具备图片不变性invaritance
简单地说,要训练前馈神经网络使其具有更高的准确性,必须在图像不同位置放置物体对其训练,效率可想而知。
catpcha-qiank.jpg

卷积神经网络

卷积神经网络解决了上述前馈神经网络中问题,即图像中的物体在不同位置训练结果是一致的。

验证码采集

验证码采集有两种方式:

  1. 使用脚本请求采集
  2. 对开源验证码库进行调用修改,自己生成
    脚本采集较为简单,获取图片验证码的链接利用脚本去批量刷新保存。

使用python脚本进行循环采集

# 验证码采集
def get_image(image_src,):
    """
    :param image_src 验证码URL:
    :return:
    """
    img= requests.get(image_src,stream=True)
    path = os.path.join(os.getcwd(),CAPTCHA_IMAGE_FOLDER)
    img_name = os.path.join(path, str(int(time.time()*1000000000))+".jpg")
    print(img_name)
    with open(img_name,"wb") as f:
        f.write(img.content)
        f.close()

而自己生成验证码只需要对相应的类库进行调用然后保存即可。如某些验证码会单独写为一个类,只需要将这个类拷贝下来在你的环境中实例化,并循环生成并保存即可。需要注意的是,在保存的过程中,其保存文件名最好与验证码内容一致,以便在进行训练数据时进行高精准分类。从而保证我们的训练数据是精确的。

验证码前置操作


在进行神经网络训练之前,需要准备训练数据,训练数据需要预处理。针对某种类型验证码,有两种方式进行处理:

  1. 获取验证码源代码,并将其改写,使其生成的文件名为正确图片文字内容,分类时可根据验证码文件名进行分类,达到分类%100正确
  2. 无法获取目标源代码,需要对验证码进行预分类,可使用tesserocr进行识别,识别准确率较低,需要人工纠错。

在进行上述操作之前,需要对验证码进行处理,如:

  1. 灰度化
  2. 二值化(非黑即白)
  3. 降噪

上述操作有暂时有两种方式:

  1. 使用Python的PIL库和公开算法进行处理
  2. 使用Python3+opencv进行处理

二值化

# 对图片进行二值化 使用PIL库
def thresh_image_pil(image_file):
    """
    对图片进行二值化(非黑即白)的PIL方式
    :param img:
    :param out:
    :return:
    """
    image = Image.open(image_file)
    # 灰度图
    lim = image.convert('L')
    # 灰度阈值设为165,低于这个值的点全部填白色
    threshold = 165
    table = []
    for j in range(256):
        if j < threshold:
            table.append(0)
        else:
            table.append(1)
    bim = lim.point(table, '1')
    return bim
def thresh_image_cv(image_file):
    """
    对图片二值化(非黑即白)的openv方式
    :param image_file:
    :return:
    """
    # 加载图片并进行灰度化
    image = cv2.imread(image_file)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 扩展边框,  ---可选
    # gray = cv2.copyMakeBorder(gray, 8, 8, 8, 8, cv2.BORDER_REPLICATE)

    # 图片二值化,将其转化为非黑即白
    thresh = cv2.threshold(gray.copy(), 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    return thresh

两种处理的结果如下:

  1. 原始图像:
  2. 使用PIL库进行二值化
  3. 使用CV进行二值化

对以上图片进行对比,发现CV库的处理更为优秀,进行降噪处理时我们就使用CV库的二值化验证码进行测试

降噪

在二值化之后需要对其进行降噪处理:

#推荐使用
def clearNoise(image_file):
    """
    传入一个二值化的图片并利用PIL库进行降噪,除去验证码背景的黑点
    :param image_file 二值化图片路径:
    :return:
    """
    im = Image.open(image_file)
    data = im.getdata()
    w, h = im.size
    black_point = 0

    for x in range(1, w - 1):
        for y in range(1, h - 1):
            mid_pixel = data[w * y + x]  # 中央像素点像素值
            if mid_pixel < 50:  # 找出上下左右四个方向像素点像素值
                top_pixel = data[w * (y - 1) + x]
                left_pixel = data[w * y + (x - 1)]
                down_pixel = data[w * (y + 1) + x]
                right_pixel = data[w * y + (x + 1)]

                # 判断上下左右的黑色像素点总个数
                if top_pixel < 10:
                    black_point += 1
                if left_pixel < 10:
                    black_point += 1
                if down_pixel < 10:
                    black_point += 1
                if right_pixel < 10:
                    black_point += 1
                if black_point < 1:
                    im.putpixel((x, y), 255)
                #print(black_point)
                black_point = 0
    for x in range(1, w - 1):
        for y in range(1, h - 1):
            if x < 2 or y < 2:
                im.putpixel((x - 1, y - 1), 255)
            if x > w - 3 or y > h - 3:
                im.putpixel((x + 1, y + 1), 255)

    return im

降噪处理后的图片:

进行对比一下:
分别是 原始图片-二值化图片-降噪图片

图像噪声处理的继续探究

opencv提供了四种技术去降噪等处理

对于以上降噪处理的不同使用场景可参考
https://blog.csdn.net/on2way/article/details/46828567

通过对验证码进行测试,发现使用opencv处理验证码中的“椒盐噪声”不是特别友好。

# 利用opencv中值滤波去除椒盐噪声
def clearNoiseCV(image):
   img = cv2.imread(image)
   #中值滤波
   img_medianblur = cv2.medianBlur(img, 3)
   return img_medianblur

原图:

中值滤波:

这里cv2.medianBlur的第二个参数发现只能采用奇数,采用1时没有明显的降噪效果,而使用3时的降噪效果过度。
猜测可能原因是验证码的”椒盐噪声“的点过大导致。
因此,这里继续使用PIL库及降噪算法进行降噪处理,舍弃opencv降噪方式。

腐蚀和膨胀

由于降噪处理后的验证码并不明显,还达不到我们的要求,一些噪点没有完全去除。另外验证码中的横线无法通过降噪去除,这时可以通过对图片进行先膨胀,然后进行腐蚀

# 腐蚀和膨胀,去除图片横线
def dilate_erod_image(image_file):
   """
   对图片进行腐蚀和膨胀,有效去除干扰线
   :param image_file:
   :return:
   """
   img = cv2.imread(image_file, -1)
   # 腐蚀和膨胀的参数设置,测试当参数为2,3时最佳
   kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,3))
   # 对图片进行腐蚀和膨胀
   dilated = cv2.dilate(img, kernel)
   eroded = cv2.erode(dilated, kernel)

   return eroded

降噪后的图片:

对降噪后的图片进行膨胀和腐蚀:

图片分割

在完成对图片的降噪去横线等处理后,需要将图片进行分割,即每个字母一个图片。
前面说到过,分割的方式有三种,这里使用opencv的blob块分割方式,简单方便:

需要注意的是cv2.findContours()函数接受的参数为二值图,即黑白的(不是灰度图),所以读取的图像要先转成灰度的,再转成二值图

 # 图片分割
def extractImg(transferedImg,path):
    # 加载图片并灰度化
    image = cv2.imread(transferedImg)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    # 添加边界
    gray = cv2.copyMakeBorder(gray, 8, 8, 8, 8, cv2.BORDER_REPLICATE)

    # 图片二值化
    thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    # 获取blob图片块
    contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # opencv版本适配
    contours = contours[0] if imutils.is_cv2() else contours[1]

    letter_image_regions = []

    # 对获取的blob进行循环读取
    for contour in contours:
        # 获取blob图片边界信息
        (x, y, w, h) = cv2.boundingRect(contour)

        # 处理字体相交的情况,若宽大于高的一背,则认为字体相交,平均切割即可
        if w / h > 1.25:
            # 平均切割
            half_width = int(w / 2)
            letter_image_regions.append((x, y, half_width, h))
            letter_image_regions.append((x + half_width, y, half_width, h))
        else:
            # 正常处理
            letter_image_regions.append((x, y, w, h))

    # 处理切割后的图片排序,确认从左向右排序
    letter_image_regions = sorted(letter_image_regions, key=lambda x: x[0])

    # 单独保存切割后的图片
    for letter_bounding_box in letter_image_regions:
        x, y, w, h = letter_bounding_box
        letter_image = gray[y - 2:y + h + 2, x - 2:x + w + 2]

        cv2.imwrite(path, letter_image)

切割前的图片:

切割后的图片(示例):
catpcha-extract.jpg

方式2:

# 图片切割,等距切割
def sliceImg(image, outDir, count = 4):
   img = Image.open(image)
   w, h = img.size
   eachWidth = int(w / count)
   for i in range(count):
       box = (i * eachWidth, 0, (i + 1) * eachWidth, h)
       img.crop(box).save(os.path.join(outDir, "{}.jpg").format(str(int(time.time()*1000000000))))

opencv与PIL格式的转化

在图片处理的过程中,有时候发现使用opencv更为优秀,而有时候则使用PIL,由于两种库所处理的图片格式不同,简单来说,就是使用PIL库打开的图片后续不能继续使用opencv去处理,造成了很大不便。
PIL转化为opencv格式

 cv_noise = noise.convert('RGB')
 cv_noise = np.array(cv_noise)
 # Convert RGB to BGR
 cv_noise = cv_noise[:, :, ::-1].copy()

预分类

在进行完一系列的图片处理之后,将图片分割为了一系列单个字符,接下来便是对这些单个图片进行分类,也就是上图中所示的每个图都对应一个文件夹,分类约准确,训练的模型识别率更高。
Python的pytesseract库可对图片进行预识别,识别的精度约为%50-%70,进行预识别之后需要进行人工纠错,以保证训练数据的准确性。

def ocr_for_Category(extracted_file):
    """
    对分割后的图片进行预识别和分类,预识别的准确率大致在%50~%60之间,预分类之后需要进行人工纠错
    :return:
    """
    # 验证码中一般不会出现`1`和`I`,因为易混淆
    catpcha_words = "ABCDEFGHJKLMNOPQRSTUVWXYZ234567890"
    """
    需要处理pytesseract不能识别或识别为空的情况,使用try...except包裹
    """
    try:
        img = Image.open(extracted_file)
        # pytesseract对分割后的图片进行识别,转化为字符
        code = pytesseract.image_to_string(img, config="-psm 10")
        print("code:", code)
        if code in catpcha_words:
            path = os.path.join(os.getcwd(), CATEGORY_FOLDER, code)
            if not os.path.exists(path):
                os.mkdir(path)
            file = os.path.join(path, "{}.jpg".format(str(int(time.time() * 1000000000))))
            img.save(file)
    except:
        pass

预分类并人工纠错之后的效果如图所示

catpcha-category.jpg

模型训练

我们的目标是去训练出一个模型,该模型能对某种类型的验证码进行准确识别,而不是类似OCR的通用识别方式。
同样的,模型训练时也会对图片进行一系列的处理。如:灰度值,二值化,切割等。
Keras底层使用之一为Tensorflow,采用的是CNN卷积神经网络的方式进行处理,其处理操作一般为:

输入->卷尺->池化->卷尺->池化->卷尺->池化->全联接->全联接->输出

作为一个安全人员,我们没有精力去较为深入地理解神经网络的底层详细原理,只需要了解一些基本的概念即可。
在这里,我们只需要知道,keras会依据我们分类好的训练数据,将数据处理为标签模型,即训练的输入为训练数据,输出为标签和模型,其中标签存储为model_labels.dat,模型存储为captcha_model.hdf5,后期进行识别时加载标签模型去处理。

LETTER_IMAGES_FOLDER = "category_images"
MODEL_FILENAME = "captcha_model.hdf5"
MODEL_LABELS_FILENAME = "model_labels.dat"


# initialize the data and labels
data = []
labels = []

# loop over the input images
for image_file in paths.list_images(LETTER_IMAGES_FOLDER):
   print(image_file)
   # Load the image and convert it to grayscale
   # 加载图片并转换为灰色
   image = cv2.imread(image_file)
   image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

   # Resize the letter so it fits in a 20x20 pixel box
   # 将切割后的图片重定义大小
   image = resize_to_fit(image, 20, 20)

   # Add a third channel dimension to the image to make Keras happy
   # 添加一个channel
   image = np.expand_dims(image, axis=2)

   # Grab the name of the letter based on the folder it was in
   label = image_file.split(os.path.sep)[-2]

   # Add the letter image and it's label to our training data
   data.append(image)
   labels.append(label)


# scale the raw pixel intensities to the range [0, 1] (this improves training)
data = np.array(data, dtype="float") / 255.0
labels = np.array(labels)

# Split the training data into separate train and test sets
(X_train, X_test, Y_train, Y_test) = train_test_split(data, labels, test_size=0.25, random_state=0)

# Convert the labels (letters) into one-hot encodings that Keras can work with
lb = LabelBinarizer().fit(Y_train)
Y_train = lb.transform(Y_train)
Y_test = lb.transform(Y_test)

# Save the mapping from labels to one-hot encodings.
# We'll need this later when we use the model to decode what it's predictions mean
with open(MODEL_LABELS_FILENAME, "wb") as f:
   pickle.dump(lb, f)

# Build the neural network!
# 创建一个Sequential模型
model = Sequential()

# First convolutional layer with max pooling
model.add(Conv2D(20, (5, 5), padding="same", input_shape=(20, 20, 1), activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

# Second convolutional layer with max pooling
model.add(Conv2D(50, (5, 5), padding="same", activation="relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))

# Hidden layer with 500 nodes
model.add(Flatten())
model.add(Dense(500, activation="relu"))

# Output layer with 32 nodes (one for each possible letter/number we predict)
model.add(Dense(32, activation="softmax"))

# Ask Keras to build the TensorFlow model behind the scenes
model.compile(loss="categorical_crossentropy", optimizer="adam", metrics=["accuracy"])

# Train the neural network
model.fit(X_train, Y_train, validation_data=(X_test, Y_test), batch_size=32, epochs=10, verbose=1)

# Save the trained model to disk
model.save(MODEL_FILENAME)

## 识别验证码
接下来只需加载模型,按照图片预处理的方式进行灰度、二值化、去干扰、切割,然后利用模型去识别即可。

MODEL_FILENAME = "captcha_model.hdf5"
MODEL_LABELS_FILENAME = "model_labels.dat"

CAPTCHA_IMAGE_FOLDER = "catpcha_images"
#ERODED_IMAGE_FOLDER = "eroded_images"



# Load up the model labels (so we can translate model predictions to actual letters)
# 反序列化标签
with open(MODEL_LABELS_FILENAME, "rb") as f:
   lb = pickle.load(f)

# 加载训练好的模型
model = load_model(MODEL_FILENAME)


# Grab some random CAPTCHA images to test against.
# In the real world, you'd replace this section with code to grab a real
# CAPTCHA image from a live website.
catpcha_file_images = list(paths.list_images(CAPTCHA_IMAGE_FOLDER))
catpcha_file_images = np.random.choice(catpcha_file_images, size=(10,), replace=False)


# loop over the image paths
for image_file in catpcha_file_images:
   #print(image_file)
   image = cv2.imread(image_file)

   # Load the image and convert it to grayscale
   image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

   # Add some extra padding around the image
   image = cv2.copyMakeBorder(image, 20, 20, 20, 20, cv2.BORDER_REPLICATE)

   # threshold the image (convert it to pure black and white)
   # 二值化,即将图片处理为非黑即白
   thresh = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

   # find the contours (continuous blobs of pixels) the image
   # 将图片继续进行块化
   contours = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

   # Hack for compatibility with different OpenCV versions
   # 匹配opencv版本
   contours = contours[0] if imutils.is_cv2() else contours[1]

   #cv2.imshow("Output", thresh)
   #cv2.waitKey()
   letter_image_regions = []

   # Now we can loop through each of the four contours and extract the letter
   # inside of each one
   for contour in contours[:4]:
       # Get the rectangle that contains the contour
       (x, y, w, h) = cv2.boundingRect(contour)

       # Compare the width and height of the contour to detect letters that
       # are conjoined into one chunk
       if w / h > 1.25:
           # This contour is too wide to be a single letter!
           # Split it in half into two letter regions!
           half_width = int(w / 2)
           letter_image_regions.append((x, y, half_width, h))
           letter_image_regions.append((x + half_width, y, half_width, h))
       else:
           # This is a normal letter by itself
           letter_image_regions.append((x, y, w, h))
   # If we found more or less than 4 letters in the captcha, our letter extraction
   # didn't work correcly. Skip the image instead of saving bad training data!
   if len(letter_image_regions) != 4:
       continue

   # Sort the detected letter images based on the x coordinate to make sure
   # we are processing them from left-to-right so we match the right image
   # with the right letter
   letter_image_regions = sorted(letter_image_regions, key=lambda x: x[0])

   # Create an output image and a list to hold our predicted letters
   output = cv2.merge([image] * 3)
   predictions = []

   # loop over the lektters
   for letter_bounding_box in letter_image_regions:
       # Grab the coordinates of the letter in the image
       x, y, w, h = letter_bounding_box

       # Extract the letter from the original image with a 2-pixel margin around the edge
       letter_image = image[y - 2:y + h + 2, x - 2:x + w + 2]

       # Re-size the letter image to 20x20 pixels to match training data
       letter_image = resize_to_fit(letter_image, 20, 20)

       # Turn the single image into a 4d list of images to make Keras happy
       letter_image = np.expand_dims(letter_image, axis=2)
       letter_image = np.expand_dims(letter_image, axis=0)

       # Ask the neural network to make a prediction
       prediction = model.predict(letter_image)

       # Convert the one-hot-encoded prediction back to a normal letter
       letter = lb.inverse_transform(prediction)[0]
       predictions.append(letter)

       # draw the prediction on the output image
       cv2.rectangle(output, (x - 2, y - 2), (x + w + 4, y + h + 4), (0, 255, 0), 1)
       cv2.putText(output, letter, (x - 5, y - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (0, 255, 0), 2)

   # Print the captcha's text
   captcha_text = "".join(predictions)
   print("CAPTCHA text is: {}".format(captcha_text))

   # Show the annotated image
   cv2.imshow("Output", output)
   cv2.waitKey()

效果如图:

catpcha-res.jpg

我将以上的代码push到github上,欢迎star
https://github.com/dr0op/catpcah_learning

总结

对于机器学习验证码识别的探究,从一个安全人员的角度,大致了解了机器学习在安全上的一部分利用。

REFERENCE

机器学习之验证码识别
https://blog.csdn.net/alis_xt/article/details/65627303
15分钟实战机器学习:验证码识别
https://blog.csdn.net/shebao3333/article/details/78808066
机器学习KNN归类算法实现验证码识别
https://blog.csdn.net/happengft/article/details/69943937
机器学习之简单识别验证码
https://www.iswin.org/2016/10/15/Simple-CAPTCHA-Recognition-with-Machine-Learning/
使用tensorflow自动化识别验证码(一)
https://xz.aliyun.com/t/1505
使用tensorflow自动化识别验证码(二)
https://xz.aliyun.com/t/1552
使用tensorflow自动化识别验证码(三)--CNN模型的基础知识概述及模型优化
https://xz.aliyun.com/t/1822
(reCATPCHA)一款识别图像验证码的burp suite插件
https://xz.aliyun.com/t/458/
理解深度学习中的卷积
http://www.hankcs.com/ml/understanding-the-convolution-in-deep-learning.html
卷积神经网络解释
https://www.zhihu.com/question/39022858
keras中文文档
https://keras-cn.readthedocs.io/en/latest/

Responses