web-dev-qa-db-ja.com

タイミングセーフな文字列比較-長さリークの回避

汎用的に使用するための一般的なタイミングセーフな比較関数を作成しているとしましょう。両方の文字列の長さが等しい場合に安全になるようにすることはよく知られています。ただし、文字列の長さが異なる場合(任意)、どのようにして安全性を確保できるかについては、よくわかりません。

1つの文字列は「既知」と見なされ、もう1つの文字列は「不明」と見なされます。攻撃者が制御できるのは「不明な」値のみであると想定します。理想的には、この関数は既知の文字列の長さに関する情報を漏らさないはずです。

次のような簡単な実装。

// returns 1 is same, 0 otherwise
int timing_safe_compare(char *known, size_t known_len, char *unknown, size_t unknown_len) {
    // Safe since all strings **will** be null terminated
    size_t mod_len = known_len + 1;
    size_t i;
    int result = 0;

    result = known_len - unknown_len;
    for (i = 0; i < unknown_len; i++) {
        result |= known[i % mod_len] ^ unknown[i];
    }

    return result == 0 ? 1 : 0;
}

ただし、ここでの問題は、キャッシュ情報のリークがある可能性があることです。

たとえば、x64のWordサイズは64ビットです。したがって、1つのレジスタに8文字を収めることができます。既知の値が7文字以下の文字列である場合(known_lenに1を追加するため)、不明な文字列がそうであっても、既知の文字列に対して別の読み込み操作を実行する必要はありません。

つまり、不明な文字列のサイズが既知の文字列と1つ以上のWordの境界で異なる場合、実行される「作業」の合計量が変わる可能性があります。

私の最初の本能は、同じサイズの文字列のみを比較することですが、その後、長さ情報がリークされます(実行はいくつかの異なるブランチに従うため)。

したがって、これには2つの質問が残ります。

  1. この小さな違いは心配するのに十分ですか?

  2. この種の違いは、既知のサイズに関する情報をまったく漏らさずに防ぐことができますか?

21
ircmaxell

cachesが原因で、長さに関する情報を漏らさずにarbitrary長さの文字列を処理することは非常に難しいようです(つまり、方法がわかりません)。定義上、非常に長い文字列は多くの領域を占めるため、文字列を読み取るとキャッシュとの相互作用が発生します。 RAM=から文字列にアクセスすると、キャッシュミスがトリガーされ、キャッシュから他のデータ要素が削除され、アプリケーションコードの将来の動作に影響を与えます。キャッシュミスは数十または数百のクロックサイクルにもなります。ブランチの予測ミスよりも、外部から見ると少なくとも10倍見えますブランチについて心配する場合は、キャッシュについてもっと心配する必要があります。

ただし、paddingでチートできます。比較したい2つの文字列を、ゼロで満たされた2つの大きな同じサイズのバッファーの先頭に書き込むように配置できると仮定します。また、値0のバイトは通常の文字列(たとえば、これらはC文字列)に表示できないと仮定します。次に、同じ長さの2つのbuffersをリークなしで比較するだけです。バッファ長はリークしますが、これは固定された一定の既知のパラメータであるため、問題ありません。

これは問題を解決しません。動かします。ここで、作成した文字列がサイズ情報を漏らさずにバッファに書き込むことができることを確認する必要があります。一般的に言えば、もはやstringsはありません。 与えられた固定長のバイナリ値があり、それを大きなmemcpy()でコピーします。これらの値はたまたま文字列解釈であり、バイトは文字と見なされ、値ゼロの最初のバイトまでです。


より高い視点から見ると、「安全な文字列比較機能」を持つことは、タイタニック号にバケツを運ぶようなものです。コードが秘密データを処理している場合、everythingデータを操作すると、タイミング攻撃を受ける可能性があります。一般に、アプリケーションには次の2つの種類があります。

  • only秘密の部分が単一の暗号要素であり、他のすべてがパブリックである場合、いくつかのリークのないプリミティブを使用することには意味があり、全体的なセキュリティが向上します。典型的な例は Certification Authority で、唯一の秘密の部分はCA秘密鍵です。署名アルゴリズムが秘密を漏らさない限り、システム全体はタイミング攻撃に対して堅牢です。同様に、パスワードベースの認証は行うが、それ以外は公開データのみを含むWebサイトでも問題ありません。

  • 機密情報がシステム全体に広がっている場合(パスワードベースの認証を行って一部の機密データへのアクセスを許可するWebサイトなど)、文字列の比較に集中すると要点を逃します。 サーバーコード全体をリークフリーにする必要があります。これはかなり困難な作業です(そして、その方法は実際にはわかりません)。

