原文 http://www.dotblogs.com.tw/joysdw12/archive/2013/06/08/captcha-cracked.aspx
前言
这次来讲个比较有趣的主题,就是该如何破解网路上那些防止机器人攻击的图形验证码,谈到图形验证码破解,想必各位嘴角一定微微上扬了吧XD,看来学坏好像都比较有兴趣一点,但其实知道破解的原理后,之后要做防范也比较清楚该如何处理了← 主因:P。
在开始破解前先来看一下基本上的破解原理与方法,可以先参考此篇 使用PHP对网站验证码进行破解 文章,文章中提到了破解图形验证码有几个基本步骤,如下:
- 取出字模
- 二值化
- 计算特征
- 对照样本
Step 1 取出字模
首先取出字模就是将要破解的图形验证码先抓取回来,而取得的字模图片必须要包含所有会出现的文字,例如0 - 9 的数字图片,当有了字模后就能够将字模进行二值化。
Step 2 二值化
二值化是什么? 二值化就是将数字字模转换成0 与1 的结果,将图片上数字的部分用1 替换而0 则代表背景,例如我有一张数字3 的图片,在经过二值化后就会变成以下结果。
000000000000000000000
000000011111100000000
000001110001110000000
000000000000111000000
000000000000110000000
000000011111100000000
000000000000110000000
000000000000111000000
000001110001110000000
000000011111000000000
000000000000000000000
Step 3 计算特征
当我们将图片数字转成二值化后,这些二值化的01 代码就变成了样本库,所以在计算特征的步骤里,就是要在产生验证码的页面将验证码图片取得,取得后因为验证码可能包含干扰元素,就必须要先去除干扰元素后将图片二值化取得特征。
Step 4 对照样本
最后的步骤就是要将第三步骤二值化的值拿去比对我们的样本库,通常在比对的时候一定会产生误差值,例如以下转换后的二进值:
000000000000000000000
000000011111100000000
0000 1 111000111 1 000000
000000000000111000000
000000000000110000000
000000011111100000000
000000000000110000000
000000000000111000000
000001110001110000000
0000000111110000 1 0000
000000000000000000000
可以看到以上二进值红色的1 的部分就是所谓的噪点,因为图片在不同的位置下所产生的图片像素可能会不一样,所以我们在对照样本时可以设定一个允许容忍噪点的范围,就是有点模糊比对的意思。
实作破解
接下来的说明将使用 [VB]使用图形验证码范例 此文章的产生方式来举例说明,先举例以下三种图形验证码样式说明,如下:
- 第一种是没有任何干扰单纯只有数字的验证码,这种验证码非常容易破解,只需要将图片进行灰阶处理后再分别取出单元字块比对即可。
- 第二种是多加了噪音线干扰的验证码,其实这个噪音线有跟没有一样,一样只要经过灰阶处理后再针对噪音线的像素去除即可破解。
- 第三种是多加了噪音点干扰的验证码,这种验证码破解处理就比较麻烦点,需要针对噪音点的周围判断是否能去除,但是其实只要有足够的样本可以对照也是可以破解的。
除了 以上举例的这几种外,在 Caca Labs 也有举出好几种验 证码格式与能够破解的机率表,可以去看一看,接下来就开始实作破解,以下范例使用到Web与AP,透过AP浏览网页并抓取网页内的验证码图形处理破解。
取得验证码图形
第一步首先要取得验证码的图片,因为破解主要使用AP 处理,所以在这里我们可以使用WebBrowser 类别搭配Microsoft.mshtml 命名空间处理,在WebBrowser 网页载入完成触发的DocumentCompleted 事件中取得图片并转换成Bitmap 型别做后续处理,如下代码:
01 |
private void webBrowser1_DocumentCompleted( object sender, WebBrowserDocumentCompletedEventArgs e) |
03 |
WebBrowser wb = sender as WebBrowser; |
04 |
var doc = wb.Document.DomDocument as HTMLDocument; |
05 |
HTMLBody body = doc.body as HTMLBody; |
06 |
IHTMLControlRange range = body.createControlRange(); |
08 |
IHTMLControlElement imgElement = |
09 |
wb.Document.GetElementById( "imgCaptcha" ).DomElement as IHTMLControlElement; |
10 |
range.add(imgElement); |
11 |
range.execCommand( "copy" , false , Type.Missing); |
12 |
Image img = Clipboard.GetImage(); |
16 |
CaptchaCracked( new Bitmap(img)); |
18 |
wb.Document.GetElementById( "txtCaptchaCode" ).SetAttribute( "value" , txtCode.Text); |
第一种图形破解
先来看看第一种图形该如何破解,第一种图形非常没有挑战性,我们要先撰写针对验证码处理的相关代码,产生一个CaptchaCrackedHelper 类别,并加入一些属性配置。
01 |
public class CaptchaCrackedHelper |
06 |
public Bitmap BmpSource { get ; set ; } |
10 |
private int GrayValue { get ; set ; } |
14 |
private int AllowDiffCount { get ; set ; } |
18 |
private DecCodeList DecCodeDictionary { get ; set ; } |
20 |
public CaptchaCrackedHelper() { } |
21 |
public CaptchaCrackedHelper( |
22 |
Bitmap pBmpSource, int pGrayValue, int pAllowDiffCount, DecCodeList pDecCodeDictionary) |
24 |
BmpSource = pBmpSource; |
25 |
GrayValue = pGrayValue; |
26 |
AllowDiffCount = pAllowDiffCount; |
27 |
DecCodeDictionary = pDecCodeDictionary; |
第二步骤,因为原始图片可能包含很多色彩,而之后的判断是使用灰阶值的高低来做为区分数字或背景的依据,所以要将图片先进行灰阶处理,加入灰阶处理方法,如下
04 |
public void ConvertGrayByPixels() |
06 |
for ( int i = 0; i < BmpSource.Height; i++) |
07 |
for ( int j = 0; j < BmpSource.Width; j++) |
09 |
int grayValue = GetGrayValue(BmpSource.GetPixel(j, i)); |
10 |
BmpSource.SetPixel(j, i, Color.FromArgb(grayValue, grayValue, grayValue)); |
17 |
/// <param name="pColor">color-像素色彩</param> |
18 |
/// <returns></returns> |
19 |
private int GetGrayValue(Color pColor) |
21 |
return Convert.ToInt32(pColor.R * 0.299 + pColor.G * 0.587 + pColor.B * 0.114); |
第三步骤,灰阶处理后接下来就要重新取得图片的范围,因为之后必须要将图片切割成一个数字一张图,所以要去除掉多余的空白处,如下
04 |
/// <param name="pCharsCount">int-字元数量</param> |
05 |
public void ConvertBmpValidRange( int pCharsCount) |
08 |
int posX1 = BmpSource.Width, posY1 = BmpSource.Height; |
10 |
int posX2 = 0, posY2 = 0; |
13 |
for ( int i = 0; i < BmpSource.Height; i++) |
15 |
for ( int j = 0; j < BmpSource.Width; j++) |
17 |
int pixelVal = BmpSource.GetPixel(j, i).R; |
18 |
if (pixelVal < GrayValue) |
20 |
if (posX1 > j) posX1 = j; |
21 |
if (posY1 > i) posY1 = i; |
22 |
if (posX2 < j) posX2 = j; |
23 |
if (posY2 < i) posY2 = i; |
29 |
int span = pCharsCount - (posX2 - posX1 + 1) % pCharsCount; |
30 |
if (span < pCharsCount) |
32 |
int leftSpan = span / 2; |
34 |
posX1 = posX1 - leftSpan; |
35 |
if (posX2 + span - leftSpan < BmpSource.Width) |
36 |
posX2 = posX2 + span - leftSpan; |
39 |
Rectangle cloneRect = new Rectangle(posX1, posY1, posX2 - posX1 + 1, posY2 - posY1 + 1); |
40 |
BmpSource = BmpSource.Clone(cloneRect, BmpSource.PixelFormat); |
第四步骤,在重新取得图片的有效范围后就要将图片进行切割,如上所述一个数字将是一张图片,而此切割后的图片将作为之后对照的样本。
04 |
/// <param name="pHorizontalColNumber">int-水平切割数</param> |
05 |
/// <param name="pVerticalRowNumber">int-垂直切割数</param> |
06 |
/// <returns></returns> |
07 |
public Bitmap[] GetSplitPicChars( int pHorizontalColNumber, int pVerticalRowNumber) |
09 |
if (pHorizontalColNumber == 0 || pVerticalRowNumber == 0) |
11 |
int avgWidth = BmpSource.Width / pHorizontalColNumber; |
12 |
int avgHeight = BmpSource.Height / pVerticalRowNumber; |
14 |
Bitmap[] bmpAry = new Bitmap[pHorizontalColNumber * pVerticalRowNumber]; |
17 |
for ( int i = 0; i < pVerticalRowNumber; i++) |
19 |
for ( int j = 0; j < pHorizontalColNumber; j++) |
21 |
cloneRect = new Rectangle(j * avgWidth, i * avgHeight, avgWidth, avgHeight); |
22 |
bmpAry[i * pHorizontalColNumber + j] = BmpSource.Clone(cloneRect, BmpSource.PixelFormat); |
第五步骤,切割完成图片后就要将数字图片进行二值化,在此就是透过GrayValue 属性指定的值进行区分,如果色彩小于GrayValue 值就是数字,大于GrayValue 值就是背景。
02 |
/// 取得图片转换后的01编码,0为背景像素1为灰阶像素 |
04 |
/// <param name="pBmp">bitmap-单一图片</param> |
05 |
/// <returns></returns> |
06 |
public string GetSingleBmpCode(Bitmap pBmp) |
09 |
string code = string .Empty; |
10 |
for ( int i = 0; i < pBmp.Height; i++) |
11 |
for ( int j = 0; j < pBmp.Width; j++) |
13 |
color = pBmp.GetPixel(j, i); |
14 |
if (color.R < GrayValue) |
第六步骤,当连图片都切割好时就剩下要将图片转成二值化编码丢到样本字典里做比对,在此我的样本字典产生方式是先透过以上这些方法,执行程式后于第五步骤时将0 - 9 的二值化编码值先取得,取得后建入样本字典内供之后比对时可以用来对照使用,如比对不到时返回X。
04 |
/// <param name="pSourceCode">string-图片编码</param> |
05 |
/// <returns></returns> |
06 |
public string GetDecChar( string pSourceCode) |
08 |
string tmpResult = "X" ; |
09 |
for ( int i = 0; i < DecCodeDictionary.List.Count; i++) |
11 |
foreach ( string code in DecCodeDictionary.List[i].Code.ToArray()) |
13 |
int diffCharCount = 0; |
14 |
char [] decChar = code.ToCharArray(); |
15 |
char [] sourceChar = pSourceCode.ToCharArray(); |
16 |
if (decChar.Length == sourceChar.Length) |
18 |
for ( int j = 0; j < decChar.Length; j++) |
19 |
if (decChar[j] != sourceChar[j]) |
21 |
if (diffCharCount <= AllowDiffCount) |
22 |
tmpResult = i.ToString(); |
最后,我们就能够开始进行测试,在一开始WebBrowser 的DocumentCompleted 事件里我呼叫了CaptchaCracked 方法进行破解,方法如下
001 |
private void CaptchaCracked(Bitmap pBmpImg) |
003 |
CaptchaCrackedHelper.DecCodeList decCodeList = |
004 |
new CaptchaCrackedHelper.DecCodeList(); |
005 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
007 |
Code = new string [] { |
008 |
"001110001001000100110010011001001100100110010011001001000011000" , |
009 |
"0111100010010001001100100110010011001001100110010010010000111000110000" , |
010 |
"0111000100100010011001001100100010010011001101100100100001110100100000" } |
012 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
014 |
Code = new string [] { |
015 |
"011100000010000001000000100000010000001000000100000010000111100" , |
016 |
"011100000010000011000001100000010000011000001100000110000111110" , |
017 |
"0001100000010000001000000100000011000011000000100000010000001100000000" , |
018 |
"0000000000110000001000000100000110000001000001100000010000001000001110" } |
020 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
022 |
Code = new string [] { |
023 |
"001100001111000000100000010000000000001000001000000111100111100" , |
024 |
"0001100001111001000100000010000000000001000001000001111100111100000000" , |
025 |
"001110001111001001100000010000000000001000001001000111101111100" } |
027 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
029 |
Code = new string [] { |
030 |
"001110001111000000100000110000111000000100000011011001000111000" , |
031 |
"0011110011110000001000001100001110000001100000100110010001110000000000" , |
034 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
036 |
Code = new string [] { |
037 |
"000010000011000001100000110000001000001100011111000001000000100" , |
038 |
"000010000001000001100001110000011000000100011111000011000000100" , |
039 |
"0001000000100000110000001000000101000110000111100000100000010000000000" , |
040 |
"0000000000001000001100000110000001000000100000110001111100000100000010" } |
042 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
044 |
Code = new string [] { |
045 |
"001111000111100100000011100001111000000110000001001000000111100" , |
046 |
"0011110001101000000000111000011011000001100001010010000001111100001000" , |
049 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
051 |
Code = new string [] { |
052 |
"000011000110000010000011110001001100100110010011001101100011100" , |
053 |
"0000100001100000100000111100010011001001100100110011011000111000000000" , |
056 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
058 |
Code = new string [] { |
059 |
"011111001111100000000000010000001000000000000100000010000000000" , |
060 |
"1111110011011000000000000100000010000000000011000000100000000000000000" , |
063 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
065 |
Code = new string [] { |
066 |
"001110001000100110010011110000111000100110010011001001100111100" , |
067 |
"1111000100010011001001111000011000010001001001100100110001110000000000" , |
068 |
"0000000000111000100100011001001111000011100110011011000100100110001110" , |
069 |
"0001000000101000100010011101001111000011110010011001001000100110001010" } |
071 |
decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes() |
073 |
Code = new string [] { |
074 |
"001110001001000100110010011001100100011100000010000110000110000" , |
075 |
"001110001101100100110110011001101100111110000010000111000110000" , |
076 |
"001110001001000100110010011001101100111110000010000000000000000" , |
077 |
"0000000101110011001010100110010011001100100011110000010000110000010000" } |
080 |
CaptchaCrackedHelper cracked = new CaptchaCrackedHelper(pBmpImg, 128, 6, decCodeList); |
083 |
cracked.ConvertGrayByPixels(); |
084 |
picBox2.Image = cracked.BmpSource; |
087 |
cracked.ConvertBmpValidRange(5); |
088 |
picBox3.Image = cracked.BmpSource; |
091 |
Bitmap[] bitmap = cracked.GetSplitPicChars(5, 1); |
092 |
picBoxP1.Image = bitmap[0]; |
093 |
picBoxP2.Image = bitmap[1]; |
094 |
picBoxP3.Image = bitmap[2]; |
095 |
picBoxP4.Image = bitmap[3]; |
096 |
picBoxP5.Image = bitmap[4]; |
098 |
txtCodeStr.Text = string .Empty; |
099 |
txtCode.Text = string .Empty; |
100 |
foreach (Bitmap bmp in bitmap) |
102 |
string result = cracked.GetSingleBmpCode(bmp); |
103 |
txtCodeStr.Text += result + "@" ; |
104 |
txtCode.Text += cracked.GetDecChar(result); |
以上方法中一开始先建立字典内容,因为这只是简单的范例,所以我直接在开始时候建立字典,当然也可以将字典建立在资料库中,在此的字典样本越多的话比对结果将越准确,字典建立完成后就依照之前所说明的步骤进行处理,经过测试第一种的破解成功率约99%,执行后的结果如下:
第二种图形破解
第一种成功破解后那第二种该如何处理? 第二种验证码加入了噪音线干扰,其实针对噪音线我们只需要再多加一个处理方法即可,可以用小画家将图片先撷取出来,用小画家查看噪音线的像素RGB 值是多少,再透过排除方法去除噪音线色彩值区间内的像素点,如下:
04 |
public void RemoteNoiseLineByPixels() |
06 |
for ( int i = 0; i < BmpSource.Height; i++) |
07 |
for ( int j = 0; j < BmpSource.Width; j++) |
09 |
int grayValue = BmpSource.GetPixel(j, i).R; |
10 |
if (grayValue <= 255 && grayValue >= 160) |
11 |
BmpSource.SetPixel(j, i, Color.FromArgb(255, 255, 255)); |
经过测试破解率也有90%以上,执行后的结果如下:
第三种图形破解
而第三种图形跟第二种处理要做的事情一样是要而外加入方法处理,噪音点的处理方式就比较麻烦,因为噪音点可能会跟数字连在一起,而其实只要数字间有相连或干扰判断上都会比较复杂,但是还是能破解,只是手续比较多罢了,在此我只用个判断像素周围是否是白色作为处理方法,其实不太准确,实际上应还需要更多去杂质处理,如下:
04 |
public void RemoteNoisePointByPixels() |
06 |
List<NoisePoint> points = new List<NoisePoint>(); |
08 |
for ( int k = 0; k < 5; k++) |
10 |
for ( int i = 0; i < BmpSource.Height; i++) |
11 |
for ( int j = 0; j < BmpSource.Width; j++) |
16 |
if (i - 1 > 0 && BmpSource.GetPixel(j, i - 1).R != garyVal) flag++; |
17 |
if (i + 1 < BmpSource.Height && BmpSource.GetPixel(j, i + 1).R != garyVal) flag++; |
18 |
if (j - 1 > 0 && BmpSource.GetPixel(j - 1, i).R != garyVal) flag++; |
19 |
if (j + 1 < BmpSource.Width && BmpSource.GetPixel(j + 1, i).R != garyVal) flag++; |
20 |
if (i - 1 > 0 && j - 1 > 0 && BmpSource.GetPixel(j - 1, i - 1).R != garyVal) flag++; |
21 |
if (i + 1 < BmpSource.Height && j - 1 > 0 && BmpSource.GetPixel(j - 1, i + 1).R != garyVal) flag++; |
22 |
if (i - 1 > 0 && j + 1 < BmpSource.Width && BmpSource.GetPixel(j + 1, i - 1).R != garyVal) flag++; |
23 |
if (i + 1 < BmpSource.Height && j + 1 < BmpSource.Width && BmpSource.GetPixel(j + 1, i + 1).R != garyVal) flag++; |
26 |
points.Add( new NoisePoint() { X = j, Y = i }); |
28 |
foreach (NoisePoint point in points) |
29 |
BmpSource.SetPixel(point.X, point.Y, Color.FromArgb(255, 255, 255)); |
34 |
public class NoisePoint |
36 |
public int X { get ; set ; } |
37 |
public int Y { get ; set ; } |
经过测试破解成功率不高...哈哈: P,如果增加字典档样本应该能够再提升成功率,执行结果如下:
结论
以上就是简单图形验证码破解范例,本篇主要的目的在于了解心术不正的人(我?),是使用哪种原理来进行图形验证码的破解,造成机器人攻击的情况发生,正所谓知己知彼,如果知道对方会使用的伎俩的话,相对于我们就能够预先防范,而要能够增加验证码的安全性的话,最好就是将字符连在一起或不规则旋转字符,这样就能够增加破解的困难度,但基本上最后都还是能被破解的。
PS:拿去做坏事不要找我...
范例程式码
TCaptchaCracked.rar
参考资料
使用PHP对网站验证码进行破解
用于验证码图片识别的类(C#源码)
MSHTML Reference