原文 http://www.dotblogs.com.tw/joysdw12/archive/2013/06/08/captcha-cracked.aspx

前言


  这次来讲个比较有趣的主题,就是该如何破解网路上那些防止机器人攻击的图形验证码,谈到图形验证码破解,想必各位嘴角一定微微上扬了吧XD,看来学坏好像都比较有兴趣一点,但其实知道破解的原理后,之后要做防范也比较清楚该如何处理了← 主因:P。

  在开始破解前先来看一下基本上的破解原理与方法,可以先参考此篇  使用PHP对网站验证码进行破解 文章,文章中提到了破解图形验证码有几个基本步骤,如下:

  1. 取出字模
  2. 二值化
  3. 计算特征
  4. 对照样本

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]使用图形验证码范例  此文章的产生方式来举例说明,先举例以下三种图形验证码样式说明,如下:

[ASP.NET] 图形验证码破解-以简单图形为例-LMLPHP

  • 第一种是没有任何干扰单纯只有数字的验证码,这种验证码非常容易破解,只需要将图片进行灰阶处理后再分别取出单元字块比对即可。
  • 第二种是多加了噪音线干扰的验证码,其实这个噪音线有跟没有一样,一样只要经过灰阶处理后再针对噪音线的像素去除即可破解。
  • 第三种是多加了噪音点干扰的验证码,这种验证码破解处理就比较麻烦点,需要针对噪音点的周围判断是否能去除,但是其实只要有足够的样本可以对照也是可以破解的。

  除了 ​​以上举例的这几种外,在Caca Labs  也有举出好几种验 ​​证码格式与能够破解的机率表,可以去看一看,接下来就开始实作破解,以下范例使用到Web与AP,透过AP浏览网页并抓取网页内的验证码图形处理破解。

取得验证码图形

  第一步首先要取得验证码的图片,因为破解主要使用AP 处理,所以在这里我们可以使用WebBrowser 类别搭配Microsoft.mshtml 命名空间处理,在WebBrowser 网页载入完成触发的DocumentCompleted 事件中取得图片并转换成Bitmap 型别做后续处理,如下代码:

01private void webBrowser1_DocumentCompleted( object sender, WebBrowserDocumentCompletedEventArgs e)
02{
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();
07    // 取得网页上验证码图片
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();
13    Clipboard.Clear();
14    picBox1.Image = img;
15    // 转换成Bitmap 物件进行破解
16    CaptchaCracked( new Bitmap(img));
17    // 将验证码写入文字框
18    wb.Document.GetElementById( "txtCaptchaCode" ).SetAttribute( "value" , txtCode.Text);
19}

第一种图形破解

  先来看看第一种图形该如何破解,第一种图形非常没有挑战性,我们要先撰写针对验证码处理的相关代码,产生一个CaptchaCrackedHelper 类别,并加入一些属性配置。

01public class CaptchaCrackedHelper
02{
03    /// <summary>
04    /// 存放来源图档
05    /// </summary>
06    public Bitmap BmpSource { get set ; }
07    /// <summary>
08    /// 区分背景与数字的灰阶值
09    /// </summary>
10    private int GrayValue { get set ; }
11    /// <summary>
12    /// 可容忍的错误噪点数
13    /// </summary>
14    private int AllowDiffCount { get set ; }
15    /// <summary>
16    /// 对照样本字典
17    /// </summary>
18    private DecCodeList DecCodeDictionary { get set ; }
19     
20    public CaptchaCrackedHelper() { }
21    public CaptchaCrackedHelper(
22        Bitmap pBmpSource, int pGrayValue, int pAllowDiffCount, DecCodeList pDecCodeDictionary)
23    {
24        BmpSource = pBmpSource;
25        GrayValue = pGrayValue;
26        AllowDiffCount = pAllowDiffCount;
27        DecCodeDictionary = pDecCodeDictionary;
28    }
29}

  第二步骤,因为原始图片可能包含很多色彩,而之后的判断是使用灰阶值的高低来做为区分数字或背景的依据,所以要将图片先进行灰阶处理,加入灰阶处理方法,如下

01/// <summary>
02/// 将每点像素色彩转换成灰阶值
03/// </summary>
04public void ConvertGrayByPixels()
05{
06    for int i = 0; i < BmpSource.Height; i++)
07        for int j = 0; j < BmpSource.Width; j++)
08        {
09            int grayValue = GetGrayValue(BmpSource.GetPixel(j, i));
10            BmpSource.SetPixel(j, i, Color.FromArgb(grayValue, grayValue, grayValue));
11        }
12}
13 
14/// <summary>
15/// 计算灰阶值
16/// </summary>
17/// <param name="pColor">color-像素色彩</param>
18/// <returns></returns>
19private int GetGrayValue(Color pColor)
20{
21    return Convert.ToInt32(pColor.R * 0.299 + pColor.G * 0.587 + pColor.B * 0.114); //灰阶公式
22}

  第三步骤,灰阶处理后接下来就要重新取得图片的范围,因为之后必须要将图片切割成一个数字一张图,所以要去除掉多余的空白处,如下

