整数操作の脆弱性を持つコードの調査

Michael Howard
Secure Windows Initiative

April 28, 2003

概要: 整数を操作するときの脆弱性、およびアプリケーションを保護するために使用できるセキュリティ計画について説明します。

数年前には、 整数オーバーフロー攻撃について知っている人はほとんどいませんでしたが、 現在では数日ごとに新しい攻撃方法が出現しているようです。 以下に、最近数か月で発見された、 整数オーバーフローのセキュリティ バグをいくつか一覧します。

今月のコラムでは、これらのバグの発生要因、 コード内でのバグの検出方法、 およびバグの修正方法について概要を示します。

今回の記事の本題に入る前に、2003 年 4 月にサンフランシスコで開催された RSA Security Conference で、『Writing Secure Code』が RSA Conference Award の Industry Innovation 賞を受賞したという嬉しいお知らせがあります。 詳細については、 http://www.rsaconference.net/rsa2003/pages/press\_body\_awards1.html (英語) を参照してください。

では、話題を整数攻撃に戻しましょう。

整数 (Integer) とは何か、 皆さんご存知だと思うので説明を省略します。 整数には基本的に符号付きと符号なしの 2 種類が存在することもご存知でしょう。 符号付き整数は、値が負の数であるときには 2 の補数で計算され、 最上位ビットが 1 に設定されます。 また、整数にはさまざまなサイズがあり、 一般的な長さは 64 ビット、32 ビット、16 ビット、および 8 ビットであることもご存知でしょう。 このコラムのために整数についてお話することは以上です。 これだけのことを理解していれば十分です。

セキュリティが脆弱になる整数の操作には、 主に以下の 3 種類があります。

  • オーバーフローとアンダーフロー
  • 符号の有無に関するエラー
  • 切り捨て

これらは、単独ではセキュリティ エラーの原因にはならない場合があります。 しかし、上記の問題が 1 つでも発生するコードでメモリを操作する場合には、 バッファ オーバーラン エラーが発生したり、 アプリケーションで障害が発生する可能性が高くなります。 それぞれの問題について、詳しく見てみましょう。

オーバーフローとアンダーフロー

次のコードの問題点をすぐに挙げてください。

                  
bool func(size_t cbSize) {
   if (cbSize < 1024) {
      // 文字列の最後の Null は扱いません
      char *buf = new char[cbSize-1];
      memset(buf,0,cbSize-1);

      // 処理

      delete [] buf;

      return true;
   } else {
      return false;
   }
}

このコードは正しいでしょうか。 cbSize が 1 KB より小さいことを確認し、 new または malloc が常に 1 KB を正しく割り当てます。 今のところ、newmalloc の戻り値を確認する必要があることは無視しましょう。 また、cbSizesize_t なので、 負の数にはなり得ません。 しかし、cbSize が 0 の場合はどうなるのでしょうか。 バッファを割り当てているコードを見てください。 要求しているバッファ サイズから 1 が引かれています。0 から 1 を引くと、 符号なし整数である size_t 変数は (32 ビット変数の場合) 0xFFFFFFFF、 つまり 4GB にラップされます。 アプリケーションは停止、いや、もっとひどいことになります。

次のコードも同様の問題があります。

                  
bool func(char *s1, size_t len1, 
          char *s2, size_t len2) {
   if (1 + len1 + len2 > 64) 
      return false;

   // 加算により最後の Null を調整します
   char *buf = (char*)malloc(len1+len2+1);
   if (buf) {
      StringCchCopy(buf,len1+len2,s1);
      StringCchCat(buf,len1+len2,s2);
   }

   // buf を使用する残りの処理

   if (buf) free(buf);

   return true;
}

