signed intでの配列アクセスはマジヤバイ(こともある)

次のC/C++なコードには問題があります。

#define A_SIZE 6
static int a[A_SIZE];

void vuln(int n, int val)
{
 assert(n < A_SIZE);
 a[n] = val;
}

配列アクセスの添字となる変数aの方が signed int なのが問題です。nとして負数を渡すと面白い(というか恐ろしい?)動作をします。


ええと・・(int)(0x80000000U + n) を vulnの第一引数として渡すと*1、vuln内の配列xのx[n]に書き込むことができてしまうのです。もちろん、配列の境界チェックは行われません。だから、

int main(void)
{
 // test1: a[4] = 123; と同じ。
 //        これはまぁ配列の範囲内だからいいけど・・
 vuln(0x80000004U, 123);

 // test2: overwrite return address (or saved-ebp) 
 //        without invalidating the canary word !!
 vuln(0x8000000AU, 123);
}

test2のような引数を渡すと、攻撃者に任意のコードを実行されてしまう恐れがあります。たとえ配列がスタック上に確保されている場合であっても、ProPoliceなどのcanary系プロテクションでは検出できないメモリの改ざんとなるので、厄介です。

細かい解説

(昨日の記事からのコピペ)


C99規格の§6.5.2.1(配列の添字演算子)や§6.5.6(加減演算子)の脚注88を見ると、int型の配列aに対して、a[n] は

 *((int*)((char*)(a)) + ((n) * sizeof(int)))

と等価ですので、負数を与えると、配列tableの終わりよりもっと後ろの方にもアクセスできてしまいます*2。つまり、メモリ上のほぼお好きな4バイトをお好きな値に書き換えることができてしまいます。

まとめ

  • 配列の添字が負数かどうかも必ずチェックしよう
  • そのまえに、そもそもsigned int型の添字で配列にアクセスするのはやめよう。添字については、プログラムの全域でstd::size_tを使うようにしよう。

ということで。


結論は当然っぽいんだけど、被害の程度があまり知られていないような気がしたので書いてみました。

*1:負の数を渡すことになります

*2:えーと、(n) * sizeof(int)の結果はsize_t型であることに注意してください