スタックプロテクターは alloca 割り当てを使うプログラムも守ってくれるか?
先だって同僚からそんな疑問を受け、気になっていたので調べた。結論から言うと、守ってくれる。
そもそもスタックプロテクターは、関数呼び出し時にスタックフレーム内の戻りアドレスの直上にカナリア変数を置き、呼び出し元に返る前にカナリア変数がオーバーフローによって破壊されていないことを確かめるという実装だから(つまりカナリア変数が壊れていたら戻りアドレスも壊されているだろうから、そこへ ret してはいけないと判断する)、スタックが可変長だろうと関係なく守ってくれるっていうわけ。
というわけでサンプルコード。
/* stack-protector.c */ #include <stdlib.h> /* for alloca */ #include <memory.h> /* for memset */ void f() { memset(alloca(1), 0x90, 56); } int main(void) { return f(), 0; }
コンパイルして実行すると確かに処理を中断してくれる:
$ gcc -O2 -fstack-protector -o stack-protector stack-protector.c $ ./stack-protector *** stack smashing detected ***: stack-protector terminated ^@Illegal instruction $
アセンブルリストを眺めてみよう (gcc -O2 -fstack-protector -S stack-protector.c で生成):
.file "stack-protector.c" .text .p2align 4,,15 .globl f .type f, @function f: pushl %ebp movl $56, %ecx movl %esp, %ebp subl $56, %esp movl __stack_chk_guard, %eax movl %eax, -4(%ebp) xorl %eax, %eax leal 27(%esp), %eax movl $144, %edx andl $-16, %eax movl %ecx, 8(%esp) movl %edx, 4(%esp) movl %eax, (%esp) call memset movl -4(%ebp), %eax xorl __stack_chk_guard, %eax jne .L5 leave ret .L5: .p2align 4,,6 call __stack_chk_fail .size f, .-f .p2align 4,,15 .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp call f xorl %eax, %eax popl %edx popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)" .section .note.GNU-stack,"",@progbits
関数ラベル f 以降の 5, 6 行目で eax レジスターを経由して __stack_chk_guard 変数の値 (カナリア) をフレームポインター (ebp レジスターに格納されている値) から -4 バイトオフセットしたアドレスに格納している。
その後引数などを設定して 14 行目で memset を呼びだす。それから 15 行目でフレームポインターの隣のアドレスに退避したカナリア値を eax レジスターに取り出し、もとのカナリア値と xor する。
xor は同値確認の常套手段で、同じ値で xor をとると 0 になる性質を利用するもの。 17 行目はゼロフラグによる条件ジャンプ命令。直前の xor 命令でゼロになっていればゼロフラグがたつため leave ret で呼び出し元に戻り、そうでなければ __stack_chk_fail 関数を呼び出してアボート処理に移る。
gdb で実際に何が起きているか確認してみよう。
※gdb でソースと対応をとるためには -g オプションをつけてコンパイルすること。
$ gdb stack-protector ... (gdb) start Breakpoint 1 at 0x8048491: file stack-protector.c, line 11. Starting program: /misc/cf-users/i/ik/ikb/code/stack-protector main () at stack-protector.c:11
プログラムカウンターが指しているのは関数ラベル f の呼び出し。
(gdb) x/2i $pc 0x8048491 <main+17>: call 0x8048430 <f> 0x8048496 <main+22>: xor %eax,%eax
ステップ実行して f の逆アセンブルリストを出してみる。
(gdb) si f () at stack-protector.c:5 (gdb) disas Dump of assembler code for function f: 0x08048430 <f+0>: push %ebp 0x08048431 <f+1>: mov $0x38,%ecx 0x08048436 <f+6>: mov %esp,%ebp 0x08048438 <f+8>: sub $0x38,%esp 0x0804843b <f+11>: mov 0x80496bc,%eax 0x08048440 <f+16>: mov %eax,0xfffffffc(%ebp) 0x08048443 <f+19>: xor %eax,%eax 0x08048445 <f+21>: lea 0x1b(%esp),%eax 0x08048449 <f+25>: mov $0x90,%edx 0x0804844e <f+30>: and $0xfffffff0,%eax 0x08048451 <f+33>: mov %ecx,0x8(%esp) 0x08048455 <f+37>: mov %edx,0x4(%esp) 0x08048459 <f+41>: mov %eax,(%esp) 0x0804845c <f+44>: call 0x8048350 <memset@plt> 0x08048461 <f+49>: mov 0xfffffffc(%ebp),%eax 0x08048464 <f+52>: xor 0x80496bc,%eax 0x0804846a <f+58>: jne 0x804846e <f+62> 0x0804846c <f+60>: leave 0x0804846d <f+61>: ret 0x0804846e <f+62>: mov %esi,%esi 0x08048470 <f+64>: call 0x8048360 <__stack_chk_fail@plt> End of assembler dump.
カナリア値を設定しているのは
(gdb) x/x 0x80496bc 0x80496bc <__stack_chk_guard@@LIBSSP_1.0>: 0xe9563252
6 行目までステップ実行して、 ebp レジスターの 4 バイトとなりのアドレスの値を確認する。たしかにカナリア値が退避されている。
(gdb) si 6 (gdb) x/i $pc 0x8048443 <f+19>: xor %eax,%eax (gdb) x/x $ebp - 4 0xbfc4d8e4: 0xe9563252
ここで memset されるアドレス付近の値をダンプしてみよう。
(gdb) si 4 (gdb) x/20w $eax - 16 0xbfc4d8b0: 0x00000007 0x080496bc 0x00000004 0xb7e9bf1e 0xbfc4d8c0: 0xb7f3baf9 0xb7f65ff4 0x00000004 0x080483b0 0xbfc4d8d0: 0xb7f6c500 0x08049694 0xbfc4d8e8 0x0804832d 0xbfc4d8e0: 0xb7f8cff4 0xe9563252 0xbfc4d8f8 0x08048496 0xbfc4d8f0: 0xb7e43c8c 0xbfc4d910 0xbfc4d958 0xb7e4dea8
それから C ソースでの実行単位で 1 行分処理を進めて memset の呼び出しを完了し、もう一度付近の値をダンプする。
たしかに 0x90 で埋まっている。
(gdb) s (gdb) x/20w $eax - 16 0xbfc4d8b0: 0xbfc4d8c0 0x00000090 0x00000038 0xb7e9bf1e 0xbfc4d8c0: 0x90909090 0x90909090 0x90909090 0x90909090 0xbfc4d8d0: 0x90909090 0x90909090 0x90909090 0x90909090 0xbfc4d8e0: 0x90909090 0x90909090 0x90909090 0x90909090 0xbfc4d8f0: 0x90909090 0x90909090 0xbfc4d958 0xb7e4dea8
そして ebp - 4 の値を確認すると、たしかに 0x90909090 で埋められていることがわかる (バッファオーバーラン)。
(gdb) x/x $ebp - 4 0xbfc4d8e4: 0x90909090
ここで実行を継続するとスタックスマッシングプロテクターに制御が移り、アボートする。
(gdb) cont Continuing. *** stack smashing detected ***: stack-protector terminated ^@ Program received signal SIGILL, Illegal instruction. 0xb7fd6c65 in ?? () from /usr/lib/libssp.so.0 (gdb) cont Continuing. Program terminated with signal SIGILL, Illegal instruction. The program no longer exists. (gdb)
蛇足ながら、オーバーランする前のダンプを解説:
0xbfc4d8e0: 0xb7f8cff4 0xe9563252 0xbfc4d8f8 0x08048496 ~~~~~~~~~~ ~~~~~~~~~~ ~~~~~~~~~~ canary val frame ptr ret addr
フレームポインターは呼び出しもとの関数のスタックのトップアドレスで、リターンアドレスが続く。 gdb 実行直後で二命令逆アセンブルしておいたけれど、 call の次の命令番地になっていることがわかる。