operator newの隠れた整数オーバーフローとVisualStudio 2005のVC++

void vuln(std::size_t nelem) {
  int* p = new int[nelem];
  std::memset(p, 12345, nelem);
}

上記の関数には整数オーバーフローに起因するヒープベースバッファオーバーフロー脆弱性あるんですが、どこがまずいかわかりますか?


まずいのはnewしている部分です。C++コンパイラは内部で、nelem * sizeof(int) 、つまりnelem * 0x4U を計算してから、その結果を引数にしてoperator newをcallするんですが*1、もしnelemの値が 0x40000001U だったりしたらどうなるでしょうか?これを4倍した値は0x100000004ですが、33bit目より上は捨てられてしまうので、結局計算値は 4 になります。つまり、vuln(0x40000001U); という呼び出しの中では、new int[0x40000001U] ではなく new int[4]; が行われるわけです。その後0x4000001Uだけmemsetした瞬間に、このコードはクラッシュします。memsetならたいした害はないですが、これがmemcpyだったら条件によっては任意コード実行の脆弱性につながりますよね。


こういうのって、手で対処コードを書くのは結構面倒で、ついさぼってしまったりしがちだと思うんですが、つい先日リリースされた Visual Studio 2005付属のVC++からは、これに対処するためのコードをコンパイラが自動的に挿入してくれるんだそうです。


まだVS2005をインストールしていないので確認してはいないんですが、Michael Howardさん(MSのセキュリティチームの偉い人)の2005/12/06のblogに書いてあるのでたぶん間違いないでしょう。次のように、掛け算のあとに seto でEFLAGSのOF(overflow flag)をチェックしています。そして、オーバーフローが起きると、operator new に引数として固定値(UINT_MAX == 4GB)がわたるようなトリックになっています。このnewは通常失敗して例外(std::bad_allocかな)が飛ぶので、処理が先に進まず安全*2ということのようです。

; _count$ = esi

; 15   :    return new CFoo[count];

  00004     33 c9       xor  ecx, ecx
  00006     8b c6       mov  eax, esi
  00008     ba 04 00 00 00    mov  edx, 4
  0000d     f7 e2       mul  edx
  0000f     0f 90 c1    seto cl
  00012     f7 d9       neg  ecx
  00014     0b c8       or   ecx, eax
  00016     51          push ecx
  00017     e8 00 00 00 00    call ??2@YAPAXI@Z     ; operator new

こういう、コンパイラでの対処は面白いですね。g++にも搭載されないかなぁ(すでにあったりします?)。


Write Great Code〈Vol.1〉ハードウェアを知り、ソフトウェアを書く Write Great Code〈Vol.1〉ハードウェアを知り、ソフトウェアを書く の2〜4章は、数値表現や演算のお話ですね。買いだ。

*1:本当は詳細部分は未規定だけど概ね正しい。詳細はISO/IEC14882:2003 §5.3.4/10, 5.3.4/12を参照のこと

*2:最悪クラッシュはするかもしれないけど任意コード実行はされない