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

介紹圖形驗證碼破解原理,實作簡單圖形驗證碼破解範例(二值化)。

前言


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

private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
    WebBrowser wb = sender as WebBrowser;
    var doc = wb.Document.DomDocument as HTMLDocument;
    HTMLBody body = doc.body as HTMLBody;
    IHTMLControlRange range = body.createControlRange();
    // 取得網頁上驗證碼圖片
    IHTMLControlElement imgElement = 
        wb.Document.GetElementById("imgCaptcha").DomElement as IHTMLControlElement;
    range.add(imgElement);
    range.execCommand("copy", false, Type.Missing);
    Image img = Clipboard.GetImage();
    Clipboard.Clear();
    picBox1.Image = img;
    // 轉換成 Bitmap 物件進行破解
    CaptchaCracked(new Bitmap(img));
    // 將驗證碼寫入文字框
    wb.Document.GetElementById("txtCaptchaCode").SetAttribute("value", txtCode.Text);
}

 

第一種圖形破解

  先來看看第一種圖形該如何破解,第一種圖形非常沒有挑戰性,我們要先撰寫針對驗證碼處理的相關代碼,產生一個 CaptchaCrackedHelper 類別,並加入一些屬性配置。

public class CaptchaCrackedHelper
{
    /// <summary>
    /// 存放來源圖檔
    /// </summary>
    public Bitmap BmpSource { get; set; }
    /// <summary>
    /// 區分背景與數字的灰階值
    /// </summary>
    private int GrayValue { get; set; }
    /// <summary>
    /// 可容忍的錯誤噪點數
    /// </summary>
    private int AllowDiffCount { get; set; }
    /// <summary>
    /// 對照樣本字典
    /// </summary>
    private DecCodeList DecCodeDictionary { get; set; }
    
    public CaptchaCrackedHelper() { }
    public CaptchaCrackedHelper(
        Bitmap pBmpSource, int pGrayValue, int pAllowDiffCount, DecCodeList     pDecCodeDictionary)
    {
        BmpSource = pBmpSource;
        GrayValue = pGrayValue;
        AllowDiffCount = pAllowDiffCount;
        DecCodeDictionary = pDecCodeDictionary;
    }
}

 

  第二步驟,因為原始圖片可能包含很多色彩,而之後的判斷是使用灰階值的高低來做為區分數字或背景的依據,所以要將圖片先進行灰階處理,加入灰階處理方法,如下

/// <summary>
/// 將每點像素色彩轉換成灰階值
/// </summary>
public void ConvertGrayByPixels()
{
    for (int i = 0; i < BmpSource.Height; i++)
        for (int j = 0; j < BmpSource.Width; j++)
        {
            int grayValue = GetGrayValue(BmpSource.GetPixel(j, i));
            BmpSource.SetPixel(j, i, Color.FromArgb(grayValue, grayValue, grayValue));
        }
}

/// <summary>
/// 計算灰階值
/// </summary>
/// <param name="pColor">color-像素色彩</param>
/// <returns></returns>
private int GetGrayValue(Color pColor)
{
    return Convert.ToInt32(pColor.R * 0.299 + pColor.G * 0.587 + pColor.B * 0.114); // 灰階公式
}

 

  第三步驟,灰階處理後接下來就要重新取得圖片的範圍,因為之後必須要將圖片切割成一個數字一張圖,所以要去除掉多餘的空白處,如下

