这里在页面最底部嵌入了一个 iframe ,里面装了广告代码,这里的最外层的 id 名id="BAIDU_SSP__wrapper_u2444091_0" 就很适合成为我们判断是否是恶意代码的一个标志,假设我们已经根据拦截上报收集到了一批黑名单列表:
// 建立正则拦截关键词 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ];
接下来我们只需要利用这些关键字,对 document.write 传入的内容进行正则判断,就能确定是否要拦截document.write 这段代码。
```javascript // 建立关键词黑名单 var keywordBlackList = [ 'xss', 'BAIDU_SSP__wrapper', 'BAIDU_DSPUI_FLOWBAR' ]; /** * 重写单个 window 窗口的 document.write 属性 * @param {[BOM]} window [浏览器window对象] * @return {[type]} [description] */ function resetDocumentWrite(window) { var old_write = window.document.write; window.document.write = function(string) { if (blackListMatch(keywordBlackList, string)) { console.log('拦截可疑模块:', string); return; } // 调用原始接口 old_write.apply(document, arguments); } } /** * [黑名单匹配] * @param {[Array]} blackList [黑名单] * @param {[String]} value [需要验证的字符串] * @return {[Boolean]} [false -- 验证不通过,true -- 验证通过] */ function blackListMatch(blackList, value) { var length = blackList.length, i = 0; for (; i < length; i++) { // 建立黑名单正则 var reg = new RegExp(whiteList[i], 'i'); // 存在黑名单中,拦截 if (reg.test(value)) { return true; } } return false; }
我们可以把 resetDocumentWrite 放入上文的 installHook 方法中,就能对当前 window 及所有生成的 iframe 环境内的 document.write 进行重写了。
锁死 apply 和 call
接下来要介绍的这个是锁住原生的 Function.prototype.apply 和 Function.prototype.call 方法,锁住的意思就是使之无法被重写。
这里要用到 Object.defineProperty ,用于锁死 apply 和 call。
Object.defineProperty
Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象。
Object.defineProperty(obj, prop, descriptor)
其中:
我们可以使用如下的代码,让 call 和 apply 无法被重写。
// 锁住 call Object.defineProperty(Function.prototype, 'call', { value: Function.prototype.call, // 当且仅当仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变 writable: false, // 当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除 configurable: false, enumerable: true }); // 锁住 apply Object.defineProperty(Function.prototype, 'apply', { value: Function.prototype.apply, writable: false, configurable: false, enumerable: true });
为啥要这样写呢?其实还是与上文的 重写 setAttribute 有关。
虽然我们将原始 Element.prototype.setAttribute 保存在了一个闭包当中,但是还有奇技淫巧可以把它从闭包中给“偷出来”。
试一下:
(function() {})( // 保存原有接口 var old_setAttribute = Element.prototype.setAttribute; // 重写 setAttribute 接口 Element.prototype.setAttribute = function(name, value) { // 具体细节 if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {} // 调用原始接口 old_setAttribute.apply(this, arguments); }; )(); // 重写 apply Function.prototype.apply = function(){ console.log(this); } // 调用 setAttribute document.getElementsByTagName('body')[0].setAttribute('data-test','123');
猜猜上面一段会输出什么?看看:
居然返回了原生 setAttribute 方法!
这是因为我们在重写 Element.prototype.setAttribute 时最后有 old_setAttribute.apply(this, arguments);这一句,使用到了 apply 方法,所以我们再重写 apply ,输出 this ,当调用被重写后的 setAttribute 就可以从中反向拿到原生的被保存起来的 old_setAttribute 了。
这样我们上面所做的嵌套 iframe 重写 setAttribute 就毫无意义了。
使用上面的 Object.defineProperty 可以锁死 apply 和 类似用法的 call 。使之无法被重写,那么也就无法从闭包中将我们的原生接口偷出来。这个时候才算真正意义上的成功重写了我们想重写的属性。
建立拦截上报
防御的手段也有一些了,接下来我们要建立一个上报系统,替换上文中的 console.log() 日志。
上报系统有什么用呢?因为我们用到了白名单,关键字黑名单,这些数据都需要不断的丰富,靠的就是上报系统,将每次拦截的信息传到服务器,不仅可以让我们程序员第一时间得知攻击的发生,更可以让我们不断收集这类相关信息以便更好的应对。
这里的示例我用 nodejs 搭一个十分简易的服务器接受 http 上报请求。
先定义一个上报函数: