このエントリでは、安全確保支援士試験の対策として、情報処理技術者試験の過去問を解説します。
過去問を解きながら、知識のインプットができるようにしたつもりです。ぜひ、問題に取り組んでいただき、その後、本エントリを見ていただければと思います。
さて、今回は、BOF(バッファオーバフロー)編 第一弾。おそらく、BOF の過去問の中では最も易しいであろう、「2007 年度(平成 19 年度)春期 テクニカルエンジニア 情報セキュリティ試験 午後 1 問 1」を解いていきます。
《問題》
情報処理技術者試験のページからダウンロードしておいてください
《凡例》
|
それでは、午後 1 の 問 1 を、解き進めましょう。
まずは、設問をながめます。
ぱっと見、ほぼほぼ BOF に関する技術的な内容です。
設問 1、まずは、a からいきます。3 ページの下、T 君のセリフの箇所です。
『バッファオーバフローの結果 [ a ] 領域に確保された変数の値が、意図に反して書き換えられる可能性』 とあります。
メモリには、いくつかの領域がありますが、バッファオーバフローの文脈では、まず、「スタック」 または 「ヒープ」 を思い浮かべてください。すぐあとに見ますが、ここは、「スタック」 が正解です。
ほぼ全ての受験生が正解したはずです。絶対に落とせません。
次に、b と c です。4 ページの一番上、T 君のセリフです。ここでは、どの変数がバッファオーバフローを起こして、どの値が書きかえられるかが問われています。
『図1 脆弱性のあるプログラム例』 を見てください。
プログラムが何をやっているのか、全行にコメントがあります。C/C++ 言語をあまり知らなくても、BOF の原理がわかっていれば解けます。
このプログラムは、13 行目で、「外部から指定された、第一引数 argv[1]」 を処理しています。strcpy 関数を見たら、ぴくっと反応しなければなりません。こいつは、バッファの上限が指定できない関数です。使うべきではありません。
この strcpy 関数を用いて、プログラム外部からの入力 argv[1] を、"ノーチェック" で、128 バイトの領域(val2)にコピーしています。入力が 128 バイトを超えると、バッファがあふれます。
c を解くには、少し前提知識が必要となります。簡単に、確認させてください。
ここでのゴールは、プログラムを実行した際の、メモリ空間がどのようになっているのか、把握することです。
この図の左の絵は、メモリ空間の全体像です。上のアドレスが 「低」 く、下のアドレスが 「高」 いことに注意してください。
その中に、「スタック領域」 と呼ばれる領域があります。
スタックというデータ構造は、ご存じだと思います。本を積み上げるイメージです。積み上がっている本の一番上に、本を置くことを、PUSH と呼び、一番上の本をとることを、POP と呼びました。このデータ構造の特徴は、「最初に積んだ本は、最後にならないととれません(FILO)」、同じことですが、「最後に積んだ本が、最初にとれます(LIFO)」 というものでした。
後でも説明しますが、関数が呼ばれるたびに、この 「本」 が積みあがっていきます。
大事なことは、スタック領域は、アドレスで言えば、高い方から低い方へと成長するということです。真ん中の絵は、上のアドレスを 「低」 く、下のアドレスを 「高」 く描いたので、イメージ通り、下から上に成長するようになっています。
逆に、「ヒープ領域」 は、上から下へ成長します。ヒープ領域とは、プログラムの実行中に、データを展開する領域です。
スタック領域に積みあがっている 1 冊 1 冊の 「本」 は、「スタックフレーム」 と呼ばれます。それが、真ん中の絵です。そのスタックフレーム(1 冊の本)のレイアウトが、右の絵になります。
「ローカル変数」 と 「引数」 は、ここに入ってきます。
このメモリのレイアウトが書けないと、本問を解くのは厳しいです。おそらく、この問題を選択した人の、大半が描けたと思います。
設問 1 の c に戻ります。
今回のスタックフレームが、どのようなレイアウトになっているかわからないと、val2 がどの領域にあふれていくか、わかりません。本問では、3 ページの最後から、4 ページの最初にかけて、val1, val2, val3 それぞれの領域の先頭アドレスが与えられています。これに従うと、右の絵のようなレイアウトになるはずです。
データは、低いアドレスから高いアドレスに格納されていきます。ですので、val2 に 128 バイトより大きなデータがやってくると、val1 の領域にあふれます。
図 1 のプログラム、13 行目にある strcpy 関数について補足します。繰り返しになりますが、こいつは、書き込みバイト数の上限を指定できない関数です。
strcpy 関数のように、文字列を扱う関数には、書き込みバイト数の上限を指定できる、代替関数があります。概ね、関数名の中に n がついた名称となっています。
この表の 3 つの関数、strcpy、strcat、sprintf 関数は、特に有名です。頭にいれておいて、損はありません。
d いきます。4 ページ、図 2 のすぐ下、S 主任の発言です。
抽象度が高い設問なので、素直に 「権限昇格」 と答えましょう。おそらく、大半の人が正解してきます。とりこぼしは厳しいです。
e と f は、いったん、とばします。先に、下線部 ① があるので、そっちをやります。
4 ページの真ん中、下線部 ① です。ふたたび、図 1 のプログラムを見てください。
19 行目のコメントに、『名前が val1 のファイルを入力ファイルとして開く』 とあります。そこで、val1 の値が 'afile' になるようにデータをいじればよいとわかります。
この絵の、ローカル変数の部分を拡大してみます。
ローカル変数 val1, val2, val3 の箇所を拡大すると、これらは、メモリ内では、左の絵のように展開されています。上から val3, val2, val1 となっています。val1 には、'somefile' と格納されています。
最後の \n は、文字列の終端を表す値です(メモリ上には 0x00 と展開されます)。「文字列はココまでですよ」 と教えてくれる値です。NULL(ヌル, ナル)文字や、EOS(End of String)などと呼ばれています。
ですので、コマンドラインに、128 byte 分データのあとに、'afile' という文字列を入れてあげると、val2 がオーバフローし、右の絵のような状態になります。
val2 を 「c」 で埋めましたが、何でもよいです。
なお、コマンドラインの引数(文字列)は、メモリ内に展開した際に、自動的に NULL 文字が追加されます(でないと、どこまでが引数で指定した文字列なのか、わかりませんから)。
ということで、[ ア ] は、128 byte でした。面倒なバイト計算は、一切ありません。わかる人は、瞬殺です。
できれば正解したいところです。とれなくても、これが致命傷にはならないと思います。
さて、設問 1 の [ e ] に戻ります。
ここも、少しだけ、前提知識が必要なので、確認させてください。
ここからのインプットのゴールは、関数呼び出し時のスタックの様子についてイメージをわかせることです。
右にある、main 関数から next 関数を呼び出すプログラムについて、検討していきます
(next 関数の処理は、何でもよいのですが、ここでは、引数に 1 を足して返します)。
大きな流れは、こんな感じです。
- next 関数を呼び出す
→ スタックフレームを生成し、スタック領域へ PUSH します。後で戻ってこられるように、今、自分がいる位置を、スタックフレームに入れておきます - next 関数を終了する
→ next 関数が呼び出される前の状態に戻して、スタックフレームを POP します
なんとなくつかめたら、細部を見ていきましょう。
左の絵も、右の絵も、メモリ空間の一部を表しています。左は、「命令コード領域」 で、まさに、ソースコードに書いた処理を実現するための命令が入っています。CPU は、ここから命令をとってきて、解釈して、実行します。とってくることを 「フェッチ」、解釈することを 「デコード」 と呼んだことを思い出してください。
まず、左の絵 「命令コード領域」 を見てください。
CPU がどの命令をとってきて実行するかというと、「EIP レジスタ」 に格納されているアドレスからです。
レジスタとは、CPU の記憶回路(CPU 内で、データを保持するところ)です。例えば、Intel の x86 という CPU には、16 個のレジスタがあります。その中に、EIP(インストラクションポインタ)と呼ばれるレジスタがあります。ここに、実行する命令を指し示すアドレスが入っています。
ごちゃごちゃ言いましたが、つまりは、「EIP が指すところから命令をフェッチ、デコード、実行する」 というわけです。
右の絵は、おなじみ、スタックフレームです。今は、main 関数のフレームがスタックされています。
左の絵では、EIP は main 関数の命令の先頭を指しています。ここから、順に命令を実行していきます。
main 関数の、最初の命令を実行すると、EIP の値がカウントアップされ、次の命令を指します。そこから、フェッチ、デコード、実行です。
main の命令を、次々に実行していきます。
状況が大きく変わるのは、main 関数が、next 関数を呼び出したときです。next 関数の命令は、main 関数とは別のところにあります。next 関数が、複数の関数から呼び出されることを考えると、合理的ですよね。このため、EIP の値は、大きく変わり、next 関数の先頭を指します。next 関数の最初の命令まで 「ジャンプ」 するわけです。
このとき、next 関数のスタックフレームをつくり、スタック領域に PUSH します。スタックフレームに、引数やローカル変数の情報が入るのは、これまで見てきた通りです。
ここでもう一つ、スタックフレームに、戻りアドレス(リターンアドレスとも呼びます)RET を入れています。
これは、next 関数の処理が終了した際に、元の処理、つまり、呼び出し元である main 関数の続きの処理ができるようにするためです。
そして、EIP の値に、next 関数の命令があるアドレスを格納し、ジャンプします。
next 関数の命令を実行しています。
さて、next 関数の処理が終了しますよ。
きました。緑の矢印です。EIP に、戻りアドレス(RET)の値を入れて、ジャンプです。呼び出し前の位置まで戻ってきました。
next 関数のスタックフレームを POP します。これによって、きれいに、main 関数の続きが実行されていくのです。
この挙動を、おさえておきましょう。
まとめます。もう一度、冒頭で説明したことを繰り返します。この ① ~ ④ の動きがわかっていれば、目標達成です。
● next 関数を呼び出す
① スタックフレームを生成し、スタック領域へ PUSH します。
その際、後で戻ってこられるように、① 今自分がいる位置を、
スタックフレームに入れておきます。
② next 関数の命令にジャンプします。
● next 関数を終了する
③ next 関数が呼び出される前の状態に戻して、
④ スタックフレームを POP します。
ようやく [ e ] です。解答の骨格は、「関数」 となります。
しかし、他の選択肢と異なり、これだけ 「6 字以内」 と、字数が少しだけ多くなっています。単に 「関数」 という答えでは、満点はもらえないと思います。
この問題文中に、こんなセリフがあります。『戻りアドレスが書き換えられてしまう』 と。この、最も典型的な例に、触れておきます。
攻撃者は、「偽の戻りアドレス」 と 「悪意のコード」 の両方が含まれるデータを使って、バッファをあふれさせます。もちろん、戻りアドレスの領域も、攻撃者の用意したデータで上書きされます。そのとき、戻りアドレスの領域を悪意のコードの先頭になるように調整します。そうすると、スタックフレームが POP したときに、自分の用意したコードを実行することができるのです(このようなコードを、「shell コード」 と呼びます)。
となると、BOF の脆弱性を悪用されると、任意のコードが実行される可能性があります。
続いて、5 ページの上、[ f ] です。
『malloc 関数などによって、[ f ] 領域に確保された変数』 とあります。そのすぐ下には、『[ a ] 領域と [ f ] 領域に確保された変数については』 ともあります。
malloc 関数は、指定されたバイト分、「ヒープ領域」 のメモリを確保する関数です。使用後には、free 関数を呼び出して、確保していたメモリを開放する必要があります
答えは、「ヒープ」 です。絶対におとせません。
設問 2 の (2) です。'afile' の内容が表示されてしまうことによる、セキュリティ上の問題が問われています。
管理者のみが read/write できるところ、一般利用者でも read/write できるということは、すぐにわかります。
私は、それ以上は書けませんでした。。
採点講評には、次のようにあります。
『・・・ afile の内容の表示についてだけ記述した解答が多かった。・・・ 任意のファイルの内容を表示できることに気づいてほしかった』
これ、大部分の人は、わかってたと思いますけど、、まぁよいです。
『afile の内容の表示についてだけ記述した解答が多かった』 とあるので、合否に影響する問題ではないと、信じたいです。
問題文中に、BOF の対策が書いてあるので、少しだけ補足します。
5 ページ [ f ] の近く T 君のせりふに、『どちらもプログラム作成上の対策は同じと考えてよい』 とあります。
これは、「スタックオーバフローも、ヒープオーバフローも、想定したメモリ領域があふれることに起因しているから、メモリ領域を超えないように、チェックすればよい」 と、考えたからでしょう。
その後の S 主任のせりふに、『バッファオーバフローを防ぐ根本的な対策は、ループ文の中で配列への書き込みを行う場合や、組み込み関数を使う場合などで、方法が異なる』 とあります。なので、コーディングルールとしては、状況に応じて、対策を示す必要があります。
例えば、バッファへの書き込みを、ループで処理するときには、バッファの境界チェックを、ループの終了条件に含めることが対策になります。一方、組み込み関数を使うときには、危険な関数を使わず、安全な関数を使うことが対策になります。何が危険で、どうすれば安全に実装できるのか、示す必要があります。
設問 2 の (3) です。
文章で解答する場合は、50 字以内です。かなり簡潔に書かなければなりません。
strcpy が、「無制限に」 文字列をメモリに書き込むからあふれます。なので、入力を、128 バイト未満に制限してあげないといけません。128 バイトぴったりだと、NULL 文字があふれてしまうので、「以下」 ではなく 「未満」 としなければなりません
解答には、「かつ」 と 「128 バイト未満」 が必須となるでしょう。
私は、次のようにしました。
「条件文を、コマンドライン引数が 1 より大きく、かつ、この引数の長さが 128 バイト未満であるとする」 です。
IPA の解答のように、『論理積として加える』 などという表現は、私には、なかなかでてきません。
コードで書くと、とてもシンプルになります。ただし、(1) 論理積が && になること、(2) 文字列の長さを取得するのに strlen 関数を使うこと、の 2 つがわからなければ、書けません。
私は、文章で書きました。strlen 関数に自信がなかったからです。
設問 3 です。下線部 ② にいきましょう。プログラミング以外で、BOF を緩和する方法についてです。
完全に知識問題です。もう、知らなかったら書けません。
結論、ローカル変数と SFP(スタックフレームポインタ)の間に、推測されない値(一定の桁数の疑似乱数)を挿入します。普通にプログラムを実行している場合には、この値は変わらないはずです。これが変わったことを検知し、プログラムを停止する仕組みがあります。
このような、推測されない値を、「カナリア」 や 「クッキー」 と呼びます。
この、IPA の解答例程度の内容を把握しておけば、十分だと思います。
コンパイラや実行環境で、BOF を緩和する方法については、5 ページ中央の T 君のセリフにあります。
・ DEP(Data Execution Prevention)
・ Libsafe など
もう 1 つ付け加えるのであれば、
・ ASLR(Address Space Layout Randomization)
です。ここに書いた程度のことを、おさえておきましょう。
それでは、これまでやってきたことを簡単に復習しましょう。
まず、メモリ空間です。
- メモリ空間には、「スタック領域」 と 「ヒープ領域」 がありました
- 「スタック領域」 は、我々のイメージとは逆に、高いアドレスから低いアドレスに向かって成長しました
次に、関数呼び出しです。
- スタック領域には、関数が呼び出されるたびに、スタックフレームが PUSH されました
- スタックフレームには、引数、ローカル変数、リターンアドレス、スタックフレームポインタがありました
- 関数が終了すると、リターンアドレスを使って、元のアドレスに戻り、スタックフレームは POP されました
最後に BOF です。
- 想定を超えるサイズのデータにより、メモリ上のバッファがあふれると、例えば本問であれば、任意のファイルが開かれる可能性がありました。RET を書き換えることで、任意のプログラムを実行される可能性もありました
- プログラミングでの対策は、プログラム外の値が、想定している領域からオーバフローしないように実装することでした。例えば、(1) サイズをチェックしてから、確保した領域にデータを格納する(2) strcpy 関数を strncpy 関数に変更し、サイズの上限を指定する
- コンパイラや実行環境により BOF を緩和させる技術として、カナリア値をはさむこと、DEP、Libsafe、ASLR を紹介しました
独断と偏見で、なんとなくランク付けしてみました。
- [A] は、必ずとりたい問題です
- [B] は、できればとりたい問題です
- [C] は、ぶっちゃけとれなくても、合否に影響がない(だろう)問題です
- [A] をとり、[B] を半分くらいとれば、十分合格ラインを超えると思います
最後まで読んでいただき、ありがとうございました。