mix-blend-modeを使って表示内容を抜き取るサイドチャネル攻撃

CSS3で[mix-blend-mode](https://developer.mozilla.org/ja/docs/Web/CSS/mix-blend-mode)というプロパティが策定されました。
CSSだけで[画像加工ができる](https://qiita.com/sottar/items/2a6559c009acde6476c2)というすごいやつです。
ところで先日こちらに[クロスオリジンで中身を抜き取ることができる](https://it.srad.jp/story/18/06/02/2058226/)という脆弱性が発見され、修正されていました。
以下は発見者による解説記事、[Side-channel attacking browsers through CSS3 features](https://www.evonide.com/side-channel-attacking-browsers-through-css3-features/)の日本語訳です。

# Side-channel attacking browsers through CSS3 features

![01.png](https://qiita-image-store.s3.amazonaws.com/0/26088/b02d4e0b-270c-f02a-0f4a-8efc24fe468a.png)

HTML5とCSS3では膨大な量の機能が追加されたため、ブラウザに対する攻撃手段も増えました。
したがって、これら機能間の相互作用によって、予期しなかったセキュリティ上の問題が発生したとしても不思議なことではありません。
この記事では、その中でも実用的な攻撃手段と、その調査について解説します。

## tl;dr:

・CSS3のmix-blend-modeを使って、画面表示されているコンテンツをクロスオリジンで抜き取る脆弱性を発見した。
・悪意あるサイトにアクセスするだけで、認可無しでFacebookのプロフィール画像、ユーザ名、いいね、訪問者などを取得することに成功した。
・Chrome、Firefoxなど主要ブラウザに影響があった。

私は偶然、Facebookのユーザ名と画像が表示されているFacebookログインボタンが貼ってある[Pinterestのホームページ](https://pinterest.com/)を目にしました。

![02.png](https://qiita-image-store.s3.amazonaws.com/0/26088/bb1432dd-2e6f-e9a9-cca6-c53656d3d5dd.png)

same-originポリシーにより、Pinterestはiframeで表示されているコンテンツにはアクセスすることができません。

それが本当に真であるか、ブラウザの色々な機能を使って試してみようとしました。
その調査は、考えていた以上に多くの内容に広がり、この記事で詳しく述べる興味深いバグや、攻撃に関する共同研究を行うまでになりました。

## 1 Bug Discovery

クロスオリジンのリソースへのDOMアクセスはデフォルトでは禁止されています。
しかし、iframeのコンテンツ自体は現在のサイトと同じコンテキストで表示されているので、何らかの情報を漏らす可能性があるブラウザ機能が存在しないかを検証することにしました。
Darioと私はクロスオリジンiframe上にtransparency、rotation、mix-blend-modeといった様々なCSS機能をテストしてみました。

調査の結果、mix-blend-modeを使ったサイドチャネル攻撃が可能なバグを発見することができました。
この機能は2016年にCSS3に導入され、FirefoxやChromeなどのブラウザで利用できます。
Internet ExplorerやMicrosoft Edgeは機能をサポートしておらず、Safariは影響を受けないようです。
ブラウザのサポート状況は[Mozillaの開発者サイト](https://developer.mozilla.org/en-US/docs/Web/CSS/mix-blend-mode#Browser_compatibility)で見ることができます。

さらに調査したところ、この問題は既にChromiumチームによって発見されており、2017/03/07にうっかり[一般公開](https://groups.google.com/a/chromium.org/forum/#!topic/paint-bugs/RIjWpTd_tf0)されていました。
私たちがこのリークを報告し、元のスレッドは再びプライベートに戻されました。
最後に、このバグは2018/02/22に[バグトラッカー](https://bugs.chromium.org/p/chromium/issues/detail?id=699028)で公開され、CVE-2017-15417が割り当てられました。
Firefoxでは最近のバージョン60で修正されたので、私たちはこの記事を公開することにしました。

### 1.1 Attack Setup

サイドチャネルのバグにより、以下の攻撃が可能になります。

・クロスオリジンのiframeであれど、DIV要素内のmix-blend-modeプロパティを上書きすることはできます。
・このプロパティのレンダリングは、対象ピクセルの色に応じてかかる時間が異なります。
・よって個々のレンダリングにかかる時間を測定することによって、iframe内のコンテンツを読み取ることが可能になります。

バグの詳細を解説する前に、リアルへの影響と使用例を見てみましょう。

### 1.2 Use Cases

攻撃対象として魅力的なのは、ログインしているWebサイトの情報を取得することです。
特にiframeで提供されるコンテンツに対しては読み取り攻撃を仕掛けることができます。
幸いなことに、Facebookのメッセージ履歴やAmazonの注文履歴など、特に重要なコンテンツは、他サイトからは簡単にはiframeできません。

[クリックジャッキング](https://en.wikipedia.org/wiki/Clickjacking)などの攻撃からユーザを保護するために、JavaScriptによるFrame Bustingや、[X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options)ヘッダといった強固な保護機能が導入され、iframeを許可するページはユーザが制御できるようになっています。

しかし、Facebookのログインボタンはiframe可能でありながら、他のWebサイトに取得させたくない個人情報を含んでいます。

#### 1.2.1 De-anonymizing Facebook Users

発見したバグを再現するコンセプトHTMLページを作成しました。
このファイルを開くと、iframe内にFacebookエンドポイントを表示し、中身を抜き取るデモンストレーションを行います。

![03.gif](https://qiita-image-store.s3.amazonaws.com/0/26088/324d94b0-f489-47fb-7eaf-4c1ee5e301a3.gif)
![04.gif](https://qiita-image-store.s3.amazonaws.com/0/26088/40cea59c-5d48-2b2e-3147-9df3457ca8ca.gif)
Facebookユーザ名 ( 上 ) とプロフィール画像 ( 下 ) をiframeから抜き取るサンプル

上記のようにユーザ名とプロフィール画像が漏れる可能性があります。
別記しますが、これは事前にFacebookにログインしておく必要があります。
その後Facebookのログインエンドポイントをiframeで表示し、コンテンツを抜き取り、右下のローカルコンテキストに表示します。
さらに別の画像をオーバーレイ表示することで、ユーザが気付かないように完全にバックグラウンドで行うことも可能です。

Facebookはさらに以下のようなエンドポイントを提供しています。
・Likeした全友人のプロフィール画像を表示する[ページプラグイン](https://developers.facebook.com/docs/plugins/page-plugin/)
・任意のページに対して[Likeしたか否か](https://www.evonide.com/other/like_leak.gif)のステータス

概念実証コードは非常に実験的で非効率的なものとなっていますが、Chromeでは以下の実時間で攻撃を行うことができました。
Firefoxでも似たようなものです。

・～20秒：ユーザ名取得
・～5分：プロフィール画像
・～500ミリ秒：特定サイトにLikeしたか否か

パフォーマンスを向上させるには、1ピクセルずつリークするのではなく、文字全体を一気に読み取るようにする必要があるでしょう。
また、低解像度の白黒画像でいいのであればプロフィール画像抜き取りのパフォーマンスは大幅に向上します。
リーク速度は、JavaScriptのパフォーマンスの制限により、16ミリ秒あたり1ピクセル ( 60Hz ) が上限です。
[Fantastic Timers and Where to Find Them: High-Resolution Microarchitectural Attacks in JavaScript](https://gruss.cc/files/fantastictimers.pdf)のような最近の攻撃を用いることで、この脆弱性の抜き取り速度をさらに向上させることができるかもしれません。

#### 1.2.2 Accessing Files behind Access Control Lists

それ以外のユースケースとしては、プライベートな画像、APIレスポンス、テキストファイルなど、ログインが必要な各Webサイトのコンテンツがリークする可能性があります。

単純なサイドチャネルの脆弱性が、広汎に好ましくない影響を与えることがわかります。
何故この攻撃が成り立つのかを以下で解説していきます。

## 2 CSS Blend Modes

CSS Blend modeは2016年のCSS3で導入され、ChromeやFirefoxといったブラウザでサポートされました。
これは複数のレイヤーを相互作用させることができる機能で、PhotoshopやGimpのような画像編集ソフトでは標準装備されています。
少なくとも16種類のBlend modeがありますが、以下の画像ではそのうち6種類を可視化しています。

![05.png](https://qiita-image-store.s3.amazonaws.com/0/26088/db2e8e23-0733-9420-f493-358daba92ff1.png)

例えば`multiply`モードは2色を乗算した値になります。

・new_color = color(下レイヤ) × color(上レイヤ)

下層が白 ( 255, 255, 255 ) で上層が赤 ( 255, 0, 0 ) の場合、結果は赤 ( 255, 0, 0 ) になります。
Blend Modeの詳細については[Basics of CSS Blend Modes](https://css-tricks.com/basics-css-blend-modes/)を参照してください。

Blend Modeのレイヤについて、クロスオリジンのiframeとやりとりできることという結果は非常に意外なものでした。
私たちはこのことについてさらに調査しました。

### 2.1 Side-Channel Attacking Blend Modes

最初の疑問は、下層レイヤの色によって、Blend Modeを適用した場合のレンダリング時間が異なるのではないかということでした。

乗算のような単純な関数は、下層レイヤの色にかかわらず計算には同じ時間しかかからないため、例として適切ではないでしょう。
Blend Modeのドキュメントを読んでみると、非常に複雑な計算を要する`saturation`というBlend Modeが見つかりました。

```js
[...]
ClipColor(C)
    l = Lum(C)
    n = min(Cred, Cgreen, Cblue)
    x = max(Cred, Cgreen, Cblue)
 
    if n < 0.0
        Cred = l + (((Cred - l) * l) / (l - n))
        Cgreen = l + (((Cgreen - l) * l) / (l - n))
        Cblue = l + (((Cblue - l) * l) / (l - n))
 
    if x > 1.0
        Cred = l + (((Cred - l) * (1 - l)) / (x - l))
        Cgreen = l + (((Cgreen - l) * (1 - l)) / (x - l))
        Cblue = l + (((Cblue - l) * (1 - l)) / (x - l))
    return C
 
SetLum(C, l)
    d = l - Lum(C)
    Cred = Cred + d
    Cgreen = Cgreen + d
    Cblue = Cblue + d
    return ClipColor(C)
 
[...]
 
SetSat(C, s)
    if(Cmax > Cmin)
        Cmid = (((Cmid - Cmin) x s) / (Cmax - Cmin))
        Cmax = s
    else
        Cmid = Cmax = 0
    Cmin = 0
    return C;
 
// Compute the saturation blend mode.
Saturation(Cb, Cs) = SetLum(SetSat(Cs, Sat(Cb)), Lum(Cb))
```

全コードは[Compositing and Blending W3C Candidate Recommendation](https://www.w3.org/TR/compositing-1/#blendingnonseparable)で見ることができます。
このコードから2点気付くことがあります。
・この計算はmultiplyよりずっと複雑である
・入力された色に応じて異なる計算を行う

要点は入力に応じて計算量が異なるというところで、すなわちサイドチャネル攻撃の可能性を示しています。
理論は間違っていないはずで、私たちは興奮しましたが、実際に動くコードを作るのは予想外に複雑でした。

一回の計算ではレンダリング時間に計測できるほどの差が出ないので、効果を高めるために数千のレイヤを重ねることにしました。
Blend Modeをsaturationに設定することで、最終的に攻撃に成功しました。
レイヤの色がAである場合、色がBであった場合の2倍のレンダリング時間がかかりました。
ピクセルの色がレンダリング時間に影響することがわかったため、これをリークさせることもできるだろうと思われました。

#### 2.1.1 Blend Mode Selection

原理をもっと理解するため、また攻撃対象として適切なBlend Modeを選ぶため、Chromeなどのブラウザで使われているグラフィックライブラリ[Skia](https://github.com/google/skia)を調査しました。
Skiaはテキスト、図形、画像を表示するために使用されていて、Blend Modeのソースコードは[https://github.com/google/skia/blob/master/src/gpu/glsl/GrGLSLBlend.cpp](https://github.com/google/skia/blob/master/src/gpu/glsl/GrGLSLBlend.cpp)にあります。

ソースコードを調べると`SkBlendMode::kHue`や`SkBlendMode::kSaturation`などが最も計算量の多いコードであることがわかりました。
いくつかのテストの後、私たちはBlend Mode `saturation`に狙いを定めました。

### 2.2 Leaking Black Pixels

saturation Blend Modeにおいて多数のレイヤを積み重ねると、ターゲットのピクセルの色によって、計算量が累積してどんどん増加するか、もしくは非常に単純な計算になるか、どちらかになることがわかってきました。
特にいずれかの色の値が10より大きい場合はレンダリングがゆっくりになることが、実験によってわかりました。

注意：このセクションでは、ピクセルとレイヤを1×1サイズという前提で扱っています。

これは興味深いように思えますが、実際にテストするためにはいくつかの前処理を追加する必要がありました。
例えば、カラー値が正確に0であるかは以下のようにテストします。

![06.png](https://qiita-image-store.s3.amazonaws.com/0/26088/219de93d-2508-2948-f7aa-c79ca6844b08.png)

この図では、前処理のための青色レイヤをひとつと、検出用の灰色のレイヤを大量に積み重ねています。
この例では、ターゲットのピクセル色がRGB( 0, ***, *** ) に一致する場合にのみ、レンダリングに2倍の時間がかかります。

#### Saturation Stack (gray)

灰色レイヤは、saturation Blend Modeを適用させるための同色のレイヤです。
この下にある入力レイヤの色の値が10より大きい場合、この層は測定可能なレンダリング遅延を引き起こします。
10以下の場合この部分はほとんど何もせず、高速にレンダリングされます。

#### Preprocessing Stage (blue)

ピクセルは任意の色を持っているため、そのままSaturation Stackに渡してもほとんど何もわかりません。
そこで必要の無い色要素は、0で乗算するフィルタ層を通してミュートします。

```js
preprocess(target_pixel) = rgb(11, 0, 0) // if color(target_pixel) == rgb(0, _, _)
                           rgb(0, 0, 0) // otherwise
```

このフィルタを通すことで、ターゲットのピクセル色がRGB( 0, ***, *** ) である場合にのみレンダリングが遅くなるようになりました。
このテストによって、既にiframeからテキストを取得できるほどにはなりましたが、これをさらに進めて、ピクセル色を完全に特定できることを示します。

### 2.3 Leaking Precise Colors

既に上記テストのフィルタ層を調整することでピクセル色の完全特定は可能なのですが、それを行うには1ピクセルにつき256*3=768回のテストを行わなければなりません。

新しい目標は、ターゲットピクセルの色要素の特定のビットが立っているかを確認するテストを考えることでした。
各色は8ビットで、3つの色があるため、正確な色の特定には24回テストを行えばよいということになります。

このテストについては、複雑な多層構造の前処理が必要でした。
このテストの設計はかなり楽しい知的探求で、異なるBlend Modeを積み重ねることによって計算を考えるのはクールな作業でした。

新たな`scan tower`は次のようになりました。

![07.png](https://qiita-image-store.s3.amazonaws.com/0/26088/c59fffeb-f019-d792-0fd1-4538db6a7708.png)

見てのとおり、前処理が複雑になっている以外は同じです。

#### Preprocess Stack

ターゲットのピクセル色について、特定のビットが立っている場合にのみ、多層レイヤの重い処理が実行されるようにしています。

```js
preprocess(target_pixel) = rgb(11, 0, 0) // if 8th bit of red target pixel channel is 1
                           rgb(0, 0, 0) // otherwise
```

この部分は全体的に非常に複雑で奇妙なので、細かい部分は飛ばしてください。

赤色 RGB( 160, 0, 0 ) 例を考えてみましょう。
ここでの目的は赤チャネルの最上位ビットを取得することです。
対象のバイナリ表現は ( 10100000b, 0, 0 ) で、目標は左端の1です。
このビットを取り出すには、前処理レイヤの3点、Difference、Lighten、Differenceを以下のように調整します。

Difference：これ以前に検出した全てのビットの合計。まだ何も漏れていないのでRGB (00000000b, 0, 0) となる。
Lighten：`2^(対象ビット)-1`。最上位ビットであるため`2^7-1`、よって RGB ( 127, 0, 0 ) とする。
Difference：Lightenレイヤと同じ色。よって RGB ( 127, 0, 0 ) 。

前処理層での全ての操作は以下のとおりになります。

| Blend-Type | 操作 | 入力 | 自分 | 結果 |
|---|---|---|---|---|
| Difference |  abs(入力 – 自分) | rgb(160, 0, 0) | rgb(0, 0, 0) | rgb(160, 0, 0) |
| Lighten |  max (自分, 入力) | rgb(160, 0, 0) | rgb(127, 0, 0) | rgb(160, 0, 0) |
| Difference |  abs(入力 – 自分) | rgb(160, 0, 0) | rgb(127, 0, 0) | rgb(33, 0, 0) |
| Color-Dodge |  (入力 == 0) ? 0 : 255 | rgb(33, 0, 0) | rgb(255, 255, 255) | rgb(255, 0, 0) |
| Multiply | 入力 *=rgb(11, 0, 0) | rgb(255, 0, 0) | rgb(11, 0, 0) | rgb(11, 0, 0) |

上で説明したように、Saturationレイヤにrgb( 11, 0, 0 ) が入力された場合はレンダリングが遅くなり、赤の最上位ビットが1であることが判明します。
2番目のビットを検出する場合は、最初のDifference、Lighten、Differenceの3レイヤの対象ビットを調整します。

| Blend-Type | 操作 | 入力 | 自分 | 結果 |
|---|---|---|---|---|
| Difference |  abs(入力 – 自分) | rgb(160, 0, 0) | rgb(128, 0, 0) | rgb(32, 0, 0) |
| Lighten |  max (自分, 入力) | rgb(32, 0, 0) | rgb(63, 0, 0) | rgb(63, 0, 0) |
| Difference |  abs(入力 – 自分) | rgb(63, 0, 0) | rgb(63, 0, 0) | rgb(0, 0, 0) |
| Color-Dodge |  (入力 == 0) ? 0 : 255; | rgb(0, 0, 0) | rgb(255, 255, 255) | rgb(0, 0, 0) |
| Multiply | 入力 *=rgb(11, 0, 0) | rgb(0, 0, 0) | rgb(11, 0, 0) | rgb(0, 0, 0) |

上記例の出力は0であるため、素早くレンダリングされ、2番目のビットは0であることがわかります。
これを繰り返すことで、任意のピクセルの色を特定することが可能になります。

## 3 POC

[Facebook Name leak POC](https://github.com/evonide/misc/blob/master/fb_name_leak_poc.html)
[Facebook Name & Picture leak POC](https://github.com/evonide/misc/blob/master/fb_leak_general_poc.html)

注意：このPOCは多分に実験的であり、全体的に品質はよくありません。

## 4 Misc

Safariに対する攻撃は成功しませんでした。
調査したところ、Blend Modeの計算はベクトル化され、既に対処がなされていました。

・[https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/graphics/filters/FilterOperation.cpp](https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/graphics/filters/FilterOperation.cpp)
・[https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/graphics/ca/cocoa/PlatformCAFiltersCocoa.mm](https://github.com/WebKit/webkit/blob/master/Source/WebCore/platform/graphics/ca/cocoa/PlatformCAFiltersCocoa.mm)
・[https://github.com/WebKit/webkit/blob/master/Source/WebCore/css/StyleResolver.cpp](https://github.com/WebKit/webkit/blob/master/Source/WebCore/css/StyleResolver.cpp)

### 4.1 Related Work

かつて存在した、似たような攻撃。

#### 2013: [Pixel Perfect Timing Attacks with HTML5](https://www.contextis.com/media/downloads/Pixel_Perfect_Timing_Attacks_with_HTML5_Whitepaper.pdf)

SVG filterなどのHTML5の機能を使って、クロスオリジンでのピクセル値を取得するタイミング攻撃。
今回の問題と非常によく似ている。

#### 2015: [On Subnormal Floating Point and Abnormal Timing](https://cseweb.ucsd.edu/~dkohlbre/papers/subnormal.pdf)

Firefoxにおいて、浮動小数点演算に時間差があることを利用してSVGにタイミング攻撃する方法。

#### 2016: [lcamtuf’s blog – CSS mix-blend-mode is bad for your browsing history](https://lcamtuf.blogspot.ch/2016/08/css-mix-blend-mode-is-bad-for-keeping.html)

mix-blend-modesとクリックジャッキングを用いて、訪問済サイトの情報を抜き出す方法。
ただしサイドチャネル攻撃とは無関係。

### 4.2 Responsible Disclosure

私たちは、実証に成功した後、すぐにFacebookに連絡しました。
この攻撃がクリックジャッキングではないことを理解してもらったところ、この脆弱性の修正はFacebookにとっては不可能であることが明らかになりました。
彼らにとって唯一の修正手段は、全てのエンドポイントを削除することだったのです。

同時に私たちは、ChromeとSkiaの開発者に対して、この問題に対処するよう求めました。
最後に、私たちのうっかりミスによってMozillaへの連絡が遅れました。

このバグは、Blend Modeの計算をベクトル化することによって対処されました。
修正後のSkiaのコードは[こちら](https://cs.chromium.org/chromium/src/third_party/skia/src/jumper/SkJumper_stages.cpp?rcl=9018952290a468886c819405c6d9495b4aa5d7d4&l=416)にあります。
バグを修正したついでにレンダリング速度の高速化までできたcommitは[ここ](https://skia-review.googlesource.com/c/skia/+/17992)と[ここ](https://github.com/google/skia/commit/b2ef4d6d152300f8a1148aff8a673036b676be33)で確認できます。

## 5 Timeline

2017-03-07：Max Mayによる[初のレポート](https://bugs.chromium.org/p/chromium/issues/detail?id=699028)がChromiumのメーリングリストに流れた。
2017-05-22：我々が独立にこの脆弱性を発見した。
2017-05-25：[Skia](https://github.com/google/skia/commit/b2ef4d6d152300f8a1148aff8a673036b676be33)で脆弱性が修正された。
2017-06-15：FacebookのVRPにバグを提出した。
2017-07-27：Facebookでは修正が不可能であるという最終回答を得た。
2017-11-26：MozillaのVRPにバグを提出した。
2017-12-06：Chrome 63.0でバグが修正された。
2018-05-15：Firefox Quantum 60.0でバグが修正された。

## 6 Conclusion

サイドチャネルの脆弱性は、最近Meltdown/Spectreでも注目されたように、非常に根深くしばしば修正が困難なことがあります。
ますます増えつつあるブラウザの機能とそれらに対する厳しいパフォーマンス要求が、予想もしない形で問題となって出てきたとしても驚くには値しません。
特に、mix-blend-modeは、CSS3とWebkitが持つ大量のレンダリング機能の、ほんの氷山の一角に過ぎません。

私たちはFacebookに対する攻撃の可能性を検証しただけです。
しかし、Web上には、同じような攻撃を受ける可能性のある機密リソースが大量に存在します。
残念ながら私たちは、今後も同じような脆弱性がたくさん発見されるだろうと予想しています。

P.S. 重要なリソースには`X-Frame-Options`ヘッダを入れて、iframeを拒否するようにする必要があります。


# 感想

サイドチャネル攻撃の中でも典型的なタイミング攻撃です。
しかし描画内容によってレンダリング速度に差が出るという話なので、今後も色々と出てきそうな気がしますね。
あと何か重たい処理を入れて処理落ちとかされたらどうなるんだろう。
『異なる処理でも同じ処理落ちさせろ』とかぼくにはとてもできない。

また、この調査ですが『色によってレンダリング時間に差がある』だけで終わらせず、そこから色を完全に特定するところまで調べてるところが面白いですね。