安全確保支援士対策 BOF 編 [1]

このエントリでは、安全確保支援士試験の対策として、情報処理技術者試験の過去問を解説します。

過去問を解きながら、知識のインプットができるようにしたつもりです。ぜひ、問題に取り組んでいただき、その後、本エントリを見ていただければと思います。

 

さて、今回は、BOF(バッファオーバフロー)編 第一弾。おそらく、BOF の過去問の中では最も易しいであろう、「2007 年度(平成 19 年度)春期 テクニカルエンジニア 情報セキュリティ試験 午後 1 問 1」を解いていきます。

 

《問題》

    情報処理技術者試験のページからダウンロードしておいてください

《凡例》

  • 『・・・』は、問題文からの引用を表します
  • (解答)は、IPA が発表した解答例を表します

 

それでは、午後 1 の 問 1 を、解き進めましょう。

まずは、設問をながめます。

f:id:higher_tomorrow:20200205164602p:plain

ぱっと見、ほぼほぼ BOF に関する技術的な内容です。

 

f:id:higher_tomorrow:20200205164606p:plain

設問 1、まずは、a からいきます。3 ページの下、T 君のセリフの箇所です。

『バッファオーバフローの結果 [  a  ] 領域に確保された変数の値が、意図に反して書き換えられる可能性』 とあります。

メモリには、いくつかの領域がありますが、バッファオーバフローの文脈では、まず、「スタック」 または 「ヒープ」 を思い浮かべてください。すぐあとに見ますが、ここは、「スタック」 が正解です。

ほぼ全ての受験生が正解したはずです。絶対に落とせません。

 

f:id:higher_tomorrow:20200205164611p:plain

次に、b と c です。4 ページの一番上、T 君のセリフです。ここでは、どの変数がバッファオーバフローを起こして、どの値が書きかえられるかが問われています。

 

『図1 脆弱性のあるプログラム例』 を見てください。

プログラムが何をやっているのか、全行にコメントがあります。C/C++ 言語をあまり知らなくても、BOF の原理がわかっていれば解けます。

このプログラムは、13 行目で、「外部から指定された、第一引数 argv[1]」 を処理しています。strcpy 関数を見たら、ぴくっと反応しなければなりません。こいつは、バッファの上限が指定できない関数です。使うべきではありません。

 

この strcpy 関数を用いて、プログラム外部からの入力 argv[1] を、"ノーチェック" で、128 バイトの領域(val2)にコピーしています。入力が 128 バイトを超えると、バッファがあふれます。

 

c を解くには、少し前提知識が必要となります。簡単に、確認させてください。

f:id:higher_tomorrow:20200205164615p:plain

ここでのゴールは、プログラムを実行した際の、メモリ空間がどのようになっているのか、把握することです。

この図の左の絵は、メモリ空間の全体像です。上のアドレスが 「低」 く、下のアドレスが 「高」 いことに注意してください。

その中に、「スタック領域」 と呼ばれる領域があります。

スタックというデータ構造は、ご存じだと思います。本を積み上げるイメージです。積み上がっている本の一番上に、本を置くことを、PUSH と呼び、一番上の本をとることを、POP と呼びました。このデータ構造の特徴は、「最初に積んだ本は、最後にならないととれません(FILO)」、同じことですが、「最後に積んだ本が、最初にとれます(LIFO)」 というものでした。

後でも説明しますが、関数が呼ばれるたびに、この 「本」 が積みあがっていきます。

大事なことは、スタック領域は、アドレスで言えば、高い方から低い方へと成長するということです。真ん中の絵は、上のアドレスを 「低」 く、下のアドレスを 「高」 く描いたので、イメージ通り、下から上に成長するようになっています。

逆に、「ヒープ領域」 は、上から下へ成長します。ヒープ領域とは、プログラムの実行中に、データを展開する領域です。

スタック領域に積みあがっている 1 冊 1 冊の 「本」 は、「スタックフレーム」 と呼ばれます。それが、真ん中の絵です。そのスタックフレーム(1 冊の本)のレイアウトが、右の絵になります。

「ローカル変数」 と 「引数」 は、ここに入ってきます。

 

このメモリのレイアウトが書けないと、本問を解くのは厳しいです。おそらく、この問題を選択した人の、大半が描けたと思います。

 

f:id:higher_tomorrow:20200205164619p:plain

設問 1 の c に戻ります。

今回のスタックフレームが、どのようなレイアウトになっているかわからないと、val2 がどの領域にあふれていくか、わかりません。本問では、3 ページの最後から、4 ページの最初にかけて、val1, val2, val3 それぞれの領域の先頭アドレスが与えられています。これに従うと、右の絵のようなレイアウトになるはずです。

データは、低いアドレスから高いアドレスに格納されていきます。ですので、val2 に 128 バイトより大きなデータがやってくると、val1 の領域にあふれます。

 

