コース目次 / 第4章
バッファオーバーフローを観察する(最終章)
gdbで、8バイトの箱に書き込まれたAAAA...がその境界を越えて隣接メモリに広がる様子を、生バイトで見ます。保護なし/ありでのクラッシュの違いから、スタックカナリアという多層防御を観察します。武器化はしません。
前章で見た「箱の大きさと書き込む長さの不一致」が、実際に何を起こすのかを、自分の手元で・自分が作ったプログラムに対して観察します。このコースが行うのは観察までです。制御を乗っ取る・シェルコードを実行する、といった武器化は扱いません。
標的 — わざと小さな箱を使う関数
下を bof.c として保存します。8バイトの箱に、長い文字列を strcpy するだけの、最小の例です。
#include <stdio.h>
#include <string.h>
void vulnerable(char *input) {
char buf[8]; // 8バイトの箱
strcpy(buf, input); // 箱の大きさを確認せずコピーする
printf("buf の中身: %s\n", buf);
}
int main(void) {
vulnerable("AAAAAAAAAAAAAAAAAAAAAAAA"); // 24文字 + 終端
printf("ここまで到達すれば、関数は無事に戻ってきた\n");
return 0;
}
まず、保護なしでビルドして、動かす
-fno-stack-protector を付けて、あえて保護を切ってビルドします(理由はあとで分かります)。
gcc -O0 -g -fno-stack-protector -o bof bof.c
./bof
echo "終了コード: $?"
実行すると、buf の中身: … は表示されず、Segmentation fault で異常終了します。$?(終了コード)を見ると 139——これは「シグナル11(SIGSEGV)で終了した」ことを示します。
何が起きたのか。strcpy が、8バイトの箱をはるかに超えて書き込み続け、関数から戻るためのアドレス(戻り先)まで上書きしてしまったため、vulnerable 関数が終わろうとしたとき、存在しない場所へジャンプしようとしてクラッシュしました。
gdb で、あふれる様子を、生バイトで見る
このコースが見せるのは、ここまでです。 どこにジャンプさせるかを操作したり、実行される命令を仕込んだりはしません。あくまで「箱からあふれている」という事実を、自分の目で確かめるだけです。
gdb -q -batch \
-ex "break bof.c:5" \
-ex "run" \
-ex "next" \
-ex "print buf" \
-ex "x/32xb buf" \
./bof
Breakpoint 1, vulnerable (input=0x... 'A' <repeats 24 times>) at bof.c:5
5 strcpy(buf, input);
6 printf("buf の中身: %s\n", buf);
$1 = "AAAAAAAA"
0x7fffffffce98: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffcea0: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffcea8: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x7fffffffceb0: 0x00 0xcf 0xff 0xff 0xff 0x7f 0x00 0x00
print buf は、buf を「8バイトの文字列」として律儀に “AAAAAAAA” と表示します——gdb は buf の宣言どおりのサイズしか知らないからです。
でも、その下の x/32xb buf(buf から32バイトを、生の16進バイトで表示)を見てください。0x41(文字 ‘A’)が、宣言された8バイトをはるかに超えて、3行(24バイト)ぶん続いています。これが、strcpy が実際にやったこと——「箱」という概念を無視して、渡されたバイト列をひたすら書き込み続けた証拠です。この先に、関数の戻り先アドレスなどが並んでいて、それも上書きされたために、さっきのクラッシュが起きました。
既定の保護(スタックカナリア)で、もう一度
先ほど -fno-stack-protector であえて切った保護を、今度は外さずに(gcc の既定のまま)ビルドします。
gcc -O0 -g -o bof_protected bof.c
./bof_protected
echo "終了コード: $?"
*** stack smashing detected ***: terminated
終了コード: 134
同じ穴のはずなのに、今度は違う壊れ方をしました。*** stack smashing detected ***——これはスタックカナリアという仕組みです。関数の入り口で、戻り先アドレスの手前に予測できない値(カナリア)を1つ置いておき、関数から戻る直前に「その値が変わっていないか」を確認します。strcpy の書き込みがカナリアの位置まで届いていたので、「何かが上書きされた」ことを、戻り先を使う前に検知して、安全に強制終了しました。
これが、多層防御の実例
sec-securecoding で学んだ多層防御を、いま目の前で見ました。
本命の対策——バッファの大きさを確認するコード(strncpy や、長さを渡すバージョンの関数)を書くこと——がここには無い。だから穴は空いたまま。
でも保険——スタックカナリア——が、その穴を「静かに悪用される」ものから「大きな音を立てて安全に落ちる」ものに変えました。
現代のコンパイラと OS には、この他にも ASLR(メモリ配置を毎回ランダムにずらし、狙った場所を当てにくくする)や NXビット(データ領域のメモリを、実行可能な命令として扱わせない)といった、何層もの保険が既定で効いています。あなたがこの章で見た「保護なし版」は、これらの保険が無かった時代に、実際に起きていたことです。
持ち帰る一言
箱の大きさを超えた書き込みは、隣接するメモリを、静かに侵食する。 gdbの生バイト表示が、それを目に見える証拠にしてくれました。現代の防御(スタックカナリア・ASLR・NX)は、この侵食を「気づかれない悪用」から「安全な強制終了」に変える、多層防御の実例です。次はまとめて、アセンブリの世界へ橋を渡します。
こうなっていればOK
卒業まであと1章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。