今回はいよいよアセンブラでの関数
ただし、
復帰位置の取得
「関数」の原理
「関数」
C/
復帰位置
(アドレス) を記録しておいて、 関数での処理が完了したなら、 復帰位置に制御遷移する
という手順を踏めば、
たとえば以下のような実装によって、
※ 呼び出し元
:
leal rp, %eax # 復帰位置 rp を eax に格納
jmp func1
rp: # 復帰位置(Return Position)
:
※ 呼び出し先
func1:
:
jmpl *%eax # eax 位置に復帰
呼び出し先func1)
専用命令の使用
先の実装例では、jmpを用いた関数呼び出しに先立って、
しかし、
関数から復帰する位置は、jmp 命令の次の命令位置以外にありえません。そして一般的なCPUであれば、
つまり、
期待に違わず、
Intel x86アーキテクチャの場合、call"ret"
※ 呼び出し元
:
call func1
:
※ 呼び出し先
func1:
:
ret
局所的情報の保持
局所変数領域の確保
個々の関数実行時における固有の変数領域、
局所変数領域は、
このような特徴を持つ局所変数領域を実現するために、
スタック構造を用いて各関数の局所的な情報を格納する領域を
まずは関数での処理の開始の際に、
- ESPの値を4
[3] だけ減算 - EBP
(Extended Base Pointer) の値をESPの位置に記録 - ESPをEBPの位置に移動
- 関数で必要となる局所変数領域の分だけESPを減算
この時、
スタックフレーム中に確保された局所変数領域へのデータの読み書きは、
たとえば、
void
func1(){
int index;
index = 1;
:
}
# 局所変数領域の確保
subl $4, %esp
# esp の移動
movl %ebp, (%esp)
# ebp の格納
movl %esp, %ebp
# ebp を esp 位置に移動
subl $4, %esp
# index 領域の分だけ esp を移動
# 局所変数へのアクセス
movl $1, -4(%ebp)
EBPと局所変数格納領域アドレスとの差分である"-4"を用いた即値付き間接アドレッシングで、
また、
- ESPの位置をEBPの位置に移動
- ESPの位置から以前のEBP値を取り出してEBPを復旧
- ESPの値を4だけ加算
アセンブラで実装するなら以下のようになりますpop 命令で実施しています)。
movl %ebp, %esp # esp を ebp 位置に移動
popl %ebp # ebp の復旧と、esp の移動
これにより、
ところで、
しかし、enter命令、leave命令が提供されています。これらを用いて書き換えたプログラムを以下に示します。
enter/leaveでの実装func1:
enter $4, $0
movl $1, -4(%ebp)
leave
ret
復帰先アドレスの保存
専用命令call/retを使用したプログラム例では、
実は、call命令はスタック上に復帰先アドレスを格納するため、
call命令は復帰先アドレスをスタック領域にプッシュし、ret命令はスタック領域からポップした復帰先アドレスをEIPレジスタに格納=制御遷移します。
低レイヤーから見た関数呼び出し
今回は、
冒頭でjmpを使用しました。
一般的には専用の命令であるcallおよびretを使うわけですが、jmpでの実現方法と変わりはありません。つまり
ある関数の途中から別の関数の先頭へと制御遷移する「関数呼び出し」も、ある関数の(論理的)末尾から別の関数の途中へと制御遷移する「呼び出し元復帰」も、どちらも制御遷移である
ということです。
「それがどうしたの?」
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からの復帰直前にスタックを操作することで
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つで、ret命令による復帰先が、
アークインジェクションと同様にバッファオーバーラン
しかし、