LoginSignup
0
0
はじめての記事投稿

Hacking: The Art of Exploitation メモ #1 関数のプロローグについて

Last updated at Posted at 2023-07-08

この記事について

この記事は「Hacking: The Art Of Exploitation」を読んだ私の個人的なメモシリーズ#1となります。
今回はGDBやC言語、低レイヤーなどの知識を必要とするかとは思われますが、
こんなメモを読むのは大体そういう方(失礼)だと思うのであしからず。

言葉だけでなく、実際のコードを見て手を動かしたほうが理解も捗るかと思い掲載しました。

関数のプロローグについて

今回は関数のプロローグについてのメモです。

関数のプロローグとは、関数を呼び出して実行し、呼び出し元に戻る際に必要となる情報(局所変数のメモリ、rbp、戻りアドレスなど)をスタックに積み上げる処理のことを指します。

関数のプロローグを実際に確認するため、以下のようなプログラムを考えてみましょう。
あくまで、関数のプロローグを見るための適当なプログラムです。

test.c
#include <stdio.h>

void  func(void)
{
	int x = 100;
	char str[] = "Hello, World!";
}

int main(void)
{
	func();
	return 0;
}

これをコンパイルしつつデバッグ情報を付与した後に、GDBデバッガを起動させます。

gcc -o test test.c -g
gdb -q ./test

GDBデバッガに入ったら、まずは個人的に分かりやすいと思っているintel記法へと設定を変更します。

(gdb) set disassembly-flavor intel

いよいよ本題です。
ソースコードをアセンブリとして表示してみます。

(gdb) disass main
Dump of assembler code for function main:
   0x000055555555519d <+0>:     endbr64
   0x00005555555551a1 <+4>:     push   rbp
   0x00005555555551a2 <+5>:     mov    rbp,rsp
   0x00005555555551a5 <+8>:     call   0x555555555149 <func>
   0x00005555555551aa <+13>:    mov    eax,0x0
   0x00005555555551af <+18>:    pop    rbp
   0x00005555555551b0 <+19>:    ret
End of assembler dump.

(gdb) disass func
   0x0000555555555149 <+0>:     endbr64
   0x000055555555514d <+4>:     push   rbp
   0x000055555555514e <+5>:     mov    rbp,rsp
   0x0000555555555151 <+8>:     sub    rsp,0x20
   0x0000555555555155 <+12>:    mov    rax,QWORD PTR fs:0x28
   0x000055555555515e <+21>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000555555555162 <+25>:    xor    eax,eax
   0x0000555555555164 <+27>:    mov    DWORD PTR [rbp-0x1c],0x64
   0x000055555555516b <+34>:    movabs rax,0x57202c6f6c6c6548
   0x0000555555555175 <+44>:    mov    QWORD PTR [rbp-0x16],rax
   0x0000555555555179 <+48>:    mov    DWORD PTR [rbp-0xe],0x646c726f
   0x0000555555555180 <+55>:    mov    WORD PTR [rbp-0xa],0x21
   0x0000555555555186 <+61>:    nop
   0x0000555555555187 <+62>:    mov    rax,QWORD PTR [rbp-0x8]
   0x000055555555518b <+66>:    sub    rax,QWORD PTR fs:0x28
   0x0000555555555194 <+75>:    je     0x55555555519b <func+82>
   0x0000555555555196 <+77>:    call   0x555555555050 <__stack_chk_fail@plt>
   0x000055555555519b <+82>:    leave
   0x000055555555519c <+83>:    ret
End of assembler dump.

特に、func関数のアセンブリの中にある

   0x000055555555514d <+4>:     push   rbp
   0x000055555555514e <+5>:     mov    rbp,rsp
   0x0000555555555151 <+8>:     sub    rsp,0x20

この部分に注目してください。

この部分こそが関数のプロローグと呼ばれる処理の実態なのです。

なにをしているの?

最初に述べたように、関数のプロローグは、関数の実行と呼び出し元に戻るための情報をスタックに積み上げます。
関数のプロローグを理解するためには、rspレジスタとrbpレジスタの理解から始めましょう。
rspとrbpは適当な話、スタックに積まれた一つのフレーム(関数実行時に必要となる情報の集まり)の上下のアドレスを保持しています。
つまりrspとrbpが保持しているアドレスの間に局所変数などか格納されているということです。

func関数を呼び出す前と、呼び出されてfuncの処理が行われているときのrspとrbpが持つアドレスを見てみましょう。
まずはbreakコマンドにより、7行目(func関数内)と11行目(func関数呼び出し前)にブレイクポイントを設定し、runコマンドで最初のブレイクポイント、func関数呼び出し前の11行目で処理を停止させます。

(gdb) break 7
(gdb) break 11
(gdb) run

次にfunc関数を呼び出す前のrspとrbpが保持するアドレスについて見ていきましょう。
以下のコマンドで、レジスタが保持している値を確認できます。

(gdb) i r $rsp $rbp
rsp            0x7fffffffe060      0x7fffffffe060
rbp            0x7fffffffe060      0x7fffffffe060

