知名建案指名合作,專業外牆清洗
讓大樓亮晶晶,清潔服務隨傳隨到
拆除工程專業壓克力、金屬字、帆布、水晶字、不鏽鋼廣告招牌設計
材質選擇多,歡迎來電洽詢

首頁  •  j2h 論壇 • 程式設計討論     • 

[ASP ] 圖形驗證碼破解-以簡單圖形為例

房東:黃大哥
發表時間:2014-10-25


前言




  這次來講個比較有趣的主題,就是該如何破解網路上那些防止機器人攻擊的圖形驗證碼,談到圖形驗證碼破解,想必各位嘴角一定微微上揚了吧 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

000011110001111000000

000000000000111000000

000000000000110000000

000000011111100000000

000000000000110000000

000000000000111000000

000001110001110000000

000000011111000010000

000000000000000000000


 


  可以看到以上二進值紅色的 1 的部分就是所謂的噪點,因為圖片在不同的位置下所產生的圖片像素可能會不一樣,所以我們在對照樣本時可以設定一個允許容忍噪點的範圍,就是有點模糊比對的意思。


 


實作破解




  接下來的說明將使用 [VB] 使用圖形驗證碼範例 此文章的產生方式來舉例說明,先舉例以下三種圖形驗證碼樣式說明,如下:



 



  • 第一種是沒有任何干擾單純只有數字的驗證碼,這種驗證碼非常容易破解,只需要將圖片進行灰階處理後再分別取出單元字塊比對即可。

  • 第二種是多加了噪音線干擾的驗證碼,其實這個噪音線有跟沒有一樣,一樣只要經過灰階處理後再針對噪音線的像素去除即可破解。

  • 第三種是多加了噪音點干擾的驗證碼,這種驗證碼破解處理就比較麻煩點,需要針對噪音點的周圍判斷是否能去除,但是其實只要有足夠的樣本可以對照也是可以破解的。


 


  除了以上舉例的這幾種外,在 Caca Labs 也有舉出好幾種驗證碼格式與能夠破解的機率表,可以去看一看,接下來就開始實作破解,以下範例使用到 Web 與 AP,透過 AP 瀏覽網頁並抓取網頁內的驗證碼圖形處理破解。


 


取得驗證碼圖形


  第一步首先要取得驗證碼的圖片,因為破解主要使用 AP 處理,所以在這裡我們可以使用 WebBrowser 類別搭配 Microsoft.mshtml 命名空間處理,在 WebBrowser 網頁載入完成觸發的 DocumentCompleted 事件中取得圖片並轉換成 Bitmap 型別做後續處理,如下代碼:












01 private 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 類別,並加入一些屬性配置。












01 public 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>










04 public 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>










19 private 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>










05 public 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="pHorizontalColNumber">int-水平切割數</param>










05 /// <param name="pVerticalRowNumber">int-垂直切割數</param>










06 /// <returns></returns>










07 public 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 * pHorizontalColNumber + 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>










06 public 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>










06 public 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)