转载请注明出处:http://blog.csdn.net/tang9140/article/details/42869269
首先申明下,本文章纯作为个人喜好的技术性研究,请不要用于非法操作盈取不正当利益,你懂的。
问题引出
大概在2014年12月的17、18号,大量针对12306的刷票软件出现了非法请求或‘使用第三方购票软件’提示,并且验证码识别出错。后面得知12306为了对恶意抢票软件、插件进行遏制与防控,通过手段能识别出抢票软件和用户购票的行为差别。
那么问题来了。。。,我就很好奇,他们通过什么手段做到的呢。
补充下,我之前是有做过JAVA版的抢票软件,仅做学习用途,没其它目的。在12306加了第三方软件检测后,我的刷票软件同样出现了‘非法请求’提示,连登录都进不去。于是引发了我的思考。
抢票软件本质是什么?
我个人认为抢票软件无非是用机器人(实际上所谓的机器人就是一个软件,这里只是形象表述)代替实体人去购买车票。大家都知道机器反应快,因此在同时开抢的前提下,肯定是机器比实体人快,这也是为什么黄牛党能买到车票,而你买不到车票的原因。继续往下思考,为什么机器人能够代替实体人去进行买票的一系列操作?这就引出了下一个问题。
用户在浏览器上购买车票的一系列操作,从技术角度看,本质上发生了什么?
大家都知道,12306网站提供的是基于B/S架构的WEB服务,是建立在http协议之上的服务。http协议是典型的请求-应答模式的协议,是无状态的协议。实际上用户在浏览器上的所有操作,最终都是依托浏览器发送请求到服务端,服务端接收到请求后进行相应的业务处理并将响应结果返回到浏览器端,浏览器再显示给用户。更具体点说,浏览器接收用户的各种事件(例如鼠标单击事件,键盘输入事件)后,然后在后台发送http请求到服务端,同时浏览器后台会接收到服务端的响应内容并展示为HTML页面。
更一般化,对于服务器来说,只要收到的请求是符合HTTP协议的就会进行处理,它不关心请求是通过浏览器发送过来的还是刷票软件发送过来的(浏览器本身也是一个软件,一般操作系统都自带浏览器)。从上面分析可以看出,只要第三方软件去完全模拟浏览器发送符合规范的HTTP请求,WEB服务器就会当作合法的请求并进行处理。那么问题又来了,为什么刷票软件会出现“非法请求”提示呢,这不跟你刚才的分析有矛盾吗?这个嘛,其实不矛盾。请注意我刚才说的是“完全模拟浏览器”,之前作为学习版的抢票软件并没有严格按照浏览器方式去发送HTTP请求,这也就导致12306能通过一些技术手段检测出非法的请求。那么,引出了我们的终极问题
12306怎么进行技术检测的,怎么区别正常请求跟非法请求?
在回答这个问题前,大家先要了解下HTTP协议。HTTP请求消息分为四部分:请求行、请求头、空行、可选的请求消息体;共有八种请求方法,最常用的就两种:GET请求和POST请求。GET请求将参数带在URL后面(没有消息体),而POST请求将参数带在消息体中。请求头中可能包含Cookie信息等。回到正题,12306对于非法请求检测无非对三方面进行检查,即请求头、Cookie、请求参数。
12306检测三方面:
1、请求头
在模拟HTTP请求时,需要注意请求头的顺序。经过本人测试,如果登录请求的‘Cookie请求头’放置在‘Connection请求头’后面时,会提示‘非法请求’
2、Cookie
Cookie请求头同样要注意顺序,先是JSESSIONID(中间其它cookie)最后是BIGipServerotn,current_captcha_type
3、请求参数
还是顺序的问题,请严格参照浏览器发送请求参数的顺序进行发送。除此之外,12306在登录和提交订单时还增加了动态key验证。你需要先获取到动态js文件的url地址,然后访问该js文件内容,提取出其中的key值,并用该js文件中的加密算法对key加密后形相应的value值。
通过上述三方面的检测,就能够发现一些非法请求。当然有第三方软件检测,也就有反检测。在12306推出这一系列第三方软件检测、监控及更换验证码(我相信有三套不同的验证码)后的数小时,一些刷票软件就进行了破解。其实本人觉得所谓的第三方软件检测完全是治标不治本,就好像游戏中的反外挂检测一样,在强大的外挂研发人员看来,完全不堪一击。不管你加了多少次验证,不管你怎么更换验证规则,都会很快的被破解。我倒觉得,与其把时间花在怎么反外挂上,还不如内部提供外挂,借鉴游戏的做法,12306倒不如内部提供一键抢票功能,不需要经过选车次,选乘客,提交下单验证码,再次确认等复杂过程。直接按照用户事先设定好的规则一键抢票(需要保证用内部一键抢票功能比外部刷票软件抢到票的概率要高),这样还来得实在些,你怎么看呢。支持的赞个
码字不易,源头来自http://blog.csdn.net/tang9140。
另附上关于KEY加密算法的JAVA版实现代码,感兴趣的同学可以看下
public class DynamicJsUtil { private static String keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; static class Base32 { private static int delta = 0x9E3779B8; public static String longArrayToString(int[] data, boolean includeLength) { int length = data.length; int n = (length - 1) << 2; if (includeLength) { int m = data[length - 1]; if ((m < n - 3) || (m > n)) return null; n = m; } StringBuilder sb = new StringBuilder(""); for (int i = 0; i < length; i++) { int i0 = data[i] & 0xff; int i8 = data[i] >>> 8 & 0xff; int i16 = data[i] >>> 16 & 0xff; int i24 = data[i] >>> 24 & 0xff; if (i0 != 0) sb.append((char)i0); if (i8 != 0) sb.append((char)i8); if (i16 != 0) sb.append((char)i16); if (i24 != 0) sb.append((char)i24); } String result; if (includeLength) { result = sb.substring(0, n); } else result = sb.toString(); return result; } public static int[] stringToLongArray(String str, boolean includeLength) { int length = str.length(); int arrsize = length % 4 == 0 ? length / 4 : length / 4 + 1; int[] result = new int[arrsize]; for (int i = 0; i < length; i += 4) { if (i + 4 > length) { int char8 = i + 1 >= length ? 0 : str.charAt(i + 1) << 8; int char16 = i + 2 >= length ? 0 : str.charAt(i + 1) << 16; int char24 = i + 3 >= length ? 0 : str.charAt(i + 1) << 24; result[i >> 2] = str.charAt(i) | char8 | char16 | char24; } else result[i >> 2] = str.charAt(i) | str.charAt(i + 1) << 8 | str.charAt(i + 2) << 16 | str.charAt(i + 3) << 24; } if (includeLength) { int[] newArr = new int[arrsize + 1]; System.arraycopy(result, 0, newArr, 0, arrsize); newArr[arrsize] = length; result = newArr; } return result; } public static String encrypt(String str, String key) { if (str == "") { return ""; } int[] v = stringToLongArray(str, true); int[] k = stringToLongArray(key, false); if (k.length < 4) { int[] newArr = new int[4]; System.arraycopy(k, 0, newArr, 0, k.length); k = newArr; } int n = v.length - 1; int z = v[n], y = v[0]; int mx, e, p, sum = 0; int q = 6 + 52 / (n + 1); while (0 < q--) { sum = sum + delta & 0xffffffff; e = sum >>> 2 & 3; for (p = 0; p < n; p++) { y = v[p + 1]; mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z); z = v[p] = v[p] + mx & 0xffffffff; } y = v[0]; mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k[p & 3 ^ e] ^ z); z = v[n] = v[n] + mx & 0xffffffff; } return longArrayToString(v, false); }; } public static String encode32(String input) { input = escape(input); StringBuilder output = new StringBuilder(); int length = input.length(); int chr1, chr2, chr3; int enc1, enc2, enc3, enc4; int i = 0; do { chr1 = input.charAt(i++); enc1 = chr1 >> 2; output.append(keyStr.charAt(enc1)); if (i < length) { chr2 = input.charAt(i++); enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); output.append(keyStr.charAt(enc2)); if (i < length) { chr3 = input.charAt(i++); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; output.append(keyStr.charAt(enc3)).append(keyStr.charAt(enc4)); } else { enc3 = ((chr2 & 15) << 2) | (0 >> 6); output.append(keyStr.charAt(enc3)).append(keyStr.charAt(64)); } } else { enc2 = ((chr1 & 3) << 4) | (0 >> 4); output.append(keyStr.charAt(enc2)).append(keyStr.charAt(64)).append(keyStr.charAt(64)); } } while (i < length); return output.toString(); } static String bin216(String s) { s += ""; String output = ""; int l = s.length(); for (int i = 0; i < l; i++) { char c = s.charAt(i); String temp = Integer.toString(c, 16); output += temp.length() < 2 ? "0" + temp : temp; } return output; } public static String escape(String src) { char j; StringBuffer tmp = new StringBuffer(src.length() * 2); for (int i = 0; i < src.length(); i++) { j = src.charAt(i); if (Character.isDigit(j) || Character.isLowerCase(j) || Character.isUpperCase(j)) tmp.append(j); else if (j < 256) { if (j == '*' || j == '@' || j == '-' || j == '_' || j == '+' || j == '.' || j == '/') { tmp.append(j); } else { tmp.append("%"); if (j < 16) tmp.append("0"); tmp.append(Integer.toString(j, 16).toUpperCase()); } } else { tmp.append("%u"); tmp.append(Integer.toString(j, 16)); } } return tmp.toString(); } /** * 获取动态加密value值 * @param key * @return */ public static String getRandomParamValue(String key) { return encode32(DynamicJsUtil.bin216(Base32.encrypt("1111", key))); } }