01/// <summary>
02/// 转换图片有效范围
03/// </summary>
04/// <param name="pCharsCount">int-字元数量</param>
05public void ConvertBmpValidRange( int pCharsCount)
06{
07    // 图片最大X, Y,处理后变成起始X, Y
08    int posX1 = BmpSource.Width, posY1 = BmpSource.Height;
09    // 图片起始X, Y,处理后变成最大X, Y
10    int posX2 = 0, posY2 = 0;
11 
12    // 取得有效范围区域
13    for int i = 0; i < BmpSource.Height; i++)
14    {
15        for int j = 0; j < BmpSource.Width; j++)
16        {
17            int pixelVal = BmpSource.GetPixel(j, i).R;
18            if (pixelVal < GrayValue) //如像该素值低于指定灰阶值则进行缩小区域
19            {
20                if (posX1 > j) posX1 = j; //如X2像素位置大于图片宽度则缩小宽度
21                if (posY1 > i) posY1 = i; //如Y2像素位置大于图片高度则缩小高度
22                if (posX2 < j) posX2 = j; //如X1像素位置小于图片宽度则缩小宽度
23                if (posY2 < i) posY2 = i; //如Y1像素位置小于图片宽度则缩小宽度
24            }
25        }
26    }
27 
28    // 确保图片可以平均切割图片
29    int span = pCharsCount - (posX2 - posX1 + 1) % pCharsCount;
30    if (span < pCharsCount)
31    {
32        int leftSpan = span / 2;
33        if (posX1 > leftSpan)
34            posX1 = posX1 - leftSpan;
35        if (posX2 + span - leftSpan < BmpSource.Width)
36            posX2 = posX2 + span - leftSpan;
37    }
38    // 产生变更后的图片
39    Rectangle cloneRect = new Rectangle(posX1, posY1, posX2 - posX1 + 1, posY2 - posY1 + 1);
40    BmpSource = BmpSource.Clone(cloneRect, BmpSource.PixelFormat);
41}

  第四步骤,在重新取得图片的有效范围后就要将图片进行切割,如上所述一个数字将是一张图片,而此​​切割后的图片将作为之后对照的样本。

01/// <summary>
02/// 取得切割后的图
03/// </summary>
04/// <param name="pHorizo​​ntalColNumber">int-水平切割数</param>
05/// <param name="pVerticalRowNumber">int-垂直切割数</param>
06/// <returns></returns>
07public Bitmap[] GetSplitPicChars( int pHorizontalColNumber, int pVerticalRowNumber)
08{
09    if (pHorizontalColNumber == 0 || pVerticalRowNumber == 0)
10        return null ;
11    int avgWidth = BmpSource.Width / pHorizontalColNumber;
12    int avgHeight = BmpSource.Height / pVerticalRowNumber;
13    // 产生存放图片容器阵列
14    Bitmap[] bmpAry = new Bitmap[pHorizontalColNumber * pVerticalRowNumber];
15    // 重新取得数字区域
16    Rectangle cloneRect;
17    for int i = 0; i < pVerticalRowNumber; i++)
18    {
19        for int j = 0; j < pHorizontalColNumber; j++)
20        {
21            cloneRect = new Rectangle(j * avgWidth, i * avgHeight, avgWidth, avgHeight);
22            bmpAry[i * pHorizo​​ntalColNumber + j] = BmpSource.Clone(cloneRect, BmpSource.PixelFormat);
23        }
24    }
25    return bmpAry;
26}

  第五步骤,切割完成图片后就要将数字图片进行二值化,在此就是透过GrayValue 属性指定的值进行区分,如果色彩小于GrayValue 值就是数字,大于GrayValue 值就是背景。

