学校では教えてくれないこと

メモリを壊してみましょう

2007.5

前回の連載中に「スタックアンダーフローというのは何ですか?」という質問をもらったので、今回はそれから説明していくね。

スタックアンダーフロー

前回の「スタックってなあに?(2)」で説明したように、スタックオーバーフローとは、スタックメモリのサイズを超えてpushしてしまうことだったよね。これに対しスタックアンダーフローは、

スタックが空の状態で、さらにpopしようとした。

場合のことを指すことになる。

通常、C言語などの関数呼び出しに使われているスタック構造では、こういった状況は起こらないはずなんだ。なぜなら、「スタックが空」というのは「タスクが関数を何も呼びだしていない状態」であり、これをさらにPopさせる手段がないからなんだ。もし起こるとすれば、言語処理系(コンパイラなど)に問題がある可能性が考えられる。

ただし、アセンブラ言語で書かれたプログラムなら起こりえるんだ。これは言語として関数呼び出しの構造を持っていないため、スタックのpush/pop制御をプログラマが意識して行う必要があるからなんだ。アセンブラ言語については、またの機会で詳しく取り上げる予定だよ。

バッファ・オーバーフロー

「セキュリティホールが見つかりました」というニュースを耳にしたことがあると思うんだけど、知っているかな。セキュリティホールとは、悪用される可能性のある「ソフトウエアの穴」が空いていることを言うんだ。その中でも最も多いのが「バッファ・オーバーフロー」という類のもの。

バッファ・オーバーフローが発生すると、任意のコードを実行される恐れがあって、非常に危険な脆弱性とされているんだ。これはどういうものなのかわかるかな。組込みにも非常に重要だから、簡単なコード例で説明するね。

コード例(1)

以下のソースコード(overflow1.c)を見てみよう。

overflow1.c ソースコード

このソースコードをLinuxでコンパイルして実行した結果が下のようになるよ。$が、bashのプロンプトを示している。

コード

ソースコード中では配列a[]に対しては何も操作していないにもかかわらず、内容が書き換わってしまっているよね。どうして、こんなことが起こったのだろうか?

原因は、配列b[]に対するstrcpy()なんだ。b[]は8バイト分の領域として宣言されているけど、strcpy()で「それを超える」16バイト分(15文字+終端文字'\0')の書き込みをしているためにa[]の内容が侵害されてしまったんだ。

ここで新たな疑問が浮かんでくるね。配列サイズを超えて書き込むのが良くないのはわかるけど、なぜb[]の領域を超えて書き込みをしたら、a[]が壊れてしまうのだろうか?

スタック領域

それは、a[], b[]がどちらもスタック領域に配置されているためなんだ。関数の内部で宣言する自動(Auto)変数は、スタック領域と呼ばれるメモリ上に確保されるのは覚えているよね。スタック領域では、その名前が示すとおり、LIFO(Last In First Out)構造で、上に上に(アドレスが減る方向に)積みあがるように変数を配置していく。このため、上記の例では、以下のように先に宣言されたa[]の上にb[]が積まれる形になるんだ。

説明図

ここで、b[]に"00000000abcdefg"の16バイトを書き込むとどうなるだろうか。b[]の後ろには連続してa[]の領域があるから、溢れてはみ出した"abcdefg"がa[]のデータを上書きしてしまうわけなんだ。

説明図

コード例(2)

これを応用して、もうひとつソースコード例を見てみよう。

overflow2.c ソースコード

今度は、配列cmd[]に設定されたlsコマンドを実行するというプログラムだよ。

コマンドライン引数として文字列が渡された場合は、配列tmp[]にコピーしている。配列cmd[]に対しては、何も操作をしていない。Linuxでコンパイル・実行した結果は、下のようになるよ。引数がない、あるいは8文字未満の場合は、lsコマンドが実行される。ところが、引数に「任意の8文字+任意のコマンド」を渡すと、任意のコマンドを実行することができてしまうんだ。

コード例

どこに問題があるかはもうわかるよね。どちらも「配列サイズを考慮せずにstrcpy()している」のが不具合の原因なんだ。strcpy()という関数の内部では、配列サイズのチェックは行ってくれないから、使う側で注意してあげないといけない。

メモリ破壊のバグは危険!

バッファ・オーバーフローの仕組みはもうわかったかな?前述の2例は、バッファ・オーバーフローにより別の配列データを壊すというものだったけど、例えばb[-1]のように負のインデックス番号を与えればバッファ・アンダーフローも簡単に起こすことができる。これらにより、実際にどんなトラブルが引き起こされるかは「壊した部分に何があったか」によって異なるんだ。

Windows, Linuxなどの汎用OSは、メモリ保護機能を持っていて、ユーザ・アプリケーションがOSカーネルを使っているメモリ領域を簡単にアクセス(書き換え)できないようになっているんだ。アプリケーションのプロセスでメモリ破壊を起こしても、たいていの場合は例外メッセージ(Segmentation Faultなど)を表示してアプリケーションが終了する程度の被害で済むんだ。

この場合、OSカーネルとしては生きているから、システム全体が停止するまでにはならない。でも、組込み機器で使われるOSでは、メモリ保護機能を持たないものが多く、これらのOSの場合は、アプリケーションからOSカーネルのメモリ領域をいとも簡単に壊すことができてしまう。そうすると、システム全体がいとも簡単に吹き飛んでしまうから、ユーザからみると突然、機器が固まってしまうことになるよね。組込み機器ではあってはならないことだ。

メモリ破壊によるトラブルは、組込み技術者が一度は必ずハマるといっても過言ではないくらい身近なものなんだ。でも、バッファ・オーバーフローのような配列の範囲を超える書き込みは、コンパイル時にはエラーどころか警告もでないため、気づかないことが多い。

また、組込み機器が動作中に、メモリのどの部分がいつ・どのように壊れたのかを特定して改善をするのは「至難の業」で、調査・デバッグに大変な時間と労力を費やすことになってしまう。そういう意味では、最も危険なバグといえるだろうね。

組込み向けのソフトウエアは、このようなメモリ関連のバグを埋め込まないよう、特に意識して設計・コーディングされているんだよ。

ページのトップへ