本鱼拟成立工作室承接项目开发/软件定制/云设施开发运维/办公设备技术支持等,如您有相关需求,欢迎来询 | ::博客文章推荐::

2013.911-9.12 12306检测技术浅析

: WEB前端 木魚 3954℃ 7评论
 【论坛链接:http://bbs.fishlee.net/forum.php?mod=viewthread&tid=37&fromuid=1

 
我记得上次有人问我,说今年的12306网站有任何改进吗。我很认真的想了想,说别说改进了,就是外观的改动也没看着。作为对我这句话的回击,一两天后上演了本年度最大的改动,对订票类扩展的检测。其实我很理解不了这种奇葩的逻辑。因为谁都知道封堵不是办法,梳纳才是王道。梳是提高自己的品质,减少用户对第三方的依赖;而纳则是完善自身的规则,尽量缔造完善的操作环境。

12306本身有很多很明显的漏洞和缺陷。比如订票查询必须的5秒钟其实只是界面上的限制,服务器的查询没有任何限制。比如查询结果的mmstr是没有绑定用户的,一个账户查出所有用户可用。比如订单提交页的5秒时间没有任何提示。这些从12306存在的第一天开始就存在,在这些没有被微博大规模转发引人注目时,他们是懒得去修正的。
封堵……这种费力不讨好的事情,看起来很像小孩子才会做的事情。

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

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

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

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

 
(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[i]) > -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(dataincludeLength) {
            var length = data.length;
            var n = (length  1) << 2;
            if (includeLength) {
                var m = data[length  1];
                if ((m < n  3) || (m > n)) return null;
                n = m;
            }
            for (var i = 0; i < length; i++) {
                data[i] = String.fromCharCode(data[i] & 0xff, data[i] >>> 8 & 0xff, data[i] >>> 16 & 0xff, data[i] >>> 24 & 0xff);
            }
            if (includeLength) {
                return data.join(").substring(0, n);
            } else {
                return data.join(");
            }
        };
        function stringToLongArray(stringincludeLength) {
            var length = string.length;
            var result = [];
            for (var i = 0; i < length; i += 4) {
                result[i >> 2] = string.charCodeAt(i) | string.charCodeAt(i + 1) << 8 | string.charCodeAt(i + 2) << 16 | string.charCodeAt(i + 3) << 24;
            }
            if (includeLength) {
                result[result.length] = length;
            }
            return result;
        };
        this.encrypt = function(stringkey) {
            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[n],
            y = v[0];
            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[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);
        };
    };
    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(datatextStatus) {
                if (timmer) clearInterval(timmer);
            },
            error: function(XMLHttpRequesttextStatuserrorThrown) {}
        });
    }
    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')[0];
            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[0] + '" value="' + encode32(bin216(Base32.encrypt(keyVlues[1], keyVlues[0]))) + '" />');
                    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[i]).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[i])[0]) {
                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[k]) > -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[k]) > -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[i])[0] && $(styleArr[i]).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[i];
                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是完全有时间和精力的,所以我对他们于自己的功能上一点不上心愈发得愤慨。

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

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

本日志备份自 QQ 空间,原文地址:http://user.qzone.qq.com/286495995/blog/1379007781

喜欢 (0)
发表我的评论
取消评论
表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
(7)个小伙伴在吐槽
  1. [ft=,2,]对12306彻底无语了[/ft]

    无奈的人生2013-09-14 09:15 回复
  2. 对12306彻底无语了。木鱼辛苦了[em]e160[/em]

    雨南2013-09-13 10:20 回复
  3. 让他们都玩蛋去吧

  4. 呵呵,人家只是上班的 ,没必要那么拼命 再说 上级领导人家故意这样做的,你以为这些事 他们会不知道吗

    老张2013-09-13 09:11 回复
  5. var keywordArr = [{ key: ".enter_right", values: ["亲", "抢票", "助手"] }, { key: ".cx_form", values: ["点发车", "刷票"] }, { key: "#gridbox", values: ["只选", "仅选"] }, { key: ".enter_w", values: ["助手"] }];

    这帮傻比程序员,

    后海2013-09-13 08:40 回复
  6. @{uin:392132774,nick:天然呆﹌} @{uin:664399589,nick:椛開椛榭+}

    没意思2013-09-13 07:53 回复
  7. 怎么都抢不到票[em]e109[/em]

    没有耳朵的猫2013-09-13 04:58 回复