剖析中国移动的流量气泡
在使用4G上网浏览网页的时候,经常会出现下面这个气泡。即使是用电脑使用了手机的个人热点也不能例外。
有的时候,这个东西可以说很贴心,它可以实时告诉你还剩多少流量,是该猛着用还是省着用。当你不小心使用电脑狂用自己个人热点的流量的时候,这个流量气泡也会提醒你“__你在烧钱ing__!”,能够防止更大的损失。
不过也有时候,这个东西会非常烦人。譬如,它偶尔会弹广告!建议您办理每月400元的移动豪华套餐,or 参与大富豪抽奖……老子忙着呢,抽你妹啊!
它的实现原理是什么呢?中国移动,如何能够在任意浏览的网页里,插入自己的内容?
其实很简单,中国移动只是嵌入一段很短的js标签,这个标签所指向内容的代码就会运行,接下来10086就可以在整个页面里兴风作浪了,增加一个iframe,在iframe里实现自己的气泡、各种流量查询功能、积分、广告……
下面是插入的javascript的内容:
1 | <script type="text/javascript" id="1qa2ws" |
当然我不确定那个id是不是会泄露自己的信息,我也不知道别人的id是不是也是这个,但为了友好还是写出来了。By the way, 这个script在非移动网络是失效的。当你使用的不是中国移动蜂窝网络,是无法链接221.179.140.145的。
当然了,即使在蜂窝网络下:
1 | zach$ ping -c 5 221.179.140.145 |
在蜂窝网络下也访问不了?并不是,这最多说明中国移动把ICMP关了吧。
不管它的访问地址了,先看看js代码里有什么猫腻。打开sript标签中的js代码:
1 | (function(){try{var h=function(){if(top.tlbs&&!top.tlbsEmbed){top.tlbsEmbed=true;var u=top.tlbs,y=top.tlbs.iframejs.split("|"),t='<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';for(var d=0;d<y.length;d++){t+='<script src="'+y[d]+'" defer charset="UTF-8"><\/script>'}t+="</head></html>";var v=document.createElement("iframe");v.style.display="none";document.body.appendChild(v);try{var x=v.contentWindow.document;x.write(t);x.close()}catch(w){if(/MSIE/g.test(navigator.userAgent)){if(location.href.indexOf("www.people.com.cn")>=0||location.href.indexOf("www.caijing.com.cn")>=0){return}}v.src="javascript:void((function(){document.open();document.domain='"+document.domain+"';document.write('"+t+"');document.close()})())"}}};var a=function(d){if(d.readyState){d.onreadystatechange=function(){if(d.readyState=="loaded"||d.readyState=="complete"){d.onreadystatechange=null;h()}}}else{d.onload=function(){h()}}};var r=function(e){var d=e.length,u="";for(var t=0;t<d;t++){if(!(/^(ptid|pcid|mcid|mtid|src|type|id)$/.test(e[t].name))){u=u+"&"+e[t].name+"="+e[t].value}}return u};var j=function(){var z=document,D=z.getElementById("1qa2ws"),e=D.getAttribute("mtid"),y=D.getAttribute("mcid"),C=D.getAttribute("src"),w=z.head||z.getElementsByTagName("head")[0],A=D.attributes,B=r(A);s=z.createElement("script");a(s);s.charset="UTF-8";var v=new Date();var x=v.getTime();if(/Windows NT/g.test(navigator.userAgent)){e=D.getAttribute("ptid")||e;y=D.getAttribute("pcid")||y}s.src=C.split("tlbsgui")[0]+"tlbsserver/jsreq?tid="+e+"&cid="+y+"&time="+x+encodeURI(B);w.appendChild(s)};if(parent==self){if(location.href.indexOf("nba.sina.cn")>=0){window.onload=function(){j()}}else{j()}}else{var g=top.window.document;if(null==g){return}var m=g.getElementById("1qa2ws");if(m!=null){return}else{var b=document.createElement("script"),n=document.getElementById("1qa2ws"),p=n.getAttribute("mtid"),f=n.getAttribute("mcid");b.setAttribute("type","text/javascript");b.setAttribute("id","1qa2ws");b.src=n.getAttribute("src");b.setAttribute("mtid",p);b.setAttribute("mcid",f);b.setAttribute("ptid",n.getAttribute("ptid")||p);b.setAttribute("pcid",n.getAttribute("pcid")||f);top.window.document.body.appendChild(b)}}}catch(k){var l=document;var q=l.getElementById("1qa2ws");var o=q.getAttribute("src");var i=k.message;i+="&time="+new Date().getTime();var c=document.createElement("script");c.onload=c.onreadystatechange=function(){if(!this.readyState||this.readyState==="loaded"||this.readyState==="complete"){c.onload=c.onreadystatechange=null;document.body.removeChild(c)}};c.src=o.split("tlbsgui")[0]+"tlbsserver/stagelog?"+i;document.body.appendChild(c)}})(window); |
这么乱,连个换行都没有?!看个鬼啊!没关系,我们有JSNICE,一个让混淆过的JS更有亲和力的工具,自带缩进,自带类型提示,甚至会有一定的反混淆的功能,把混淆后的变量名尽可能地按照意思还原回去。棒!经过它的处理之后,代码变成了下面这个样子,是不是易读多了?
1 | (function() { |
这个代码加长了之后有184行,所以还是有一些复杂的。不过大概还是分析一下架构:
主题分为这么几个部分,首先是一个外围的主函数function(){}(window);,主函数内部用一个try{}catch(e){}括起来,方便进行错误处理。
然后try内部定义了一些函数,包括
- done : 用于最终的iframe构建
- loadScript : 用于判断信息是否收集完全
- extend : 用于把JS的Key-Value对转化为应对HTTP GET请求的&;串
- init : 增加流量气泡的函数入口
为了方便理解,我做了一个调用init前的逻辑序列图:
打开网页,script运行的时候,首先是不会管这些函数的,而是直接运行line120的判断语句。如果parent==self那么就运行init()
这里专门对于_nba.sina.cn_做了一个判断,我估计是作为一个体育网站,经常有直播的内容,而文字直播经常是用ajax实现的,或许和10086的这个添加的iframe有某些冲突,被专门投诉了吧,所以对于这个网站,加载方式不太一样,所以使用了onload函数,在加载页面的时候运行init。
sina.cn和sina.com.cn还不是完全一样,sina.cn默认是专门为手机浏览器设计的。
如果parent != self的话,就说明当前的视口不是顶级浏览器,很可能是出于某个iframe嵌套中,所以要找到顶级窗体的内容(var doc = top.window.document)然后在顶级窗体中进行处理。
第138行,找到ID为1qa2ws的项目,这个我在前面也说了,那个旧市script的ID。如果已经有这个ID了,那就说明,顶级的窗体已经家再过相应的script了,所以有一个气泡就够了,下层的就别操心了,所以139行检测到存在这个script,就直接退出了。
如果不存在,那就说明,作为下层iframe的内容,还需要再挣扎一下,142行到158行,就是在尝试构建上文中的那个script标签的内容,然后把这个script放置到顶层的浏览器视口中。
161行,cache error,如果出错了怎么办,首先找到刚才的script,把src值赋予expr,然后把错误信息综合进入data中吧,最后再使用document建立一个新的script - node,把data发送到tlbsserver/stagelog那边去,让它们在后台写入log,让开发人员慢慢分析去了。
到这里这一层逻辑就结束了。
下面,关于init、extend、done、loadScript又是什么,它们之间的关系又是什么呢?
这里给一个init后的示意图:
下面具体研究上述的四个函数。可以知道,入口函数肯定是init的,因为其他几个在主代码中都没有用到。init函数从86行开始到119行。
首先它找到1qa2ws的ID,也就是那个Script标签,然后获取一些feature,创建了一个新的script s,118行,把s放到svg中。svg就是head。
什么鬼?这个script中,最重要的init的目的,无非是由新增一个script到head中?
是的,就是这个样子,所以很多玄机应该都在这个s中罢。
具体地,这个s的内容是:
1 | s.src = expr.split("tlbsgui")[0] + "tlbsserver/jsreq?tid=" + guess + "&cid=" + dep + "&time=" + size + encodeURI(marker); |
其中的expr就是原来script的src部分,截取tlbsgui之前的部分,再加上后面一堆东西。
那么新的src应该就是
1 | http://221.179.140.145:9090/tlbsserver/jsreq?tid=4&cid=2&time=1449641644008%MARKER |
%SIZE的具体内容应该是得到的时间var size = defaultCenturyStart.getTime();defaultCenturyStart其实就是new Date(),故弄玄虚。
%MARKER的具体内容就是element.attributes了。extend函数就是把这些key-value对用&=连缀起来,作为GET的参数。
我用手机访问了一下http://221.179.140.145:9090/tlbsserver/jsreq?tid=4&cid=2&time=1449641644008,没有加MARKER,还好可以访问到,获得了一个应该是json。
1 | top.tlbs= |
好了,这应该就非常清晰了,因为这个json,把name、网址端口、css、js的地址都发送回来了,还有各种辅助的参数。拿到这些内容,就可以最终访问了。
有人会问,这个js地址,都用|来分隔了,键入这个地址肯定没法使用啊!别着急,这个并不是直接用的,而是用处理函数的,而具体的处理函数就在Done里面,对于|,见11行。
这个过程是:首先使用loadScript函数,检测一下相关的load工作有没有完成,完成了之后,就去运行Done函数。
Done函数就在读取这个json中的内容,Done函数的过程,就是建立一个iframe,把json中的参数填写进去,这个iframe的效果不是别的,就是那个流量气泡!
在Done的错误处理中,有一小段
1 | if (/MSIE/g.test(navigator.userAgent)) { |
我也没仔细看,估计是跟人民网、财经网有不兼容,或者这两个网跟中国移动说了“别再我的网站上加插件!”,所以检测到网址中有这个字符的时候,就不再挣扎着放置iframe了。
我觉得,也许不想让中国移动在自己的网页上放置气泡的方法,就是在自己的网址中放入”www.people.com.cn"字样。并没有测试,也没有完整追究这两个网站,仅仅是猜想。
我还尝试下载了上述一些js,再次强调只有移动网络内部才能够访问得到,其他网络是无法访问221.179.140.145等网址的。
其中http://221.179.140.145:9090/tlbsgui/baseline/common/js/UA.js?v=20151209141500这个js的内容很简单:
1 | top.tlbs.blacklist =[] |
随便找另一个js:http://221.179.140.145:9090/tlbsgui/customize/L_bar/bjyd/js/simplifiedCloseHandler.js?vv=104'
它的内容是:
1 | $(function(){function e(){$d.dialog.find("[name=toolbar-close]").bind("click",f)}var d=top.tlbs.url+"tlbsgui/customize/L_bar/bjyd/css/simplifiedClose_min.css",c='<t:d class= "close-contents"><t:ii class="intro-text">{$RES_SIMPLIFIED_PROMPT}</t:ii><t:ii class="ts-closeoption"><input type="radio" id="radio1" name="minimize" value="minimize" checked="checked"><t:d class="radioImg1"></t:d><span id ="label1" class="w-minimize">{$RES_SIMPLIFIED_MINIMIZE}</span></input><input type="radio" id="radio2" name="close" value="close"><t:d class="radioImg2"></t:d><span id ="label2" class="w-close">{$RES_SIMPLIFIED_CLOSE}</span></input></t:ii></t:d><t:d class="ts-action"><t:d class="ts-cancel"><t:ii class="ts-i">{$RES_SIMPLIFIED_CANCEL}</t:ii></t:d><t:d class="vertical-line"></t:d><t:d class="ts-yes"><t:ii class="ts-i">{$RES_SIMPLIFIED_CONFIRM}</t:ii></t:d></t:d>';$(document).ready(function(){e();params=top.tlbs.config.simplifiedCloseMenu;i(top.tlbs,$d.dialog,params);j()});var i=function(l,k,m){if(m==1){$(k).find("[name=toolbar-close]").empty();if($(k).find("ts-header").length<=0){}$(k).find("[name=toolbar-close]").css("display","block").append(c.res(top.tlbs.res,"RES")).siblings().hide();$(k).find("label").css("display","inline-block").css("font-size","0.8em");$(k).find(".ts-action").css("padding","0em").css("background","#fff");l.load.css(d)}};var j=function(){var n={w:0,h:0};resizeFactor=1;var k=navigator.userAgent;var l=function(){var o=tlbs.data.screen.width(),p=top.window.innerHeight;if(/Chrome/i.test(k)&&(Math.abs(n.w-o/resizeFactor)>10||Math.abs(n.h-p)>10)){m()}};setInterval(l,250);var m=function(){var o=tlbs.data.screen.width(),p=top.window.innerHeight;if(o==0||!$d.tlbs){setTimeout(m,100);return}else{if(!tlbs.ua.resize){return}}n={w:o,h:p};var q=(typeof window.orientation=="number"&&typeof window.onorientationchange=="object");if(q){if(top.window.orientation==0||top.window.orientation==180){$d.dialog.find(".radioImg2").removeClass("radioImg2lands");$d.dialog.find(".radioImg1").removeClass("radioImg1lands")}else{$d.dialog.find(".radioImg2").addClass("radioImg2lands");$d.dialog.find(".radioImg1").addClass("radioImg1lands")}}};return m};var f=function(m){var l=$(m.target).closest(".ts-cancel");var k=$(m.target).closest(".ts-yes");var q=$(m.target).closest("#label1");var p=$(m.target).closest("#label2");var o=$(m.target).closest(".radioImg1");var n=$(m.target).closest(".radioImg2");if(q.is("#label1")){top.document.getElementById("radio1").checked=1;top.document.getElementById("radio2").checked=0;$d.dialog.find(".radioImg2").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_unCheck.png)");$d.dialog.find(".radioImg1").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_check.png)")}else{if(p.is("#label2")){top.document.getElementById("radio2").checked=1;top.document.getElementById("radio1").checked=0;$($d.dialog).find(".radioImg2").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_check.png)");$($d.dialog).find(".radioImg1").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_unCheck.png)")}else{if(o.is(".radioImg1")){top.document.getElementById("radio1").checked=1;top.document.getElementById("radio2").checked=0;$d.dialog.find(".radioImg2").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_unCheck.png)");$d.dialog.find(".radioImg1").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_check.png)")}else{if(n.is(".radioImg2")){top.document.getElementById("radio2").checked=1;top.document.getElementById("radio1").checked=0;$d.dialog.find(".radioImg2").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_check.png)");$d.dialog.find(".radioImg1").css("background-image","url("+top.tlbs.format.url(top.tlbs.workspace)+"/images/content/radio_unCheck.png)")}else{if(l.is(".ts-cancel")){top.tlbs.autohide.enable(true);top.tlbs.autohide.activate();$d.dialog.hide()}else{if(k.is(".ts-yes")){a();top.tlbs.autohide.enable(true);top.tlbs.autohide.activate();$d.dialog.hide()}}}}}}};function a(){var k=g();if(k=="close"){h();if(top.tlbs.config.SimplifiedCloseMenuOption==0){top.tlbs.log("type=1&op=5");top.tlbs.log("type=5&op=5&appid=010&functionId=005&tid="+top.tlbs.tid+"&cid="+top.tlbs.cid)}else{if(top.tlbs.config.SimplifiedCloseMenuOption==1){top.tlbs.log("type=1&op=3");top.tlbs.log("type=5&op=3&appid=010&functionId=003&tid="+top.tlbs.tid+"&cid="+top.tlbs.cid)}else{if(top.tlbs.config.SimplifiedCloseMenuOption==2){top.tlbs.log("type=1&op=4");top.tlbs.log("type=5&op=4&appid=010&functionId=004&tid="+top.tlbs.tid+"&cid="+top.tlbs.cid)}else{if(top.tlbs.config.SimplifiedCloseMenuOption==3){top.tlbs.log("type=1&op=6");top.tlbs.log("type=5&op=6&appid=010&functionId=006&tid="+top.tlbs.tid+"&cid="+top.tlbs.cid)}}}}}else{if(k=="minimize"){top.tlbs.autohide.activate();b()}else{alert("please select one radio button")}}}var b=function(){top.tlbs.animate.popup.hide(function(){top.tlbs.animate.content.hide(function(){top.tlbs.log("type=1&op=2");top.tlbs.log("type=5&op=2&functionId=002&tid="+top.tlbs.tid+"&cid="+top.tlbs.cid)})})};var h=function(){top.tlbs.animate.popup.hide(function(){top.tlbs.animate.content.hide(function(){$(top.document.body).children(".tlbs").css("display","none")})});if(top.tlbs.banner){top.tlbs.banner.elem.css("display","none");$(top.document.body).css("margin-top",0);top.tlbs.animate.resolveScroll.setMarginTop("0px")}if(top.tlbs.ball){top.tlbs.ball.hidden=true;top.tlbs.ball.mixedPkg=true;$("tlbs-flux",top.document).remove()}};function g(){var k;if(top.document.getElementById("radio2").checked){k=top.document.getElementById("radio2").value}else{if(top.document.getElementById("radio1").checked){k=top.document.getElementById("radio1").value}else{k=null}}return k}}); |
这个内容就多了,而且能够找到很多资源,譬如/images/content/radio_check.png等等,这些肯定就是最终的那些控件图标了。
又随便找一个css:http://221.179.140.145:9090/tlbsgui/baseline/L_bar/css/tlbs_min.css?vv=104,这个就复杂很多,有很多CSS内容。这也就最终支持了气泡的CSS样式吧。
好了,再深挖就不知道要挖到哪里去了,时间有限就先分享这么多了。