MENU

校园网 FlashCookie 检测共享技术分享

2017 年 12 月 01 日 • 技术流

本校校园网会不定期地劫持用户访问的 HTTP 网页,填充为 FlashCookie 检测代码。会对用户浏览网页的体验造成一定影响。本问题于2017年11月13日发现并反馈给校园网官方。临近发文之时,问题依旧,只是复现频率明显减少。

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

问题简述

在访问 HTTP 网页时,有一定概率收到伪造的 HTTP 响应包,内容全部是 FlashCookie 检测代码而无一点正常网页的内容。在脚本执行完后,将刷新网页。如若再次触发检测,则重复一遍,否则将看到正常的网页内容。

如果用户开启了一些广告 / 隐私过滤器,则可能拦截检测脚本。这时被劫持后的网页中本身就有脚本代码,导致页面出现两秒空白,然后才刷新。在正常情况下很可能当即就刷新了页面,空白停留时间不会过长。

技术说明

如访问 http://free.apprcn.com/http://downloads.pandorabox.com.cn/,任意在其中页面点来点去,将有机会观察到页面出现空白。装上隐私獾(Privacy Badger)后使页面空白时间停留更长,观测效果更佳。或直接在 Firefox 等浏览器中查看源代码,刷新 view-source 页,当触发机制时会看到下列代码:

图1

从图中内嵌脚本文件的所属 IP 1.1.1.3,可知必是校园网所作无疑。据悉为深信服提供的技术。

表面上看上去,检测系统给用户生成了一个标识符,并试图通过 FlashCookie 这种追踪技术植入到客户端中,难以注意和清除。尽管我认为在当今,许多浏览器都默认禁用 Flash,应该不会有多大用。值得一提的是,似乎之前有 360 浏览器用户报告容易被代理/共享检测系统误封,个人猜测可能是因为 360 浏览器默认打开了 Flash 导致被跟踪,加上其他行为从而引发误判。不过这段都是存疑的,详见下文。

资料共享

被劫持后的网页

<html>
<head>
<script language="javascript">setTimeout("location.replace(location.href.split(\"#\")[0])",2000);</script>
<script type="text/javascript" src="http://1.1.1.3:89/cookie/flash.js"></script>
<script language="javascript">setURL("1.1.1.3");supFlash("181760452");</script>
</head>
<body>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" width="0" height="0" id="m" align="center"><param name="allowScriptAccess" value="always" />
<param name="movie" value="http://1.1.1.3:89/cookie/flashcookie.swf" />
<param name="quality" value="high" />
<param name="FlashVars" value="srv=1.1.1.3" />
<embed src="http://1.1.1.3:89/cookie/flashcookie.swf"FlashVars="srv=1.1.1.3" quality="high" width="0" height="0"  name="m" align="center" allowScriptAccess="always" type="application/x-shockwave-flash"pluginspage="http://www.macromedia.com/go/getflashplayer" />
</object>
</body></html>

脚本文件

上述页面除了有直接写的 JS 代码外,还内嵌了一个地址为 http://1.1.1.3:89/cookie/flash.js 的脚本文件,内容如下:

var url = "";
function setURL(ip){url = "http://"+ip+":89/cookie/flashcookie.html";}
function loadPage(){location.replace(location.href.split("#")[0]);}

////add by yxf@2014/08/27
/**
 *@描  述:增加UA判断,排除移动终端上报cookies值和时间间隔值
 *@返回值:
 *      true, 允许上报; false,不允许上报
 */
