福建省高考成绩暴力查询踩坑集
zsx2016/6/23 in 记录整理 / 4 / 2524

6月25号,福建省高考成绩新鲜出炉。

作为2016届福建省的一名高考生,我自然是非常在意成绩的。支付宝等公司推出了一些“高考成绩预约”服务,但是老实说我还不是很信任它们;它们的时效因为用户数量多所以也往往不足。到高考放榜当天,考试查询服务器肯定会被挤爆。为了早点知道自己的考试成绩,也为了挤上小霸王服务器,于是我打算由计算机定时抓取,结果,踩坑踩了半天。

本文涉及以下方面:验证码识别、RSA算法exponent、ScriptControl的使用。


验证码识别



打开查询系统,看到验证码。首先要做验证码识别。于是,拖回100个验证码。


blob.png


可以看到的是,这种验证码破解非常简单。它符合以下特征:

  1. 字符位置固定

  2. 字符旋转角度固定

  3. 字体固定

  4. 无粘连、噪音干扰小

  5. 背景和字体区别度大

不得不说,这是一个拿来识别验证码的比较好的入门教材,也正适合我来进行学习。

我的处理方案是:(最简单的)二值化 -> (最简单的)去噪 -> (最简单的)切割 -> (最简单的)转字符串 -> (最简单的)查字模库 -> 出结果

技术选型

原来打算用Nodejs / Python,不过它们的图形库要安装的依赖太多,在Windows下安装麻烦(我都不敢相信,装GTK是要做什么)。于是选择了C#。使用jTessBoxEditor来训练tesseract效果很不理想,而且反而让我发现了这俩玩意的一堆麻烦BUG(似乎是依赖库的锅)。那就干脆手写好了。




预处理

二值化

这里有一张验证码。

blob.png


观察其特征:背景颜色为淡色,验证码字符在其上以明显区别于背景色的颜色显示。这样的验证码通过二值化算法进行处理是实现起来比较方便。二值化,是把图片处理成只有两种颜色的处理方式。比如这张图,我们期望把背景抠成白色,但是验证码字符全部黑色且清晰可见。这样,降低了计算的复杂度,不丢失重要的【验证码】信息,非常方便后续操作。

图像二值化算法有很多种,适用于不同的用途:

blob.png

当然,这张图只需要灰度化 + 阈值处理即可。先将其灰度化。

blob.png

灰度化的目的是,把背景颜色变淡,验证码字体颜色变深,从而排除深浅色的干扰,保证后续把图片转为仅黑白两色时判断无误。因为我们使用的二值化算法实在是太简易了,这种预处理也正好可用。我们可以直接对比这两张验证码没有经过灰度化时,被下一步处理的样子:

未灰度化

blob.png

灰度化

blob.png

话说回来,一个好的二值化算法不会因为图片被弄成这样就丢失重要信息的。不过这也无所谓了,我识别的就是这种简单的验证码。

        /// <summary>

        /// Binarizate the specified original bitmap.

        /// </summary>

        /// <param name="originalBitmap">The original bitmap object.</param>

        /// <returns></returns>

        public static Bitmap Binarizate(Bitmap originalBitmap)

        {

            var newBitmap = Clone(originalBitmap);

            for (int i = 0; i < newBitmap.Width; i++)

            {

                for (int j = 0; j < newBitmap.Height; j++)

                {

                    var color = newBitmap.GetPixel(i, j);



                    /// <see cref="http://www.scantips.com/lumin.html"/>

                    /// <see cref="http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color"/>

                    var gray = (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11); 

                    var newColor = Color.FromArgb(gray, gray, gray); // 此像素点灰度化

                    var bppColor = newColor.B < 127 ? Color.Black : Color.White; // 判断灰度化后的像素点的蓝色是否达到一定值,是就白色,否就黑色。

                    newBitmap.SetPixel(i, j, bppColor);

                }

            }

            return newBitmap;

        }


去噪

二值化后,我们能看到它仍然有不少噪点,那就进行去噪处理。

blob.png

再分析一下这个验证码。除了几个大点外,剩下的都是1px宽的线和1像素大小的点。那么,最简单的思路就是判断旁边某个像素点有几个点的颜色是黑色的,就可以了。