いずれにせよ、言語が「高水準」である場合、特定のコードをサイドチャネル攻撃から保護することは困難になります。 PHPなどの言語では、自動メモリ管理(ガベージコレクター)と文字列管理(文字列は整数のようにvaluesです)はまったく役に立ちません。これが、Cで実装された低レベルのプリミティブ(リークのない文字列比較関数など)を提供する必要がある理由ですが、問題ははるかに大きく、PHPコードも多く含まれています) 。

15
Thomas Pornin

キャッシュリークを介してメモリアクセスパターンを観察できる攻撃者を想定している場合、シークレットの長さを学習する攻撃者から保護しようとするのはばかげています。彼はいつも知っています。これを防ぐ唯一の方法は、segfaultingなしで文字列の末尾を超えてアクセスできることを保証することです。これは、プログラミング言語ですべての文字列を過剰に割り当てずにできないことはほぼ確実です。

4
orlp

この機能が必要なPHPプログラマーのニーズを調査しましたか?

私が考えることができる実用的なアプリケーションでは、パスワード、セッショントークンなどを確認します。既知の文字列は比較的小さく、たとえば64バイト未満です。 1つのIntelキャッシュライン内。したがって、簡単な実装では実際には異なるキャッシュアクセスパターンは発生しません。

長さを漏らさずに長い文字列を比較する必要がある場合は、代わりにハッシュを比較することを検討してください。

4
paj28

単に...

...文字列を比較しないでください、それらを比較しますハッシュ

はい、これは時間の安全性のためにこれを意味します(パスワードの安全性の副作用、取り残されています)。

これは何をするのか

ハッシュを比較するとき、あなたはしないフォローについて心配する必要があります(最初は明白ではないかもしれません)

  • 長い文字列の場合、ハッシュ処理に時間がかかります

    なぜ?ユーザーの入力と比較する正しいハッシュは(明らかに)固定長であり、ユーザーが取得できる唯一の情報は、プログラムは彼の入力をハッシュするために使用します(最悪の場合、これは秘密のハッシュアルゴリズムについていくつかのヒントを与える可能性があり、秘密はとにかく依存してはなりません)

    唯一の例外は、正しいハッシュをどこかに保存できない場合です。最初に別名パスワードを直接処理するを計算する必要があると、まったく同じパスワード/長さの問題が再び発生します。

  • ブランチの予測ミスまたはキャッシュミス

    明らかに、前述のように、特定のユーザーの正しいパスワードがどれだけ長くても、正しいハッシュは常に同じ長さです。

真剣に、この簡単なプロセスは非常に難しいものからささいな問題を作ります。

ハッシュ強度について

弱いハッシュプロセス(またはばかばかしいエントロピーを持つプロセス)を使用している場合は、直接のパスワードの等価性(ハッシュの比較からの肯定的な結果の後に)をさらにチェックして、衝突から身を守ることを検討できます。

ただし、衝突が発生した場合は、タイミング情報がリークします/時間がかかります。

結論:弱いハッシュアルゴリズムを使用せず、saltを適用すれば、大丈夫です! ;-)

2
Levite

私が間違っている場合は修正してください(Thomasに返信するだけでなく、一般に元の質問にも答えます)が、コードをリークのないチェックに向けて努力できるはずです。この例では、「既知」は既知の値であり、バッファに事前に埋め込まれています。既知の値が「qwerty」であり、最大長64を許可する場合、「qwerty」は64のバッファーに事前入力(初期化され、ロード時に1回保存)されます。これにより、メモリーの負荷が常に一定になるはずです。何も与えずに)。この例では、キャッシュミスによってキャッシュにあるかどうかのみがわかります。元の投稿でコードを複製します。

int check(char *known, size_t known_len, char *unknown, size_t unknown_len, size_t max_len) {

size_t i;
int result = 0;

  // Constant time check, only gives away maximum length.
  if (unknown_len > max_len)
      return 0;

  // Will only give away the length of the attackers string, unless it was already too large (condition above).  Don't bother doing an extra memcpy on your known or the attackers.
  for (i = 0; i < unknown_len; i++) {
      result |= known[i] ^ unknown[i];
  }

  return result == 0 ? 1 : 0;
}
1
Nicholas Psomas