上一篇我们介绍了字符验证码在诞生之初面对图像识别算法时的情况,使用这种算法识别字符验证码的方式鲁棒性比较差,有可能验证码添加一种新的干扰或者底噪,识别算法就要进行大幅调整和更新,而且每次更新都需要手工分析。采用这种方式的攻击者在对抗中很难占到优势。 随着 AI 社区 CV (Computer Vision)方向的发展,CNN 在很多视觉相关的任务上的表现都取得了显著的进步,同时也在无形中扮演了字符验证码的「掘墓人」的角色。当下 CNN 的各种科研论文、开源项目、科普文章俯拾皆是,本文我们会把重点放在字符验证码的安全性是如何被 CNN 模型逐渐瓦解的。
卷积神经网络(Convolutional neural network),一种广泛应用于图像和视频识别、图像分类、自然语言处理等任务的神经网络。现在普遍认为生物学上关于哺乳动物大脑工作原理的研究对 CNN 的起源产生很大影响,其中最著名的是 Hubel 和 Wiesel 关于猫的视觉实验,这个成果也为他们赢得了 1981 年的诺贝尔奖。
早在 1980 年,日本学者 K Fukushima 就设计了一个 neocognitron 模型,现代大部分常用的 CNN 结构都已经在这个模型上得以体现[1]。之后 LeCun 先是把反向传播(Back Propagation)引入到类 neocognitron 模型的训练中,又在 1998 年提出了著名的 LeNet-5,并在 MNIST 识别任务上技压群雄[4]。但是后来 CNN 反而沉寂了 10 年的时间,直到 2012 年 ILSVRC(ImageNet Large Scale Visual Recognition Challenge)竞赛上,AlexNet 以仅 15.3% 错误率的绝对优势登顶,AI 社区又重新掀起了 CNN 等深度神经网络的研究热潮,仅每年 ILSVRC 竞赛的冠亚军,就诞生出 ResNet、GoogLeNet、VGG 这些早已写入教科书的模型结构。
左边是 neocognitron 内部层和层之间相互关联的示意图[1],右边是 LeNet-5 的结构图[4],可以看出两者之间的相似,或者说传承与发展
CNN 的关键结构包括用来提取空间特征的卷积层(Convolutional layer)以及用于减小特征尺寸的下采样(Down-sampling)层等。
卷积是一种数学运算,连续定义为
其中 f 和 g 都是在 R 上的可测函数,卷积操作就是把其中一个函数翻转之后与另一个函数的乘积的积分。
离散定义为
可以看做离散点上两个函数(其中一个翻转过)值的加权求和。在二维离散函数上就成了常见的CNN 卷积层计算原理:
严格按照卷积的定义,进行加权求和之前应该把函数 f 或者 g(这里分别是输入的矩阵和 filter)其中一个进行转置,但是实际 CNN 的卷积操作实现中没有这一步,因此卷积神经网络中进行的「卷积」运算绝大多数都是用「相关」(correlation)运算实现的,不过这并不影响什么。
用于快速减小 feature map 的尺寸,进而减少较深的 NN 中参数的数量以及降低计算消耗。一般最常用的是 max pooling 操作,就是取目标区域各自的最大值作为本层的输出:
一些 CNN 结构中也会使用 stride > 1 的 Conv Layer 去取代 max pooling,比如 GoogLeNet[3]。
上一期我们介绍了早在 2003 年就有研究人员利用图像识别算法对字符验证码进行识别并得到一个比较不错的识别效果。而 CNN 这个在 1998 年就提出并被实际应用于银行支票等图像识别场景的利器,自然也会被放到验证码的角斗场中。而在这两者之间的对抗中,字符验证码几乎从一开始就处于被碾压的状态。
Kumar Chellapilla 等在 2004 年就尝试使用基于[5]的 CNN 结构对当时实际使用中的几款字符验证码(包括我们的老朋友 EZ-Gimpy)进行识别,而且是不借助语言模型(字符验证码词库总量有限的特性)的实验,分别在 Mailblocks、Register、EZ-Gimpy、Ticketmaster、Yahoo version 2 和 GMail 这几款当时业界常用验证码数据集上达到 66.2%、47.8%、34.4%、4.9%、45.7% 以及 4.89% 的准确率[6]。
紧接着,Kumar Chellapilla 又在 2005 年利用 7 组对比实验,对人类和机器在指定条件(大写字母 A-Z 和数字 0-9、字体大小为 30、Times New Roman 字体等)下完成单个字符识别任务的能力进行了比对[7]。每组实验分别在字符图片中加入类型不同且程度递增的扭曲或干扰,人类组招募了 44 个视力(或者矫正视力)正常、年龄在 21-58 岁的同一家公司的职员在线完成实验;机器组采用基于[5]的 CNN 结构(在单独的训练集上分别训练后),用识别正确率作为对比指标。
7 组对比实验中,只有少数几组的结果上人类和机器的表现基本持平;其他实验中,随着干扰的升级,人类的准确率严重下降,而机器的准确率基本不受影响或影响较小。因此作者得出结论,“组成HIP(验证码的另一个名字,Human Interaction Proofs)的识别任务对机器更容易,对人更难”,同时建议字符验证码的安全强度应该更多依赖于字符分割(segmentation)的难度而不是字符识别的难度。实际上,这也是后来绝大多数主流字符验证码在设计上遵循的重要原则。
KC 对比实验中的第 4 组,左边是干扰难度递增的字符图片示例,右边是在不同难度下机器和人类参与者的识别准确率曲线对比,可以看出机器的识别效果基本一直都优于人类参与者。
05 年之后,KC 的实验结论得到学术、工业界的广泛认同,越来越多的验证码都在让字符难以被确定位置(segmentation resistance)这一方向上努力。比如 Microsoft CAPTCHA、Hotmail、reCAPTCHA 等。
微软的 MSN 验证码[14]
在这个趋势下,这些针对字符验证码的识别实验也基本都按照以下两个步骤的思路进行:
其中 segmentation 阶段的技巧非常多样,包括填色(color filling)、分析字符宽度(character width)和特征(character feature)等;而分离出的单个字符,就可以用 CNN 达到 95% 的识别率[7]。按照这个思路,J. Yan 及其团队从 2008 到 2013 年分别在Microsoft CAPTCHA、Megaupload、reCAPTCHA、Yahoo等数据集上达到 61%、78%、33%、36%-89% 的准确率[14-17]。
Megauplaod 验证码,在 segmentation resistance mechanism 的路上走得格外远的一位参赛选手 可以看到在这个时期,CNN 更多的是「打辅助」的角色。
2013 年的时候,Google 团队就宣布了 reCAPTCHA 的一个重要更新:基于高级的风险分析,reCAPTCHA 可以分别给人类用户和机器推送不同难度的字符图片[8]。一年之后他们的另一篇博客[9]再次强调了这一点,并引用了 Google 的一篇论文里的实验结果[10]来解释这样做的原因:
Goodfellow 的团队在研究利用深层卷积神经网络解决街景标牌上字符识别的问题时,设计了一个把定位、分割和识别三个步骤合为一体的端到端(End to end)模型,在 SVHN(Street View House Numbers) 数据集上达到 96%的准确率。
之后为了进一步测试模型在识别任意人工合成的干扰字符图像任务上的泛化效果,他们在 reCAPTCHA 字符验证码上进行实验,使用的模型深度甚至比在 SVHN 上的更浅(9 个卷积层,相比于 SVHN 数据集上实验效果最好的 11 个卷积层),而数据集是由 reCAPTCHA 题目中最难的那些图片组成的(论文没有具体介绍对于「最难」题目的选择标准,推测可能是之前的识别实验准确率最低,或是在实际生产中用户答错率最高的那部分题目图片)。
在这样的前提下,模型对图片文本的成功转写(transcribing)率(对图片字符的识别准确率)为 99.8%,远远优于人类在这个任务上的表现(基于以上对于数据集选取标准的推测)。因此无论是论文[10]还是 reCAPTCHA 团队的博文,都会强调 reCAPTCHA 已经不把用户输入的字符是否正确作为唯一区分人机的标准,而是采用分析用户交互行为等更高级的手段。
2018 年的 3 月 31 号,字符验证码 reCAPTCHA V1 终止了服务,现在大家熟知的是要求用户选择符合要求的图片的 V2 版本以及没有界面的 V3 版本。
本次实验随机生成 30000 张图片,有 29726 个标签(目录),因为有的标签生成了 2 张或者多张图片。以如下方式组织数据集:
CAPTCHA 库默认配置下生成的字符验证码风格如下:
可以看到里面包含了字符验证码常见的字符扭曲、重叠和点、线的干扰。
step2:加载数据集(原仓库作者自己写了一个 generator,这里为了简便,用 tensorflow.data.Dataset 等 API 实现):
def make_dataset(path, batch_size, n_label): def parse_image(filename): image = tf.io.read_file(filename) image = tf.image.decode_png(image, channels=3) image = tf.image.resize(image, [H, W]) image = tf.image.per_image_standardization(image) return image def configure_for_performance(ds): ds = ds.shuffle(buffer_size=1000) ds = ds.batch(batch_size) ds = ds.repeat() ds = ds.prefetch(buffer_size=tf.data.experimental.AUTOTUNE) return ds filenames = glob(path + '/*/*') random.shuffle(filenames) labels = [[tf.keras.utils.to_categorical(((ord(i)-48)%39), n_label) for i in filename.split("/")[-2]] for filename in filenames] filenames_ds = tf.data.Dataset.from_tensor_slices(filenames) images_ds = filenames_ds.map(parse_image, num_parallel_calls=tf.data.experimental.AUTOTUNE) labels_ds = tf.data.Dataset.from_tensor_slices(labels) ds = tf.data.Dataset.zip((images_ds, labels_ds)) ds = configure_for_performance(ds) return ds
篇幅所限,其余部分代码可以参考 tensorflow.data.Dataset 文档等资料。
step3:模型结构代码。模型只包括 3 个 2 维卷积层、3 个下采样层和两个全连接层,3.4M 个参数。相比于当下常用的 CNN 模型动辄上百层、参数上千万,这个结构是比较简洁和原始的。
input_layer = Input(shape=(H, W, C)) x = layers.Conv2D(32, 3, activation='relu')(input_layer) x = layers.MaxPooling2D((2, 2))(x) x = layers.Conv2D(64, 3, activation='relu')(x) x = layers.MaxPooling2D((2, 2))(x) x = layers.Conv2D(64, 3, activation='relu')(x) x = layers.MaxPooling2D((2, 2))(x) x = layers.Flatten()(x) x = layers.Dense(1024, activation='relu')(x) x = layers.Dense(D * N_LABELS, activation='softmax')(x) x = layers.Reshape((D, N_LABELS))(x) model = models.Model(inputs=input_layer, outputs=x) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics= ['accuracy'])
step4:训练模型:
ds = make_dataset("captcha_30000", batch_size, N_LABELS) val_ds = make_dataset("captcha_1000", batch_size, N_LABELS) history = model.fit(ds, steps_per_epoch=30000//batch_size, epochs=10, validation_data=val_ds, validation_steps=1000//batch_size )
训练的 batch_size 取 128,验证数据集使用单独生成的 1000 个样本,训练 10 个 epoch:
可以看到在第 5 个 epoch 左右的时候就已经收敛到接近 80% 的 Validation 准确率。
step5:验证一下实验结果
import numpy as np from matplotlib import pyplot generator = ImageCaptcha(width=100, height=60) chars_dict = "0123456789abcdefghijklmnopqrstuvwxyz" fig, axes = plt.subplots(3, 3, figsize=(10, 10)) for i in range(9): label = "".join(random.choices(chars_dict, k=4)) img = generator.generate_image(label) image = tf.keras.preprocessing.image.img_to_array(img) image = tf.image.per_image_standardization(image) pred = model.predict(tf.expand_dims(image, axis=0)) pred = np.argmax(pred,axis=-1) pred = "".join([chr(i+ord('0')) if i<10 else chr(i+ord('a')-10) for i in pred[0]]) ax = axes.flat[i] ax.imshow(img) ax.set_title('pred: {}'.format(pred)) ax.set_xlabel('true: {}'.format(label)) ax.set_xticks([]) ax.set_yticks([])
训练过程仅仅耗时 11 min(8C/32G/GTX1070),整个实验的核心代码(构建模型、加载数据、训练)不到 50 行(虽然用代码行数证明观点有些诡辩的味道)。
这里使用一个极简单的 CNN 结构(只有三个 Conv Layer,LeNet-5 都有两个),在 30K 数据上很快达到 80% 左右的准确率(Validation Accuracy),而通过多次试验以及仓库原作者的实验结果(原仓库代码的实验结果是针对 4 位数字字符验证码的,分别在 30241 和 302410 个样本的情况下达到 87.6% 和 98.8% 的准确率)可以发现模型的效果很大程度上依赖于数据集的数量,在一定范围内,训练可用的数据越多,模型效果越好;同时还可以发现随着数据量的增大,准确率的提升越来越慢,也就是到训练后期为了提高几个百分点的准确率,可能要增加近一倍的数据量。
虽然今天有各种各样的途径可以获取验证码图片这种常见的数据集,但是作为这个任务几乎仅剩的门槛,还是有很多成熟的方法可以应对数据量的问题,比如最常用的数据增广(Data Augmentation),也就是对现有的图片数据进行随机翻转,缩放,剪裁,调整对比度、饱和度等 CV 操作,从而达到增多训练数据集的效果。这里对原始实验代码进行简单修改:
def make_dataset(path, batch_size, n_label, is_training=False): def parse_image(filename): image = tf.io.read_file(filename) image = tf.image.decode_png(image, channels=3) image = tf.image.resize(image, [H, W]) image = tf.image.per_image_standardization(image) if is_training: image = tf.image.resize_with_crop_or_pad(image, H+10, W+10) image = tf.image.random_crop(image, (H, W, 3)) image = tf.image.random_brightness(image, 0.3) return image ... model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss='categorical_crossentropy', metrics= ['accuracy']) ... ds = make_dataset("captcha_10000", batch_size, N_LABELS, True) val_ds = make_dataset("captcha_1000", batch_size, N_LABELS) history = model.fit(ds, steps_per_epoch = 10000//batch_size, epochs = 50, validation_data = val_ds, validation_steps = 1000//batch_size )
只是对原训练数据集的样本进行了一个缩放剪裁和亮度的调整,作为示例。只使用 10K 图片,训练 50 个 epoch(这里调大 Adam 的学习率),可以在 28 个 epoch 左右达到接近 80% 的 ValAcc。图片增广方式的选取要根据实际需求,比如这个任务中随机翻转,不管是竖向还是横向,都不是一个好的选择。
加入两种简单的增广并调大 learning rate 之后,模型在只有原先数据集三分之一的数据上得到同样接近 80% 的验证准确率 。
通过图片增广,可以进一步降低字符验证码识别任务对数据集数量的要求,用有限的数据取得更好的效果。此外 keras.applications 模块 还提供很多成熟的 CNN 模型以及这些模型在 imagenet 上的学习结果,也就意味着如果采用迁移学习的方式,整个实验过程还可以进一步简化。
除[11]之外,GitHub 上还有很多结构更复杂、功能更强大,同时又被封装得更易于使用的开源项目,大多数都是这样的端到端模型,有的项目甚至还提供了有图形界面的 Win32 可执行程序,基本做到了开箱即用,把字符验证码的识别任务难度降低到几乎只需要考虑如何获取样本的程度。而在各种**平台泛滥的当下,这类样本数据的获取其实也没有很高的难度了。
综上所述,传统字符验证码在强大的CNN技术下已经毫无安全性可言,而且随着人工智能技术的普及,攻击的成本与门槛变得极为低下,因此可以说传统字符验证码的安全体系已经被全面瓦解。与这种局面恰好同时发生的是,互联网已经成为现代社会经济活动的最重要平台,线上资产呈现几何级数的增加。验证码作为保护线上资产的第一道关口,有着无可比拟的重要性,它的安全阵线的全面瓦解给各行各业企业的线上资产造成了巨大的风险,而且损失的金额逐年增加到数千亿的级别。
迫于强大的安全压力,传统字符验证码走进了死胡同,各种为了安全性提升的变形扭曲等手段不仅没有丝毫挽回一点局面,反而让用户的使用体验降低到难以忍受的境地。或许是由于 CAPTCHA 字符验证码的概念太深入人心,或许是面对强大的人工智能设计一款绝对通用的验证码真的很困难,全球范围内面临这个痛点多年来依然束手无策,直到一个来自中国武汉的极客团队创造性的提出了一个划时代的验证理念。他们看到人工智能的发展大趋势之下,必须非常创造性的真正利用人的生物行为特征,构建一个以人工智能为内核的验证码,才能做到持久智能的安全,而以这种理念打造的验证码被称为行为式验证。
接下来,请看下篇:「验证码与人工智能的激荡二十年:行为革命」
[1] Fukushima, K. . "Neocognitron: A self-organizing neural network model for a mechanism of pattern recognition unaffected by shift in position." Biological Cybernetics 36.4(1980):193-202.
[2] Wikipedia - Convolutional neural network
[3] Szegedy, C. , et al. "Going deeper with convolutions." 2015 IEEE Conference on Computer Vision and Pattern Recognition (CVPR) IEEE, 2015.
[4] Lecun, Y. , and L. Bottou . "Gradient-based learning applied to document recognition." Proceedings of the IEEE 86.11(1998):2278-2324.
[5] Simard, Patrice Yvon , D. Steinkraus , and J. C. Platt . "Best Practices for Convolutional Neural Networks Applied to Visual Document Analysis." 7th International Conference on Document Analysis and Recognition (ICDAR 2003), 2-Volume Set, 3-6 August 2003, Edinburgh, Scotland, UK IEEE Computer Society, 2003.
[6] Chellapilla, K. , and P. Y. Simard . "Using Machine Learning to Break Visual Human Interaction Proofs (HIPs)." DBLP DBLP, 2004:265--272.
[7] Larson, K. , et al. "Computers beat Humans at Single Character Recognition in Reading based Human Interaction Proofs (HIPs). " (2005).
[8] Google Security Blog, reCAPTCHA just got easier (but only if you’re human) by Vinay Shet, Product Manager, reCAPTCHA
[9] Google Security Blog, Street View and reCAPTCHA technology just got smarter by Vinay Shet, Product Manager, reCAPTCHA
[10] Goodfellow, I. J. , et al. "Multi-digit Number Recognition from Street View Imagery using Deep Convolutional Neural Networks." Computer Science (2013).
[11] GitHub - JackonYang/captcha-tensorflow
[12] GitHub - lepture/captcha
[13] pypi - captcha
[14] J. Yan and A. S. E. Ahmad, “A low-cost attack on a microsoft CAPTCHA,” in Proceedings of the 15th ACM conference on Computer and Communications Security, CCS’08, pp. 543–554, USA, October 2008.
[15] A. S. El Ahmad, J. Yan, and L. Marshall, “The robustness of a new CAPTCHA,” in Proceedings of the 3rd European Workshop on System Security, EUROSEC’10, pp. 36–41, April 2010.
[16] A. S. E. Ahmad, J. Yan, and M. Tayara, “The Robustness of Google CAPTCHAs,” Computing Science Technical Report CS- TR-1278, Newcastle University, 2011.
[17] H. Gao, W. Wang, J. Qi, X. Wang, X. Liu, and J. Yan, “The robustness of hollow CAPTCHAs,” in Proceedings of the ACM SIGSAC Conference on Computer and Communications Security, CCS 2013, pp. 1075–1085, November 2013.