随便从不知道哪个角落来抄了一段代码……

        /// <summary>

        /// Removes the noise.

        /// </summary>

        /// <param name="dgGrayValue">The dg gray value.</param>

        /// <param name="MaxNearPoints">The maximum near points.</param>

        /// <returns></returns>

        public static Bitmap RemoveNoise(Bitmap originalBitmap, int dgGrayValue = 128, int MaxNearPoints = 3)

        {

            int nearDots = 0;

            var newBitmap = Clone(originalBitmap);

            for (int i = 0; i < newBitmap.Width; i++)

                for (int j = 0; j < newBitmap.Height; j++)

                {

                    var piexl = newBitmap.GetPixel(i, j);

                    if (piexl.R < dgGrayValue)

                    {

                        nearDots = 0;

                        //判断周围8个点是否全为空

                        if (i == 0 || i == newBitmap.Width - 1 || j == 0 || j == newBitmap.Height - 1)  //边框全去掉

                        {

                            newBitmap.SetPixel(i, j, Color.FromArgb(255, 255, 255));

                        }

                        else

                        {

                            if (newBitmap.GetPixel(i - 1, j - 1).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i, j - 1).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i + 1, j - 1).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i - 1, j).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i + 1, j).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i - 1, j + 1).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i, j + 1).R < dgGrayValue) nearDots++;

                            if (newBitmap.GetPixel(i + 1, j + 1).R < dgGrayValue) nearDots++;

                        }



                        if (nearDots < MaxNearPoints)

                            newBitmap.SetPixel(i, j, Color.FromArgb(255, 255, 255));   //去掉单点 && 粗细小3邻边点

                    }

                    else  //背景

                        newBitmap.SetPixel(i, j, Color.FromArgb(255, 255, 255));

                }

            return newBitmap;



        }

效果如图:

blob.png


切割

原来呢,这里应该用各种【识别粘连】【边缘检测】等算法来处理的。但是!但是!

blob.png

直接用Photoshop的切片工具看。没错,这6个码是平均摆在6个位置的中点然后随便旋转一个角度形成的。这样直接平均切割不就好了吗……随便找了一个图像平均切割算法,切成以下6个图形:

     

第6张图片的8稍有问题,不必在意。本来应该使用某种旋转算法把它旋转回正确的方向的,不过无所谓了。后面可以通过建立字模库来处理这种问题。


转字符串

前面提到,我们已经把这张图变成了【只有黑色和白色的】【只含有1个数字的】六张图片了。为了便于和字模库比对,我们需要把它转为字符串。毕竟,Bitmap没有IndexOf和Like。

具体操作?黑色为1,白色为0,组合输出。

        /// <summary>

        /// Converts Bitmap to String

        /// </summary>

        /// <param name="bitmap">bitmap</param>

        /// <returns></returns>

        public static string BitmapToString(Bitmap bitmap)

        {

            var stringBuilder = new StringBuilder();

            for (int i = 0; i < bitmap.Width; i++)

            {

                for (int j = 0; j < bitmap.Height; j++)

                {

                    var color = bitmap.GetPixel(i, j);

                    stringBuilder.Append(color.R < 127 ? "1" : "0");

                }

            }

            return stringBuilder.ToString();

        }

把六张图悉数转为字符串,即可准备比对。


建立字模库

事实上,这个验证码的旋转角度等是固定的。直接建立字模库,不需要任何数学知识,明显会方便很多。

首先把那100张验证码全部下载下来并按照前面的部分切割成各张小图,并保存。然后人工识别,手动归类。如以下图形都代表着“3”。

blob.png


把每张图片归类后,计算它们对应的字符串。

        public static void InitStore(string storePath)

        {

            for (var i = 0; i <= 9; i++)

            {

                studiedStore[i] = new List<string>();

                Directory.GetFiles($"{storePath}\\{i}").ToList().ForEach(str => {

                    studiedStore[i].Add(BitmapToString(new Bitmap(str)));

                });

            }

        }

把计算出来的值存到什么地方,字模库即建立成功。比如我是在resx里建立的。


字符比对

当我们查询的时候,随便找一个字符比对算法循环查询就好了。最简单的算法就是逐字符比较计算不同字符数量,取最少不同字符的那一项所代表的值为结果即可。



成品

https://github.com/zsxtoys/FjeeaRefresher/blob/master/FjeeaRefresher/ValidationCodeParser.cs


  1. 这码最后两位数字不知道什么情况下会改变,不做任何处理的情况下的建库对这种识别率不高,然而这种东西一变就是几个小时,必须手动加内容。



密码加密

