木魚 发表于 2013-9-13 01:27:11

2013.911-9.12 12306检测技术浅析

我记得上次有人问我,说今年的12306网站有任何改进吗。我很认真的想了想,说别说改进了,就是外观的改动也没看着。作为对我这句话的回击,一两天后上演了本年度最大的改动,对订票类扩展的检测。其实我很理解不了这种奇葩的逻辑。因为谁都知道封堵不是办法,梳纳才是王道。梳是提高自己的品质,减少用户对第三方的依赖;而纳则是完善自身的规则,尽量缔造完善的操作环境。
12306本身有很多很明显的漏洞和缺陷。比如订票查询必须的5秒钟其实只是界面上的限制,服务器的查询没有任何限制。比如查询结果的mmstr是没有绑定用户的,一个账户查出所有用户可用。比如订单提交页的5秒时间没有任何提示。这些从12306存在的第一天开始就存在,在这些没有被微博大规模转发引人注目时,他们是懒得去修正的。
封堵……这种费力不讨好的事情,看起来很像小孩子才会做的事情。

好吧废话不多说了。说说12306的变更。

首先是验证码地址的变更。这个变更基本上没啥技术含量,但是神奇的是原地址居然还是有效的,便给了用户始终输不对的错觉。
地址从 passCodeAction.do 改为 passCodeNewAction.do。

然后,大招憋出来了。
在登录页面、查询页面、订单提交页面,分别会插入三个动态的脚本文件。基础地址都是这样的:
https://dynamic.12306.cn/otsweb/dynamicJsAction.do?jsversion=7576&method=loginJs

method=后面的内容根据页面不同会有差别,在登录、查询、订单提交页面,分别是 loginJS、queryJS、orderJS。
这个脚本的内容大致相同,有细微差别。典型内容如下:

