1. HOME
  2. ブログ
  3. CPUのアーキテクチャから徹底解説バッファオーバーフローで何が起こるか第2回 関数利用時のスタック(アドレス)の動きを詳しく追う

CPUのアーキテクチャから徹底解説
バッファオーバーフローで何が起こるか
第2回 関数利用時のスタック(アドレス)の動きを詳しく追う

前回は、バッファオーバーフロー脆弱性が悪用されるしくみを理解するために、CPUのアーキテクチャレベルから、プログラムの動き方を見てきました。pcレジスタやspレジスタといった基本について学びました。今回は、さらに踏み込んで、関数を実行する際のスタックポインタの動きなどについて解説しましょう。第1回プログラムが動くしくみの続編です。

C言語は再利用可能な命令群を関数として定義できます。関数の実行は前述のように分岐命令によって自由に呼び出せます。すなわち、関数を呼び出すことによって、実行するアドレスを変えることになります。本連載ではこのしくみが重要になります。そこで、関数がどのように実行されているのかを説明します。

関数を実行

プログラムでは関数の動きを、分岐命令とスタックを使って実現します。図5はC言語の関数呼び出しの実装と、変換後のアセンブリ言語を示したものです。このプログラムの関数呼び出しと関数からの戻りがどのように実行されるかを見てみます。

関数呼び出しのC言語ソースコードと変換後プログラム

関数呼び出し

関数呼び出しは、複数の命令で構成されているため、命令ごとに図を用いて説明します。なお、関数は引数やローカル変数をスタックに記憶します。そのため関数呼び出しごとに、これらを記憶するための領域をスタックに確保します。この領域をスタックフレームといいます。スタックフレームの範囲は、先頭アドレスをspレジスタ、底のアドレスをfpレジスタで示します。

関数呼び出しはアドレス0x00000128から始まります(図6①)。
また呼び出しの分岐命令としてbl命令が使われています(注釈:関数の呼び出し方法によって、違う分岐命令が使われる場合があります)。bl命令は分岐と同時に、1つ下の命令のアドレスをlrレジスタにセットする命令です。したがってbl命令を実行すると、分岐命令の値がpcレジスタにセットされます。(図6②)。また分岐命令の次の命令のアドレスが、lrレジスタにセットされます。(図6③)

図6 関数呼び出しにおける分岐実行

先ほどの分岐命令で実行するアドレスが変わったため、次はアドレス0x00000200のpush命令を実行します(図7①)。CPUはpush命令によって、lrレジスタの内容をスタックに追加します(図6②)。同じくfpレジスタの内容をスタックに追加します(図6③)。スタックに値を2つ積んだので、spレジスタの値は元の値より2つ上(-8)に変わります(図7④)。またspレジスタの値を2つ上に進めたため、スタックフレームの先頭も2つ上のアドレスに変わります(図7⑤)。

なお、レジスタの内容をスタックに追加する理由は、さらに関数呼び出しを行うときのためです。lrレジスタやfpレジスタは1つしかないため、直近の呼び出し元の情報しか保持できません。仮に呼び出された関数の中で、さらに関数呼び出しを行う場合、これらのレジスタの内容を別の場所に記憶しておく必要があります。スタックに保持しておけば、元々の呼び出し元の情報を保持することができます。

図 7 関数呼び出しにおける呼び出し元情報記憶

関数呼び出しの最後はアドレス0x00000204の実行です(図8①)。
CPUはadd命令によってfpレジスタにspレジスタの内容+4をセットします(図8②)。このときfpレジスタの値をsp+4にしたため、スタックフレームの底も、spから1つ下(+4)の位置に移動します(図8③)。この時点でスタックフレームはmain関数の利用領域と被らない領域になりました(注釈:関数で引数やローカル変数を使う場合、スタックフレームの先頭が、さらに上のアドレスとなります。本記事では関数呼び出しと戻りの説明を平易にするため、引数やローカル変数を使う場合を省略します)。

図8 関数呼び出しにおけるスタックの範囲変更

以上で関数の呼び出しは完了です。

関数から戻る

関数からの戻りも複数の命令で構成されているため、命令ごとに図を用いて説明します。
関数からの戻りはアドレス0x00000250から始まります(図9①)。CPUはsub命令によってspレジスタにスタックフレームの底(=fpレジスタの内容)から1つ上(-4)のアドレスをセットします(図9②)。このときfpレジスタの値を変えたため、スタックフレームの先頭はスタックフレームの底の1つ上となります図9③。

図9 関数から戻る場合におけるスタックの範囲変更

次に、アドレス0x00000254のpop命令を実行します(図10①)。
CPUはpop命令によって関数呼び出し処理のときにスタックに積んでいたlrレジスタの内容を、pcレジスタにセットします(図10②)。同じく関数呼び出し処理のときにスタックに積んでいたfpレジスタの内容を、fpレジスタにセットします(図10③)。

またスタックから値を取り出したため、spレジスタの値は2つ下(+8)になります(図10④)。
spレジスタとfpレジスタの値を関数呼び出し前の内容に変えたため、スタックフレームの先頭と底も、関数呼び出し前の時点の位置に移動します(図10⑤)。

図10 関数から戻る場合における呼び出し元情報の取り出し

先ほどのpop命令実行によって、pcレジスタの値がスタックに記憶していた戻り先アドレス(0x0000012C)に変わりました。そのため戻り先アドレス(0x0000012C)の命令が実行されます図11①。すなわち実行アドレスが関数から戻ったことになります。

図11 関数から戻った状態

以上で関数からの戻りは完了です。

おわりに

本記事ではプログラムの動くしくみについて2回にわたり解説しました。

細かい話が多いですが、次回以降で解説するバッファオーバーフローのしくみを理解するためには不可欠な知識ですので、ぜひ第1回、第2回ともに読み返すなどしていただければと思います。 次回はスタックに想定より大きな情報を入れすぎて、あふれてしまう現象「スタックバッファオーバーフロー」について解説します。


著者
株式会社ATTC(エーティーティーシー) 尾關晃充

元々はWebアプリケーションのサーバサイドエンジニアだった。
ATTC入社後はセキュリティ製品開発に携わっている。
現在は「ATTC Control Flow Integrity」の開発担当を務めセキュリティや
脆弱性対策に取り組んでいる。


「CPUのアーキテクチャから徹底解説 バッファオーバーフローで何が起こるか」の最新話は、メールマガジンにてもご案内致しています。是非JAPANSecuritySummit Updateのメールマガジンにご登録ください。
メールマガジンの登録はこちらからお願いします。

関連記事

サイバーセキュリティの課題をテーマ別に紹介中!!