先程、rspとrbpが保持しているアドレスの間に局所変数などか格納されていると書きましが、main関数内では局所変数を宣言していないため、スタック上には領域が確保されていないのです。つまりrspとrbpの値は同じアドレスを指します。

funcを呼び出す前のスタックの状態が確認ができたら、次はfunc実行中のの状態を確認してみましょう。
contコマンドで、次のブレイクポイントである7行目(func関数内)へと処理を進め、先ほどと同じinfo registerコマンドを使用して、レジスタを確認します。

(gdb) cont
(gdb) i r $rsp $rbp
rsp            0x7fffffffe030      0x7fffffffe030
rbp            0x7fffffffe050      0x7fffffffe050

アドレスが先程よりも小さくなっていることが分かると思いますが、これはmain関数のスタックフレームが存在している0x7fffffffe060の上にfunc関数のスタックフレーム(関数実行に必要な情報)が積み上げられたためです。
またrspとrbpには0x20の差がありますが、この差の中に局所変数などが格納されています。

もう少し詳しく

今度は命令を追って、具体的に値が変更される流れを見ていきましょう。
今のスタックのイメージは先程のrsp・rbpのアドレスを参考に、このような状態からスタートします。
stack1.jpg

まずはmain関数内にあるcall命令からです。

   0x00005555555551a5 <+8>:     call   0x555555555149 <func>

call命令は戻りアドレスをスタックへpush(積み上げ)し、ripを呼び出し先の関数の先頭アドレスに変更します。
また、戻りアドレスとはmain関数内のcall命令の次の命令のアドレスを意味します。

つまり以下の命令のアドレスが、戻りアドレスとして0x7fffffffe060の上にpushされるのです。

   0x00005555555551aa <+13>:    mov    eax,0x0

そしてpushを行った場合、rspは減算されます。
つまりrspは、保持している0x7fffffffe060から0x8が減算されて0x7fffffffe058という値を格納するわけです。
stack2.jpg

また、ripに格納される呼び出し先の関数(func)の先頭アドレスはこれになります。

   0x0000555555555149 <+0>:     endbr64

ripとは次に実行する命令のアドレスを保持するレジスタのことです。プロセッサはこれを使って次の命令の場所を把握し、実行します。call命令を使用することで、呼び出し先の関数の命令が格納されているアドレスへとripを書き換え、関数の実行が実現します。

では次の命令、つまり関数のプロローグ最初の命令を見ていきましょう。

   0x000055555555514d <+4>:     push   rbp

これは、rbpをスタックへとpushする命令です。main関数へ制御を返す際にrbpの値を復元するために後々用いられます。
0x7fffffffe058の上にrbpの現在の値がpushされるため、rspのアドレスは先ほどと同じように0x8だけ小さくなるので0x7fffffffe050となるわけです。
stack3.jpg

次の命令はrbpへrspのアドレスをmov命令によってコピーします。これによって作成するfunc関数のスタックフレーム、そのボトムをrbpが表すようになります。

   0x000055555555514e <+5>:     mov    rbp,rsp

stack4.jpg

これで、関数から呼び出し元へと帰る際の準備が完了しました。
あとは局所変数などを格納するためにrspの値を減算して、領域を確保するだけです。

   0x0000555555555151 <+8>:     sub    rsp,0x20

sub命令はrspから0x20を引き算するよう命令しています。
stack5.jpg

これにて関数プロローグを全て追うことができました。

確認

本当に一連の動作が行われているのかを確認してみましょう。
examineコマンドを使用してrspのアドレスからmain関数のフレームである0x7fffffffe060のあたりまでを確認します。
ちなみに本書は32ビットコンピュータを使用しているようで、gではなくwなのですが、64ビットコンピュータを使用する場合はgをつけたほうが見やすいと思います。

(gdb) x/8xg $rsp
0x7fffffffe030: 0x0000006400000002      0x2c6f6c6c6548fbff
0x7fffffffe040: 0x0021646c726f5720      0xfea8e158512f7600
0x7fffffffe050: 0x00007fffffffe060      0x00005555555551aa
0x7fffffffe060: 0x0000000000000001      0x00007ffff7db8d90

下から2行目には右に戻りアドレス、左にrbpが格納されていることが確認できます。
また、上から1行目の4バイトまでを数えると0x00000064が格納されており、これは10進数の100であることもわかります。
("Hello, World!"だけ見つからないのですが、多分この中に先頭アドレスがあるはずです。てへ)

(追記) : 先頭アドレスの格納だと思い込んでいたのですが、実際には実体そのものが格納されていました。
上から2行目の右側と、3行目の左側の値をASCIIコード表と比べてみると

0x 2c 6f 6c 6c 65 48 fb ff
   ,  o  l  l  e  H

0x 00 21 64 6c 72 6f 57 20
   \0 !  d  l  r  o  W  Space

上記のように、Hello, World! が確認できます。

最後に

おそらく間違えているところが多々あると思います。その際にはぜひともご指摘ください。

普段何気なくプログラムを書いていれば関数にはよくお世話になりますが、理屈を知るとプログラムの深い部分を感じられていいですね。

0
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0