搜索
热搜: 活动 交友 discuz
查看: 9266|回复: 44
收起左侧

[12306] 2013.911-9.12 12306检测技术浅析

[复制链接]
  • TA的每日心情
    奋斗
    2019-1-5 01:55
  • 签到天数: 138 天

    [LV.7]常住居民III

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

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

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

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

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


    [JavaScript] 纯文本查看 复制代码
    (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(data, includeLength) {
                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(string, includeLength) {
                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(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[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(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')[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是完全有时间和精力的,所以我对他们于自己的功能上一点不上心愈发得愤慨。

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

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

    该用户从未签到

    发表于 2013-9-13 01:46:43 | 显示全部楼层
    就是嘛,太无聊了,逻辑奇葩的不行
     发表于 2013-9-13 02:04:50
    铁道部火车票开发厂家估计很无奈,专家在此,铁道部想糊弄大家的成本立马高了很多。[tthread=mb25chen, 精灵]http://app.qlogo.cn/mbloghead/ecf503dd335e82439e7c[/tthread]
     发表于 2013-9-13 02:04:51
    早点休息,不要熬夜[tthread=yingzi, 鹰紫]http://app.qlogo.cn/mbloghead/e259442d1366b8f6f6d0[/tthread]
     发表于 2013-9-13 02:04:52
    虽然看不懂,不过好像很厉害,码字不容易,赞一个^_^作为新人,在这里不敢大声说话,也不敢得罪人,只能默默地顶完贴转身就走,不求深藏功与名,只求前排混脸熟[tthread=Kobukuro8, 可苦可乐]http://app.qlogo.cn/mbloghead/fa3612e5e64172234d6a[/tthread]
     发表于 2013-9-13 02:23:23
    早点休息,不要熬夜[tthread=yingzi, 鹰紫]http://app.qlogo.cn/mbloghead/e259442d1366b8f6f6d0[/tthread]
  • TA的每日心情
    郁闷
    2013-10-16 11:43
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2013-9-13 08:44:44 | 显示全部楼层
    有改动啊  之前是 table排版 现在是DIV的了;P
     发表于 2013-9-13 08:56:54
    个别客户端验证码错误,但是还是有ok的。[tthread=ifollow5, 自由互联]http://app.qlogo.cn/mbloghead/5b3acbdad3d2ba5e13ea[/tthread]
     发表于 2013-9-13 08:56:55
    插件版放弃算了,让他改去吧,免得到临时偷偷改动一下,不知情的买不到票,老鱼还要挨骂[tthread=siemon_kiara, 土豆炒文竹]http://app.qlogo.cn/mbloghead/102724ee0e3da88b0a1e[/tthread]
     发表于 2013-9-13 08:56:56
    铁道部的程序员都是吃大粪的!是不是铁道部把软件包给哪个学校给学生当作业了![tthread=gaolei702441035, 无名前辈]http://app.qlogo.cn/mbloghead/7a1fc3ff8cc686e63c84[/tthread]
    您需要登录后才可以回帖 登录 | 入住

    本版积分规则

    申请友链| Archiver| 手机版| 鱼·后花园

    GMT+8, 2024-4-19 12:45 , Processed in 0.021921 second(s), 17 queries , Redis On.

    Powered by Discuz! X3.4

    © 2005-2024 鱼·后花园

    快速回复 返回顶部 返回列表