f:id:higher_tomorrow:20200205164623p:plain

図 1 のプログラム、13 行目にある strcpy 関数について補足します。繰り返しになりますが、こいつは、書き込みバイト数の上限を指定できない関数です。

strcpy 関数のように、文字列を扱う関数には、書き込みバイト数の上限を指定できる、代替関数があります。概ね、関数名の中に n がついた名称となっています。

この表の 3 つの関数、strcpy、strcat、sprintf 関数は、特に有名です。頭にいれておいて、損はありません。

 

f:id:higher_tomorrow:20200205164628p:plain

d いきます。4 ページ、図 2 のすぐ下、S 主任の発言です。

抽象度が高い設問なので、素直に 「権限昇格」 と答えましょう。おそらく、大半の人が正解してきます。とりこぼしは厳しいです。

e と f は、いったん、とばします。先に、下線部 ① があるので、そっちをやります。

 

f:id:higher_tomorrow:20200205164638p:plain

4 ページの真ん中、下線部 ① です。ふたたび、図 1 のプログラムを見てください。

19 行目のコメントに、『名前が val1 のファイルを入力ファイルとして開く』 とあります。そこで、val1 の値が 'afile' になるようにデータをいじればよいとわかります。

この絵の、ローカル変数の部分を拡大してみます。

 

f:id:higher_tomorrow:20200205232741p:plain
ローカル変数 val1, val2, val3 の箇所を拡大すると、これらは、メモリ内では、左の絵のように展開されています。上から val3, val2, val1 となっています。val1 には、'somefile' と格納されています。

最後の \n は、文字列の終端を表す値です(メモリ上には 0x00 と展開されます)。「文字列はココまでですよ」 と教えてくれる値です。NULL(ヌル, ナル)文字や、EOS(End of String)などと呼ばれています。

ですので、コマンドラインに、128 byte 分データのあとに、'afile' という文字列を入れてあげると、val2 がオーバフローし、右の絵のような状態になります。

val2 を 「c」 で埋めましたが、何でもよいです。

なお、コマンドラインの引数(文字列)は、メモリ内に展開した際に、自動的に NULL 文字が追加されます(でないと、どこまでが引数で指定した文字列なのか、わかりませんから)。

 

f:id:higher_tomorrow:20200205164647p:plain

ということで、[  ア  ] は、128 byte でした。面倒なバイト計算は、一切ありません。わかる人は、瞬殺です。

できれば正解したいところです。とれなくても、これが致命傷にはならないと思います。

 

f:id:higher_tomorrow:20200205164652p:plain

さて、設問 1 の [  e  ] に戻ります。

ここも、少しだけ、前提知識が必要なので、確認させてください。

 

f:id:higher_tomorrow:20200205164655p:plain

ここからのインプットのゴールは、関数呼び出し時のスタックの様子についてイメージをわかせることです。

右にある、main 関数から next 関数を呼び出すプログラムについて、検討していきます

(next 関数の処理は、何でもよいのですが、ここでは、引数に 1 を足して返します)。

大きな流れは、こんな感じです。

  • next 関数を呼び出す
    → スタックフレームを生成し、スタック領域へ PUSH します。後で戻ってこられるように、今、自分がいる位置を、スタックフレームに入れておきます
  • next 関数を終了する
    → next 関数が呼び出される前の状態に戻して、スタックフレームを POP します

なんとなくつかめたら、細部を見ていきましょう。

 

f:id:higher_tomorrow:20200205203537p:plain

左の絵も、右の絵も、メモリ空間の一部を表しています。左は、「命令コード領域」 で、まさに、ソースコードに書いた処理を実現するための命令が入っています。CPU は、ここから命令をとってきて、解釈して、実行します。とってくることを 「フェッチ」、解釈することを 「デコード」 と呼んだことを思い出してください。

まず、左の絵 「命令コード領域」 を見てください。

CPU がどの命令をとってきて実行するかというと、「EIP レジスタ」 に格納されているアドレスからです。

レジスタとは、CPU の記憶回路(CPU 内で、データを保持するところ)です。例えば、Intel の x86 という CPU には、16 個のレジスタがあります。その中に、EIP(インストラクションポインタ)と呼ばれるレジスタがあります。ここに、実行する命令を指し示すアドレスが入っています。

ごちゃごちゃ言いましたが、つまりは、「EIP が指すところから命令をフェッチ、デコード、実行する」 というわけです。

 

右の絵は、おなじみ、スタックフレームです。今は、main 関数のフレームがスタックされています。

 

左の絵では、EIP は main 関数の命令の先頭を指しています。ここから、順に命令を実行していきます。

 

f:id:higher_tomorrow:20200205203648p:plain

main 関数の、最初の命令を実行すると、EIP の値がカウントアップされ、次の命令を指します。そこから、フェッチ、デコード、実行です。

 

