昨天尝试了一系列的可疑模块拦截试验,尽管最终的方案还存在着一些兼容性问题,但大体思路已经明确了:
静态模块:使用 MutationObserver 扫描。
动态模块:通过 API 钩子来拦截路径属性。
提到钩子程序,大家会联想到传统应用程序里的 API Hook,以及各种外挂木马。当然,未必是系统函数,任何 CPU 指令都能被改写成跳转指令,以实现先运行自己的程序。
无论是在哪个层面,钩子程序的核心理念都是一样的:无需修改已有的程序,即可先执行我们的程序。
这是一种链式调用的模式。调用者无需关心上一级的细节,直管用就是了,即使有额外的操作对其也是不可见的。从最底层的指令拦截,到语言层面的虚函数继承,以及更高层次的面向切面,都带有这类思想。
对于 JavaScript 这样灵活的语言,任何模式都可以实现。之前做过一个网页版的变速齿轮,用的就是这类原理。
JavaScript 钩子小试
要实现一个最基本的钩子程序非常简单,昨天已演示过了。现在我们再来给 setAttribute 接口实现一个钩子:
// 保存上级接口 var raw_fn = Element.prototype.setAttribute; // 勾住当前接口 Element.prototype.setAttribute = function(name, value) { // 额外细节实现 if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) { if (/xss/.test(value)) { if (confirm('试图加载可疑模块:\n\n' + url + '\n\n是否拦截?')) { return; } } } raw_fn.apply(this, arguments); }; // 创建脚本 var el = document.createElement('script'); el.setAttribute('SRC', 'http://www.etherdream.com/xss/alert.js'); document.body.appendChild(el); Run |
类似昨天的访问器拦截,现在我们对 setAttribute 也进行类似的监控。因为它是个函数,所有主流浏览器都兼容。
钩子泄露
看起来似乎毫无难度,而且也没什么不对的地方,这不就可以了吗?
如果最终就用这代码,那也太挫了。我们把原始接口都暴露在全局变量里了,攻击者只要拿了这个变量,即可绕过我们的检测代码:
var el = document.createElement('script');
// 直接调用原始接口
raw_fn.call(el, 'SRC', 'http://www.etherdream.com/xss/alert.js');
document.body.appendChild(el);
Run