QUICをゆっくり解説(6):増幅攻撃との戦い

2021年08月30日 月曜日


【この記事を書いた人】
山本 和彦

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

「QUICをゆっくり解説(6):増幅攻撃との戦い」のイメージ

増幅攻撃(amplification attack)とは、攻撃者が第三者の管理するサーバにデータの量を増幅させて標的に送りつけるDoS攻撃の一種です。

攻撃者は、小さなデータをサーバに送りつけます。このとき、IPパケットの始点アドレスに標的のIPアドレスを詐称して入れます。プロトコルによっては、サーバは大きな応答を返します。返答先は、標的のIPアドレスになります。

この攻撃にたくさんのサーバを利用すれば、攻撃者の回線には余裕があるが、標的の回線はパンクさせるという状況を実現できます。

攻撃者にとって、TCPには増幅効果を期待できません。攻撃者が送信するSYNのTCPセグメントと、サーバが返すSYN+ACKのTCPセグメントは大きさがほとんど同じだからです。TCPでは、増幅効果を狙わず、大量のパケットを送りつける反射攻撃(reflection attack)が主流です。

一方、UDPを利用したプロトコルが増幅攻撃に悪用されることがいくつもありました。QUICでは、ハンドシェイクの際に、サーバが証明書など大きなデータを返信しています。増幅攻撃に悪用されないでしょうか?

3倍まで返し

QUICで、増幅攻撃を緩和する規則として、以下の2つがあります。

  • クラアントが送るInitialパケットを含むUDPペイロードの大きさは、1,200バイト以上でなければならない。サーバは1,200バイトより小さなパケットを受け取った場合は、それを破棄する。
  • クライアントのIPアドレスが正当と判断できるまでは、サーバが送れるQUICパケットの大きさは、受け取ったバイト数の3倍までである

0-RTTの場合を除くと、クライアントがInitialパケットを送る場合、UDPペイロードに収められるのは、典型的にはInitailパケットのみです。ですので、Initialパケットの大きさを1,200バイト以上にする必要があります。

そのためには、CRYPTOフレームに加えて、PADDINGフレームを格納するのが一般的です。PADDINGフレームの型の値は0x00で、フレームのペイロードはありません。つまり、0x00のバイト列がパディングになります。

相手のIPアドレスが正当だと判断できるのは、(後述のトークンがなければ)相手からHandshakeパケットを受け取って、それを問題なく処理できたときと定義されています。クライアントからのInitialパケットに対して、サーバがInitialパケットやHandshakeパケットを返す際は、クライアントのIPアドレスは正当だと判断されていません。よって、たとえば証明書が大きな場合は、すべてを送り終える前に「3倍までルール」にひっかかってしまいます。

そこで、クライアントは適切なタイミングで大きなパケットを送信し、サーバの送信を再開させる必要があります。常套手段としては、ACKフレームにPADDINGフレームを付けます。ServerHelloを完全に受け取っていればHandshakeパケットとして、そうでなければInitailパケットとして送信します(下図)。

リトライ

3倍まででも増幅させたくないというサーバ管理者もいらっしゃるでしょう。これを実現するために、リトライという仕組みがあります。

Initialパケットのヘッダには、トークンというオプションのフィールドがあります。トークンとは、サーバが生成する情報です。クライアントには単なるバイト列です。しかし、生成したサーバだけは、情報の内容を解釈できます。

リトライを採用するサーバでは、クライアントから送られてきたInitialパケットのヘッダにトークンがない場合、トークンを生成し、Retryパケットに格納して送り返します。Retryパケットを受け取ったクライアントは、先ほど送ったInitialパケットにトークンを付加して、再び送ります(下図)。

リトライを採用するサーバでは、クライアントから送られてきたInitialパケットのヘッダに有効なトークンがあれば、IPアドレスが詐称されてないことは確かなので、(「3倍までルール」なしで)ハンドシェイクを開始します。

QUICの実装の多くでは、リトライが実装されています。QUICサーバの管理者は、設定でリトライを採用するか指定します。

0-RTTとリトライ

前回、2回目のハンドシェイクでは0-RTTパケットでアプリケーションデータを送れると説明しました。しかし、これまでの説明だと、リトライを採用しているサーバは、0-RTTパケットを処理せずに、リトライさせるように思えます。一往復すると 0-RTT の意味がなくなってしまいます。

前回説明した NewSessionTicket と同様に、トークンも1つ前のコネクションで受け取ったものが使えます。次回のコネクションのためのトークンは、NEW_TOKENフレームに格納されて、サーバからクライアントに送られます。実は上記の図には、NEW_TOKENフレームが描かれています。

なぜ1,200バイト?

現在のインターネットでは、PMTU(Path MTU:通信路全体のMTU)として1,280バイトを保証することになっています。一般的にIPv4とIPv6のヘッダの大きさは、それぞれ20バイトと40バイトです。また、UDPのヘッダの大きさは8バイトです。このため、IPv6を使った場合、1,232バイトのQUICパケットは相手に届くことが保証されています。1,200バイトは、必ず届いて、そこそこ大きい値として定められています。

山本 和彦

2021年08月30日 月曜日

Haskellコミュニティでは、ネットワーク関連を担当。 4児の父であり、家庭では子供たちと、ジョギング、サッカー、スキー、釣り、クワガタ採集をして過ごす。

Related
関連記事