f:id:higher_tomorrow:20200205203751p:plain

main の命令を、次々に実行していきます。

状況が大きく変わるのは、main 関数が、next 関数を呼び出したときです。next 関数の命令は、main 関数とは別のところにあります。next 関数が、複数の関数から呼び出されることを考えると、合理的ですよね。このため、EIP の値は、大きく変わり、next 関数の先頭を指します。next 関数の最初の命令まで 「ジャンプ」 するわけです。

 

f:id:higher_tomorrow:20200205203812p:plain

このとき、next 関数のスタックフレームをつくり、スタック領域に PUSH します。スタックフレームに、引数やローカル変数の情報が入るのは、これまで見てきた通りです。

ここでもう一つ、スタックフレームに、戻りアドレス(リターンアドレスとも呼びます)RET を入れています。

これは、next 関数の処理が終了した際に、元の処理、つまり、呼び出し元である main 関数の続きの処理ができるようにするためです。

そして、EIP の値に、next 関数の命令があるアドレスを格納し、ジャンプします。

f:id:higher_tomorrow:20200205203831p:plain

next 関数の命令を実行しています。

 

f:id:higher_tomorrow:20200205203846p:plain

さて、next 関数の処理が終了しますよ。

f:id:higher_tomorrow:20200205203903p:plain

きました。緑の矢印です。EIP に、戻りアドレス(RET)の値を入れて、ジャンプです。呼び出し前の位置まで戻ってきました。

 

f:id:higher_tomorrow:20200205203917p:plain

next 関数のスタックフレームを POP します。これによって、きれいに、main 関数の続きが実行されていくのです。

この挙動を、おさえておきましょう。

 

f:id:higher_tomorrow:20200205164736p:plain

まとめます。もう一度、冒頭で説明したことを繰り返します。この ① ~ ④ の動きがわかっていれば、目標達成です。

 

● next 関数を呼び出す

    ① スタックフレームを生成し、スタック領域へ PUSH します。
    その際、後で戻ってこられるように、① 今自分がいる位置を、
    スタックフレームに入れておきます。

    ② next 関数の命令にジャンプします。

● next 関数を終了する

    ③ next 関数が呼び出される前の状態に戻して、
    ④ スタックフレームを POP します。

 

f:id:higher_tomorrow:20200205164740p:plain

ようやく [  e  ] です。解答の骨格は、「関数」 となります。

しかし、他の選択肢と異なり、これだけ 「6 字以内」 と、字数が少しだけ多くなっています。単に 「関数」 という答えでは、満点はもらえないと思います。

 

この問題文中に、こんなセリフがあります。『戻りアドレスが書き換えられてしまう』 と。この、最も典型的な例に、触れておきます。

攻撃者は、「偽の戻りアドレス」 と 「悪意のコード」 の両方が含まれるデータを使って、バッファをあふれさせます。もちろん、戻りアドレスの領域も、攻撃者の用意したデータで上書きされます。そのとき、戻りアドレスの領域を悪意のコードの先頭になるように調整します。そうすると、スタックフレームが POP したときに、自分の用意したコードを実行することができるのです(このようなコードを、「shell コード」 と呼びます)。

となると、BOF の脆弱性を悪用されると、任意のコードが実行される可能性があります。

 

f:id:higher_tomorrow:20200205164746p:plain

 続いて、5 ページの上、[  f  ] です。

『malloc 関数などによって、[  f  ] 領域に確保された変数』 とあります。そのすぐ下には、『[  a  ] 領域と [  f  ] 領域に確保された変数については』 ともあります。

malloc 関数は、指定されたバイト分、「ヒープ領域」 のメモリを確保する関数です。使用後には、free 関数を呼び出して、確保していたメモリを開放する必要があります

答えは、「ヒープ」 です。絶対におとせません。

 

f:id:higher_tomorrow:20200205164750p:plain

設問 2 の (2) です。'afile' の内容が表示されてしまうことによる、セキュリティ上の問題が問われています。

管理者のみが read/write できるところ、一般利用者でも read/write できるということは、すぐにわかります。

私は、それ以上は書けませんでした。。

採点講評には、次のようにあります。

『・・・ afile の内容の表示についてだけ記述した解答が多かった。・・・ 任意のファイルの内容を表示できることに気づいてほしかった』

これ、大部分の人は、わかってたと思いますけど、、まぁよいです。

『afile の内容の表示についてだけ記述した解答が多かった』 とあるので、合否に影響する問題ではないと、信じたいです。

 

f:id:higher_tomorrow:20200205164755p:plain

問題文中に、BOF の対策が書いてあるので、少しだけ補足します。

5 ページ [  f  ] の近く T 君のせりふに、『どちらもプログラム作成上の対策は同じと考えてよい』 とあります。

