gRPCサーバにHTTP2レイヤのDDoS攻撃が通じるか試してみる

この記事は[NTTテクノクロス Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ntt-tx) の19日目の記事です。

こんにちは。NTTテクノクロスの上原(@ueyasu)です。

攻撃手法の説明は省略していますが **DDoSは検証目的であってもインターネット上のサーバに対して動作させることの無いようお願いします。
インターネット上のサーバへ攻撃した場合、法令により罰則が科される可能性があります。**

自分のクラウド環境であっても多量のパケットが流れると請求額が増えますし、プロバイダに迷惑をかけるため、閉じたネットワークのなかで試してください。

## TL;DR

- gRPCはトランスポートレイヤにHTTP2を利用しているのでHTTP2のDDoSが撃てる
- HTTP2レイヤの事象はアプリケーションレイヤのログだけでは原因分析が難しい
- 外部に公開する場合はトラフィックモニタリングを忘れずに
- こんな攻撃するヤツはたぶんいない

## はじめに

中学生くらいの子供がパソコンを触ったとき、高確率で罹患するのが闇のハッカーになりたい病です。
そのような子供が手を出しやすい、簡単な攻撃というイメージが先行しているDDoS攻撃ですが、昨今はbotnetを高度に利用したDDoS as a Serviceとして商業化が進んでいます。
高度化したDDoS攻撃は思いもよらぬプロトコルを利用したりするものです。この記事では、gRPCサーバ構築時に見落としがちなHTTP2に対してDDoS攻撃を行ってみます。

### gRPCについて

