引言
1)验证码的基本知识及来由
网络安全技术中的验证码的主要目的是强制人机交互来抵御机器自动化攻击。用来防止机器模拟http行为,直接抓取文本进行导航;或直接提交文本进行登录尝试。在现在带宽较大的今天,在线密码穷举带宽已经不能作为瓶颈了,验证码识别以2M ADSL连接实际测试,20线程大概每秒可以完成30个左右的连接,如果是6位数字密码,在不考虑字典完全穷举的时候也只需要几个小时便可破解,严重的威胁了网络账号的安全,因此,网络登录注册验证码的普及势在必行。
而如今国内大部分的验证码设计者并不得要领,要么不了解图像处理、机器视觉、模式识别、人工智能的基本概念;要么设计出的验证码连人都难以识别,导致用户体验度下降。 比如工商银行的WAP手机银行验证码,只有4位,而且验证码的薄弱形同虚设,使用穷举计算机很快就能破解一个六位数字密码的账户。当然,也有设计得比较好的,比如Yahoo、Google、baidu等。
2)验证码的展望
未来的网络安全验证码,可能更多地使用渐进色层、同级灰度色差,更多曲线反转、随机字符数量、字符粘连等手段防止机器的识别,但加密与破解总是一对孪生兄弟,不可能消失一方的。我们只是希望通过本文,给编写验证码算法的人员一些建议,使我们的网络更安全,操作也不会因此而繁琐。
算法分析
在验证码处理方面,我们大概要涉及到如下内容:人工智能、模式识别、机器视觉、图像处理。
1) 主要流程:如果我们要从一幅图片中识别出验证码;又或者我们要从一幅图片中检测并识别出一个字符,其步骤可概括如下:
图像采集:取得一个验证码,就直接通过HTTP抓HTML,然后分析出图片的URL,下载保存。
预处理:检测是正确的图像格式,转换到合适的格式,压缩,剪切出ROI,去除噪音,灰度化,转换色彩空间。
检测:找出文字所在的主要区域。
前处理:文字的切割、缩放和扭曲校正。
训练:通过各种模式识别,机器学习算法,来挑选和训练合适数量的训练集。训练的样本并非越多越好。通过学习,泛化能力差的问题可能会出现在这里。
识别:输入待识别的处理后的图片,转换成分类器需要的输入格式,通过输出的类和置信度,来判断大概可能是哪个字母。识别本质上就是分类。
2)关键概念
图像处理:一般指针对数字图像的某种数学处理,比如投影、钝化、锐化、细化、边缘检测、二值化、压缩,以及各种数据变换等等。
二值化:一般图片都是彩色的,按照逼真程度,可能很多级别。为了降低计算复杂度,方便后续的处理,如果在不损失关键信息的情况下,能将图片处理成黑白两种颜色,那就最好不过了。
细化:找出图像的骨架,图像线条可能是很宽的,通过细化将宽度降为1,某些地方可能大于1。不同的细化算法,可能有不同的差异,比如是否更靠近线条中间,比如是否保持联通行等。
边缘检测:主要是理解边缘的概念。边缘实际上是图像中图像像素属性变化剧烈的地方,可以通过一个固定的门限值来判断,也可以是自适应的。门限可以是图像全局的,也可以是局部的。不能说哪个就一定好,不过大部分时候,自适应的局部的门限可能要好点。被分析的可能是颜色,也可能是灰度图像的灰度。
机器视觉:利用计算机来模式实现人的视觉,比如物体检测、定位、识别。按照对图像理解的层次的差别,分高阶和低阶的理解。
模式识别:对事物或者现象的某种表示方式(数值、文字,我们这里主要想说的是数值),通过一些处理和分析来描述、归类、理解、解释这些事物、现象及其某种抽象。
人工智能:这种概念比较宽,上面这些都属于人工智能这个大的方向。简单点不要过分学院派的理解就是,把人类的很“智能”的东西给模拟出来,协助生物的人来处理问题,特别是在计算机里面。
验证码识别原理及代码演示
本来拿一个银行网站来进行验证码解密是很危险的,但我们发现,工行的算法已经进行了改变,所以姑且以之前的工行WAP银行做个举例好了,同时也希望工行的加密能越做越好。
其实工行的WAP验证码是很简单的,是未加干扰的原始字符打印图片而已。针对这种验证码,我们将使用点阵库校验的方式进行,首先从整个程序的编写及操作顺序开始。
首先要知道我们需要取得的字的点阵有哪些。工行的WAP银行验证码只有0~9,10个数字,那么我们先将验证码图片下载到本机,这里我们必须将所有字符的图样都下载到本机,以便建立基础点阵库。
得到这些图片文件后,我们将用程序来获得图片点阵。从本地磁盘加载一个图像文件,这个文件是我们已经下载好的。首先应该让程序先将0~9的图像都“识别”一遍,使我们的程序“记住”它们的点阵,样例如图1所示。该图像包含的验证码,从左到右就是0123,将这个图像逐点转换灰度,也就是将彩色图片先进行灰度化、去色,变成黑白照片,便于下一步操作。
{ for (int i = 0; i < bmpobj.Height; i++)//遍历高度 { for (int j = 0; j < bmpobj.Width; j++) //遍历宽度,双层for就循环了整个图片的像素点 { int tmpValue = GetGrayNumColor(bmpobj.GetPixel(j, i)); bmpobj.SetPixel(j, i, Color.FromArgb(tmpValue, tmpValue, tmpValue)); } } }
灰度化之后,像素的RGB三色都是相同的值了,亮度从0~255(HxFF)。但用于识别程序,灰度值并不能很好的区分背景色和前景色,尤其是对于渐进的背景来说,所以我们还要将图像进一步处理,就是将灰度图片2值化,类似的算法还有分水岭算法等。因为本文中的验证码相对简单,故直接使用2值化转换,寻找有效区并转为单色黑白图。
{ int dgGrayValue = 128 //灰度背景分界值 int CharsCount = 4 //有效字符数,已知 int posx1 = bmpobj.Width; int posy1 = bmpobj.Height; int posx2 = 0; int posy2 = 0; for (int i = 0; i < bmpobj.Height; i++)//找有效区 { for (int j = 0; j < bmpobj.Width; j++) { int pixelValue = bmpobj.GetPixel(j, i).R; //取得红色值R,因为转成黑白图后,红、黄、蓝三位都是一样的值,所以这里取什么色值都是一样的 if (pixelValue < dgGrayValue) //根据灰度值 { if (posx1 > j) posx1 = j; if (posy1 > i) posy1 = i; if (posx2 < j) posx2 = j; if (posy2 < i) posy2 = i; } } } //确保能整除 int Span = CharsCount - (posx2 - posx1 + 1) % CharsCount; //可整除的差额数 if (Span < CharsCount) { int leftSpan = Span / 2; //分配到左边的空列,如span为单数,则右边比左边大1 if (posx1 > leftSpan) posx1 = posx1 - leftSpan; if (posx2 + Span - leftSpan < bmpobj.Width) posx2 = posx2 + Span - leftSpan; } //复制新图 Rectangle cloneRect = new Rectangle(posx1, posy1, posx2 - posx1 + 1, posy2 - posy1 + 1); bmpobj = bmpobj.Clone(cloneRect, bmpobj.PixelFormat); } Bitmap[] pics = GetSplitPics(4, 1); //分割,pics[0]中的图片如图2所示
图2
在平均分割图片的部分,设置水平上分割数为RowNum,垂直上分割数为ColNum,返回分割好的图片数组,程序编写如下:
public Bitmap[] GetSplitPics(int RowNum, int ColNum) { if (RowNum == 0 || ColNum == 0) return null; int singW = bmpobj.Width / RowNum; int singH = bmpobj.Height / ColNum; Bitmap[] PicArray = new Bitmap[RowNum * ColNum]; Rectangle cloneRect; for (int i = 0; i < ColNum; i++)//找有效区 { for (int j = 0; j < RowNum; j++) { cloneRect = new Rectangle(j * singW, i * singH, singW, singH); PicArray[i * RowNum + j] = bmpobj.Clone(cloneRect, bmpobj.PixelFormat);//复制小块图 } } return PicArray; }
此时图像分割已结束,pics 的长度应该是4,并且每一个pics就是一个验证码的位图,经过错误处理,修边,和去除无用背景空白,修正完的位图为数字0。
得到有效图形后,由外面传入该图形,设置灰度背景分界值为“dgGrayValue”,有效字符数为CharsCount,程序编写如下:
public Bitmap GetPicValidByValue(Bitmap singlepic, int dgGrayValue) { int posx1 = singlepic.Width; int posy1 = singlepic.Height; int posx2 = 0; int posy2 = 0; for (int i = 0; i < singlepic.Height; i++)//找有效区 { for (int j = 0; j < singlepic.Width; j++) { int pixelValue = singlepic.GetPixel(j, i).R; if (pixelValue < dgGrayValue) //根据灰度值 { if (posx1 > j) posx1 = j; if (posy1 > i) posy1 = i; if (posx2 < j) posx2 = j; if (posy2 < i) posy2 = i; }; }; }; //复制新图 Rectangle cloneRect = new Rectangle(posx1, posy1, posx2 - posx1 + 1, posy2 - posy1 + 1); return singlepic.Clone(cloneRect, singlepic.PixelFormat); }
至此,pics图像组中就是有效的点阵图了。下面我们把pics中的图形转换为代表点阵的字符串,返回灰度图片的点阵描述字串,1表示灰点,0表示背景。设置灰度图为singlepic,背前景灰色界限为dgGrayValue。
string code = GetSingleBmpCode(pics[0], 128); public string GetSingleBmpCode(Bitmap singlepic, int dgGrayValue) { Color piexl; StringBuilder sbCode = new StringBuilder(); for (int posy = 0; posy < singlepic.Height; posy++) for (int posx = 0; posx < singlepic.Width; posx++) { piexl = singlepic.GetPixel(posx, posy); if (piexl.R < dgGrayValue)// Color.Black ) sbCode.Append('1'); else sbCode.Append('0'); } return sbCode.ToString(); }
此时,code中的字符串就代表字符0在工行WAP银行上图像验证码的值了;以此类推,我们可以得到一个完整的,代表图像0~9的数组,字符表的顺序为0~9,A~Z,a~z。
现在,图片点阵数组已经取得了,接下来我们看看如何把一个图片识别出来吧!已知如下的点阵表:
string[] CodeArray = new string[] { "0011100011011011000111100011110101111010111100011110001101101100011100","001100011100111100001100001100001100001100001100001100111111","0111110110001100000110000110000110000110000110000110000011000111111111","0111110110001100000110000011001111000000110000011000001111000110111110","0000110000111000111100110110110011011111110000110000011000001100001111","00011111000110000001100000011000000111111000000010000000100000001001100000001111","001110011000110000110000111111110001110001110001110001011111","00011111000110000000000000000000000000011000001110000110100001101000011000000110","0111110110001111000111100011011111011000111100011110001111000110111110","0111110110001111000111100011011111100000110000011000001100001100111100" };
开始处理比较操作:
StringBuilder sbResult = new StringBuilder(); { for (int i = 0; i < 4; i++) { string code = GetSingleBmpCode(pics[i], 128); //得到代码串 System.Collections.Generic.Dictionary
至此,sbResult中的4个数字就是图像上的4个数字了。
结论
验证码识别肯定不只是这么简单,但现在还是有很多网站都在用这种未经任何变换的验证码,所以我们的网络安全还任重而道远。
上面的验证码识别是一个最基本的算法,但是很多扩展算法都可以基于上面的思路进行扩充。例如有些验证码进行了旋转输出,那么上面的程序可以在校对的时候,进行360度旋转,旋转后的图像再取得序列,再和图像序列比较,直至得到最符合的。有些验证码添加了边框,此时我们可以先去掉边框再进行切割匹配。
通过上面的算法可以得出,我们今后在设计验证码的时候,应该注意如下因素:
1)在噪音等类型的使用上,尽力让字符和用来混淆的前景和背景不容易区分,尽力让噪音长得和字母一样。
2)特别好的验证码的设计,要尽力发挥人类擅长而AI算法不擅长的。比如粘连字符的分割和手写体(通过印刷体做特别的变形也可以),而不要一味的去加一些看起来比较复杂的噪音或者其他的花哨东西。
3)从专业的机器视觉的角度来说,网络安全验证码的设计,一定要让破解者在识别阶段,反复在低阶视觉和高阶视觉之间多反复几次才能识别出来,这样可以大大降低破解难度和破解的准确率。