这篇文章是一些关于用初等数学的方法来使一些验证码无效的简单介绍。切入正题之前先八卦顺带科普一下。其实关于这方面的话题原来不是安全方面的内容,不过是由于验证码挡住了暴力破解的路子,所以我们要把这颗讨厌的石头给踢开。另外,文中提到的一些例子实际上是低估了算法的能力,这是人为造成的漏洞。同时我们可以借这个机会,看看当前在各个大网站里,部分做技术的人到底有什么样的水平,也算是提高一下自己的自信心。最后,关于这类东西,有些部分涉及到测试的问题,比如Model checking和自动化检测等,如果有朋友以后读这方面的研究生,做做这方面的工作也是挺有趣的,至少论文比较容易出。
小知识:本文所讲的“验证码”是指在网络上确定身份、防止暴力破解、DOS攻击的一种技术,解释起来比较麻烦,实际上上网的朋友都遇到过:动网最新论坛输入账户和密码后并不能直接登陆,还需要输入一串数字验证码;网上银行输入账户和密码也不能正确登陆,还需要输入另一串数字,而这些数字就是本文所讲的验证码中的一种!
在此郑重申明的是:作为作者的我不为这些东西负责,我把铁矿石炼成了钢片,你如果安上刀把用它去杀人,那可就是你自己的事情了!
废话到此结束,让我们进入正题!
简单应用
先说技术上实现起来很麻烦,但危害相对较小的一个应用。某国内著名门户网站的校友录(出于众所周知的原因,我这里只能用马赛克来代替这个网站的网址,想必大家平时也常去的),访问这个网站的时候,Cache里面就有验证图片,拷出来用就可以了。写文章的时候它已经作了简单的调整,不过没有关系,我们先看以前的,然后看看当前的情况。
在校友录留言的时候,一定要让你输入四个数字的验证码,说法是防止恶意灌水。在留言的页面查看源代码可以看到,图片是从这个地方得到的:
另外还有一个格式为Hidden的表项,里面是Key的值,和上面的Key值一样。到这里我们可以猜测一个什么结果呢?首先为了减轻服务器的负担,它没有为每个连接建立一个用于验证码的Session,而是在发表留言的时候从客户端得到用户的输入以及这个隐藏的项,通过一系列的算法来判定你的提交是否正确。这是很容易理解的,不过给人的感觉似乎不太可靠。
我们继续做实验。将上面的代码复制到一个新建的HTML文档中,直接用浏览器打开。几秒钟后出现在我们面前的是和上次看到的不同的图片,但是数字是一样的,多刷新几次看看,图片变了,但是图片上面的数字还是一样的!这是一个很重要的问题,也就是说Key的值与图片最后显示的数字有着一一对应关系,而图片本身无论怎么变,都不会影响到Key和图片上的数字。直观一些,我们用下面的示意图来说明:
pic(图片)----------> +------------------+
|| | Result(结果)|
Key(种子)---------> +------------------+
种子就是服务器指定的Key的值,在浏览器层面上我们看到的是图片PIC,在服务器上我们提交的验证码要与结果Result相比较。对于某校友录而言,它的工作方式可以看成从Key生成到Result,而用户是从PIC生成到Result。
脚本小子:这里其实是相当聪明的做法,由于计算机和算法能力的限制,模式识别和人工智能一直没有很大的突破性的进展,验证码就是有效的利用了计算机不能做到而人能够轻易作到的事情来分辨人与机器,尽管某校友录的技术或者管理人员并不知道个中原理和细节,但是他们还是聪明的用了这一种方法。
客观地说,有好多事情其实想法本身是很好的,不过在具体做的时候,却由于技术或者理解上的原因弄得很糟糕。空口无凭,让我们来具体分析一下,看看这个校友录的验证码到底强壮性如何。
首先是Key。Key和Result显然是一一对应的,校友录肯定有某种算法来完成Key到Result的函数映射,然而我们却不关心这种算法,也没有必要去关心,因为Key和Result居然是我们自己提交的。这个问题也可以这样来看,本来应该是服务器提出问题,然后我们回答,然后服务器再判断,而实际的情况却成了我们提出问题和答案,服务器来判断是否正确。这并不是一个正确的Challenge,某校友录的技术人员显然是没有这方面的常识,设想:如果我清楚了一个问题和它的正确答案,提交给你的时候,你除了回答正确还能做些什么呢?如果我每次都提交同样的这个问题和它的正确答案,我想除非服务器除了故障,否则回答都应该是“正确通过了验证码”——但是这种时候验证码还在起作用吗?根本就是一个摆设罢了!
第一种绕过的方法出来了,就是自己伪造经过确认的Key和Result。这里的“经过确认”有很多种方法,最简单的就是用眼睛看,看了后记录下来反复使用就可以了。
你说投机取巧?也许吧,如果有两条路子可以爬上山,你愿意坐索道还是愿意走楼梯?如果回答是后面的那个,OK,I服了U,有自虐倾向的朋友请继续往下看。
就算不用这种绕开的方法,还是可以从Key到Result的。当然不是直接去找寻算法,也不是构造一个足够大的数据库来供查询,而是直接可以从图片到结果——让程序来判定图片到底是什么数字。
这种做法看起来不太可能,因为验证码的一个首要条件就是:人能够很容易的识别,但是计算机却不能。这是一个基本的大前提,如果这个大前提都不成立,那么验证码根本就是一个笑话。但是事实总是很耐人寻味的,就像考研和蹦极一样,有些事情你不亲自去做做你是不会相信有那么简单的——如果你和我一样,也花上一个下午的时间去抓回来四百来个图片的Sample,你就不会再相信这种验证码牢不可破了。
有兴趣的话,你可以自己抓来看看,抛开背景图案上像芝麻一样的黑点(后面管这些叫背景噪音),对于同一个数字,比如2,你有没有看到有三种不同的写法?没有……确实没有,如果不注意观察,很有可能就被某校友录给蒙了。这里没有颜色的区别,没有字体的变形,也没有让人恼火的颜色渐变,就只有光秃秃的两种颜色——黑黑的字和白白的背景,以及不过两种不同大小的四个数字在上面。总共有多少种数字呢?从零到九一共十个数字,有两种字体再乘上二,看上去华丽的验证码不过是从二十个不同的数字中随机的选了四个而已。
不是还有背景噪音么?
确实,这些可爱的背景噪音们部分地挡住了我们的视线。如果是单纯的在一个二维平面上去寻找看是否存在某个图形,这是一个参加过校级程序设计大赛的中学生都能在半个小时内完成的简单题目。现在有了背景噪音,纯粹的匹配算法会不会失效呢?我们仔细地想想,如果这张图片已经充分地处理过并且存放在一个简单的二维数组里面,而且我们清楚地知道那二十个基本数字的形状,纯的匹配办法还是有价值的。一张图片上总是四个数字(后面还有详细说明),把这四个数字所在的部分大体上分离出来成为四个部分,每一部分依次与那二十个基本形状相比较,总会有一个完全能够地重合。这时候我们再引入背景噪音的概念,由于背景噪音总是和字符的颜色是相同的(其实是经验,肉眼没有能够分辨出来),所以待匹配(识别)的图像部分中,加入背景噪音后只可能比之前的黑点要多,这个基本不影响匹配的结果——如果我们用标准的二十个样本图案作为基准,发现在一次比较中,某些位置在样本中是字体而在待匹配(识别)的图像部分中是空白背景,这显然就不和当前比较的图案相同。
这是很容易理解的,比如我们拿着照片看人,照片是新照的,不过现实中的人脸上不巧被蚊子咬了一个疙瘩,我们这样比较依然是没有错的,因为我们以相片为基准,相片上有的两个眼睛一个鼻子一个嘴巴都和待辨别的人脸上的器官能够完全吻合,尽管多了一个蚊子疙瘩,我们还是有理由认为他就是照片上的人(多说一句,只满足了必要条件),但如果我看到的人是个三只眼睛的,显然能够判定不是。也就是说,加入噪音以后,匹配的图案只是经验上能肯定,但不匹配的图案是能准确判定的,去伪之后,自然是存真。对于这个的证明,离散数学中第一章有个析取三段论,有空可以翻翻,这里就不写了(脚本小子:记忆尤深的一章,想当初每次考试前都信誓旦旦的要复习,但书总是翻不过这一章。历次考试莫不如此……)。
有人可能还会抬杠,倘若你的数字是9,刚好背景噪音都集中在了左下角,变成一个8怎么办?对于这种情况,我首先应该表示遗憾,如果真的有这种情况发生,我建议你放下手上的杂志跑楼下转角处买体彩去。理论上来说发生这种事情的几率小于十万分之一,一旦发生,肉眼都不能正确识别(谁想得到呢?),我们不应该苛求电脑,毕竟前面说过,加了无法去掉的背景噪音以后,匹配只是满足了必要条件,要想充分满足,除非还有别的条件可以用。
真的有别的条件么,别的条件在什么地方?
有!在GIF文件里面。你也许不熟悉GIF文件,我们可以这样想象,不同的数字是可以对应相同颜色的,这个校友录上的GIF文件还原后的数字集合是怎么样的呢?来看一个例子(仅仅是片断)。
0100000000000000
0000000000000000
0000222222200000
0000220000000000
0000220000000000
0000220222001000
0000000000001000
脚本小子:这里简单的解释一下,GIF文件以LZW压缩算法把二维的图像进行压缩,原始图像上的每一个像素对应解压后的GIF数据中的一个数字(索引)。这个数字并不是原始图像的值,而是对应的一种颜色的序号,在还原的时候要通过这个值去查GIF文件头中定义的颜色(调色盘),然后再进行显示。打个比方,比如数字是1,查到的颜色是#CCCCCC#,那么这个地方应该显示灰色——查了好久才找到的东西,累个半死。
背景的索引是0,噪音的索引是1,而数字轮廓的索引是2,它们是分开的。
很难理解某校友录的工程师出于何种考虑进行了这样的GIF编码,这根本就是人为造成的一个缺陷,其结果是我们对GIF文件进行解码以后没有必要进行背景去噪这一个步骤,因此我们的识别程序是完美的(数学中叫条件充要)。
综合上面提到的各步分析,识别这个校友录程序的基本框架是:获取GIF图像,GIF图像解码,去掉解码后为1(背景噪音)的部分,划分成四个基本块,依次与二十个标准样本进行比较,最后获得结果。
到写完文章的时候再看这个校友录,情况已经发生了微小的变化。这个校友录在生成验证图片的时候,已经结合了登陆的Session,所以单纯静态地提交Key与验证码的值已经不能绕过验证。但是生成的图片过于简单的老问题依然存在,用正当的方法获取到验证码之后,用上面的代码还是可以自动识别。有一点需要注意:标准样本这个时候已经发生了一点小小的变化,数字样本有一半需要更新,这个工作是很简单的,三十分钟就可以完成——如果你知道我在讲什么而你又刚好明白它的危害的话!
我们来模拟一下这个校友录验证码的设计思路。我想他们的工程师是这样想的:首先随机生成一个Key,通过这个Key和一个初始化的种子Seed经过一系列的函数变换,生成一个四位数的验证码Result,这个Result再被变换成为中间的图像并随机加上背景噪音,最后再生成GIF图像。Result到GIF图像并不是一个(单值)函数映射,估计在这一步麻痹了这些工程师们,实际上这一步是不重要的,关键在于变换的算法,太简单就没意义了。
脚本嚣张:这里补充一句,由经验看来,Key的值每一次都是增长的,很有可能这个数字与点击率或者访问量相关,如果真是这样,预测性质的攻击也完全有可能——希望我们只是在杞人忧天。
OK,简单的说一下危害。这里这个地方危害很小,最多也就灌灌水什么的,或许有些别有用心的人会弄点“怎么利用网络赚大钱”等东西大量地往上面帖,但危害都不怎么大,就象黑防论坛长期都在删这样的帖子一样。
灌灌水你当然不会觉得有什么危害了,如果是电信或者银行也有这个问题你怎么办?
手机的密码都是6位的数字,银行的卡也就是6位的数字密码,如果让你在手机或者ATM上去暴力破解,首先没有这个硬件,其次速度太慢,毕竟一百万个可能性要想猜出来还是有点困难。现在网上有的地方允许你输入手机号码和密码就可以订制服务和发送短信,仅有的一点保护措施也就是很简单的验证码,网上银行也有类似的情况,这时候用电脑来跑密码的话,速度就比较可以接受了,一旦密码跑出来,可就不是灌灌水这样简单的事情。这种状况的安全,还真叫人不放心自己的票子。
某省移动的验证码自动识别程序也是个很搞笑的东西,从技术上来看,这个验证码到了能够做到的最简单的极致,除了不设验证码,没有比这个更为脆弱的保护机制了。结合这个东西,你可以做什么?像以前流行的破解300电话卡的方法一样,固定一个密码,然后穷举卡号(电话号码),假以时间,肯定是可以猜出些东西。这种被破解掉的厄运会不会降临到你我头上呢?神仙姐姐可能才知道。
还有,刚和某杀毒公司吵了一架的银行很安全么?看到这里,我想你自己会有一个判断。如果你感兴趣并且想具体看看那些验证码的话,可以参考一篇叫做《使用 HTTPS 编写客户端程序》的翻译文章,因为单纯用右键另存为无法把那些BMP图片保存下来。不过我建议你即使做出来了识别码的自动识别工具也不要妄图去暴力破解密码,原因不是说你跑不出来(我承认非常容易跑出密码来),而是你玩不过。作为炫耀的资本当然是可以的,在雷区以外,我一向都是很自由地释放自己的活力的,呵呵(脚本小子:这点是作者特别自负的地方,也是俺最深恶痛绝的地方,详细情况天知地知,唉……)。
同样的东西还出现在那些诸如网上卖卡片的网站,用“飞不起来的肥鸟”作标志的即时通讯软件的网站等等的上面,反正大部分都适用于那个简单的比较算法,就算是有点变形,也可以用比较初等的办法识别出来(限于篇幅,这里就不一一介绍了)。如果有时间,可以写一个比较通用的识别程序(外面也有,车牌识别的,不过是商业用途),只要给定一些图片并告诉程序是什么(作为训练集),以后就可以自己识别了,这样子比较方便。
最后谈谈自己的看法。用测试的眼光看,辅助验证无非就是给一个最后的机会,让你确认一下是否要提交请求,同时也给接受者一个保证——这确实是用户亲自提交的。功能上看来,辅助验证并不针对密码保护和防止暴力破解,作为潜在的功能可能也会有些效果,然而在运用中往往这种效果被夸大。对于防止暴力破解,很容易想到的一个保护方法是限定规定时间登陆次数,但这种工作在很多地方被一种畸形的辅助验证码给代替掉,结果导致了漏洞的产生。这种意义上的问题,在模型检测中似乎是不能自动检测出来的,或许,应该有另外技术来检测这种“夸大适用范围”或者叫做“误用”的错误?期待ing!