(function($) {
    function fw(kw) {
      var hasKey = false;
      var values = kw['values'];
      var html = $(kw['key']).html();
      if (html) {
            for (var i = 0; i < values.length; i++) {
                if (html.indexOf(values) > -1) {
                  hasKey = true;
                  break;
                }
            }
      }
      return hasKey;
    }
    function bin216(s) {
      var i, l, o = "",
      n;
      s += "";
      b = "";
      for (i = 0, l = s.length; i < l; i++) {
            b = s.charCodeAt(i);
            n = b.toString(16);
            o += n.length < 2 ? "0" + n: n;
      }
      return o;
    };
    var Base32 = new
    function() {
      var delta = 0x9E3779B8;
      function longArrayToString(data, includeLength) {
            var length = data.length;
            var n = (length - 1) << 2;
            if (includeLength) {
                var m = data;
                if ((m < n - 3) || (m > n)) return null;
                n = m;
            }
            for (var i = 0; i < length; i++) {
                data = String.fromCharCode(data & 0xff, data >>> 8 & 0xff, data >>> 16 & 0xff, data >>> 24 & 0xff);
            }
            if (includeLength) {
                return data.join('').substring(0, n);
            } else {
                return data.join('');
            }
      };
      function stringToLongArray(string, includeLength) {
            var length = string.length;
            var result = [];
            for (var i = 0; i < length; i += 4) {
                result = string.charCodeAt(i) | string.charCodeAt(i + 1) << 8 | string.charCodeAt(i + 2) << 16 | string.charCodeAt(i + 3) << 24;
            }
            if (includeLength) {
                result = length;
            }
            return result;
      };
      this.encrypt = function(string, key) {
            if (string == "") {
                return "";
            }
            var v = stringToLongArray(string, true);
            var k = stringToLongArray(key, false);
            if (k.length < 4) {
                k.length = 4;
            }
            var n = v.length - 1;
            var z = v,
            y = v;
            var mx, e, p, q = Math.floor(6 + 52 / (n + 1)),
            sum = 0;
            while (0 < q--) {
                sum = sum + delta & 0xffffffff;
                e = sum >>> 2 & 3;
                for (p = 0; p < n; p++) {
                  y = v;
                  mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k ^ z);
                  z = v = v + mx & 0xffffffff;
                }
                y = v;
                mx = (z >>> 5 ^ y << 2) + (y >>> 3 ^ z << 4) ^ (sum ^ y) + (k ^ z);
                z = v = v + mx & 0xffffffff;
            }
            return longArrayToString(v, false);
      };
    };
    var keyStr = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
    function encode32(input) {
      input = escape(input);
      var output = "";
      var chr1, chr2, chr3 = "";
      var enc1, enc2, enc3, enc4 = "";
      var i = 0;
      do {
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;
            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }
            output = output + keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
            chr1 = chr2 = chr3 = "";
            enc1 = enc2 = enc3 = enc4 = "";
      } while ( i < input . length );
      return output;
    };
    function aj() {
      var dobj = new Object();
      dobj['jsv'] = window.helperVersion;
      $.ajax({
            url: '/otsweb/loginAction.do?method=el',
            data: dobj,
            type: 'POST',
            success: function(data, textStatus) {
                if (timmer) clearInterval(timmer);
            },
            error: function(XMLHttpRequest, textStatus, errorThrown) {}
      });
    }
    var timmer = null; (function check(src) {
      checkSelf();
      function checkSelf() {
            var formArr = $('form');
            if (formArr.length > 1) {}
      }
      timmer = setInterval(gc, 2000);
    })('1_111');
    $(document).ready(function() { (function() {
            var form = $('#orderForm');
            var oldSubmit;
            if (null != form && form != 'undefined') {
                form.oldSubmit = form.submit;
                form.submit = function() {
                  var keyVlues = gc().split(':');
                  var inputObj = $('<input type="hidden" name="' + keyVlues + '" value="' + encode32(bin216(Base32.encrypt(keyVlues, keyVlues))) + '" />');
                  var myObj = $('<input type="hidden" name="myversion" value="' + window.helperVersion + '" />');
                  inputObj.appendTo($(form));
                  myObj.appendTo($(form));
                  form.oldSubmit();
                  delete inputObj;
                  delete myObj;
                }
            }
      })();
    });
    function gc() {
      var key = 'MjU2MDUwMQ==';
      var value = '';
      var cssArr = ['fishTimeRangePicker', 'updatesFound', 'tipScript', 'refreshButton', 'fish_clock', 'refreshStudentButton', 'btnMoreOptions', 'btnAutoLogin', 'fish_button', 'defaultSafeModeTime', 'ticket-navigation-item'];
      var csschek = false;
      if (cssArr && cssArr.length > 0) {
            for (var i = 0; i < cssArr.length; i++) {
                if ($('.' + cssArr).length > 0) {
                  csschek = true;
                  break;
                }
            }
      }
      if (csschek) {
            value += '0';
      } else {
            value += '1';
      }
      var idArr = ['refreshStudentButton', 'fishTimeRangePicker', 'helpertooltable', 'outerbox', 'updateInfo', 'fish_clock', 'refreshStudentButton', 'btnAutoRefresh', 'btnAutoSubmit', 'btnRefreshPassenger', 'autoLogin', 'bnAutoRefreshStu', 'orderCountCell', 'refreshStudentButton', 'enableAdvPanel', 'autoDelayInvoke', 'refreshButton'];
      var idchek = false;
      for (var i = 0; i < idArr.length; i++) {
            if ($('#' + idArr)) {
                idchek = true;
                break;
            }
      }
      if (idchek) {
            value += '0';
      } else {
            value += '1';
      }
      var attrArr = ['helperVersion'];
      var attrLen = attrArr ? attrArr.length: 0;
      var attrchek = false;
      for (var p in parent) {
            if (!attrchek) {
                for (var k = 0; k < attrLen; k++) {
                  if (String(p).indexOf(attrArr) > -1) {
                        attrchek = true;
                        break;
                  }
                }
            } else break;
      }
      for (var p in window) {
            if (!attrchek) {
                for (var k = 0; k < attrLen; k++) {
                  if (String(p).indexOf(attrArr) > -1) {
                        attrchek = true;
                        break;
                  }
                }
            } else break;
      }
      var styleArr = ['.enter_right>.enter_enw>.enter_rtitle'];
      var stylechek = false;
      if (styleArr && styleArr.length > 0) {
            for (var i = 0; i < styleArr.length; i++) {
                if ($(styleArr) && $(styleArr).attr('style')) {
                  stylechek = true;
                  break;
                }
            }
      }
      if (stylechek) {
            value += '0';
      } else {
            value += '1';
      }
      var keywordArr = [{
            key: ".enter_right",
            values: ["亲", "抢票", "助手"]
      },
      {
            key: ".cx_form",
            values: ["点发车", "刷票"]
      },
      {
            key: "#gridbox",
            values: ["只选", "仅选"]
      },
      {
            key: ".enter_w",
            values: ["助手"]
      }];
      var keywordchek = false;
      if (keywordArr && keywordArr.length > 0) {
            for (var i = 0; i < keywordArr.length; i++) {
                var kw = keywordArr;
                if (fw(kw)) {
                  keywordchek = true;
                  break;
                }
            }
      }
      if (keywordchek) {
            value += '0';
      } else {
            value += '1';
      }
      if (value.indexOf('0') > -1) {
            aj();
      }
      return key + ':' + value;
    }
})(jQuery);