/// <summary>
/// 轉換圖片有效範圍
/// </summary>
/// <param name="pCharsCount">int-字元數量</param>
public void ConvertBmpValidRange(int pCharsCount)
{
    // 圖片最大 X, Y,處理後變成起始 X, Y
    int posX1 = BmpSource.Width, posY1 = BmpSource.Height;
    // 圖片起始 X, Y,處理後變成最大 X, Y
    int posX2 = 0, posY2 = 0; 

    // 取得有效範圍區域
    for (int i = 0; i < BmpSource.Height; i++)
    {
        for (int j = 0; j < BmpSource.Width; j++)
        {
            int pixelVal = BmpSource.GetPixel(j, i).R;
            if (pixelVal < GrayValue) // 如像該素值低於指定灰階值則進行縮小區域
            {
                if (posX1 > j) posX1 = j; // 如 X2 像素位置大於圖片寬度則縮小寬度
                if (posY1 > i) posY1 = i; // 如 Y2 像素位置大於圖片高度則縮小高度
                if (posX2 < j) posX2 = j; // 如 X1 像素位置小於圖片寬度則縮小寬度
                if (posY2 < i) posY2 = i; // 如 Y1 像素位置小於圖片寬度則縮小寬度
            }
        }
    }

    // 確保圖片可以平均切割圖片
    int span = pCharsCount - (posX2 - posX1 + 1) % pCharsCount;
    if (span < pCharsCount)
    {
        int leftSpan = span / 2;
        if (posX1 > leftSpan)
            posX1 = posX1 - leftSpan;
        if (posX2 + span - leftSpan < BmpSource.Width)
            posX2 = posX2 + span - leftSpan;
    }
    // 產生變更後的圖片
    Rectangle cloneRect = new Rectangle(posX1, posY1, posX2 - posX1 + 1, posY2 - posY1 + 1);
    BmpSource = BmpSource.Clone(cloneRect, BmpSource.PixelFormat);
}

 

  第四步驟,在重新取得圖片的有效範圍後就要將圖片進行切割,如上所述一個數字將是一張圖片,而此切割後的圖片將作為之後對照的樣本。

/// <summary>
/// 取得切割後的圖
/// </summary>
/// <param name="pHorizontalColNumber">int-水平切割數</param>
/// <param name="pVerticalRowNumber">int-垂直切割數</param>
/// <returns></returns>
public Bitmap[] GetSplitPicChars(int pHorizontalColNumber, int pVerticalRowNumber)
{
    if (pHorizontalColNumber == 0 || pVerticalRowNumber == 0)
        return null;
    int avgWidth = BmpSource.Width / pHorizontalColNumber;
    int avgHeight = BmpSource.Height / pVerticalRowNumber;
    // 產生存放圖片容器陣列
    Bitmap[] bmpAry = new Bitmap[pHorizontalColNumber * pVerticalRowNumber];
    // 重新取得數字區域
    Rectangle cloneRect;
    for (int i = 0; i < pVerticalRowNumber; i++)
    {
        for (int j = 0; j < pHorizontalColNumber; j++)
        {
            cloneRect = new Rectangle(j * avgWidth, i * avgHeight, avgWidth, avgHeight);
            bmpAry[i * pHorizontalColNumber + j] = BmpSource.Clone(cloneRect, BmpSource.PixelFormat);
        }
    }
    return bmpAry;
}

 

  第五步驟,切割完成圖片後就要將數字圖片進行二值化,在此就是透過 GrayValue 屬性指定的值進行區分,如果色彩小於 GrayValue 值就是數字,大於 GrayValue 值就是背景。

/// <summary>
/// 取得圖片轉換後的01編碼,0為背景像素1為灰階像素
/// </summary>
/// <param name="pBmp">bitmap-單一圖片</param>
/// <returns></returns>
public string GetSingleBmpCode(Bitmap pBmp)
{
    Color color;
    string code = string.Empty;
    for (int i = 0; i < pBmp.Height; i++)
        for (int j = 0; j < pBmp.Width; j++)
        {
            color = pBmp.GetPixel(j, i);
            if (color.R < GrayValue)
                code += "1";
            else
                code += "0";
        }
    return code;
}

 

  第六步驟,當連圖片都切割好時就剩下要將圖片轉成二值化編碼丟到樣本字典裡做比對,在此我的樣本字典產生方式是先透過以上這些方法,執行程式後於第五步驟時將 0 - 9 的二值化編碼值先取得,取得後建入樣本字典內供之後比對時可以用來對照使用,如比對不到時返回 X。

/// <summary>
/// 取得解碼後的驗證碼字元
/// </summary>
/// <param name="pSourceCode">string-圖片編碼</param>
/// <returns></returns>
public string GetDecChar(string pSourceCode)
{
    string tmpResult = "X";
    for (int i = 0; i < DecCodeDictionary.List.Count; i++)
    {
        foreach (string code in DecCodeDictionary.List[i].Code.ToArray())
        {
            int diffCharCount = 0;
            char[] decChar = code.ToCharArray();
            char[] sourceChar = pSourceCode.ToCharArray();
            if (decChar.Length == sourceChar.Length)
            {
                for (int j = 0; j < decChar.Length; j++)
                    if (decChar[j] != sourceChar[j])
                        diffCharCount++;
                if (diffCharCount <= AllowDiffCount)
                    tmpResult = i.ToString();
            }
        }
    }
    return tmpResult;
}

 

  最後,我們就能夠開始進行測試,在一開始 WebBrowser 的 DocumentCompleted 事件裡我呼叫了 CaptchaCracked 方法進行破解,方法如下

