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
  }
})()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章