このコードも適切に記述されているように見えます。 データのサイズを確認し、malloc の成功を確認し、"安全な" 文字列処理関数 StringCchCopy および StringCchCat を使用しています (これらの文字列処理関数については、 https://www.microsoft.com/japan/msdn/library/ja/jpdnsecure/htm/strsafe.asp を参照してください)。 しかし、このコードは整数オーバーフローが発生します。 len1 が 64、len2 が 0xFFFFFFFF の場合、 何が発生するでしょうか。 バッファ サイズを判断するコードは、1、64、と 0xFFFFFFFF を正しく加算しますが、 加算処理は回り込みを起こすので、 結果は 64 になります。 その結果、64 バイトのみが割り当てられ、64 バイト長の新しい文字列がビルドされ、 その文字列に 0xFFFFFFFFF バイトが連結されます。 またアプリケーションが停止し、 場合によっては一部のコードが綿密に作成されたサイズで攻撃され、 脆弱性を狙ったバッファ オーバーラン攻撃を受けることがあります。

ここで新たな教訓です。 バッファ サイズの計算を誤ると、"安全な" 文字列処理関数も安全ではなくなります。

JScript オーバーフロー攻撃

乗算のときに同様のオーバーフロー バグが発生することがありますが、 これは Microsoft JScript バグにより発生します。 このバグは、JScript の疎配列サポートを使用するときのみに発生します。

                  
var arr = new Array();
arr[1] = 1;
arr[2] = 2;
arr[0x40000001] = 3;

この例では、配列は 3 つの要素を持ち、 長さは 0x40000001 (10 進値で 1073741825) です。 しかし、この例では疎配列を使用しているので、 要素 3 つの配列分のメモリしか消費していません。

並べ替えルーチンをカスタマイズした JScript を実装する C++ コードでは、 ヒープに一時バッファが割り当てられ、 その一時バッファに 3 つの要素がコピーされ、 カスタム関数を使用して一時バッファの並べ替えが行われ、 一時バッファのコンテンツが配列に戻されます。 以下に、一時バッファを割り当てるコードを示します。

                  
TemporaryBuffer = (Element *)malloc(ElementCount * sizeof(Element));

Element は、 配列のエントリを保持するための 20 バイトのデータ構造体です。 一時バッファには約 20 GB が割り当てられるかのように見えます。 読者の中には、大部分のユーザーが持っているコンピュータには 20 GB もメモリはないので、 割り当ては失敗し、JScript の通常のメモリ不足処理ルーチンによって問題が処理されるだろうと考える方もいるでしょう。 残念ながら、そのような結果にはなりません。

32 ビット整数の演算を行うと、結果 (0x0000000500000014) が 32 ビット値では保持できない大きさになるので、 整数オーバーフローの攻撃を受けます。

                  
0x40000001  *  0x00000014  =   0x0000000500000014

C++ では溢れたビットがすべて無視されるので、0x00000014 が返されます。 このような理由から、割り当ては失敗しません。200 億バイトの割り当ては試みられず、20 バイトのみの割り当てが試みられます。 その結果、並べ替えルーチンはこのバッファが疎配列で 3 つの要素を保持できるだけの大きさがあると判断し、 要素 3 つ分に相当する 60 バイトが 20 バイトのバッファにコピーされるので、40 バイトのバッファがオーバーランします。

符号の有無に関するエラー

次のコードをざっと見てください。 最初の例とほぼ同じです。 エラーを発見できるかどうかを調べ、 発見した場合は結果を判断してみてください。

                  
bool func(char *s1, int len1, 
          char *s2, int len2) {

   char buf[128];

   if (1 + len1 + len2 > 128)
      return false;

   if (buf) {
      strncpy(buf,s1,len1);
      strncat(buf,s2,len2);
   }

   return true;
}

このコードの問題は、 文字列のサイズが符号付き整数で保存されるため、 len1 が 128 よりも大きくても len2 が負になれば、 合計が 128 バイトよりも小さくなることです。 しかし、strncpy への呼び出しにより buf バッファがオーバーフローします。

切り捨てエラー

では、最後の攻撃の種類を、 お察しのとおりコード例を使用して見てみましょう。

                  
bool func(byte *name, DWORD cbBuf) {
   unsigned short cbCalculatedBufSize = cbBuf;
   byte *buf = (byte*)malloc(cbCalculatedBufSize);
   if (buf) { 
      memcpy(buf, name, cbBuf); 
      // buf を使用する処理
      if (buf) free(buf);
      return true;
   }

   return false;
}

この攻撃、少なくともその結果は、 先ほど説明した JScript バグとやや似ています。 cbBuf が 0x00010020 の場合、 何が発生するでしょうか。 0x00010020 の下位 16 ビットのみがコピーされるので、 cbCalculatedBufSize は 0x20 にしかなりません。 したがって、割り当てが 0x20 バイトしか行われないのに、0x00010020 バイトが新しく割り当てた目的のバッファにコピーされます。 このコードを Microsoft Visual C++®/W4 オプションでコンパイルすると、 次のエラーが発生することに注意してください。

                  
warning C4244: 'initializing' : 'DWORD' から 'unsigned 
short' に変換しました。データが失われているかもしれません。

次の操作では警告が行われないことに注意してください。

                  
int len = 16;
memcpy(buf, szData, len);

memcpy の最後の引数は size_t ですが、 引数 len は符号付きです。 memcpy は、 常に 3 番目の引数が符号なしであり、 符号なしにキャストしても関数の出力は変わらないと想定しているので、 警告は行われません。

DWORDsize_t を割り当てようとすると、 警告が行われることに注意してください。 これは、32 ビット プラットフォームでデータが失われる可能性があるからではなく、64 ビット プラットフォームでデータが失われるためです。

                  
warning C4267: '=' : 'size_t' から 'DWORD' に変換しました。
データが失われているかもしれません。

既定の C++ プロジェクトはすべて -Wp64 オプションでコンパイルされるので、 この警告が行われます。 このオプションはコンパイラに対して 64 ビット移植に関する問題に注意するよう指示します。

マネージ コードでの整数の操作に関する問題

C#、Visual Basic® .NET などのマネージ言語でも整数操作のエラーが発生する場合がありますが、 コードから直接メモリにアクセスすることはないので、 被害を受ける可能性は大幅に低くなっています。 しかし、(アンマネージ コードにアクセスする許可が与えられているコードの場合) ネイティブ コードへの呼び出しを行うと、 上記のようなセキュリティの問題が発生することがあります。 CLS (Common Language Specification) の整数は符号付きです。 ありがちな誤りとして、アンマネージ コードで変数が符号なし整数として処理されているときに、 マネージ コードで符号付き整数の引数を有効とする場合があります。

これは、アンマネージ コードに渡す値は常に確認するようにという一般的なアドバイスの具体例です。 そのような操作はすべて、 オーバー フローやアンダーフローが発生した場合、System.OverflowException が発生します。 したがって、マネージ コードでの整数操作に関するバグの多くは、Visual Basic .NET の信頼性に関するエラーを引き起こします。

C# は既定では例外をスローしません。 上記の問題を確認するには checked キーワードを使用します。

                  
UInt32 i = 0xFFFFFFF0;
UInt32 j = 0x00000100;
UInt32 k;
checked {k = i + j;}

改善策

文字列操作を単純にするとセキュリティの問題が発生すると考えた方はいませんか。 脆弱なコードに対する簡単な改善策を以下に示します。

                  
if (A + B > MAX) return -1;

上記のコードを、符号なし整数を使用して次のように修正します。

                  
if (A + B >= A && A + B < MAX) {
   // 良い記述
}

最初の演算 A + B >= A で回り込みを確認し、 2 番目の演算で合計が目的の値未満であることを確認します。

JScript での操作の問題に関して、 要素数が、割り当てようとしているメモリの最大値を超えない範囲であらかじめ定義した値を超えていないことを確認できます。 たとえば、次のコードは最大で 64MB を割り当てることができます。

                  
const size_t MAX = 1024 * 1024 * 64;
const size_t ELEM_SIZE = sizeof(ELEMENT);
const size_t MAX_ELEMS = MAX / ELEM_SIZE;

if (cElems >= MAX_ELEMS)
   return false;

最後に、配列のインデックス、バッファ サイズなどには、符号なしの整数 (DWORD、size_t など) を使用します。

コード調査するときの重要なポイント

整数に関連する問題についてコードを調査したり、コンパイルするときは、以下の点に注意してください。

  • C および C++ コードは、想定される最高の警告レベルである /W4 でコンパイルします。
  • バッファ サイズおよび要素のカウントには size_t または DWORD を使用します。 そのような構造に符号付きの値を使用する理由はありません。
  • size_t は使用しているプラットフォームによって種類が異なることに注意してください。 size_t はメモリ アドレスのサイズなので、32 ビット プラットフォームでは 32 ビット値になり、64 ビット プラットフォームでは 64 ビット値になります。
  • 何らかの整数の操作 (加算、乗算など) を実行して、 結果を配列インデックスの作成やバッファ サイズの計算に使用する場合は、 オペランドが明確で狭い範囲に収まるようにします。
  • (newmallocGlobalAlloc などの) メモリ割り当てを行う関数に渡す符号付きの引数は、 符号なしの整数として処理されるので注意してください。
  • 警告 C4018、C4389、および C4244 が発生する演算に注意してください。
  • 警告 C4018、C4389、および C4244 を無視するキャストに注意してください。
  • 警告 C4018、C4389、および C4244 を無効にするすべての #pragma warning(disable, Cnnnn) を調査します。 さらに、それらをコメント アウトし、再コンパイルして、整数に関連する新しい警告をすべて確認します。
  • 異なるプラットフォームや異なるコンパイラから移行したコードでは、 想定されているデータ サイズが異なる場合があります。 注意してください。
  • マネージ コードからアンマネージ コードを呼び出す場合、 正しく符号が付いていることを確認してください。Win32 API に渡される引数の多くは符号なしの int や DWORD であり、 マネージ コード変数の多くは符号付きです。
  • 最後に、マネージ コードを使用している場合、 必要に応じて OverflowExceptions を捕捉してください。

セキュリティの欠陥を発見する

先月のセキュリティの欠陥は、多くの読者が発見しました。 その欠陥は、整数オーバーフロー攻撃でした。 では、次の C# コードのどこが悪いかわかりますか。

                  
string Status = "No";
string sqlstring ="";
try {
    SqlConnection sql= new SqlConnection(
        @"data source=localhost;" + 
        "user id=sa;password=password;");
    sql.Open();
    sqlstring="SELECT HasShipped" +
        " FROM detail WHERE ID='" + Id + "'";
    SqlCommand cmd = new SqlCommand(sqlstring,sql);
    if ((int)cmd.ExecuteScalar() != 0)
        Status = "Yes";
} catch (SqlException se) {
    Status = sqlstring + " failed\n\r";
    foreach (SqlError e in se.Errors) {
        Status += e.Message + "\n\r";
    }
} catch (Exception e) {
    Status = e.ToString();
}

Michael Howard は、 Microsoft の Secure Windows Initiative グループの Security Program Manager であり、 現在 2 版となった「プログラマのためのセキュリティ対策テクニック (原題 Writing Secure Code)」の共著者です。 また、「Designing Secure Web-based Applications for Windows 2000」の著者です。 彼の人生における興味は、 人々がセキュアなシステムの設計、構築、テスト、およびドキュメント化を行えるように手助けをすることにあります。 お気に入りのセリフは、「ある人にとっての機能は、別の人にとっての悪用の手段となる」というものです。