private void CaptchaCracked(Bitmap pBmpImg)
{
    CaptchaCrackedHelper.DecCodeList decCodeList =
           new CaptchaCrackedHelper.DecCodeList();
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 0
    {   
        Code = new string[] { 
            "001110001001000100110010011001001100100110010011001001000011000",
            "0111100010010001001100100110010011001001100110010010010000111000110000",
            "0111000100100010011001001100100010010011001101100100100001110100100000"}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 1
    {
        Code = new string[] { 
            "011100000010000001000000100000010000001000000100000010000111100",
            "011100000010000011000001100000010000011000001100000110000111110",
            "0001100000010000001000000100000011000011000000100000010000001100000000",
            "0000000000110000001000000100000110000001000001100000010000001000001110"}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 2
    {
        Code = new string[] { 
            "001100001111000000100000010000000000001000001000000111100111100",
            "0001100001111001000100000010000000000001000001000001111100111100000000",
            "001110001111001001100000010000000000001000001001000111101111100"}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 3
    {
        Code = new string[] { 
            "001110001111000000100000110000111000000100000011011001000111000",
            "0011110011110000001000001100001110000001100000100110010001110000000000",
            ""}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 4
    {
        Code = new string[] { 
            "000010000011000001100000110000001000001100011111000001000000100",
            "000010000001000001100001110000011000000100011111000011000000100",
            "0001000000100000110000001000000101000110000111100000100000010000000000",
            "0000000000001000001100000110000001000000100000110001111100000100000010"}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 5
    {
        Code = new string[] { 
            "001111000111100100000011100001111000000110000001001000000111100",
            "0011110001101000000000111000011011000001100001010010000001111100001000",
            ""}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 6
    {
        Code = new string[] { 
            "000011000110000010000011110001001100100110010011001101100011100",
            "0000100001100000100000111100010011001001100100110011011000111000000000",
            ""}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 7
    {
        Code = new string[] { 
            "011111001111100000000000010000001000000000000100000010000000000",
            "1111110011011000000000000100000010000000000011000000100000000000000000",
            ""}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 8
    {
        Code = new string[] { 
            "001110001000100110010011110000111000100110010011001001100111100",
            "1111000100010011001001111000011000010001001001100100110001110000000000",
            "0000000000111000100100011001001111000011100110011011000100100110001110",
            "0001000000101000100010011101001111000011110010011001001000100110001010"}
    });
    decCodeList.List.Add(new CaptchaCrackedHelper.DecCodes()    // 9
    {
        Code = new string[] { 
            "001110001001000100110010011001100100011100000010000110000110000",
            "001110001101100100110110011001101100111110000010000111000110000",
            "001110001001000100110010011001101100111110000010000000000000000",
            "0000000101110011001010100110010011001100100011110000010000110000010000"}
    });

    CaptchaCrackedHelper cracked = new CaptchaCrackedHelper(pBmpImg, 128, 6, decCodeList);

    // Step1 灰階化
    cracked.ConvertGrayByPixels();
    picBox2.Image = cracked.BmpSource;

    // Step2 改變圖片範圍
    cracked.ConvertBmpValidRange(5);
    picBox3.Image = cracked.BmpSource;

    // Step3 切割圖片範圍
    Bitmap[] bitmap = cracked.GetSplitPicChars(5, 1);
    picBoxP1.Image = bitmap[0];
    picBoxP2.Image = bitmap[1];
    picBoxP3.Image = bitmap[2];
    picBoxP4.Image = bitmap[3];
    picBoxP5.Image = bitmap[4];

    txtCodeStr.Text = string.Empty;
    txtCode.Text = string.Empty;
    foreach (Bitmap bmp in bitmap)
    {
        string result = cracked.GetSingleBmpCode(bmp);
        txtCodeStr.Text += result + "@";
        txtCode.Text += cracked.GetDecChar(result);
    }

}

 

  以上方法中一開始先建立字典內容,因為這只是簡單的範例,所以我直接在開始時候建立字典,當然也可以將字典建立在資料庫中,在此的字典樣本越多的話比對結果將越準確,字典建立完成後就依照之前所說明的步驟進行處理,經過測試第一種的破解成功率約 99%,執行後的結果如下:

 

第二種圖形破解

  第一種成功破解後那第二種該如何處理? 第二種驗證碼加入了噪音線干擾,其實針對噪音線我們只需要再多加一個處理方法即可,可以用小畫家將圖片先擷取出來,用小畫家查看噪音線的像素 R.G.B 值是多少,再透過排除方法去除噪音線色彩值區間內的像素點,如下:

/// <summary>
/// 噪音線處理
/// </summary>
public void RemoteNoiseLineByPixels()
{
    for (int i = 0; i < BmpSource.Height; i++)
        for (int j = 0; j < BmpSource.Width; j++)
        {
            int grayValue = BmpSource.GetPixel(j, i).R;
            if (grayValue <= 255 && grayValue >= 160)
                BmpSource.SetPixel(j, i, Color.FromArgb(255, 255, 255));
        }
}

 

  經過測試破解率也有 90%以上,執行後的結果如下:

 

第三種圖形破解

  而第三種圖形跟第二種處理要做的事情一樣是要而外加入方法處理,噪音點的處理方式就比較麻煩,因為噪音點可能會跟數字連在一起,而其實只要數字間有相連或干擾判斷上都會比較複雜,但是還是能破解,只是手續比較多罷了,在此我只用個判斷像素周圍是否是白色作為處理方法,其實不太準確,實際上應還需要更多去雜質處理,如下:

/// <summary>
/// 噪音點處理
/// </summary>
public void RemoteNoisePointByPixels()
{
    List<NoisePoint> points = new List<NoisePoint>();

    for (int k = 0; k < 5; k++)
    {
        for (int i = 0; i < BmpSource.Height; i++)
            for (int j = 0; j < BmpSource.Width; j++)
            {
                int flag = 0;
                int garyVal = 255;
                // 檢查上相鄰像素
                if (i - 1 > 0 && BmpSource.GetPixel(j, i - 1).R != garyVal) flag++;
                if (i + 1 < BmpSource.Height && BmpSource.GetPixel(j, i + 1).R != garyVal) flag++;
                if (j - 1 > 0 && BmpSource.GetPixel(j - 1, i).R != garyVal) flag++;
                if (j + 1 < BmpSource.Width && BmpSource.GetPixel(j + 1, i).R != garyVal) flag++;
                if (i - 1 > 0 && j - 1 > 0 && BmpSource.GetPixel(j - 1, i - 1).R != garyVal) flag++;
                if (i + 1 < BmpSource.Height && j - 1 > 0 && BmpSource.GetPixel(j - 1, i + 1).R != garyVal) flag++;
                if (i - 1 > 0 && j + 1 < BmpSource.Width && BmpSource.GetPixel(j + 1, i - 1).R != garyVal) flag++;
                if (i + 1 < BmpSource.Height && j + 1 < BmpSource.Width && BmpSource.GetPixel(j + 1, i + 1).R != garyVal) flag++;

                if (flag < 3)
                    points.Add(new NoisePoint() { X = j, Y = i });
            }
        foreach (NoisePoint point in points)
            BmpSource.SetPixel(point.X, point.Y, Color.FromArgb(255, 255, 255));

    }
}

public class NoisePoint
{
    public int X { get; set; }
    public int Y { get; set; }
}

 

  經過測試破解成功率不高...哈哈 : P,如果增加字典檔樣本應該能夠再提升成功率,執行結果如下:

 

結論


  以上就是簡單圖形驗證碼破解範例,本篇主要的目的在於瞭解心術不正的人(我?),是使用哪種原理來進行圖形驗證碼的破解,造成機器人攻擊的情況發生,正所謂知己知彼,如果知道對方會使用的伎倆的話,相對於我們就能夠預先防範,而要能夠增加驗證碼的安全性的話,最好就是將字符連在一起或不規則旋轉字符,這樣就能夠增加破解的困難度,但基本上最後都還是能被破解的。

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

 

範例程式碼 2016/09/24 補檔


https://drive.google.com/open?id=0B40daTESrAXwQkl1d1llb1FoUmM

 

參考資料


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

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

MSHTML Reference

 

 


以上文章敘述如有錯誤及觀念不正確,請不吝嗇指教
如有侵權內容也請您與我反應~謝謝您 :)