01/// <summary>
02/// 取得图片转换后的01编码,0为背景像素1为灰阶像素
03/// </summary>
04/// <param name="pBmp">bitmap-单一图片</param>
05/// <returns></returns>
06public string GetSingleBmpCode(Bitmap pBmp)
07{
08    Color color;
09    string code = string .Empty;
10    for int i = 0; i < pBmp.Height; i++)
11        for int j = 0; j < pBmp.Width; j++)
12        {
13            color = pBmp.GetPixel(j, i);
14            if (color.R < GrayValue)
15                code += "1" ;
16            else
17                code += "0" ;
18        }
19    return code;
20}

  第六步骤,当连图片都切割好时就剩下要将图片转成二值化编码丢到样本字典里做比对,在此我的样本字典产生方式是先透过以上这些方法,执行程式后于第五步骤时将0 - 9 的二值化编码值先取得,取得后建入样本字典内供之后比对时可以用来对照使用,如比对不到时返回X。

01/// <summary>
02/// 取得解码后的验证码字元
03/// </summary>
04/// <param name="pSourceCode">string-图片编码</param>
05/// <returns></returns>
06public string GetDecChar( string pSourceCode)
07{
08    string tmpResult = "X" ;
09    for int i = 0; i < DecCodeDictionary.List.Count; i++)
10    {
11        foreach string code in DecCodeDictionary.List[i].Code.ToArray())
12        {
13            int diffCharCount = 0;
14            char [] decChar = code.ToCharArray();
15            char [] sourceChar = pSourceCode.ToCharArray();
16            if (decChar.Length == sourceChar.Length)
17            {
18                for int j = 0; j < decChar.Length; j++)
19                    if (decChar[j] != sourceChar[j])
20                        diffCharCount++;
21                if (diffCharCount <= AllowDiffCount)
22                    tmpResult = i.ToString();
23            }
24        }
25    }
26    return tmpResult;
27}

  最后,我们就能够开始进行测试,在一开始WebBrowser 的DocumentCompleted 事件里我呼叫了CaptchaCracked 方法进行破解,方法如下

001private void CaptchaCracked(Bitmap pBmpImg)
002{
003    CaptchaCrackedHelper.DecCodeList decCodeList =
004           new CaptchaCrackedHelper.DecCodeList();
005    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 0
006    {  
007        Code = new string [] {
008            "001110001001000100110010011001001100100110010011001001000011000" ,
009            "0111100010010001001100100110010011001001100110010010010000111000110000" ,
010            "0111000100100010011001001100100010010011001101100100100001110100100000" }
011    });
012    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 1
013    {
014        Code = new string [] {
015            "011100000010000001000000100000010000001000000100000010000111100" ,
016            "011100000010000011000001100000010000011000001100000110000111110" ,
017            "0001100000010000001000000100000011000011000000100000010000001100000000" ,
018            "0000000000110000001000000100000110000001000001100000010000001000001110" }
019    });
020    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 2
021    {
022        Code = new string [] {
023            "001100001111000000100000010000000000001000001000000111100111100" ,
024            "0001100001111001000100000010000000000001000001000001111100111100000000" ,
025            "001110001111001001100000010000000000001000001001000111101111100" }
026    });
027    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 3
028    {
029        Code = new string [] {
030            "001110001111000000100000110000111000000100000011011001000111000" ,
031            "0011110011110000001000001100001110000001100000100110010001110000000000" ,
032            "" }
033    });
034    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 4
035    {
036        Code = new string [] {
037            "000010000011000001100000110000001000001100011111000001000000100" ,
038            "000010000001000001100001110000011000000100011111000011000000100" ,
039            "0001000000100000110000001000000101000110000111100000100000010000000000" ,
040            "0000000000001000001100000110000001000000100000110001111100000100000010" }
041    });
042    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 5
043    {
044        Code = new string [] {
045            "001111000111100100000011100001111000000110000001001000000111100" ,
046            "0011110001101000000000111000011011000001100001010010000001111100001000" ,
047            "" }
048    });
049    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 6
050    {
051        Code = new string [] {
052            "000011000110000010000011110001001100100110010011001101100011100" ,
053            "0000100001100000100000111100010011001001100100110011011000111000000000" ,
054            "" }
055    });
056    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 7
057    {
058        Code = new string [] {
059            "011111001111100000000000010000001000000000000100000010000000000" ,
060            "1111110011011000000000000100000010000000000011000000100000000000000000" ,
061            "" }
062    });
063    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 8
064    {
065        Code = new string [] {
066            "001110001000100110010011110000111000100110010011001001100111100" ,
067            "1111000100010011001001111000011000010001001001100100110001110000000000" ,
068            "0000000000111000100100011001001111000011100110011011000100100110001110" ,
069            "0001000000101000100010011101001111000011110010011001001000100110001010" }
070    });
071    decCodeList.List.Add( new CaptchaCrackedHelper.DecCodes()    // 9
072    {
073        Code = new string [] {
074            "001110001001000100110010011001100100011100000010000110000110000" ,
075            "001110001101100100110110011001101100111110000010000111000110000" ,
076            "001110001001000100110010011001101100111110000010000000000000000" ,
077            "0000000101110011001010100110010011001100100011110000010000110000010000" }
078    });
079 
080    CaptchaCrackedHelper cracked = new CaptchaCrackedHelper(pBmpImg, 128, 6, decCodeList);
081 
082    // Step1 灰阶化
083    cracked.ConvertGrayByPixels();
084    picBox2.Image = cracked.BmpSource;
085 
086    // Step2 改变图片范围
087    cracked.ConvertBmpValidRange(5);
088    picBox3.Image = cracked.BmpSource;
089 
090    // Step3 切割图片范围
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];
097 
098    txtCodeStr.Text = string .Empty;
099    txtCode.Text = string .Empty;
100    foreach (Bitmap bmp in bitmap)
101    {
102        string result = cracked.GetSingleBmpCode(bmp);
103        txtCodeStr.Text += result + "@" ;
104        txtCode.Text += cracked.GetDecChar(result);
105    }
106 
107}

  以上方法中一开始先建立字典内容,因为这只是简单的范例,所以我直接在开始时候建立字典,当然也可以将字典建立在资料库中,在此的字典样本越多的话比对结果将越准确,字典建立完成后就依照之前所说明的步骤进行处理,经过测试第一种的破解成功率约99%,执行后的结果如下:

