分かりやす〜い
コンピュータ技術情報

TOPに戻る
▼Processor
バッファオーバーフロー
┣ 異常な動作
┣ 本当に怖い事
┣ プログラムを作る
┣ バッファオーバーフロ
┃ ーを発生させる

┣ スタック領域の構成
┣ 何故強制終了されるの
┃ か?

┣ 強制終了されないよう
┃ に作り変える

┣ 任意のコードを実行す
┃ る

┣ of1.cとof2.cとの違い
┣ 例外
┣ 例外ハンドラの行う事
┣ of3.cを作る
┣ of1.cとof3.cとの違い
┣ of3.exeの例外ハンド
┃ ラ

┣ of3.exeを強制終了す
┃ るのは?

┣ 例外ハンドラの特徴
┣ Blue Screen
┣ BufferOverFlow応用
┃ プログラム

┗ 機械語の説明

Copyright(C) 2001-2002.ugpop. All rights reserved.




■デジタル用語辞典:
■ スタック領域の構成 ■

前回はバッファオーバーフローのバグを含むプログラムを作り、
実際に動作させてみました。

バッファオーバーフローのバグの本当に恐ろしい所として、
「エラーがあっても処理を続行してしまう。」
という事があるのでした。

しかし前回作成したプログラムを実行させると、プロセッサの
保護機能によりプログラムは強制終了され、処理が続行される
事はありませんでした。

何故、プロセッサの保護機能が動作したのでしょうか?

実は、バッファオーバーフローにより壊されてしまった部分に
は、とても重要な情報が格納されていたのです。


今回は、このとても重要な情報について説明します。
まず「とても重要な情報」が何なのかを知る為には、スタック
領域が出来上がる過程を見てみる必要があります。

まずは前回作成したプログラムをもう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」と
「呼出し元」の情報の事だったのです。


ただしこの時点では、まだプロセッサの保護機能は動作しませ
ん。
プロセッサの保護機能が動作するのは、もう少し先の事になり
ます。

続きは次回に。



▲このページの上へ

▲このページの上へ

▲このページの上へ

←前に戻る    ▲このページの上へ    続きを読む→

▲このページの上へ

▲このページの上へ

▲このページの上へ

▲このページの上へ