最近のgccでは、デフォルトでstackの改ざんを防ぐstack-smashing protector(※)
が取り入れられています。(※以下、SSP)
SSPには、バッファオーバーフローを利用したroot権奪取を防ぐ効用があります。
ここでは、簡単なテストプログラムを例にSSPを調査してみることにします。
⬇のプログラムは、単純にローカルバッファに文字列を格納して標準出力するプログラムです。
テストプログラム guard.c
#include <stdio.h>
int main(int argc, char **argv){
char buf[15];
strcpy(buf, "this is a test\n");
printf("%s", buf);
return 0;
}
$gcc -g -o0 -o guard guard.c
でコンパイルを実施、生成された実行ファイルをgdbで解析してみます。
$gdb ./guard
(gdb) disass main
とすれば、ディスアセンブリングされた結果が⬇のように表示されます。
gdb disassembling...
(gdb) disass main
Dump of assembler code for function main:
0x08048434 <+0>: push %ebp
0x08048435 <+1>: mov %esp,%ebp
0x08048437 <+3>: and $0xfffffff0,%esp
0x0804843a <+6>: sub $0x40,%esp
0x0804843d <+9>: mov 0xc(%ebp),%eax
0x08048440 <+12>: mov %eax,0x1c(%esp)
0x08048444 <+16>: mov %gs:0x14,%eax
0x0804844a <+22>: mov %eax,0x3c(%esp)
0x0804844e <+26>: xor %eax,%eax
0x08048450 <+28>: lea 0x2d(%esp),%eax
0x08048454 <+32>: movl $0x73696874,(%eax)
0x0804845a <+38>: movl $0x20736920,0x4(%eax)
0x08048461 <+45>: movl $0x65742061,0x8(%eax)
0x08048468 <+52>: movl $0xa7473,0xc(%eax)
0x0804846f <+59>: mov $0x8048570,%eax
0x08048474 <+64>: lea 0x2d(%esp),%edx
0x08048478 <+68>: mov %edx,0x4(%esp)
0x0804847c <+72>: mov %eax,(%esp)
0x0804847f <+75>: call 0x8048340 <printf@plt>
0x08048484 <+80>: mov $0x0,%eax
0x08048489 <+85>: mov 0x3c(%esp),%edx
0x0804848d <+89>: xor %gs:0x14,%edx
0x08048494 <+96>: je 0x804849b <main+103>
0x08048496 <+98>: call 0x8048350 <__stack_chk_fail@plt>
0x0804849b <+103>: leave
0x0804849c <+104>: ret
End of assembler dump.
⬆のディスアセンブルの結果のうち、SSPに関係する箇所を抽出してみると、
0x08048444 <+16>: mov %gs:0x14,%eax
0x0804844a <+22>: mov %eax,0x3c(%esp)
0x0804844e <+26>: xor %eax,%eax
となります。
上のアセンブラは...
0x08048444 %gs:0x14が示すアドレスに格納されている、”カナリヤ値”をレジスタ%eaxに格納しています。
ついで、
0x0804844a レジスタ%eaxに格納した"カナリヤ値"を、アドレス%esp+0x3cが示すスタックに格納します。
0x0804844e レジスタ%eaxの値を消去します。
関数終了付近のディスアセンブル結果を抽出してみます。
0x08048489 <+85>: mov 0x3c(%esp),%edx
0x0804848d <+89>: xor %gs:0x14,%edx
0x08048494 <+96>: je 0x804849b <main+103>
0x08048496 <+98>: call 0x8048350 <__stack_chk_fail@plt>
上のアセンブラは...
0x08048489 スタック上に格納した"カナリヤ値"を、レジスタ%edxへ格納します。
0x0804848d 元々%gs:0x14に格納されていた"カナリヤ値"と比較します。
0x08048494 オリジナルのカナリヤ値と、スタックの"カナリヤ値"が等しければ(=改ざんされてなければ)
アドレス0x0804849bへジャンプ(=プログラム退場処理へ)
0x08048496 上で、"カナリヤ値"が等しくなけば、__stack_chk_fail関数をコールします。
この関数は、glibcに定義されている関数です。
じゃあ、実際にスタックの"カナリヤ値"を改ざんしてみましょう。簡単です。
コンパイルするプログラム中、
strcpy(buf, "this is a test\n");
とありますが、buf[15]にコピーする文字列を15文字以上にしてあげれば、"カナリヤ値"を改ざん出来ます。
こいつをまた、コンパイルして実行すると、
*** stack smashing detected ***: ./guard terminated
というエラーとともにプログラムがコアを吐いて終了します。
つまり、バッファオーバーフローを利用したroot権奪取を防ぐことが出来るわけです。
ちなみにこのカナリヤ値、実行の度にランダムに設定されます。当然ですね。
なぜ”カナリヤ値”というかというと、多分ですけど、
昔、炭坑内で空気の異常をいち早く察知し、坑夫に危険を知らせる役目を”カナリヤ”が
担っていたところから来てるのかなー?と想像してみたり。
ちなみにgccのコンパイルオプションに、-fno-stack-protectorを指定すれば、
SSPをキャンセル出来るのです。そのディスアセンブリング結果は⬇のようになります。
カナリヤはもう居ませんね?
disassembling カナリヤは居ない
(gdb) disass main
Dump of assembler code for function main:
0x080483e4 <+0>: push %ebp
0x080483e5 <+1>: mov %esp,%ebp
0x080483e7 <+3>: and $0xfffffff0,%esp
0x080483ea <+6>: sub $0x20,%esp
0x080483ed <+9>: lea 0x11(%esp),%eax
0x080483f1 <+13>: movl $0x73696874,(%eax)
0x080483f7 <+19>: movl $0x20736920,0x4(%eax)
0x080483fe <+26>: movl $0x65742061,0x8(%eax)
0x08048405 <+33>: movl $0xa7473,0xc(%eax)
0x0804840c <+40>: mov $0x8048500,%eax
0x08048411 <+45>: lea 0x11(%esp),%edx
0x08048415 <+49>: mov %edx,0x4(%esp)
0x08048419 <+53>: mov %eax,(%esp)
0x0804841c <+56>: call 0x8048300 <printf@plt>
0x08048421 <+61>: mov $0x0,%eax
0x08048426 <+66>: leave
0x08048427 <+67>: ret
End of assembler dump.