function IsCanReport2Ac(){
    
    var strUseAgent = navigator.userAgent.toLowerCase();
    
    //非windows nt
    var isWinNt = strUseAgent.indexOf("windows nt") > -1;
    if (!isWinNt){  return false;}
    
    //移动终端
    var isMobile = strUseAgent.indexOf("mobile") > -1;
    if (isMobile){  return false;}
    
    //为Android
    var isAndroid = strUseAgent.indexOf("android") > -1;
    if (isAndroid){ return false;}
    
    //为ios
    var isIOS = !!strUseAgent.match(/\(i[^;]+;( u;)? cpu.+mac os x/);
    if (isIOS){ return false;}
    
    //为Symbian
    var isSymbian = strUseAgent.indexOf("symbian") > -1;
    if (isSymbian){ return false;}

    //为iPhone
    var isIPhone = strUseAgent.indexOf("iphone") > -1;
    if (isIPhone){  return false;}
    
    //为ipad
    var isIPad = strUseAgent.indexOf("ipad") > -1;
    if (isIPad){ return false;}
    
    //为ipod
    var isIPod = strUseAgent.indexOf("ipod") > -1;
    if (isIPod){ return false;}
    
    //排除一些误判的app 特征字符串
    var isInvalidAppPos = strUseAgent.search(/ baidubrowser\/\d/);//-- 百度一下客户端
    if (-1 != isInvalidAppPos){ return false;}
    
    return true;
}
////end by yxf


// 写cookies
function setCookie(name,value)
{
    var Days = 30;
    var exp = new Date();
    exp.setTime(exp.getTime() + Days*24*60*60*1000);
    document.cookie = name + "="+ escape (value) + ";expires=" + exp.toGMTString();
}

// 读取cookies
function getCookie(name)
{
    var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
 
    if(arr=document.cookie.match(reg)){
    
        return (arr[2]);
    }else{
    
        return null;
    }
}

function supFlash(cookie)
{   
    if (false === IsCanReport2Ac()){
    
        loadPage();     
        return;
    }
    
    // 获取本地cookie值
    var td_cookie = getCookie("td_cookie");
    if (td_cookie == cookie){
    
        loadPage();     
        return;
    }
    setCookie("td_cookie", cookie);
    
    var flash = 0;
    var judgeIE = !-[1,];
    var ua = navigator.userAgent.toLowerCase();
    if (ua.indexOf("taobrowser") > 0 || ua.indexOf("lbbrowser") > 0) {
    
        loadPage();
        return;
    }
    var isIE = judgeIE || ua.indexOf("msie") > 0 || ua.indexOf("trident/7.0") > 0;
    if(isIE){
        try{
            var swf1 = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');
            flash = 1;
        }
        catch(e){
            flash = 0;
        }
    }
    else {
        try{
            var swf2 = navigator.plugins['Shockwave Flash'];
            if(swf2 == undefined){
                flash = 0;
            }
            else {
                flash = 1;
            }
        }
        catch(e){
            flash = 0;
        }
    }

    if(flash === 0)
    {
        loadPage();
        return;
    }   
}

// 配置排除列表
var excludeList = new Array("ADMUI3Lg","ADMUI3Sm","Photoshop Large","Photoshop Small");

var makeCRCTable = function(){
    var c;
    var crcTable = [];
    for(var n =0; n < 256; n++){
        c = n;
        for(var k =0; k < 8; k++){
            c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
        }
        crcTable[n] = c;
    }
    return crcTable;
}

var crc32 = function(str) {
    var crcTable = window.crcTable || (window.crcTable = makeCRCTable());
    var crc = 0 ^ (-1);

    for (var i = 0; i < str.length; i++ ) {
        crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
    }

    return (crc ^ (-1)) >>> 0;
};

function isArray(value)
{
    return value && 
            typeof value === 'object' &&
            typeof value.length === 'number' &&
            !(value.propertyIsEnumerable('length'));
}

function removeExcludeFont(fontArr, excludeList)
{
    if (!excludeList.length)
    {
        return fontArr;
    }
    
    var flag = 0;
    var resArr = new Array();
    for (var i = 0; i < fontArr.length; ++i)
    {
        flag = 0;
        for (var j = 0; j < excludeList.length; ++j)
        {
            if (fontArr[i] == excludeList[j])
            { 
                flag = 1;
                break;
            }
            if (fontArr[i].match(/\.tmp/))
            {
                flag = 1;
                break;
            }
        }
        if (!flag)
        {
            resArr.push(fontArr[i])
        }
    }
    
    resArr.sort();
    return resArr;
}

function jsSetCookie(fontArr, manu, vers, os)
{
    if(manu == "" || !isArray(fontArr)){
        loadPage();
        return;
    }
    
    if(url == ""){
        loadPage();
        return;
    }
    
    var fontStr = removeExcludeFont(fontArr, excludeList).join("|\n");
    var font_param = "manu_txt=" + manu +
                 "&manu_crc=" + crc32(manu).toString() + 
                 "&version=" + vers +
                 "&font_crc=" + crc32(fontStr).toString() + 
                 "&os=" + os;
                
    var script = document.createElement("script");
    script.type = "text/javascript";
    var done = false;
    script.onload = script.onreadystatechange = function(){
        if ( !done && (!this.readyState ||
                this.readyState === "loaded" || this.readyState === "complete") ) {
            done = true;
            this.onload = this.onreadystatechange = null;
            loadPage();
            return;
        }
    };
    script.src = url +"?"+font_param+"&"+Math.random();
    document.getElementsByTagName("head")[0].appendChild(script);
}

里面有个注释是“增加UA判断,排除移动终端上报cookies值和时间间隔值”,确实能看出 UserAgent 判断,排除了移动端,但尚不清楚“排除时间间隔值”具体指什么。当客户端是移动端或某些特别照顾的浏览器,或比较 Cookie 一致时,页面会立即刷新;否则将写入 Cookie 后再刷新。

实际上,此处说的“刷新”并不严谨。重新载入后的地址并不一定等于原来的地址,具体大家可以看一看代码。

奇怪的是,从这次抓取到的信息来看,似乎并没有如表面上的采用 FlashCookie 技术,没有把浏览器 Cookie 持久化为 FlashCookie。只执行了不支持 Flash 动画(未安装控件)会刷新的动作,如果支持也并无下文。并且脚本里有个 jsSetCookie() 函数也没有被调用。希望有大牛来解释一下。

解决方案

不管如何,是想逃避这个检测也好,还是嫌弃页面空白造成的体验,我们需要一些绕过它的办法。特别是使用了一些隐私过滤器,可能导致空白有两秒之久,实在不可忍。

除了使用私密隧道保证安全性,尽量访问 HTTPS 的网站这种通用的方案,实测以下防火墙规则是有效的:

iptables -I FORWARD -p tcp --sport 80 --tcp-flags ACK ACK -m string --algo bm --string " src=\"http://1.1.1." -j DROP

至于官方?我觉得就不用期望学校能妥善解决这个事了。

文章于当日略有修改。

添加新评论

已有 13 条评论
  1. 对校园网新设备的 FlashCookie 检测,屏蔽规则如下:

    iptables -I FORWARD -p tcp --sport 80 --tcp-flags ACK ACK -m string --algo bm --string " value=\"http://18.20.18." -j DROP
  2. Router Router

    老哥,我想请假你几个问题,方便加个VX不@(太开心)

    1. @Router不好意思,不怎么使用微信。@(呵呵)

    2. router router

      @Bro.Xuqq也行呀?其实我的主要疑问就是我路由器防火墙拒绝掉1.1.1端的ip避免偷我的cookie就行了,ac对于电脑段和移动端都是这样检测的吗?我看到他还有基于应用特征检测,是通过useragent吗?

    3. @router对我的学校来说,电脑端主要是 FlashCookie,移动端是应用特征检测;FlashCookie 如今已经不怎么奏效了(多数浏览器都默认关闭 Flash 了),应用特征检测也只限于 QQ、微信等一些流行 App(特征比较明显,你可以抓包看看;其他一些应用都开始用 HTTPS 了)。所以这边很多人甚至日常开热点给别人用都没事。

    4. router router

      @Bro.Xu感谢回复,我们学校用的应该

    5. router router

      @router都是同一个牌子的ac.我再研究研究,成功了送你一个k2p. 你写的对我收益很大,这几个月一直想通过http代理的方式解决这个,不过还是你的方法比较好

    6. @router不用客气的。能有帮助就好。

  3. Router Router

    你好,我也是一名校园网用户,学校去年用了ac管理器,导致只能单台电脑与移动设备上网,折腾了许久也只能使用酸酸乳这种折中的办法,我想跟你请教一些问题,请问能加个vx什么的吗?

  4. ... ...

    ...

  5. 一只野生智障 一只野生智障

    老哥,你看那个td_cookie,不感觉是无线设备的锅吗。(ฅ´ω`ฅ)

    1. @一只野生智障怎么讲?

  6. 在此我有一点疏忽,Cookie 持久化为 FlashCookie 并非在 JS 中完成,而是由一个地址为 http://1.1.1.3:89/cookie/flashcookie.swf 的动画实现的。FlashCookie 检测应该是正常工作的。

    文章就暂时放着。哪天我如果逆向工程一下那个 Flash 动画,再对文章进行大幅度修改。