许多网站在接受HTTP输入的时候常用到一项认证码技术来防止DOS攻击。简单地说就是在显示提交数据页面之前或者同时,服务器会向客户端发送一个小图片,该图片通常由一些随机的数字或字母(很少情况下会有其他字符)组合而成,并要求在HTTP的提交表单(FORM)中正确输入该数字,无效的输入会使数据提交失败。这样的机制可以有效阻止或降低一些登陆/张贴/投票的自动机对网站产生的负面作用。
作者:doublelee
日期:2005-5-5
概述:
许多网站在接受HTTP输入的时候常用到一项认证码技术来防止DOS攻击。简单地说就是在显示提交数据页面之前或者同时,服务器会向客户端发送一个小图片,该图片通常由一些随机的数字或字母(很少情况下会有其他字符)组合而成,并要求在HTTP的提交表单(FORM)中正确输入该数字,无效的输入会使数据提交失败。这样的机制可以有效阻止或降低一些登陆/张贴/投票的自动机对网站产生的负面作用。
早期的认证图片是一个图片库的形式存放在服务器端,每次随机选取一幅图片发送到客户端。对这样的认证机制有一个相当简单的攻击方法就是,攻击者可以用自动机经过一定时间穷举,下载所有的图片并经过人工辨认,存入攻击者的数据库,攻击时,根据得到的图片或者甚至是其校验和,在数据库中查询其对应的认证码即可。对于一个4位数字的情况,攻击者只要存储一万张图片即可,这其实是一个很小的工作量。
针对上述攻击方法,现在有很多jsp,php,cgi的脚本可以动态生成这样的认证图片,即先随机产生认证码,再根据认证码动态生成图片。这样同样的认证数字生成的图片完全不同,使得储存图片库的攻击方式失效。但也许由于技术原因限制,动态生成的图片在字体变形方面通常做的很差,大多数甚至完全没有变形机制,这样的情况下,编写程序识别这样的图片也就成为了可能。
本文就以国内某网站为例,谈一下编写这样的图片自动识别工具的思路。
正文:
首先图片格式不外乎bmp,jpg,gif,可以通过开源程序全部转换为统一格式,故不予考虑。这里只考虑bmp格式作为目标,因为这是最简单和易于处理的格式。
现在我们来分析图片的构成,发出了50个请求之后,我们得到了这样50个图片,其中包含一对同样数字图片 7208
经过分析,图片内容为4位数字,不含字母或其他符号。图片格式为bmp,所有图片具有统一的大小,宽,高。经过查询bmp文件格式文档,发现其调色板也完全相同,以2字节代表一个象素。另外还发现图片所有的数字所处的相对位置固定,大小固定,字体固定,变化的只是颜色和背景中一些随机的黑色斑点。而且颜色上,也许是考虑色盲用户的存在,并没有过于鲜艳的颜色,也就是说基本上数字轮廓内的颜色以深色高灰度为主,变化并不大。背景中的斑点也是一样。数目约40个象素左右(图片共17*60=1020象素)
进一步分析,确定各数字所处的位置,所占的象素。由于bmp文件格式很简单,这个工作细心一点很快就可以完成。得到数据如下:
//图片高17,宽60,以下x,y是第一象限坐标
//每个数字高13,宽10,字间间隙为3
//起始坐标依次在(x,y) = (7,1), (20,1), (33,1), (46,1)
//每个象素占用2个字节
根据以上这些数据,可以在程序中轻松定位每一个数字并提取出代表该位置数字的所有象素的数据信息,这些信息可以存在一个2*13*10=260字节的一个数组里。
下面的问题是如何准确判断这260个字节数据代表的数字,这涉及到一个模糊匹配的问题。好在在本例中,匹配目标只有十个,干扰元素影响也不是很大。经过几次摸索,我认为产生10个标准的匹配目标即可,这个匹配目标我称之为数字模板(module),共10个,分别代表0~9数字的典型灰度分布,每个数字模板也占用260个字节。这样以后将每次得到的260字节数据与之比较,差异最小的即认为最匹配即可。
现在产生两个问题,一,模板文件哪里来?二,比较函数用什么公式?
对于第一个问题,我可不想260个字节一个字节一个字节地手工查找和填写,2600次,很累的也!那么想办法写程序统计。最开始我是统计所有的相同数字,取平均值,例如每看到一个0,假设前面已经统计过k个0,模板中的对应字节的值是前k个数字0统计的平均值,那么看到第k+1个0的图片p0(k+1)之后,修正模板为
m0(k+1)=(m0(k)*k+p0)/(k+1)
但是随即发现问题,对于不同的颜色,p0(k)的差异很大,平均值不能体现问题。更为严重的是,作为整数,这样的包含除法的平均值计算公式并不好用,误差十分的大,经过一些轮次的计算后,模板数据变的一塌糊涂,新数据也很难以起作用了。
再次对数据进行仔细地观察,发现深色象素处,高位字节的值一般小于0xc0,但具体的值由颜色决定,差异很大,但浅色象素的高位字节大于0xc0,肉眼观察图片实际上得到的印象只由象素的灰度决定,具体颜色并不要紧。因此我作了一个修正,就是取0xc0为阈值,大于此数的取1,小于此数的取0,用加法修正到模板对应的字节上,同时对每个模板增加一个记数器module_weight,记录模板是统计过多少个源图片而得来的。并且如果统计到255个就不再统计。保证各模板的数据基本权重相等。后来证明这个做法很有效。
第二个问题,我开始想使用各字节差的绝对值之和,即
Sigma(abs(x(i)-m(i))) i=0,1,...259
来表示,这里x(i)表示要判断的图片的第i个字节,m(i)表示模板中第i个字节的值。
后来发现这个数字的值受颜色影响比较大,那么我们需要增大每一对象素差异对整体比较函数的影响。为了达到这一目的,除了修改模板文件以外,还必须对判断公式进行调整。如果学过概率论的一些基本知识就知道,统计其二阶中心矩,也就是方差将是一个不错的选择。
现在公式调整为
Sigma ((x-m)^2)
好了,基本问题都解决了。调整程序试试吧。注意到每个象素是2字节,即一个short型数据,两个short型数据的差的平方应该用int型变量记录,这样260个int变量加起来会溢出的。为了程序简洁,不处理大整数,我将每个象素的2字节分开处理,其平方用short即可记录,260个unsinged short变量的和,哈哈,unsigned int怎么也不会溢出了吧。
搞定!
程序:
以下是c的源代码,还不到200行,处理bmp文件格式,包含了生成模板的功能,即study函数
//-------------------------------------------------------------------------------------------
// File Name: myocr.c
// special crafted for xxxx.com's image.
// by doublelee@etang.com
#include
#include
#define MOD_FILE "digit.mod"
unsigned char map[4][260]; //每图4个数字,每个数字用260个byte表示
unsigned char module[10][260]; //模板10个,代表0~9,也用260 bytes.
unsigned char buf[0x083a]; //文件大小,固定
unsigned char module_weight[10];//训练次数,增到255就不再增
unsigned int diff, tmpdiff ; //diff表示该数字与模板的相似度,现在用 sigma ((x-m)^2) 判断。
//tmpdiff表示到现在为止最相似的值。越小越相似
char value[4]={0}; //储存结果,值必定在0~9之间
void readmap() //从buf的相应位置读入图片数据
{
int i; //第i个数字
int x,y; //坐标
for(i=0; i<4; i++)
for(x=0; x<20; x++)
for(y=0; y<13; y++)
{
map[i][x+y*20] = buf[ 0x42 + (7+13*i+60*(y+1))*2 + x ];
//图片高17,宽60,x,y是第一象限坐标
//每个数字高13,宽10,字间间隙为3
//起始坐标依次在(x,y) = (7,1), (20,1), (33,1), (46,1)
//每个象素要2个字节,因此有以上公式,0x42是BMP头长
//其实还可以改进,每个象素只判断高位字节(和灰度关系比较大?),准确性应该还可以提高
//现在的测试结果已经比较令人满意
}
return;
}
void outputvalue() //输出结果
{
int i;
for(i=0;i<4;i++)
fprintf(stdout, "%d", value[i]);
return;
}
int read_mod()
{
FILE* fp;
fp = fopen(MOD_FILE, "r+b");
if(!fp)
{
memset(module, 0, 10*260);
memset(module_weight, 0, 10);
return 0;
}
fread(module, 10, 260, fp);
fread(module_weight, 10, 1, fp);
fclose(fp);
return 1;
}
int write_mod()
{
FILE* fp;
fp = fopen(MOD_FILE, "w+b");
if(!fp)
return 0;
fwrite(module, 10, 260, fp);
fwrite(module_weight, 10, 1, fp);
fclose(fp);
return 1;
}
int checkbmp(FILE* fd)
{
int x;
if(!fd)
return 0;
fread(&x, 1, sizeof(int), fd);
if(x!=0x083a4d42) return 0;
fseek(fd, -1, SEEK_END);
x = ftell(fd);
if(x+1!=0x083a)
return 0;
//以上是基本检查,其实可以检查更多的BMP头结构字段
rewind(fd);
fread(buf, 1, 0x083a, fd);
readmap();
return 1;
}
void checkdigit() //判断函数
{
int i,j,k;
unsigned char x;
for (i=0;i<4;i++)
{
diff = 0;
tmpdiff = 0xffffffff;
for (j=0;j<10;j++)
{
diff = 0;
for (k=0;k<260;k++)
{
x = module[j][k]>map[i][k] ?
(module[j][k]-map[i][k]) :
(map[i][k]-module[j][k]);
diff += x*x;
}//计算 Sigma ((x-m)^2)
if(diff{
value[i] = j;
tmpdiff = diff;
}
}//遍历完10个模板
}//4个数字全部解决
}
int study() //训练函数
{
int i,j;
for(i=0;i<4;i++)
{
if(module_weight[value[i]] != 0xff)
{
for(j=0;j<260;j++)
{
module[value[i]][j] += (map[i][j]>0xc0? 1:0 );
}
module_weight[value[i]]++; //又训练完一次
}
}
return 0;
}
int main(int argc, char** argv)
{
FILE* fd;
int i,ret ;
read_mod();
if(argc == 1)
fd = stdin;
if(argc > 1)
{
fd = fopen(argv[1],"rb");
if(!fd)
{
fprintf(stderr, "open file %s failed ", argv[2]);
return 0;
}
}
if(argc > 2) //后跟训练数字
{
for(i=0;i<4;i++)
{
value[i]=argv[2][i]-0x30;
if(value[i]>9 || value[i]<0)
{
fprintf(stderr, "wrong number %s ", argv[2]);
return 0;
}
}
checkbmp(fd);
ret = study();
write_mod();
return ret;
}
if(checkbmp(fd))
{
checkdigit();
outputvalue();
}
if(fd) fclose(fd);
return 1;
}
//----------------------------------------------------------------------------------------------------------------
这个程序维护和依赖一个模板文件digit.mod,如果只有一个参数,则认为是bmp文件名,根据模板文件判断其所代表的数字,例如
myocr 2110.bmp
如果有两个参数,则认为进入学习模式,根据argv[1]代表的bmp文件和argv[2]代表的数字,对模板文件digit.mod进行修改。例如
myocr 2110.bmp 2110
我写了一个bat文件执行所有的训练,多次训练结束后,digit.mod文件中所有的module_weight都变为0xff,就可以了。
以上是针对某个特定网站的图片识别程序,对其中的参数稍加调整就可以对很多这样的图片实现程序辨认。
结论:
纵观整个认证码技术体系,现在基本可以分为静态图片库技术和动态生成技术。
静态图片库方式的确可以通过强烈的变形技术防止程序识别,但太少的图片数目容易被穷举,而生成这样的图片库需要人工参与,否则难以保证可识别程度。所以代价偏高。
动态图片技术由于程序复杂度原因,并兼顾到可识别性,往往不能做到很好的变形甚至没有变形,这样给程序实现攻击提供了可乘之机,对这样的动态图片的攻击可以总结为一句话,“用程序生成的东西是很容易被程序辨认出来的。”
事实上图片认证技术对防止WEB机器人确实起到了一定的作用,但是由于其实现的局限性,并不能完全依赖这种技术保证网站的安全性。