这个脚本分为三个部分。第一部分是加密函数,实现了一个加密算法。第二部分是初始化执行函数,负责整个检测流程的初始化和引导。第三部分是检测函数,根据对应的特征来进行检测。

算法部分略去不谈,先看检测函数。检测函数基本上会分成四块。
第一块根据样式名来进行检测。样式名预置了一堆东西,基本上都是一些订票助手或抢票王会插入页面中的显示结构。
第二块根据ID检测,ID列表看起来都是一些订票助手会抢票王会插入页面中显示的内容。
第三块是Javascript变量检测。检测对应列表的JS对象属性。
第四块是HTML文本检测,检测指定的HTML结构中的文本是否带有指定的文本。
检测完成后,每个标记位检测到则设为0,没检测到则设为1,最终拼成一个四字符的字符串并返回。
值得一提的是这个检测函数名叫gc(),我一直在怀疑是不是写这个函数的伙计写到这里把自己写High了。。。

而第二块引导函数干了两件事。一件事是启动了一个定时器,每2秒钟调用一次检测函数,直到找到了证据。这种行为类似于一些软件定期扫描系统进程。当然扫描是为啥那就另当别论了。找到后上报给服务器。
第二件事就是对当前页面里面比较重要的表单(比如登录表单,提交预定表单)绑定事件,在提交的时候插入俩包含检测数据的隐藏域,提交后果断删除。对于为啥删除我一直很费解,可能是怕人发现吧。

当然,他们也很忙。这种忙体现在三个方面,一个是上报地址的变更。一开始是固定的,后来可能因为会被拦截,所以还冒出了好几个地址,而且都是整点放票的时候出现,平时不出现来迷惑你的视线。再一个就是预置的特征码变动很勤快,几乎每小时都会增加。最后就是自己的处理逻辑变更,比如提交订单的时候没有隐藏域开始时没事,后来被改得如果是整点,那么必须失败(体现为白屏)。
这些变更让我相信这群LS是完全有时间和精力的,所以我对他们于自己的功能上一点不上心愈发得愤慨。

大半年了,就这么点变动,还是变得这东西,真是让人绝望。回想历次变更,我最喜欢的是在提交订单页提交订单必须延迟五秒钟,这种延迟保证了一定的公平性,哪怕验证码被自动识别了,也能降低他们的优势。只不过设计得很粗糙——没有界面,没有提示,手快了还会告诉你验证码输错了。

以上不涉及反制细节,因为我相信这虽然无聊而蛋疼,但你们还是会上蹿下跳来做这些完全没意义的事情的。
经此一役,我相信以后独立的软件版和网页版会越来越多。你要检测你的界面,那不用就好了,没有说一定要改你的界面。
就好像我说,我没有一定要陪你们玩一样。

361°广告词 发表于 2013-9-13 01:46:43

就是嘛,太无聊了,逻辑奇葩的不行

发表于 2013-9-13 02:04:50

铁道部火车票开发厂家估计很无奈,专家在此,铁道部想糊弄大家的成本立马高了很多。http://app.qlogo.cn/mbloghead/ecf503dd335e82439e7c

发表于 2013-9-13 02:04:52

虽然看不懂,不过好像很厉害,码字不容易,赞一个^_^作为新人,在这里不敢大声说话,也不敢得罪人,只能默默地顶完贴转身就走,不求深藏功与名,只求前排混脸熟http://app.qlogo.cn/mbloghead/fa3612e5e64172234d6a

发表于 2013-9-13 02:23:23

早点休息,不要熬夜http://app.qlogo.cn/mbloghead/e259442d1366b8f6f6d0

o﹏苏格╬═☆流 发表于 2013-9-13 08:44:44

有改动啊之前是 table排版 现在是DIV的了;P

发表于 2013-9-13 08:56:54

个别客户端验证码错误,但是还是有ok的。http://app.qlogo.cn/mbloghead/5b3acbdad3d2ba5e13ea

发表于 2013-9-13 08:56:55

插件版放弃算了,让他改去吧,免得到临时偷偷改动一下,不知情的买不到票,老鱼还要挨骂http://app.qlogo.cn/mbloghead/102724ee0e3da88b0a1e

发表于 2013-9-13 08:56:56

铁道部的程序员都是吃大粪的!是不是铁道部把软件包给哪个学校给学生当作业了!http://app.qlogo.cn/mbloghead/7a1fc3ff8cc686e63c84

发表于 2013-9-13 08:56:57

辛苦了!http://app.qlogo.cn/mbloghead/6a17aae8e7804b9aede0
页: [1] 2 3 4 5
查看完整版本: 2013.911-9.12 12306检测技术浅析