1)驗證碼的基本知識及來由
網路安全技術中的驗證碼的主要目的是強制人機互動來抵禦機器自動化攻擊。用來防止機器類比http行為,直接抓取文本進行導航;或直接提交文本進行登入嘗試。在現在頻寬較大的今天,線上密碼窮舉頻寬已經不能作為瓶頸了,
驗證碼識別以2M ADSL串連實際測試,20線程大概每秒可以完成30個左右的串連,如果是6位元字密碼,在不考慮字典完全窮舉的時候也只需要幾個小時便可破解,嚴重的威脅了網路帳號的安全,因此,網路登入註冊驗證碼的普及勢在必行。
而如今國內大部分的驗證碼設計者並不得要領,要麼不瞭解影像處理、機器視覺、模式識別、人工智慧的基本概念;要麼設計出的驗證碼連人都難以識別,導致使用者 體驗度下降。 比如工商銀行的WAP手機銀行驗證碼,只有4位,而且驗證碼的薄弱形同虛設,使用窮舉電腦很快就能破解一個六位元字密碼的賬戶。當然,也有設計得比較好 的,比如Yahoo、Google、baidu等。
2)驗證碼的展望
未來的
網路安全驗證碼,可能更多地使用漸進色層、同級灰階色差,更多曲線反轉、隨機字元數量、字元粘連等手段防止機器的識別,但加密與破解總是一對孿生兄弟,不可能消失一方的。我們只是希望通過本文,給編寫驗證碼演算法的人員一些建議,使我們的網路更安全,操作也不會因此而繁瑣。
演算法分析
在驗證碼處理方面,我們大概要涉及到如下內容:人工智慧、模式識別、機器視覺、影像處理。
1) 主要流程:如果我們要從一幅圖片中識別出驗證碼;又或者我們要從一幅圖片中檢測並識別出一個字元,其步驟可概括如下:
映像採集:取得一個驗證碼,就直接通過HTTP抓HTML,然後分析出圖片的URL,下載儲存。
預先處理:檢測是正確的映像格式,轉換到合適的格式,壓縮,剪下出ROI,去除噪音,灰階化,轉換色彩空間。
檢測:找出文字所在的主要區域。
前處理:文字的切割、縮放和扭曲校正。
訓練:通過各種模式識別,機器學習演算法,來挑選和訓練合適數量的訓練集。訓練的樣本並非越多越好。通過學習,泛化能力差的問題可能會出現在這裡。
識別:輸入待識別的處理後的圖片,轉換成分類器需要的輸入格式,通過輸出的類和信賴度,來判斷大概可能是哪個字母。識別本質上就是分類。
2)關鍵概念
影像處理:一般指標對數位影像的某種數學處理,比如投影、鈍化、銳利化、細化、邊緣檢測、二值化、壓縮,以及各種資料變換等等。
二值化:一般圖片都是彩色的,按照逼真程度,可能很多層級。為了降低計算複雜度,方便後續的處理,如果在不損失關鍵資訊的情況下,能將圖片處理成黑白兩種顏色,那就最好不過了。
細化:找出映像的骨架,映像線條可能是很寬的,通過細化將寬度降為1,某些地方可能大於1。不同的細化演算法,可能有不同的差異,比如是否更靠近線條中間,比如是否保持聯通行等。
邊緣檢測:主要是理解邊緣的概念。邊緣實際上是映像中映像像素屬性變化劇烈的地方,可以通過一個固定的門限值來判斷,也可以是自適應的。門限可以是映像全 局的,也可以是局部的。不能說哪個就一定好,不過大部分時候,自適應的局部的門限可能要好點。被分析的可能是顏色,也可能是灰階映像的灰階。
機器視覺:利用電腦來模式實現人的視覺,比如物體檢測、定位、識別。按照對映像理解的層次的差別,分高階和低階的理解。
模式識別:對事物或者現象的某種表示方式(數值、文字,我們這裡主要想說的是數值),通過一些處理和分析來描述、歸類、理解、解釋這些事物、現象及其某種抽象。
人工智慧:這種概念比較寬,上面這些都屬於人工智慧這個大的方向。簡單點不要過分學院派的理解就是,把人類的很“智能”的東西給類比出來,協助生物的人來處理問題,特別是在電腦裡面。
驗證碼識別原理及代碼示範
本來拿一個銀行網站來進行驗證碼解密是很危險的,但我們發現,工行的演算法已經進行了改變,所以姑且以之前的工行WAP銀行做個舉例好了,同時也希望工行的加密能越做越好。
其實工行的WAP驗證碼是很簡單的,是未加幹擾的原始字元列印圖片而已。針對這種驗證碼,我們將使用點陣庫校正的方式進行,首先從整個程式的編寫及操作順序開始。
首先要知道我們需要取得的字的點陣有哪些。工行的WAP銀行驗證碼只有0~9,10個數字,那麼我們先將驗證碼圖片下載到本機,這裡我們必須將所有字元的圖樣都下載到本機,以便建立基礎點陣庫。
得到這些圖片檔案後,我們將用程式來獲得圖片點陣。從本地磁碟載入一個影像檔,這個檔案是我們已經下載好的。首先應該讓程式先將0~9的映像都“識別” 一遍,使我們的程式“記住”它們的點陣,範例1所示。該映像包含的驗證碼,從左至右就是0123,將這個映像逐點轉換灰階,也就是將彩色圖片先進行灰 度化、去色,變成黑白照片,便於下一步操作。
圖1
{
for (int i = 0; i < bmpobj.Height; i++)//遍曆高度
{
for (int j = 0; j < bmpobj.Width; j++)
//遍曆寬度,雙層for就迴圈了整個圖片的像素點
{
int tmpValue = GetGrayNumColor(bmpobj.GetPixel(j, i));
bmpobj.SetPixel(j, i, Color.FromArgb(tmpValue, tmpValue, tmpValue));
}
}
}
灰階化之後,像素的RGB三色都是相同的值了,亮度從0~255(HxFF)。但用於識別程式,灰階值並不能很好的區分背景色和前景色彩,尤其是對於漸進的 背景來說,所以我們還要將映像進一步處理,就是將灰階圖片2值化,類似的演算法還有分水嶺演算法等。因為本文中的驗證碼相對簡單,故直接使用2值化轉換,尋找 有效區並轉為單色黑白圖。
{
int dgGrayValue = 128 //灰階背景分界值
int CharsCount = 4 //有效字元數,已知
int posx1 = bmpobj.Width; int posy1 = bmpobj.Height;
int posx2 = 0; int posy2 = 0;
for (int i = 0; i < bmpobj.Height; i++)//找有效區
{
for (int j = 0; j < bmpobj.Width; j++)
{
int pixelValue = bmpobj.GetPixel(j, i).R;
//取得紅色值R,因為轉成黑白圖後,紅、黃、藍三位都是一樣的值,所以這裡取什麼色值都是一樣的
if (pixelValue < dgGrayValue) //根據灰階值
{
if (posx1 > j) posx1 = j;
if (posy1 > i) posy1 = i;
if (posx2 < j) posx2 = j;
if (posy2 < i) posy2 = i;
}
}
}
//確保能整除
int Span = CharsCount - (posx2 - posx1 + 1) % CharsCount;
//可整除的差額數
if (Span < CharsCount)
{
int leftSpan = Span / 2;
//分配到左邊的空列,如span為單數,則右邊比左邊大1
if (posx1 > leftSpan)
posx1 = posx1 - leftSpan;
if (posx2 + Span - leftSpan < bmpobj.Width)
posx2 = posx2 + Span - leftSpan;
}
//複製新圖
Rectangle cloneRect = new Rectangle(posx1, posy1, posx2 - posx1 + 1, posy2 - posy1 + 1);
bmpobj = bmpobj.Clone(cloneRect, bmpobj.PixelFormat);
}
Bitmap[] pics = GetSplitPics(4, 1); //分割,pics[0]中的圖片2所示
圖2
在平均分割圖片的部分,設定水平上分割數為RowNum,垂直上分割數為ColNum,返回分割好的圖片數組,程式編寫如下:
public Bitmap[] GetSplitPics(int RowNum, int ColNum)
{
if (RowNum == 0 || ColNum == 0)
return null;
int singW = bmpobj.Width / RowNum;
int singH = bmpobj.Height / ColNum;
Bitmap[] PicArray = new Bitmap[RowNum * ColNum];
Rectangle cloneRect;
for (int i = 0; i < ColNum; i++)//找有效區
{
for (int j = 0; j < RowNum; j++)
{
cloneRect = new Rectangle(j * singW, i * singH, singW, singH);
PicArray[i * RowNum + j] = bmpobj.Clone(cloneRect, bmpobj.PixelFormat);//複製小塊圖
}
}
return PicArray;
}
此時映像分割已結束,pics 的長度應該是4,並且每一個pics就是一個驗證碼的位元影像,經過錯誤處理,修邊,和去除無用背景空白,修正完的位元影像為數字0。
得到有效圖形後,由外面傳入該圖形,設定灰階背景分界值為“dgGrayValue”,有效字元數為CharsCount,程式編寫如下:
public Bitmap GetPicValidByValue(Bitmap singlepic, int dgGrayValue)
{
int posx1 = singlepic.Width; int posy1 = singlepic.Height;
int posx2 = 0; int posy2 = 0;
for (int i = 0; i < singlepic.Height; i++)//找有效區
{
for (int j = 0; j < singlepic.Width; j++)
{
int pixelValue = singlepic.GetPixel(j, i).R;
if (pixelValue < dgGrayValue) //根據灰階值
{
if (posx1 > j) posx1 = j;
if (posy1 > i) posy1 = i;
if (posx2 < j) posx2 = j;
if (posy2 < i) posy2 = i;
};
};
};
//複製新圖
Rectangle cloneRect = new Rectangle(posx1, posy1, posx2 - posx1 + 1, posy2 - posy1 + 1);
return singlepic.Clone(cloneRect, singlepic.PixelFormat);
}
至此,pics映像組中就是有效點陣圖了。下面我們把pics中的圖形轉換為代表點陣的字串,返回灰階圖片的點陣描述字串,1表示灰點,0表示背景。設定灰階圖為singlepic,背前景灰色界限為dgGrayValue。
string code = GetSingleBmpCode(pics[0], 128);
public string GetSingleBmpCode(Bitmap singlepic, int dgGrayValue)
{
Color piexl;
StringBuilder sbCode = new StringBuilder();
for (int posy = 0; posy < singlepic.Height; posy++)
for (int posx = 0; posx < singlepic.Width; posx++)
{
piexl = singlepic.GetPixel(posx, posy);
if (piexl.R < dgGrayValue)// Color.Black )
sbCode.Append('1');
else
sbCode.Append('0');
}
return sbCode.ToString();
}
此時,code中的字串就代表字元0在工行WAP銀行像驗證碼的值了;以此類推,我們可以得到一個完整的,代表映像0~9的數組,字元表的順序為0~9,A~Z,a~z。
現在,圖片點陣數組已經取得了,接下來我們看看如何把一個圖片識別出來吧!已知如下的點陣表:
string[] CodeArray = new string[] {
"0011100011011011000111100011110101111010111100011110001101101100011100","001100011100111100001100001100001100001100001100001100111111","0111110110001100000110000110000110000110000110000110000011000111111111","0111110110001100000110000011001111000000110000011000001111000110111110","0000110000111000111100110110110011011111110000110000011000001100001111","00011111000110000001100000011000000111111000000010000000100000001001100000001111","001110011000110000110000111111110001110001110001110001011111","00011111000110000000000000000000000000011000001110000110100001101000011000000110","0111110110001111000111100011011111011000111100011110001111000110111110","0111110110001111000111100011011111100000110000011000001100001100111100" };
開始處理比較操作:
StringBuilder sbResult = new StringBuilder();
{
for (int i = 0; i < 4; i++)
{
string code = GetSingleBmpCode(pics[i], 128);
//得到代碼串
System.Collections.Generic.Dictionary<char, double> EqualsPercentList = new Dictionary<char, double>(); //建立差異程度列表
for (int arrayIndex = 0; arrayIndex < CodeArray.Length; arrayIndex++)
{
if (arrayIndex < 10)//0~9
{
EqualsPercentList.Add((char)(48 + arrayIndex), 100);
//數字轉字元,c文法習慣
}
}
for (int arrayIndex = 0; arrayIndex < CodeArray.Length; arrayIndex++) //和點陣表內的字元序列進行比較
{
if (arrayIndex < 10)//0~9
{
EqualsPercentList[(char)(48 + arrayIndex)] = EqualsPercent(code, CodeArray[arrayIndex]);
//每一個映像的點陣的差異度,這裡的差異度運算是誤差程度,也就是不同的百分比
}
進行獲得匹配
{
double Perc = 20; //差異百分比必須小於20%,否則肯定不對
string SelectKey="";
foreach (char key in EqualsPercentList.Keys)
//擷取匹配程度列表中最匹配的一項
{
if (EqualsPercentList[key] < Perc)
{
Perc = EqualsPercentList[key];
SelectKey = key.ToString();
}
}
sbResult.Append(SelectKey);
}
至此,sbResult中的4個數字就是映像上的4個數字了。
結論
驗證碼識別肯定不只是這麼簡單,但現在還是有很多網站都在用這種未經任何變換的驗證碼,所以我們的網路安全還任重而道遠。
上面的驗證碼識別是一個最基本的演算法,但是很多擴充演算法都可以基於上面的思路進行擴充。例如有些驗證碼進行了旋轉輸出,那麼上面的程式可以在校對的時候, 進行360度旋轉,旋轉後的映像再取得序列,再和映像序列比較,直至得到最符合的。有些驗證碼添加了邊框,此時我們可以先去掉邊框再進行切割匹配。
通過上面的演算法可以得出,我們今後在設計驗證碼的時候,應該注意如下因素:
1)在噪音等類型的使用上,儘力讓字元和用來混淆的前景和背景不容易區分,儘力讓噪音長得和字母一樣。
2)特別好的驗證碼的設計,要儘力發揮人類擅長而AI演算法不擅長的。比如粘連字號的分割和手寫體(通過印刷體做特別的變形也可以),而不要一味的去加一些看起來比較複雜的噪音或者其他的花哨東西。
3)從專業的機器視覺的角度來說,網路安全驗證碼的設計,一定要讓破解者在識別階段,反覆在低階視覺和高階視覺之間多反覆幾次才能識別出來,這樣可以大大降低破解難度和破解的準確率。