■ スタック領域の構成 ■
前回はバッファオーバーフローのバグを含むプログラムを作り、
実際に動作させてみました。
バッファオーバーフローのバグの本当に恐ろしい所として、
「エラーがあっても処理を続行してしまう。」
という事があるのでした。
しかし前回作成したプログラムを実行させると、プロセッサの
保護機能によりプログラムは強制終了され、処理が続行される
事はありませんでした。
何故、プロセッサの保護機能が動作したのでしょうか?
実は、バッファオーバーフローにより壊されてしまった部分に
は、とても重要な情報が格納されていたのです。
今回は、このとても重要な情報について説明します。
まず「とても重要な情報」が何なのかを知る為には、スタック
領域が出来上がる過程を見てみる必要があります。
まずは前回作成したプログラムをもう1度見てみましょう。
─↓of1.c ───────────────────────
#include <stdio.h>
#include <string.h>
void main(void);
void main()
{
unsigned char* i;
unsigned char b[]= { 0x10, 0x11, 0x12, 0x13,
0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f,
0xff, 0xff, 0xff, 0xff,
};
unsigned char a[]= { 0x0, 0x1, 0x2, 0x3,
0x4, 0x5, 0x6, 0x7,
};
// スタック領域を表示する。
for(i=&b[0];i<=&a[7];i+=4)
{
printf("%08X:%02X %02X %02X %02X\n", i, *i, *(i+1), *(i+2), *(i+3));
}
// バッファbをバッファaにコピーする。
strncpy((char*)a, (char*)b, sizeof(b));
printf("copy end\n");
// スタック領域を表示する。
for(i=&b[0];i<=&a[7];i+=4)
{
printf("%08X:%02X %02X %02X %02X\n", i, *i, *(i+1), *(i+2), *(i+3));
}
}
─↑ここまで──────────────────────
このプログラムはバッファ「b」をバッファ「a」にコピーす
るだけのプログラムです。
このプログラムをビルド&実行するとバッファオーバーフロー
が発生します。
このプログラムには「main」という関数が1つしかありません。
通常、殆どの「関数の先頭」には、ある決まったコードが存在
しています。
どんなコードかと言うと、「スタック領域を確保する」コード
です。
以下にそのコードを示します。
push ebp
mov ebp, esp
sub esp, 020h
~~~~
↑
確保するサイズ
いきなりアセンブリコードが出てきて驚かれた方もいらっしゃ
るかもしれません。
しかし、アセンブリコードと言っても、たった3行です。
この「3行のコード」は、殆ど全ての関数において、最初に行
われる処理として存在しています。
アセンブリコードの詳細説明は省きますが、上記3行の命令が
実行される事により、スタック領域は以下の様になります。
レジスタESP
スタック領域├─────┤ ┌─────┐
│ │←───┤ │●スタック領域の
│ │ └─────┘ 1番上を示す。
│ │
│ │
│ 空 │
│ │
│ │
│ │
│ │
│ │
│ │ レジスタEBP
├─────┤ ┌─────┐
│ ebp │←───┤ │●スタック領域の
└─────┘ └─────┘ 1番底を示す。
レジスタESPはスタック領域の1番上を示す為に存在してい
ます。
スタック領域の1番上の事を「スタックポインタ」といいます。
レジスタESPの「SP」とは、「Stack Pointer」の略です。
また、レジスタEBPはスタック領域の1番底を示す為に存在
しています。
スタック領域の1番底の事を「ベースポインタ」といいます。
レジスタEBPの「BP」とは、「Base Pointer」の略です。
さて、これによりスタック領域を確保する事は出来ましたが、
確保しただけではまだ中身は空のままなので、この後にスタッ
ク領域を初期化するコードが存在しています。
先ほど見て頂いたプログラムで言うと、
unsigned char* i;
unsigned char b[]= { 0x10, 0x11, 0x12, 0x13,
0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b,
0x1c, 0x1d, 0x1e, 0x1f,
0xff, 0xff, 0xff, 0xff,
};
unsigned char a[]= { 0x0, 0x1, 0x2, 0x3,
0x4, 0x5, 0x6, 0x7,
};
の部分です。
この処理が実行される事により、実際にスタック領域に値が設
定される事になります。
以下に、スタック領域の初期化処理が完了した後のイメージを
示します。
レジスタESP
スタック領域├─────┤ ┌─────┐
│ i │←───┤ │●スタック領域の
├─────┤ └─────┘ 1番上を示す。
│ │
│ │
│バッファb│
│ │
│ │
├─────┤
│バッファa│
│ │ レジスタEBP
├─────┤ ┌─────┐
│ ebp │←───┤ │●スタック領域の
└─────┘ └─────┘ 1番底を示す。
スタック領域にこのプログラムで使用するデータが設定されて
います。
ここまでの処理で「main」という関数が使用するスタック領域
が出来上がった事になります。
ところで、この「main」という関数ですが、関数なので当然呼
出す人がいなければなりません。
関数を呼出す為の命令としては「call」という命令が使用され
ます。
「call」命令についての詳細は「簡単なアセンブラ言語」講座
を見て頂きたいのですが、この命令は指定された関数を呼出す
と同時に、「呼出し元」に戻る為の情報をスタック領域に格納
してくれます。
「call」命令についてもっと知りたい方はこちら。
>>>
academy002-052.htm
この「呼出し元」に戻る為の情報が格納されているイメージを
以下に示します。
先ほどのイメージとは、1番下の部分が異なるだけです。
レジスタESP
スタック領域├─────┤ ┌─────┐
│ i │←───┤ │●スタック領域の
├─────┤ └─────┘ 1番上を示す。
│ │
│ │
│バッファb│
│ │
│ │
├─────┤
│バッファa│
│ │ レジスタEBP
├─────┤ ┌─────┐
│ ebp │←───┤ │●スタック領域の
├─────┤ └─────┘ 1番底を示す。
│呼出し元 │
└─────┘
ここまでが、メイン関数がスタック領域を初期化した直後の状
態になります。
この時点ではまだバッファオーバーフローは発生していません。
ここで疑問に思われた方がいらっしゃるかもしれません。
上のイメージでは、「呼出し元」の情報がスタック領域の「1
番底」よりも更に下の方にあります。
1番底だから、それ以上底があるのはおかしいのではないでし
ょうか?
そこで先ほどの「3行のコード」を思い出してみましょう。
push ebp
mov ebp, esp
sub esp, 020h
~~~~
↑
確保するサイズ
この、たった3行のコードは、「スタック領域を確保する」為
のコードでした。
このコードが実行される事により、スタック領域の「1番上」
と「1番底」が設定されます。
そしてここから特に注意して読んで頂きたいのですが、この3
行のコードは、殆ど「全ての関数」で「最初に行われる」処理
だという事です。
つまり、関数が呼出される毎に、スタック領域の「1番上」と
「1番底」が「再設定」されるという事になります。
ちょっと難しくなってきましたね。
分かり易いように、図を使って説明します。
まずスタック領域の全体図を見てみましょう。
まだ中身は何もありません。
長いのでサッと下の方にスクロールして見て下さい。
スタック領域
┌─────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────┘
スクロール作業、お疲れ様でした。
ここで、メイン関数が呼ばれたとします。
メイン関数の先頭では、先ほどの「3行のコード」が実行され
ます。
以下に3行のコード実行直後のイメージを示します。
スタック領域
┌─────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
├─────┤←レジスタESP
│メイン関数│
│で使用され│
│るバッファ│
├─────┤←レジスタEBP
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────┘
ここでは分かりやすくするために、バッファ「a」やバッファ
「b」などの細かいデータ表現は行わず、まとめて「メイン関
数で使用されるバッファ」として表現してあります。
ここで、例えばメイン関数がサブという関数を呼出すとします。
関数を呼出す為の命令としては「call」という命令が使用され
ます。
この命令はインテルプロセッサの仕様により、スタック領域に
「呼出し元」へ戻る為の情報を出力します。
以下にサブ関数を呼出す為に「call」命令が実行された直後の
イメージを示します。
スタック領域
┌─────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
├─────┤←レジスタESP
│呼出し元 │←●「call」命令により出力された
├─────┤
│メイン関数│
│で使用され│
│るバッファ│
├─────┤←レジスタEBP
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────┘
レジスタESPの示す位置が少し上の方に移動しています。
これも「call」命令を使用する事で自動的に行われます。
この後、サブ関数の処理が開始されるわけですが、先ほど書い
たように殆ど「全ての関数」の先頭には「3行のコード」が存
在しているので、サブ関数の開始直後でも「3行のコード」が
実行されることになります。
以下に、サブ関数で3行のコードが実行された直後のイメージ
を示します。
スタック領域
┌─────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
├─────┤←レジスタESP
│サブ関数で│
│使用される│
│バッファ │
├─────┤←レジスタEBP
│呼出し元 │
├─────┤
│メイン関数│
│で使用され│
│るバッファ│
├─────┤
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────┘
3行のコードが実行されたので、レジスタESPとレジスタE
BPの示す位置が変更されています。
つまりこれにより、「1番上」と「1番底」が「再設定」され
たという事です。
これで大体お分かりになるかと思いますが、今まで「1番上」
とか、「1番底」という表現を行ってきたのは、スタック領域
全体の1番上や1番底なのではなく、「その関数限定で使用さ
れる」バッファの「1番上」とか「1番底」だったという事で
す。
もう少し見てみましょう。サブ関数が更に別の関数である、サ
ブ2という関数を呼出した場合、スタック領域はどの様なイメ
ージになるでしょうか?
以下にそのイメージを示します。
スタック領域
┌─────┐
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
├─────┤←レジスタESP
│サブ2関数│
│で使用され│
│るバッファ│
├─────┤←レジスタEBP
│呼出し元 │
├─────┤
│サブ関数で│
│使用される│
│バッッファ│
├─────┤
│呼出し元 │
├─────┤
│メイン関数│
│で使用され│
│るバッファ│
├─────┤
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
│ │
└─────┘
サブ2関数で使用されるバッファがスタック領域に確保され、
レジスタESPとEBPの示す位置が変更されました。
レジスタEBPは「サブ2関数で使用される」バッファの1番
底を示していますが、この時、レジスタEBPの下の方には何
も無いというわけではなく、サブ関数やメイン関数が使用する
為のバッファも格納されているのです。
メイン関数も他のプログラムから「call」命令により呼出され
る物なので、「メイン関数で使用するバッファ」の下の方にも
「呼出し元」の情報が格納されているという事になります。
ここで話を元に戻しましょう。
もう1度先ほどと同じイメージを見てもらいます。
レジスタESP
スタック領域├─────┤ ┌─────┐
│ i │←───┤ │●メイン関数で
├─────┤ └─────┘ 使用するスタ
│ │ ック領域の1
│ │ 番上を示す。
│バッファb│
│ │
│ │
├─────┤
│バッファa│
│ │ レジスタEBP
├─────┤ ┌─────┐
│ ebp │←───┤ │●メイン関数で
├─────┤ └─────┘ 使用するスタ
│呼出し元 │ ック領域の1
└─────┘ 番底を示す。
これは、メイン関数がスタック領域を初期化した直後の状態で
す。
ここで示したイメージは、スタック領域のほんの1部分でしか
ない事はもうお分かりになりますよね?
そしてほんの1部分でしかないので、レジスタESPとレジス
タEBPで示された範囲以外にも、データが格納されていると
いう事もお分かりいただけると思います。
ここまでが理解できないという方は、もう1度最初から読み返
してみる事をお勧めします。
この後、バッファ「b」がバッファ「a」にコピーされる処理
が行われます。
コピー処理が完了した直後のイメージを以下に示します。
レジスタESP
スタック領域├─────┤ ┌─────┐
│ i │←───┤ │●メイン関数で
├─────┤ └─────┘ 使用するスタ
│ │ ック領域の1
│ │ 番上を示す。
│バッファb├─┐
│ │ │
│ │ │コピー
├─────┤ │
│ │←┘
│ │ レジスタEBP
│バッファb│ ┌─────┐
│ │←───┤ │●メイン関数で
│ │ └─────┘ 使用するスタ
│ │ ック領域の1
└─────┘ 番底を示す。
バッファオーバーフローが発生し、「ebp」と「呼出し元」の
情報が上書きされてしまいました。
1番最初に、「バッファオーバーフローにより壊されてしまっ
た部分には、とても重要な情報が格納されていたのです。」
と書きましたが、「とても重要な情報」とは、この「ebp」と
「呼出し元」の情報の事だったのです。
ただしこの時点では、まだプロセッサの保護機能は動作しませ
ん。
プロセッサの保護機能が動作するのは、もう少し先の事になり
ます。
続きは次回に。