JS Hook 攻防案例

思考题

现有一款浏览器安全插件,会对所有页面的 Performance API 进行 Hook,以降低 JS 获取的时间精度,减少边信道攻击的风险。

该插件会把如下代码注入到页面最开始:

(function() {
  const obj = performance
  const rawFn = Performance.prototype.now

  Performance.prototype.now = function() {
    const val = rawFn.apply(obj, arguments)
    return ((val * 10) | 0) / 10
  }
})()

performance.now()    // 11346.9
performance.now()    // 12242.1

// 注:原始的 performance.now() 可获得小数点后多位

此外,包括 iframe 等子页面也会被注入该代码。

现在来思考,如何绕过这种防护方案,从而获得原生 performance.now 接口。(同时不损坏业务逻辑)

重写后的逻辑很简单,显然只能从 rawFn.apply 这里入手。

熟悉前端的应该都知道原型链的概念,例如 rawFn.apply === Function.prototype.apply,前者只是后者的一个引用而已。

所以,攻击者可重写 Function.prototype.apply 函数,然后故意调用一下 performance.now,触发 rawFn.apply,于是进入我们的 apply 函数。其中的 this 即 rawFn,就是我们想要的结果!

最后再将 Function.prototype.apply 恢复,不影响后续业务逻辑。

var rawApply = Function.prototype.apply
var rawNow

Function.prototype.apply = function() {
  rawNow = this
}

performance.now()

Function.prototype.apply = rawApply

console.log('获得原始接口:', rawNow)

作为插件的开发者,又该如何防范这种攻击?

显然,不能使用 rawFn.apply 这种潜在副作用的操作。ES6 提供了 Reflect API,可以更加原子地实现各种操作。

例如 document.createElement('DIV'),通过 Reflect 可这样调用:

Reflect.apply(document.createElement, document, ['DIV'])

由于 Reflect 下的方法都是静态方法,因此可将其备份到闭包内部的变量里,之后直接使用:

(function() {
  const apply = Reflect.apply

  const result = apply(document.createElement, document, ['DIV'])
  console.log(result)
})()

这样,即使攻击者重写了 Reflect.apply,我们也不受影响!

进一步,我们可将所有变量都提前备份,完全不依赖全局变量:

(function() {
  const document = window.document
  const createElement = document.createElement
  const apply = Reflect.apply

  const result = apply(createElement, document, ['DIV'])
  // ...
})()

换成本文开头的案例:

(function() {
  const obj = performance
  const rawFn = performance.now
  const apply = Reflect.apply

  Performance.prototype.now = function() {
    const val = apply(rawFn, obj, [])
    return ((val * 10) | 0) / 10
  }
})()
posted @ 2021-07-15 12:22  EtherDream  阅读(131)  评论(0编辑  收藏  举报