因為某些原因,最近寫了個不停注冊某網站賬號的chrome擴展。(算外掛嗎?)
該網站注冊時需要輸入驗證碼,且單次有效,所以窮舉不可取。(驗證神馬的,最討厭了!)
================================================================
首先要確定驗證碼圖片是實時生成的還是只是靜態圖片,收集大量驗證碼看看是不是有大量相同的:
最近erlang代碼寫得比較多,就用erlang實現了,至於存儲,直接放磁盤算了。
-export([start/0]).
start() ->
inets:services(),
inets:start(),
S = lists:sum([get_img(X)||X<-lists:seq(1,10000)]),
io:format("~p/~p",[S,10000]).
get_img(N) ->
io:format("~p~n",[N]),
UrlPic = "http://網址就不公開了/xxx.do?yyy=zzz",
case httpc:request(UrlPic) of
{ok,{_,_H,Data}} ->
Bin = list_to_binary(Data),
<<Id:128>> = erlang:md5(Bin),
FileName = lists:flatten(io_lib:format("img/~.16b.jpg",[Id])),
case file:read_file_info(FileName) of
{ok,_} -> io:format("exists: ~p~n",[FileName]),1;
_ -> file:write_file(FileName,Bin),0
end;
Why ->
io:format("~p~n",[Why]),
get_img(N)
end.
代碼里對獲得的圖片計算了標准md5校驗碼作為圖片文件名,判斷兩個圖片是否完全一致的辦法是看兩個圖片的校驗碼是否一致。
測試發現,當收集了300多個圖片后開始出現重復。超過5000個圖片后,碰撞率變得很高。最終當我收集了1w8張驗證碼圖片時,圖片命中率高達97%。
也許有人覺得單看驗證碼判斷圖片是否一致的方法有問題,但如果100張圖片中有97張的驗證碼都和之前的某張的一樣,但實際內容卻不同,那就見鬼了。由於我好奇心比較重,為了看看有沒有這種見鬼的事,我改了下上面的代碼,將md5沖突的新圖片保存下來人工確認,見鬼的事沒有發生,內容長度確實是完全一模一樣。
================================================================
以上說明該網站的驗證碼就是預先一大堆靜態圖片,估計總數大概是2w,每次請求時隨機取一張圖片返回。這樣做的原因估計是這么做對性能影響很小。
這么算吧:2w張圖片,人工寫1000個映射關系,命中率就5%,每次請求一幅圖片的時間不會超過500ms,平均不用10秒遇到一張已經知道驗證碼的圖片。於是,於是……於是我兩天晚上抽空填了3000個驗證碼,最后速度嘛,大概5s一個賬號。
可能有人會好奇我怎么輸入3000個驗證碼,其實是我特地寫了個小程序方便錄入:
================================================================
問題雖然基本解決,但我還是更傾向於用代碼來識別驗證碼。
感覺erlang做GUI程序不方便,還是用回C#來做這部分工作。
從網上找了好幾個.NET下的OCR引擎,識別率極低:識別出一個數字的概率大概等於瞎貓碰上死老鼠。看來靠第三方是搞不定的了,還得靠自己。
觀察驗證碼的特點:所有驗證碼都是4位0-9整數,80x30像素,1.4kB左右,色調比較單一,數字都稍微扭曲了點,但不同驗證碼的同個數字的相似度非常高:
我相信看到這,熟悉Matlab的同學一定樂死了。但是,我不熟悉,還是用土方法吧:對圖片取灰度,截取4個小圖,抽取0-9十個樣本,分析時每個數字與樣本匹配,相似度最高的就是對應數字。
很不幸,這種方法識別出來的數字也不是很靠譜,四個數字通常只能識別兩三個,完整識別出來的幾乎沒有。正當我准備對圖片進行歸一化調整時,我發現驗證碼圖片中,相同位置的相同數字相似度極高,但不同位置的相同數字相似度倒是不高。
既然規律找到了,方案自然就出來了:在取樣本時,每個位置的數字的樣本分開管理,下面是核心匹配代碼:
private int parse_one(int[] hash,int n)
{
int x = 0;
int r = 0;
for (int i = 0; i < 10; i++)
{
var r2 = match(hash,n,i);
if (r2 > r) { r = r2; x = i; }
}
return x;
}
private int match(int[] hash, int n, int i)
{
int sum = 0;
int o = i * H * W * 4 + n * W;
for (int x = 0; x < W; x++)
{
for (int y = 0; y < H; y++)
{
int g1 = cache[o + x + y * 4 * W];
int g2 = hash[x + y * W];
if (g1 > 200 && g2 > 200)
{
sum += 1000;
}
else if (g1 < 50 && g2 < 50)
{
sum += 5000;
}
else
{
sum += (255 - Math.Abs(g1 - g2));
}
}
}
return sum;
}
最終識別率:幾乎100%(試了10幾張沒發現哪個驗證碼圖片識別不出,還是低調點,90%吧)
后面就是寫個批處理功能,把收集到的驗證碼轉換成JS,供chrome擴展直接查表使用了,最終效果大概是2秒一個賬號。至於chrome擴展怎么獲取驗證碼圖片,怎么算md5,這些都不是本文的重點,在此略過。
這個網站的驗證碼方案有幾點不夠安全的地方:
1. 使用靜態驗證碼,且靜態驗證碼總數較少(后來發現原來隔一段時間就換一次,汗);
2. 各驗證碼圖片的相似度太高,隨機性不足,識別難度頗低;
3. 對於短時間注冊大量賬號的IP沒有相關應對措施。
================================================================
后記:
昨天到別的機子運行發現這個神一樣的注冊機用了沒多久竟然不靈了,驗證碼突然一個都識別不出,觀察發現該網站的驗證碼又不一樣了,暴汗,一天換一批注冊碼。
還好不是用人工輸入的辦法,打算回來繼續錄入樣本識別,發現家里的獲得的驗證碼的圖片還是原來的,莫非這些驗證碼還根據IP或者網絡線路發放?真是魔高一尺,道高一丈……
我表示還是喜歡這種類型驗證碼,最好是還可以暴力窮舉: