連載が「HUNTER×HUNTER」状態になっている件について川柳で言い訳をさせていただくと、こんな感じになる。

誰に似た 若い女性に 笑顔まく
出典: 和光堂「子育て川柳

かの「セキュリティホール memo」を運営される小島先生にご心配をいただいてしまった。恐懼しきりである。

実は冒頭にある通り、筆者の私的な時間は目下、育児に向けられているのが実情である。それはまるで月月火水木金金。世間で云うところのゴールデンウィークなんてございません。したがって編集T氏の辛抱強さに支えられながら、仕事の後はもっぱら子供の世話をする日々であり、著述業の能率は遅々として上がらなかったりしている。連載を心待ちにしている読者諸兄には申し訳ないが……とはいえ、先日、編集T氏がRSA Conference Japanのパネルディスカッションにおいて、参加者席の最前列から壇上の筆者に向けて目から怪光線「原稿よこせビーム」を放っていたことは、ここで特筆するに値するであろう。


というわけで、今週も引き続きWindowsにおけるバッファオーバーフローの話である。一点注意事項がある。今回からC言語の知識が必要となる。

スタックが一時データ置き場であることは何度も繰り返し述べてきたことであるが、ここでスタックが実際にCやC++でコーディングされたプログラムでどのように使用されるのかについて説明する。Windowsにおけるバッファオーバーフローを理解する点で重要なのは、以下の2点である。

  • CやC++でコーディングされたプログラムではサブルーチン毎にスタックが割り当てられる
  • コールスタックとデータスタックは近接の領域に設けられる

実際に、これらを説明するためにスタックベースのバッファオーバーフローを発生させるプログラム(リスト1)を使用する。ちなみに、これら素材は筆者の同僚H氏より許可をもらってご提供いただいた。この場を借りて感謝の念を贈りたい。

リスト1: スタックベースのバッファオーバーフローを発生させるプログラム


void func2(char *ptr)
{
    char buff[100];
(※1)
    strcpy(buff, ptr);
}
void func1(char *ptr)
{
    func2(ptr);
}
int main(int argc, char *argv[])
{
    func1(argv[1]); 
    return 0;
}

上記リストに示されるプログラムは、main関数(メインルーチン)からサブルーチンfunc1が呼び出され、さらにサブルーチンfunc2が呼び出されるプログラム構造となっている。このとき、リスト中の(※1)まで処理が進んだ際のスタックの状況を以下の図に示す。

図1: Func2が呼び出された際のスタックレイアウト

まず補足しよう。このシリーズの第3回目で説明しているように、スタックは特定の基点から低位のアドレス(より小さいアドレス)に向かって成長する(データが書き込まれる)。よって、各サブルーチンで使用されるスタックは呼び出しが進むにつれ、より低位のアドレスが使用される。さらに、リターンアドレス以外にベースポインタというものがスタック中に保存されているが、これは各サブルーチンで使用されるスタックの最下部を指し示すアドレスを保持する(特定のレジスタに書き込んでおく)ためのものである。

さて、一瞥してもわかるように、各サブルーチンで使用されるデータスタック・コールスタックは近接している。ちなみに、近接しているといっても、この図1のように必ずしも密接しているとは限らず、コンパイラによって生成されるコードやOSに依存していることがある──という端的な意味も込めて、func1のデータスタックとコールスタックの間に間隔を作ってある(笑)。すなわち、これはあくまでも基本概念としての構成であり、実装はかなり異なるのである。したがって実際のプログラムをデバッグして、メモリ中に「歯抜け」なデータがあったとしても何ら不思議はないのである。このことは注意しておく価値がある。

さて、このプログラムは第一引数をそのまま100バイト長のバッファ(char buff[100])にstrcpy()関数を使用してコピーするコーディングをしている。このライブラリ関数は「FreeBSD Developers' Handbook」にもあるように、「バッファオーバーフローを生じさせやすい危険な関数」のひとつとして指定している文献が多い。前述の文献に示される関数に加えて、memcpy()やMultiByteToWideCharなどもバッファオーバーフローを発生させる誤用が起きやすい関数とされている

話を戻そう。一般的に、バッファへのアクセスは100バイト長のバッファの先頭アドレスから正方向のオフセットをとることで行われる。これは本連載の第18回にも書いたが、前述の「バッファオーバーフローを生じさせやすい危険な関数」などを使用した場合も、同様に正方向にオフセットを加えていくことでデータの複製を行うことが通常である。つまり、イメージとしては図2中のbuff[100]の長方形の上部から下部に向かってデータのコピーが行われることになる。

図2:strcpy()によるデータコピー発生のイメージ

他の文献などでも指摘されているように「バッファオーバーフローを生じさせやすい危険な関数」は

  1. コピー元の文字列長をチェックせず
  2. コピー元の文字列に含まれるNULLを検出し、それを終端とみなす

という仕様を含んでいることがほとんどである。このため、コピー先のバッファの大きさを無視して、データを書き込んでしまうことがある。これが、スタックにおいて発生した場合、リターンアドレスやベースポインタの書き換えにつながりうる。これがスタックベースのバッファオーバフローの基本である。