これは、GDBを使ってCプログラムのスタック空間を表示する方法についての簡単なチュートリアルです。これはRustプログラムでも同様だと思います。しかし、ここではまだCを使っています。Cの方が使いやすいし、Cの方がバグのあるプログラムを書きやすいからです。
テスト手順
以下は、いくつかの変数を宣言し、標準入力から2つの文字列を読み込む単純なCプログラムです。つの文字列はヒープ上にあり、もう1つの文字列はスタック上にあります。
#include <stdio.h>#include <stdlib.h>int main() {char stack_string[10] = "stack";int x = 10;char *heap_string;heap_string = malloc(50);printf("Enter a string for the stack: ");gets(stack_string);printf("Enter a string for the heap: ");gets(heap_string);printf("Stack string is: %s ", stack_string);printf("Heap string is: %s ", heap_string);printf("x is: %d ", x);
このプログラムでは、おそらく皆さんが使うことのないような、極めて安全でない関数getsを使っています。しかし、わざとそのように書きました。エラーが発生したら、その理由がわかります。
ステップ0:プログラムのコンパイル
このプログラムをコンパイルするには gcc -g -O0 test.c -o test コマンドを使用します。
gオプションを付けると、チューニング情報もコンパイラーにコンパイルされます。これにより、変数の表示が簡単になります。
O0オプションはgccに最適化しないように指示するもので、x変数が最適化されないようにしたいのです。
ステップ1:GDBの起動
このようにGDBを起動します:
$ gdb ./test
GPLの情報が出力され、プロンプトが表示されます。ここでmain関数にブレークポイントを設定しましょう:
(gdb) b main
その後、プログラムを実行することができます:
(gdb) b mainStarting program: /home/bork/work/homepage/testBreakpoint 1, 0x55516d in main ()(gdb) runStarting program: /home/bork/work/homepage/testBreakpoint 1, main () at test.c:44 int main() {
さて、プログラムが立ち上がって実行できるようになりました。そろそろスタックスペースを見始めましょう。
ステップ2:変数のアドレスをチェック
変数を理解することから始めましょう。変数にはそれぞれメモリ上のアドレスがあり、次のように出力することができます:
(gdb) p &x$3 = (int *) 0x7fffffffe27c(gdb) p &heap_string$2 = (char **) 0x7fffffffe082(gdb) p &stack_string$4 = (char (*)[10]) 0x7fffffffe28e
ですから、これらのアドレスのスタックを見れば、すべての変数を見ることができるはずです!
コンセプト:スタック・ポインタ
はスタックポインタを使う必要があるので、頑張って簡単に説明します。
ESPと呼ばれるx86のレジスタに""があります。 基本的には、現在の関数のスタックの開始アドレスです。 GDBでは、$spを使ってアクセスできます。 新しい関数を呼び出したり、関数から戻ったりすると、スタックポインタの値が変わります。
ステップ3:メイン関数の冒頭で、スタック上の変数を見てください。
まず、main関数の先頭のスタックを見てみましょう。 これが現在のスタックポインタの値です:
(gdb) p $sp$7 = (void *) 0x7fffffffe072
つまり、現在の関数のスタック開始アドレスは 0x7fffffffe072です。
では、GDBを使って、現在の関数スタックの開始後の最初の40ワードを出力してみましょう。 スタックの大きさがよくわからないので、メモリの一部はスタックの一部ではないかもしれません。 しかし、少なくとも先頭はスタックの一部です。
変数stack_string xの位置を太字にし、色を変えました:
- xは赤で、
heap_string始まります。 - スタート地点の住所は
0x7fffffffe27cです heap_string始まる紫色のフォントです。
ここで奇妙なことに気づくかもしれませんが、xの値は0x5555ですが、xを! これは、実際にxを設定するのはmain関数が実行された後であり、今はmainの一番最初にいるからです。
ステップ3:10行目のコードまで実行したら、スタックをもう一度見てください。
数行飛ばして、変数が実際に初期値に設定されるのを待ちましょう。 10行目までに、xは.
まず、別のブレークポイントを設定する必要があります:
(gdb) b test.c:10Breakpoint 2 at 0xa9: file test.c, line 11.
それからプログラムを続けてください:
(gdb) continueContinuing.Breakpoint 2, main () at test.c:1111 printf("Enter a string for the stack: ");
わかりました! スタックの中身をもう一度見てみましょう! gdbではバイトのフォーマットが若干異なりますが、実はあまり気にしていません。 スタック上の変数がどこにあるか、ここで思い出してください:
- xは赤で、開始アドレスは
0x7fffffffe27cです。 - スタート地点の住所は
heap_stringです0x7fffffffe082 stack_string始まる紫色のフォントです。
次に進む前に、興味深いことをいくつか。
記憶の中の表現方法
では、0x7fffffffe28e 文字列スタックにセットされています。 メモリ上でどのように表現されるか見てみましょう。
このように文字列でバイトを出力することができます:
(gdb) x/10x stack_string0x7fffffffe28e: 0x 0x 0x6b 0x 0x000x7fffffffe 0x00
また、GDBを文字列として表示するにはx/1sを使用します:
(gdb) x/1s stack_string0x7fffffffe28e: "stack"
heap_string 私stack_string " "私は" "私は" "私は" 私は
と stack_string スタック上では全く異なる表現であることに既にお気づきでしょう:
heap_string = malloc(50);printf("Enter a string for the stack: ");メモリ上の位置へのポインタです。
以下はメモリ上の gets(stack_string); 変数の内容です:
0xa0 0x 0x 0x 0x00
x86はリトルエンディアンモードなので、メモリアドレス0xa0がheap_stringに格納されます。
heap_string 格納されているメモリー・アドレスを見るもう一つの方法は、pコマンドを使って直接表示することです:
(gdb) p heap_string$6 = 0xa0 ""
整数 x のバイト表現
xは32ビット整数で 0x0a 0x 0x00 表せます。
これらのバイトを読むにはまだ反転させる必要があるので、この数字は0xaまたは0x0aを表します。)
これでxを.
ステップ4:標準入力からの読み込み
さて、変数が初期化されたので、このプログラムを実行したときにスタック領域がどのように変化するか見てみましょう:
printf("Enter a string for the stack: ");gets(stack_string);printf("Enter a string for the heap: ");gets(heap_string);
別のブレークポイントを設定する必要があります:
(gdb) b test.c:16Breakpoint 3 at 0x05: file test.c, line 16.
それからプログラムを続けてください:
(gdb) continueContinuing.
スタックに格納されている変数には文字列を2つ、ヒープに格納されている変数にはバナナを入力します。
これを見てみましょう Breakpoint 2, main () at test.c:11
(gdb) x/1s stack_string0x7fffffffe28e: "12"
これはかなり普通に見えますよね?を入力すると、今度は.
しかし今、とても奇妙なことがあります。これはプログラムのスタック領域の内容です。紫色のハイライトがあります。
不思議なことに、 stack_string 10バイトしかサポートしていません。しかし、13文字が入力された場合はどうなるのでしょうか?
これは典型的なバッファ・オーバーフローで、stack_stringがプログラム内の別の場所に自分のデータを書き込んでしまいます。この場合、問題は発生しませんが、プログラムがクラッシュしたり、最悪の場合、非常に悪いセキュリティ問題にさらされる可能性があります。
例えば、stack_stringがheap_stringのすぐ上のメモリにあるとします。その場合、heap_stringが指すアドレスが上書きされてしまうかもしれません。stack_stringの後のメモリに何があるかはわかりません。しかし、それを使って奇妙なことができるかもしれません。
確かにバッファオーバーフローが検出されました。
わざと文字数を多く書くとき:
./testEnter a string for the stack:Enter a string for the heap: adsfStack string is:Heap string is: adsfx is: 10*** stack smashing detected ***: terminatedfish: Job 1, './test' terminated by signal SIGABRT (Abort)
私の推測では、 stack_string この関数スタックの一番下に達しているので、余分な文字は別のメモリに書き込まれます。
このセキュリティ・ホールを意図的に使用することを「スタック・スマッシング」と呼びます。
また、プログラムは強制終了されるのですが、バッファオーバーフローが発生してもすぐに強制終了されるわけではなく、バッファオーバーフローの後にさらに数行のコードが実行されるまで強制終了されないのも興味深いです。 とても不思議です!
これらはすべてバッファオーバーフローに関するものです。
これを見て heap_string
まだ heap_string 変数にバナナを入力しています。メモリ上でどのように見えるか見てみましょう。
これが、文字列が読み込まれた後のスタック・スペース上のheap_string 様子です:
ここでの値はアドレスであることに注意してください。そしてそのアドレスは変わっていませんが、指されたメモリ上に何があるか見てみましょう。
(gdb) x/a00xa0: 0x 0x6e 0xe 0x 0x000xa8: 0x
これはバナナという文字列のバイト表現です。これらのバイトはスタック空間にはありません。ヒープ上のメモリに存在します。
ヒープとスタックはどこに?
スタックとヒープがメモリの異なる領域であることはすでに説明しましたが、メモリ内のどこにあるかはどうやって知るのですか?
各プロセスには、各プロセスのメモリーマップを示す /proc/$PID/maps というファイルがあります。 この中にスタックとヒープがあります。
$ cat /proc/24963/maps... lots of stuff omitted ...-a000 rw-p :00 0 [heap]... lots of stuff omitted ...7ffffffde000-7ffffffff000 rw-p :00 0 [stack]
ここで注意すべき点は、ヒープ・アドレスは0x5555から始まり、スタック・アドレスは0x7fffffから始まるということです。 つまり、スタック上のアドレスとヒープ上のアドレスの区別は簡単です。
このようにgdbを使うと本当に便利です。
ちょっとした渦巻きツアーで、すべてを説明したわけではありませんが、メモリ上でデータが実際にどのように見えるかを見ることで、スタックが実際にどのように見えるかをよりよく理解していただければと思います。
このようにgdbで遊んでみることを本当にお勧めします。メモリに表示されるすべてのことを理解できなくても、このように自分のプログラムのメモリにあるデータを実際に見ることで、「スタック」や「ヒープ」や「ポインタ」といった抽象的な概念がずっと理解しやすくなることがわかりました。「スタック」「ヒープ」「ポインタ」のような抽象的な概念をより理解しやすくします。
より多くのエクササイズ
- test.cに別の関数を追加して、その関数の最初にブレークポイントを作り、mainからのスタックを見つけられるかどうか試してみてください! 関数を呼び出すと「スタックが小さくなる」と言われますが、gdbでそれを確認できますか?
- 関数からスタック上の文字列へのポインタを返し、何が間違っていたかを確認します。 なぜスタック上の文字列へのポインタを返すのが悪いのですか?
- C言語でスタック・オーバーフローを起こしてみて、gdbでスタック・オーバーフローを見ることで、何が起こるかを正確に理解してください!
- Rustプログラムのスタックを見て、変数を探してみてください!
- バッファオーバーフローに挑戦してみてください。各問題の解答はREADMEファイルに書かれているので、選択肢に迷いたくないのであれば、最初に解答に目を通すのは避けてください。 これらのチャレンジのアイデアは、バイナリファイルを与え、フラグ文字列を出力するためにバッファオーバーフローを引き起こす方法を見つけ出す必要があるということです。
を経由して