[gRPC](https://grpc.io) は Google 社内で利用されていた stubby をもとにした RPC フレームワークです。一般的にはサービス定義に [Protocol Buffers](https://developers.google.com/protocol-buffers) が用いられ、シンプルな定義で様々な言語から開発できます。REST API では定義の難しい双方向通信も容易に実現可能です。

今回はgRPCのアプリケーションをイチから作成するのは面倒なので出来合いのものを利用します。
[公式にGo実装のecho server exampleがある](https://github.com/grpc/grpc-go/tree/master/examples)のでそちらを利用しましょう。

以下のコマンドでclient, serverをインストールできます。

```sh
$ go get -u google.golang.org/grpc/examples/helloworld/greeter_client
$ go get -u google.golang.org/grpc/examples/helloworld/greeter_server
```

ターミナルを2つ開き、localhostで通信させてみましょう。通信に成功した場合、サーバには以下のように出力されます。

```sh
$ greeter_client
2019/12/19 00:01:02 Greeting: Hello world
```

```sh
$ greeter_server
2019/12/19 00:01:02 Received: world
```

さて、gRPCは具体的にどんな通信をしているのか確認してみましょう。Wiresharkを起動し、Loopbackインターフェイスをキャプチャします。

![http2-cap-0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/175211ac-317c-dd78-0e27-67843513e246.jpeg)

なにげにIPv6使ってますね。本題ではないのでスルーしますが。

このままだと何だかよくわかりませんが、gRPCはトランスポートレイヤにHTTP2を利用します。HTTP2として解釈させてみましょう。

[ソースを確認](https://github.com/grpc/grpc-go/blob/86c8a0cb87c78383ee062a935ccb178130d56213/examples/helloworld/greeter_server/main.go#L34)してみると、この通信はポート50051番を利用しているようです。
`...としてデコード` (as Decode) からポート50051をHTTP2と設定してみましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/b6a5ad12-4c9f-8186-b937-46d67b77b65a.jpeg" alt="as-decode-1.jpg" width="80%">

![as-decode-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/1e955276-d6c5-f6ab-a920-c38cc429d18f.jpeg)

![http2-cap-01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/ad574e7c-8477-e62e-a70d-e1009d42f80c.jpeg)


`Protocol` カラムにHTTP2が表示されるようになりました。
このように、gRPCサーバを建てるということはHTTP2サーバを建てることとほぼ同義なのです。

## HTTP2について

HTTP2は文字通りHTTP/1.1を改良したプロトコルです。あまり意識してHTTP2を使っている人は多くないかもしれませんが、GoogleやWikipediaでも利用されており、普及率は既に40%を超えています ( [Usage Statistics of HTTP/2 for Websites](https://w3techs.com/technologies/details/ce-http2) )

HTTP2ではデータをやり取りするフレーム(※ フレーム: HTTP2で通信する要素の最小単位) 以外にも、コネクションの設定を伝達するSettings Frameや帯域制御に利用されるPing Frameといったアプリケーションレイヤには見えないフレームが存在します。

Ping of death, Smurf Attackなどなど、Pingと見るとDDoSに使いたくなるのが攻撃者のサガといえましょう。今回はHTTP2のPing FrameをgRPCサーバにたくさん打ち込んだらどうなるのかを検証してみます。

なお、HTTP2の Ping FloodはCVEでいうと[CVE-2019-9512](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9512)が割り当てられています。

## HTTP2 Ping Flood

通常、GoでWebサーバとHTTP2による通信を行うときにコーダが意識する作業はありません。
というのも、golangの `net/http` モジュールは標準でHTTP2をサポートしています。サーバがHTTPであればHTTPを、HTTP2であればHTTP2をしゃべってくれます。

とはいえHTTP2を強制する手段はいくつかあり、Ping Floodのような「普通ではない使い方」を目指す場合は意図的にHTTP2のモジュールを使うことになります。今回は普段あまり使われない`net/http2`モジュールを使ってPing Frameを送信します。
実装されたコードは想像してください。


さて、このコードをビルドして動作させてみましょう。`top` でCPU負荷を見るとこんな感じになります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/2b594389-34cf-95e3-4faf-0e16b6c40d3e.png)

`pingflood_local` が攻撃プロセス、`greeter_server` が被攻撃サーバです。
CPU負荷がかかっているのが見えますが、これだと攻撃されたことによる負荷なのか、攻撃それ自身によって負荷がかかったのか分かりづらいですね。サーバを分けて、攻撃先を修正して試してみましょう。

- 攻撃側
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/79c1bf2d-f2ce-226b-ae9c-2caecec52b68.png)

- 被攻撃側
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/27120b55-734b-be75-4df3-af42c58dce19.png)

**攻撃側のほうが負荷高いやんけ！**

──というわけで、何も考えずPingフレームを投げ続けるだけの攻撃はあんまり効率よくないことがわかりました。とはいえ、**何もしていないように見えるサーバ**で、20%程度のCPU負荷を与えることができました。攻撃側が5台くらいマシンを用意すれば勝機がありそうです。

なお、試しに正規の `greeter_client` をシェル上から `while` で無限ループさせると以下のような負荷になりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/31124/703dc2ee-b5c5-fb0e-1f79-7a8215e77918.png)

こちらは13%程度なので、単純に `while` で正規アクセスをぶん回されるよりは効果ありそうですね。

この攻撃の面白いところは**gRPCサーバのアプリケーションログには何も残らないこと**です。アプリケーションログのみを見ている場合、「なぜかCPU負荷が上昇している」と見えるかと思います。

### なにがしかのログを出力させる

gRPCサーバは環境変数 `GRPC_GO_LOG_SEVERITY_LEVEL=info` を指定すると追加のログ出力を得ることができます。上記を指定した上で攻撃すると、以下のようなログが出力されます。

```
WARNING: 2019/12/19 00:11:22 grpc: Server.Serve failed to create ServerTransport:  connection error: desc = "transport: http2Server.HandleStreams received bogus greeting from client: \"\\x00\\x00\\b\\x06\\x00\\x00\\x00\\x00\\x00ping\\x00\\x00\\x00\\x00\\x00\\x00\\b\\x06\\x00\\x00\\x00\""
```

`failed to create ServerTransport` でhttp2の何かでエラーが発生しているのはわかりますが、これを読んで「Ping Floodされている！」と思う人はいないでしょう (ログ中に見える `ping` は攻撃コード内で意図的に書いたものであり、いくらでも別の言葉に変えられます)。

というわけで、多少のアタリをつける手助けにはなるかもしれませんが、ログから攻撃状況を推測するのはやはり難しいと言えます。

### どのようにして検知・防御すべきか？

一般的なHTTP2のWebサーバはCDNが前面にいる場合が多く、オリジンサーバまでインターネットからHTTP2が到達するケースは少ないと言えます。
そのため、Webサーバに対するHTTP2 Ping FloodであればCDNが代わりに負荷を請け負い、オリジンサーバは無事となるでしょう。

一方で、gRPCサーバなどAPIサーバは動的なレスポンスが必要であり、インターネットから直接アクセス可能な場合が多いです。
攻撃者がgRPCを使っていることを知っており、ポート番号も把握されていた場合、この攻撃手法はそこそこ使いやすいかもしれません。

とはいえネットワークトラフィックを見ていれば、特定のホストから多量のトラフィックが流れ込むことが簡単にわかるでしょう。

本稿で説明したようなGo実装のサーバであれば、多量のPingフレームを一気に受信した場合、RSTパケットによりコネクションを切断します。
単一ホストから切断・再接続を明らかに異常なほど繰り返すため、ホストの特定は容易です。
**アプリケーションログだけでなく、ネットワークも忘れずに監視するのが良いでしょう**。

防御についても、botnetで幅広くIPアドレスを使われるとやっかいですが、多少のホストであればブラックリストに乗せて終了です。
上記のようにわかりやすく異常な挙動を見せるため、いまどきのWAFであればbotとして自動ブロックしてくれるかもしれません。

巨大なbotnetを使われた場合は対処が難しいですが、正直この攻撃よりも効率的で強力な攻撃はたくさんあるため、そこまで準備した人がわざわざこの攻撃を使うことは無いでしょう。

## 終わりに

gRPCを使っているとHTTP2のレイヤは意識しないことが多いですが、HTTP2プロトコルをしゃべっているのは間違いありません。

これはgRPCに限った話ではなく、REST APIサーバでもHTTPやTCPをしゃべっています。このような意識しづらいレイヤは攻撃者にとって狙いやすく、攻撃された側にとっては実装で解決できない意味でもいやらしい部分です。

攻撃手法を紹介しつつ「まぁ使われないだろうな」とは説明しましたが、思いもよらぬ脆弱性が見つかってHTTP2レイヤを強く意識する必要があるときも来るかもしれません。
なんにせよ知識はあったほうが良く、備えはあったほうが良いでしょう。

──というわけで、冒頭にも書いてますがまとめです。

- gRPCはトランスポートレイヤにHTTP2を利用しているのでHTTP2のDDoSが撃てる
- HTTP2レイヤの事象はアプリケーションレイヤのログだけでは原因分析が難しい
- 外部に公開する場合はトラフィックモニタリングを忘れずに

なお、弊社でも[セキュリティ関連のソリューションを展開](https://www.trustshelter.jp?utm_source=qiita&utm_medium=ac2019&utm_campaign=19)しているので、お困りの際は力になれるかもしれません。

明日、[NTTテクノクロス Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ntt-tx) の20日は @ttttfx702p さんの記事です。


## 補足

### gRPCはTLSで接続するから安全なのでは？

キャプチャしたパケットが複合なしで開けたように、 `greeter_server` はTLSを利用せず平文で通信しています。
一般にgRPCサーバはTLSを利用することになっており、暗号化しない場合は `WithInsecure` を指定する必要があります ( [greeter_clientの実装部](https://github.com/grpc/grpc-go/blob/085c980048876e2735d4aba8f0d5bca4d7acaaa5/examples/helloworld/greeter_client/main.go#L39) )。
ですが、**TLSは通信経路の秘密を担保するものであり、DDoS耐性が高くなるものではありません**。特に、この攻撃はgRPCのエンドポイントが不明でも通じるので暗号化によって得られる恩恵は無いでしょう。
しかし、一般論として外部公開の有無に関係なく、gRPCの通信はTLSを利用するべきです。

### 送信フレーム数とキャプチャしたパケット数が一致しない

HTTT2の通信では、1パケットのなかに数フレームが詰め込まれます。そのため、フレーム数と送信パケット数は必ずしも一致しません。