验证码识别思路
因为某些原因,最近写了个不停注册某网站账号的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或者网络线路发放?真是魔高一尺,道高一丈……
我表示还是喜欢这种类型验证码,最好是还可以暴力穷举: