最近、アセンブリの勉強を少ししています。
手始めにこの記事を参考にアセンブリとバッファオーバーフローの勉強をしてみます。
まずは基本的な用語の覚書
スタック
関数の引数、関数内部のローカル変数や戻りアドレスが格納される。LIFO (Last In, First Out 後入れ先出し)。
スタック領域はメモリの上位アドレスから下位アドレスに向かって確保されていく。
ヒープ
動的に確保されたメモリ領域。例えばmallocによって動的にメモリ領域を確保する場合は、ヒープ領域上に確保される。ヒープ領域はメモリの下位アドレスから上位アドレスに向かって確保されていく。
%eip
次に実行する命令文のアドレスを格納している。命令文が実行されるたび、%eipの値は加算される (どのくらい加算されるかは実行された命令文のサイズによる)
%esp
スタック・ポインタ。スタック領域のトップに積まれたデータのメモリー・アドレスを格納する。スタックはメモリの上位アドレスから下位アドレスに向かって積み上がっていくので、%espは最下位のアドレスに格納されたデータを指す。
%ebp
ベース・ポインタ。データの格納領域の基点のメモリー・アドレスを格納する。通常、関数の最初において、%ebpには%espの値が設定される。これは関数の引数とローカル変数を追跡するためである。ローカル変数へのアクセスは%ebpからオフセットを引くことによって行われる。関数の引数へのアクセスは%ebpにオフセットを加算することによって行われる。
以下のCのコードを使ってバッファオーバーフローを試します。
#include <stdio.h> /* https://dhavalkapil.com/blogs/Buffer-Overflow-Exploit */ void secretFunction() { printf("Congratulations!\n"); printf("You have entered in the secret function!\n"); } void echo() { char buffer[20]; printf("Enter some text:\n"); scanf("%s", buffer); printf("You entered: %s\n", buffer); } int main() { echo(); return 0; }
上記は標準入力から入力された内容を標準出力に出力するプログラムです。secretFunctionという関数が定義されていますが、どこからも呼び出されていないので、このプログラムを普通に実行してもsecretFunction関数が呼び出されることはありません。
このsecretFunction関数をバッファオーバーフローを利用して呼び出すことが今回の目的です。
上記のコードをコンパイルします。
gcc buffer-overflow.c -o vul-buffer-overflow.out -fno-pie -fno-stack-protector -m32
-fno-pie: PIE形式でコンパイルしないためのオプション
-fno-stack-protector: スタックの保護機能を無効化するためのオプション
-m32: 32ビットのバイナリ形式でコンパイルすためのオプション
コンパイルしたバイナリを逆アセンブルします。
gobjdump -d vul-buffer-overflow.out
アセンブリ言語には大きくAT&T構文とIntel構文があります。画像のように、%ebpなど、レジスタに%がついているのはAT&T構文です。
AT&T構文の場合、左辺がデータの送信元、右辺がデータの送信先になります。
mov %esp,%ebp
上記の場合、%espの値を%ebpに格納するという意味になります。
Intel構文の場合は右辺がデータの送信元、左辺がデータの送信先になります。
上記の逆アセンブルの結果から以下のことが読み取れます。
secretFunction関数はアドレス00001e80から開始されます。
00001e80 <_secretFunction>:
echo関数のローカル変数には0x28バイト(10進数で40バイト)の領域が割り当てられています。
1eb3: 83 ec 28 sub $0x28,%esp
変数bufferのアドレスは%ebpから0x14バイト(10進数で20バイト)手前に位置しています。これは、つまり、変数bufferには20バイトの領域が割り当てられていることになります。
1eca: 8d 55 ec lea -0x14(%ebp),%edx
変数bufferには20バイトの領域が割り当てられており、すぐ隣は%ebp(main関数のベース・ポインタ)が格納されています。さらにその次の4バイトには戻りアドレス(関数の実行が完了したあとにジャンプするアドレス)が格納されます。
そのため、secretFunction関数を呼び出すためのバッファオーバーフローのペイロードは以下の構成になります。
[バッファサイズ 20バイト] + [EBPポインタのサイズ 4バイト] + [secretFunction関数のアドレス 4バイト]
※32ビットのシステムにおいて、レジスタは4バイト
先述の通り、secretFunction関数の開始アドレスは00001e80です。実行環境のバイトオーダーがビッグエンディアンかリトルエンディアンかで、アドレスのバイトオーダーを変更する必要があります。リトルエンディアンの場合はバイトオーダーを逆順にする必要があります。 (00 00 1e 80 -> 80 1e 00 00)
今回、ターミナル上でバッファオーバーフローのペイロードを送るため、以下のpythonを実行しました。
python -c 'print "a"*24 + "\x80\x1e\x00\x00"'
上記のpythonをバイナリにパイプして実行します。
python -c 'print "a"*24 + "\x80\x1e\x00\x00"' | ./vul-buffer-overflow.out
secretFunction関数の実行が確認できました。
echo関数が呼び出されたあとのスタックの動きをイラスト化してみました。
echoがcallされたあとの次の命令のアドレスをリターンアドレスとしてスタックに積みます(この操作はcallが実行されると自動的に行われます)。
さらにpush %ebpによってmain関数のEBPレジスタの値がスタックに積まれます。これにより、echoの実行が完了したあとにmain関数に戻れるようになります。
sub $0x28, %espによってESPレジスタの指す位置を下位のアドレスに移動させます。EBPレジスタの指す位置とESPレジスタの指す位置の間にできた領域はローカル変数の格納に利用されます。この場合は0x28、つまり10進数で40バイトの領域がローカル変数の領域として割り当てられます。
lea -0x14(%ebp), %edx 変数bufferの開始アドレスはEBPレジスタより0x14バイト(20バイト)手前の位置になります。
変数bufferの直後の4バイトの領域には先程保存したEBPの値が格納されています。さらにその次の4バイトの領域にはecho関数が完了したあとの次の命令アドレスが格納されています。今回はこの命令アドレスをsecretFunction関数の開始アドレスに書き換えます。
※32ビットのシステムにおいて、レジスタは4バイト
変数bufferの20バイト分の領域をaaaaaaaaaaaaaaaaaaaaで埋め尽くします。さらにその次の4バイト分もaaaaで埋めます。最後に次の命令アドレスが格納されている4バイト分をsecretFunction関数の開始アドレス\x80\x1e\x00\x00に書き換えます。
この命令アドレスの書き換えによって、echo関数が実行されると、次にsecretFunction関数が実行されるわけです。
ちなみにバッファオーバーフローさせずに、echo関数が正常に完了したあとのスタックの動きは以下のとおりです。
add $0x28,%espによってESPレジスタの指す位置が40バイト上位のアドレスに押し上げられます。これにより、40バイト確保されていたローカル変数の領域がフリーになります。
pop %ebpによって保存していたEBPレジスタの値を復元します。EBPおよびESPレジスタの指す位置が一番最初のスタック図と同じ位置に戻っていることが確認できます。
このadd $0x28,%esp; pop %ebpの一連の命令は、mov %ebp,%esp; pop %ebpと書くこともできます。また、これら一連の命令をleave命令1つで実現することができます。
C言語において、以下の関数にバッファオーバーフローの脆弱性があります。
gets
scanf
sprintf
strcpy
以上。
参考URL
https://dhavalkapil.com/blogs/Buffer-Overflow-Exploit/
http://www-inst.eecs.berkeley.edu/~cs161/fa08/papers/stack_smashing.pdf
https://nets.ec/Assembly
https://qiita.com/edo_m18/items/83c63cd69f119d0b9831
https://www.jpcert.or.jp/research/2011/1-strings.pdf