这玩意的密码在发送前也在前端进行了加密,相关代码:

	<script language="JavaScript" src="/UEPORTLET/scripts/secret/BigInt.js"></script>

	<script language="JavaScript" src="/UEPORTLET/scripts/secret/Barrett.js"></script>

	<script language="JavaScript" src="/UEPORTLET/scripts/secret/RSA.js"></script>

	<script language="JavaScript" src="/UEPORTLET/scripts/secret/base64.js"></script>

	<script language="JavaScript">

	<!--

	setMaxDigits(130);

	//加密方法

	function do_encrypt(domEle) {

	  var key = new RSAKeyPair("10001","","8d52ee0f601c87ddf3d9541b5312c8f4ec09315e8e2fd730cac1b7a6df268555a0380a7626eeadac38160bf47253a6b7046f02e325c51823aa2c9bb789405f7387504c5ecb2bd8d28079837f20481e8f698e508114b265a850317d43ff2d9cf5ca66e27b2884218c272600a2695867cbcfb9b4786e8b1355351129f169fe030d");

	  key.radix=Number("16");

	  var pwdVl = domEle.value;

	  if(pwdVl.length > 30){

	  	pwdVl = pwdVl.substring(0,30);

	  }

	  var res = hex2b64(encryptedString(key,pwdVl));

	  domEle.value = res;

	}

	

	//-->

	</script>

先看RSA部分。RSA的exponent部分通常值都是65537,转成base64后是AQAB……What the fuck?这怎么转?

引用文章:http://www.cnblogs.com/midea0978/archive/2007/05/22/755826.html


现在问题又来了,.NET的RSA加密无论如何结果都不对,怎么办?

能注意到,这个网站是能在IE6下跑的。那就说明,只要不用window下浏览器特色API,就可以直接拿给其它js引擎跑。为了这种事情,上Edge库是有点“杀鸡焉用牛刀”。不过,Windows系统内有内置JScript脚本引擎,并且有多种调用方式,包括Windows Script Host等。这里,我们可以用ScriptControl来调用。

事实上,我们只要调用一个COM对象就好了。比如说,这样:

        Type ScriptType = Type.GetTypeFromProgID("ScriptControl");

        object ScriptControl = Activator.CreateInstance(ScriptType);

        ScriptType.InvokeMember("Language", BindingFlags.SetProperty, null, ScriptControl, new object[] { "JScript" });

        ScriptType.InvokeMember("AddCode", BindingFlags.InvokeMethod, null, ScriptControl, new object[] { FjeeaResource.Properties.Resources.RSAJavaScript });

        string ret = ScriptType.InvokeMember("Run", System.Reflection.BindingFlags.InvokeMethod, null, ScriptControl, new object[] { "do_encrypt", Password }).ToString();

        return ret;

成品:

https://github.com/zsxtoys/FjeeaRefresher/tree/master/FjeeaRefresher


结局

成绩在6月23日下午四点猝不及防地就出来了,而且我查成绩的时候服务器一点也不挤。

扔到GitHub去吧:https://github.com/zsxtoys/FjeeaRefresher


所以这个程序有卵用啊?


参考资料


Tesseract Open Source OCR Engine (main repository)

https://github.com/tesseract-ocr/tesseract

jTessBoxEditor

http://vietocr.sourceforge.net/training.html

System.Drawing Namespace

https://msdn.microsoft.com/en-us/library/system.drawing(v=vs.110).aspx

Merging two tiff image using c#.net - Stack Overflow

http://stackoverflow.com/questions/6548771/merging-two-tiff-image-using-c-net

image - Formula to determine brightness of RGB color - Stack Overflow

http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color

机器自动识别验证码的原理是怎么样的?

https://www.zhihu.com/question/22479139

常见验证码的弱点与验证码识别

http://drops.wooyun.org/tips/141

破解北邮研究生教务系统登陆验证码

http://huangsy.me/2014/05/15/validcode.html

用于验证码图片识别的类

http://www.cnblogs.com/yuanbao/archive/2007/09/25/905322.html

验证码识别实践1:自己动手C#实现

http://blog.csdn.net/stevenkylelee/article/details/8263890

图像处理——灰度化、二值化、膨胀算法、腐蚀算法以及开运算和闭运算

http://blog.csdn.net/hellousb2010/article/details/37939809

C#简单数字验证码解析

http://www.cnblogs.com/ivanyb/archive/2011/11/25/2262964.html


如果本文对你有帮助,你可以用支付宝支持一下:

Alipay QrCode
杨乐 at 2016/11/29[回复]
这么小豆这么牛逼了,前途不可估量啊
Spikef at 2016/7/1[回复]
没记错的话,ScriptControl只能在编译为32位版本的时候才能用。
zsx at 2016/7/1[回复]
这个我倒是没试过;不过真的只有32才能用的话,还有WebBrowser和创建CScript进程这两条路子走,怎么走都不虚