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