[ASP.NET] 图形验证码破解-以简单图形为例-LMLPHP

第二种图形破解

  第一种成功破解后那第二种该如何处理? 第二种验证码加入了噪​​音线干扰,其实针对噪音线我们只需要再多加一个处理方法即可,可以用小画家将图片先撷取出来,用小画家查看噪音线的像素RGB 值是多少,再透过排除方法去除噪音线色彩值区间内的像素点,如下:

01/// <summary>
02/// 噪音线处理
03/// </summary>
04public void RemoteNoiseLineByPixels()
05{
06    for int i = 0; i < BmpSource.Height; i++)
07        for int j = 0; j < BmpSource.Width; j++)
08        {
09            int grayValue = BmpSource.GetPixel(j, i).R;
10            if (grayValue <= 255 && grayValue >= 160)
11                BmpSource.SetPixel(j, i, Color.FromArgb(255, 255, 255));
12        }
13}

  经过测试破解率也有90%以上,执行后的结果如下:

[ASP.NET] 图形验证码破解-以简单图形为例-LMLPHP

第三种图形破解

  而第三种图形跟第二种处理要做的事情一样是要而外加入方法处理,噪音点的处理方式就比较麻烦,因为噪音点可能会跟数字连在一起,而其实只要数字间有相连或干扰判断上都会比较复杂,但是还是能破解,只是手续比较多罢了,在此我只用个判断像素周围是否是白色作为处理方法,其实不太准确,实际上应还需要更多去杂质处理,如下:

01/// <summary>
02/// 噪音点处理
03/// </summary>
04public void RemoteNoisePointByPixels()
05{
06    List<NoisePoint> points = new List<NoisePoint>();
07 
08    for int k = 0; k < 5; k++)
09    {
10        for int i = 0; i < BmpSource.Height; i++)
11            for int j = 0; j < BmpSource.Width; j++)
12            {
13                int flag = 0;
14                int garyVal = 255;
15                // 检查上相邻像素
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++;
24 
25                if (flag < 3)
26                    points.Add( new NoisePoint() { X = j, Y = i });
27            }
28        foreach (NoisePoint point in points)
29            BmpSource.SetPixel(point.X, point.Y, Color.FromArgb(255, 255, 255));
30 
31    }
32}
33 
34public class NoisePoint
35{
36    public int X { get set ; }
37    public int Y { get set ; }
38}

  经过测试破解成功率不高...哈哈: P,如果增加字典档样本应该能够再提升成功率,执行结果如下:

[ASP.NET] 图形验证码破解-以简单图形为例-LMLPHP

结论


  以上就是简单图形验证码破解范例,本篇主要的目的在于了解心术不正的人(我?),是使用哪种原理来进行图形验证码的破解,造成机器人攻击的情况发生,正所谓知己知彼,如果知道对方会使用的伎俩的话,相对于我们就能够预先防范,而要能够增加验证码的安全性的话,最好就是将字符连在一起或不规则旋转字符,这样就能够增加破解的困难度,但基本上最后都还是能被破解的。

PS:拿去做坏事不要找我...

范例程式码


TCaptchaCracked.rar

参考资料


使用PHP对网站验​​证码进行破解

用于验证码图片识别的类(C#源码)

MSHTML Reference

04-23 15:15