プログラミング、リファクタリング、そしてすべてにおける究極の疑問: No. 17

その他インテル® DPC++/C++ コンパイラー特集

この記事は、インテル® デベロッパー・ゾーンに公開されている「The Ultimate Question of Programming, Refactoring, and Everything」の日本語参考訳です。


17. プライベート・データのクリアには専用の関数を使用する

Apache HTTP Server* プロジェクトから抜粋した以下のコードについて考えてみます。このエラーは、次の PVS-Studio 診断によって検出されます。

V597 The compiler could delete the ‘memset’ function call, which is used to flush ‘x’ buffer. The RtlSecureZeroMemory() function should be used to erase the private data. (V597 コンパイラーは、’x’ バッファーのフラッシュに使用される ‘memset’ 呼び出しを削除できます。プライベート・データの消去には、RtlSecureZeroMemory() 関数を使用すべきです。)

static void MD4Transform(
  apr_uint32_t state[4], const unsigned char block[64])
{
  apr_uint32_t a = state[0], b = state[1],
               c = state[2], d = state[3],
               x[APR_MD4_DIGESTSIZE];  
  ....
  /* Zeroize sensitive information. */
  memset(x, 0, sizeof(x));
}

説明

このコードでは、プログラマーはプライベート・データの消去に memset() 関数呼び出しを使用しています。しかし、この方法では実際にデータが消去されないため、これは最良の方法ではありません。正確には、消去されるかどうかはコンパイラーやその設定などに依存します。

このコードをコンパイラーの視点から見てみましょう。コンパイラーは、コードをできるだけ高速に実行できるように、さまざまな最適化を行います。その 1 つは、C/C++ 言語の観点から余分なプログラムの動作に影響しない関数呼び出しを削除します。上記のコード例では、memset() 関数がこれに当たります。この関数は、’x’ バッファーを変更しますが、このバッファーは以降の処理で使用されません。つまり、memset() 関数呼び出しは削除可能であり、そうすべきです。

重要! これから述べることは、実際のコンパイラーの動作であって、理論モデルではありません。上記の例では、コンパイラーは memset() 関数呼び出しを削除します。これは、いくつかの実験をすることで確認できます。この問題の詳細と例は、次の記事を参照してください。

  1. セキュリティー、セキュリティー! しかし、それをテストしていますか? (英語)
  2. プライベート・データの安全な消去 (英語)
  3. V597 (英語) The compiler could delete the ‘memset’ function call, which is used to flush ‘Foo’ buffer. The RtlSecureZeroMemory() function should be used to erase the private data. (V597 コンパイラーは、’Foo’ バッファーのフラッシュに使用される ‘memset’ 呼び出しを削除できます。プライベート・データの消去には、RtlSecureZeroMemory() 関数を使用すべきです。)
  4. ゼロにして忘れる — C でメモリーをゼロにする場合の警告 (英語) (この記事に関する議論 (英語) も参照)
  5. MSC06-C. Beware of compiler optimizations. (MSC06-C. コンパイラーの最適化に気を付ける)
    https://www.securecoding.cert.org/confluence/display/c/MSC06-C.+Beware+of+compiler+optimizations

memset() 呼び出しの削除に関するこのエラーは、追跡が非常に困難なため特に厄介です。デバッガーを使用する場合、プログラマーはおそらく最適化前のまだ関数呼び出しが含まれているコードで作業します。しかし、このエラーは、最適化されたバージョンのビルド時に生成されるアセンブラー・リストを確認して初めて見つけることができます。

一部のプログラマーは、これはコンパイラーのバグであり、memset() のような重要な関数の呼び出しを削除すべきではないと考えています。しかし、それは違います。この関数がほかの関数よりも重要であるということはなく、コンパイラーはその呼び出しを最適化する権利があります。事実、そのようなコードが実際に余分である場合があります。

正しいコード

memset_s(x, sizeof(x), 0, sizeof(x));

または

RtlSecureZeroMemory(x, sizeof(x));

推奨事項

コンパイラーが最適化で削除できない特別なメモリー消去関数を使用すべきです。

例えば、Visual Studio* の RtlSecureZeroMemory (英語) 関数や C11 の memset_s (英語) があります。必要に応じて、独自の安全な関数を作成することもできます。インターネットには、さまざまな例があります。そのいくつかを以下に示します。

バージョン No.1 (https://www.securecoding.cert.org/confluence/display/c/MSC06-C.+Beware+of+compiler+optimizations):

errno_t memset_s(void *v, rsize_t smax, int c, rsize_t n) {
  if (v == NULL) return EINVAL;
  if (smax > RSIZE_MAX) return EINVAL;
  if (n > smax) return EINVAL;
  volatile unsigned char *p = v;
  while (smax-- && n--) {
    *p++ = c;
  }
  return 0;
}

バージョン No.2 (英語):

void secure_zero(void *s, size_t n)
{
    volatile char *p = s;
    while (n--) *p++ = 0;
}

一部のプログラマーは、さらに配列に疑似乱数値を設定する関数を実装して、これらの関数を異なる時点で実行することで時間計測攻撃からの保護を確実にしています。これらの関数の実装についても、インターネットで見つけることができます。

コンパイラーの最適化に関する詳細は、最適化に関する注意事項を参照してください。

タイトルとURLをコピーしました