これは、「スタックオーバフローも、ヒープオーバフローも、想定したメモリ領域があふれることに起因しているから、メモリ領域を超えないように、チェックすればよい」 と、考えたからでしょう。

その後の S 主任のせりふに、『バッファオーバフローを防ぐ根本的な対策は、ループ文の中で配列への書き込みを行う場合や、組み込み関数を使う場合などで、方法が異なる』 とあります。なので、コーディングルールとしては、状況に応じて、対策を示す必要があります。

例えば、バッファへの書き込みを、ループで処理するときには、バッファの境界チェックを、ループの終了条件に含めることが対策になります。一方、組み込み関数を使うときには、危険な関数を使わず、安全な関数を使うことが対策になります。何が危険で、どうすれば安全に実装できるのか、示す必要があります。

 

f:id:higher_tomorrow:20200205164800p:plain

設問 2 の (3) です。

文章で解答する場合は、50 字以内です。かなり簡潔に書かなければなりません。

strcpy が、「無制限に」 文字列をメモリに書き込むからあふれます。なので、入力を、128 バイト未満に制限してあげないといけません。128 バイトぴったりだと、NULL 文字があふれてしまうので、「以下」 ではなく 「未満」 としなければなりません

解答には、「かつ」 と 「128 バイト未満」 が必須となるでしょう。

私は、次のようにしました。

「条件文を、コマンドライン引数が 1 より大きく、かつ、この引数の長さが 128 バイト未満であるとする」 です。

IPA の解答のように、『論理積として加える』 などという表現は、私には、なかなかでてきません。

 

f:id:higher_tomorrow:20200205164804p:plain

コードで書くと、とてもシンプルになります。ただし、(1) 論理積が && になること、(2) 文字列の長さを取得するのに strlen 関数を使うこと、の 2 つがわからなければ、書けません。

私は、文章で書きました。strlen 関数に自信がなかったからです。

 

f:id:higher_tomorrow:20200205164810p:plain

設問 3 です。下線部 ② にいきましょう。プログラミング以外で、BOF を緩和する方法についてです。

完全に知識問題です。もう、知らなかったら書けません。

結論、ローカル変数と SFP(スタックフレームポインタ)の間に、推測されない値(一定の桁数の疑似乱数)を挿入します。普通にプログラムを実行している場合には、この値は変わらないはずです。これが変わったことを検知し、プログラムを停止する仕組みがあります。

このような、推測されない値を、「カナリア」 や 「クッキー」 と呼びます。

 

f:id:higher_tomorrow:20200205164814p:plain

この、IPA の解答例程度の内容を把握しておけば、十分だと思います。

 

f:id:higher_tomorrow:20200205164820p:plain

コンパイラや実行環境で、BOF を緩和する方法については、5 ページ中央の T 君のセリフにあります。

    ・ DEP(Data Execution Prevention)

    ・ Libsafe など

もう 1 つ付け加えるのであれば、

    ・ ASLR(Address Space Layout Randomization)

です。ここに書いた程度のことを、おさえておきましょう。

 

それでは、これまでやってきたことを簡単に復習しましょう。

 

f:id:higher_tomorrow:20200205164827p:plain

まず、メモリ空間です。

  • メモリ空間には、「スタック領域」 と 「ヒープ領域」 がありました
  • 「スタック領域」 は、我々のイメージとは逆に、高いアドレスから低いアドレスに向かって成長しました

次に、関数呼び出しです。

  • スタック領域には、関数が呼び出されるたびに、スタックフレームが PUSH されました
  • スタックフレームには、引数、ローカル変数、リターンアドレス、スタックフレームポインタがありました
  • 関数が終了すると、リターンアドレスを使って、元のアドレスに戻り、スタックフレームは POP されました

最後に BOF です。

  • 想定を超えるサイズのデータにより、メモリ上のバッファがあふれると、例えば本問であれば、任意のファイルが開かれる可能性がありました。RET を書き換えることで、任意のプログラムを実行される可能性もありました
  • プログラミングでの対策は、プログラム外の値が、想定している領域からオーバフローしないように実装することでした。例えば、(1) サイズをチェックしてから、確保した領域にデータを格納する(2) strcpy 関数を strncpy 関数に変更し、サイズの上限を指定する
  • コンパイラや実行環境により BOF を緩和させる技術として、カナリア値をはさむこと、DEP、Libsafe、ASLR を紹介しました

 

f:id:higher_tomorrow:20200205164833p:plain

独断と偏見で、なんとなくランク付けしてみました。

  • [A] は、必ずとりたい問題です
  • [B] は、できればとりたい問題です
  • [C] は、ぶっちゃけとれなくても、合否に影響がない(だろう)問題です
  • [A] をとり、[B] を半分くらいとれば、十分合格ラインを超えると思います

 

最後まで読んでいただき、ありがとうございました。