アンティーク・アセンブラ~Antique Assembler

第5回 関数の機能 ~ 呼び出し元からの独立

この記事を読むのに必要な時間:およそ 4 分

ところで,先のプログラム例では,自分で直接ESP/EBPレジスタを操作して,復帰の際に必要な情報の退避や,局所変数領域の確保などを行いました。

しかし,Intel x86アーキテクチャの場合,このような関数冒頭/末尾での記述量を減らすための専用命令として,関数冒頭でのスタックフレーム確保にはenter命令,関数末尾でのスタックフレーム解放には leave命令が提供されています。これらを用いて書き換えたプログラムを以下に示します。

リスト6 enter/leaveでの実装

func1:
    enter   $4, $0
    movl    $1, -4(%ebp)
    leave
    ret

enter命令の第1引数には局所変数領域のサイズを指定します。

第2引数は関数のネスティングレベル(nesting level)を表しますが,高級言語における入れ子関数定義等でない限り 0 指定で構いません。

復帰先アドレスの保存

専用命令callretを使用したプログラム例では,復帰先アドレスの格納に関してはあえて説明しませんでした。

実は,call命令はスタック上に復帰先アドレスを格納するため,局所変数の実現方法を通してスタックそのものについて説明するまで,説明を先に延ばしていたのです。

call命令は復帰先アドレスをスタック領域にプッシュし,ret命令はスタック領域からポップした復帰先アドレスをEIPレジスタに格納=制御遷移します。

低レイヤーから見た関数呼び出し

今回は,低レイヤーからの視点で見た関数呼び出しとスタックに関するトピックとして,とある脆弱性攻撃手法について説明したいと思います。

冒頭で「関数的なもの」を実現する際に,⁠関数呼び出し」および「呼び出し元への復帰」を行う命令として,共に無条件分岐命令jmpを使用しました。

一般的には専用の命令であるcallおよびretを使うわけですが,基本的な動作原理としてはjmpでの実現方法と変わりはありません。つまり

ある関数の途中から別の関数の先頭へと制御遷移する「関数呼び出し」も,ある関数の(論理的)末尾から別の関数の途中へと制御遷移する「呼び出し元復帰」も,どちらも制御遷移である

ということです。

「それがどうしたの?」と思われるかもしれませんので,ひとつ実験をしてみましょう。

リスト7 呼ばれるはずのないfunc2

    .text
    .align  4

func1:
    ret

func2:
    ret

    .global entry_point
entry_point:
    int3
    call    func1

    .global end_of_program
end_of_program:
    int3

上記のプログラムは見ての通り,関数func1を呼び出すだけのプログラムです。

しかし,func1からの復帰直前にスタックを操作することで

図3 呼ばれてしまうfunc2

(gdb) run
....
0x00401003 in entry_point ()
(gdb) disassemble func1
Dump of assembler code for function func1:
0x00401000 <func1+0>: ret
End of assembler dump.
(gdb) disassemble func2
Dump of assembler code for function func2:
0x00401001 <func2+0>: ret
End of assembler dump.
(gdb) stepi
0x00401000 in func1 ()
    ※ ret 実行直前
(gdb) info register esp
esp        0x22ff88        0x22ff88
(gdb) x/1x 0x22ff88
0x22ff88:  0x00401008
    ※ esp 値を元に復帰位置格納先を確認
       この時点では call の次の位置
(gdb) set var *0x22ff88=0x00401001
    ※ 復帰位置格納先を func2 のアドレスで上書き
(gdb) stepi
    ※ func1 の ret を実行
0x00401001 in func2 ()
    ※ 呼び出し元へ復帰せず func2 が呼ばれる
(gdb)

関数 func1から戻るはずが,関数func2を呼び出してしまいました。

この実行例では,ある関数からの復帰が想定外の関数呼び出しにつながっただけですが,スタックの上書きによるアドレス格納を精緻に行うことで,呼び出したい一連の関数を次々に呼び出すことも不可能ではありません。

これは脆弱性攻撃手法の1つで,アークインジェクション(arch injection)と呼ばれるもので,ret命令による復帰先が,本来の呼び出し元だろうが別な関数の冒頭だろうが,CPUにとってはどちらも単なる制御遷移に過ぎない,というのがこの攻撃手法の味噌です。

アークインジェクションと同様にバッファオーバーラン(buffer overrun)脆弱性を利用する攻撃手法として,スタック上に攻撃者の実行したいプログラムを書き込むコードインジェクション(code injection)はよく知られた脆弱性攻撃の手法ですが,こちらの攻撃は,スタック領域として使用するメモリ部分をOSによって「実行禁止」にしてしまうことで回避可能です。

しかし,スタック上に上書きするのが単なるデータ(呼び出し先アドレス)であるアークインジェクションの場合,OSによるスタック領域の「実行禁止」では防ぐことはできないのです!

著者プロフィール

藤原克則(ふじわらかつのり)

Mercurial三昧の日々が嵩じて, いつの間にやら『入門Mercurial Linux/Windows対応』を上梓。凝り性なのが災いして,年がら年中アップアップな一介の実装屋。最近は仕事の縁が元で,OpenSolarisに入れ込む毎日。