4. シグナルの実装(オーバーフロー)
4.6 オーバーフロー
プログラムを書き始めた当初は、しょっちゅうバッファオーバーフローを起こすプログラムを忍ばせたものです。C言語はメモリ関係の管理がプログラマに依存しているので、大規模な業務アプリをCで作る場合のメモリ管理の堅牢さは、プロジェクトの成否を占うものでした。つまりC言語はバッファオーバーフローを生み出しやすい言語と言っても過言ではなく、バグが潜んでいるとそこを衝かれて権限を利用されてしまいます。
実際にオーバーフローが発生するとSIGSEGVシグナルが発生します。このシグナルが発生するとデフォルトではプログラムが終了し、コアファイルが作られます。よってSIGSEGVシグナルを捕捉すればよいのですが、実際はそう簡単にいかない落とし穴があります。
なお、オーバーフローには下記の2種類があります。
- バッファオーバーフロー:予約されたメモリ領域を超えて書き込みする事。
- スタックオーバーフロー:関数が実行する際に割り当てられたメモリ領域を超えてメモリを使用する事。
OSがこれらの異常を検知すると、速やかにプロセスに対してSIGSEGVシグナルが発行されます。
4.6.1 バッファオーバーフロー
バッファオーバーフローが発生するとSIGSEGVシグナルが発行されるので、それを捕捉するハンドラを用意すればよいです。ただし、以下の問題点があります。
- SEGVが発生した際、それがバッファオーバーフローによるものなのか判定が必要。
- 速やかに終了する際はexitすればよいが、復帰する場合はどうするか(今までのプログラムはハンドラ内でexitしていません)。
問題点1については、sa_handlerを使用する限り、シグナルイベントしか得られませんから対応できません。さらに、sa_sigactionを使用し他のパラメータからバッファオーバーフローを判定しようとしても、判定できる情報がないので、やはりできません。
問題点2についてですが、そもそもSIGSEGVやSIGBUS等はプログラム内のバグから発生するシグナルです。そのまま終了する分には問題ないのですが、終了しないとオーバーフローを起こした場所に復帰する事になり、ハンドラとの無限ループに陥ります。復帰方法については後述する4.9章で考えることにして、ここでは復帰は考えず、ハンドラ内で終了する仕様にします。
以上から、バッファオーバーフローを検知するサンプルは下記のようになります。
/*
gcc signal_test_bufover.c -o signal_test_bufover -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <signal.h> #define BUF_SIZE 16 void signal_handler( int signum ); int main( ) { char * wrk = malloc( BUF_SIZE ); struct sigaction sa; sa.sa_handler = signal_handler; sigaction( SIGSEGV, &sa, 0 ); while( 1 ) { *wrk ++ = 0xcc; } return 0; } void signal_handler( int signum ) { fprintf( stdout, "sig : [%d]\n", signum ); exit( 1 ); }
尚、SIGSEGVは、sa_sigaction を使用する事で、メモリ使用不正が発生した時のメモリアドレスを取得できます。
下記のプログラム signal_segv_addr.c は不正アドレスに対するオペレーション違反。更に下記のプログラム signal_segv_addr_2.c はメモリに対するパーミッション違反です。
/*
gcc signal_segv_addr.c -o signal_segv_addr -W -Wall -Werror
*/ #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <sys/mman.h> void handler( int signo, siginfo_t * siginfo,void * dummy ); int main( ) { char * p = 0; struct sigaction sa; sa.sa_sigaction = handler; sa.sa_flags = SA_SIGINFO; sigemptyset( &sa.sa_mask ); sigaction( SIGSEGV, &sa, 0 ); p = ( char * )0x1234; *p = 0; return 0; } void handler( int signo, siginfo_t * siginfo,void * dummy ) { ( void )signo; ( void )dummy; fprintf( stdout, "addr = %pH, code = %s\n", siginfo -> si_addr, ( siginfo -> si_code == SEGV_MAPERR ) ? "SEGV_MAPERR" : "SEGV_ACCERR" ); exit( 1 ); }
/*
gcc signal_segv_addr_2.c -o signal_segv_addr_2 -W -Wall -Werror
*/ #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <sys/mman.h> #include <string.h> #include <errno.h> #include <unistd.h> void handler( int signo, siginfo_t * siginfo,void * dummy ); int main( ) { char * p = 0; struct sigaction sa; int pagesize = getpagesize( ); sa.sa_sigaction = handler; sa.sa_flags = SA_SIGINFO; sigemptyset( &sa.sa_mask ); sigaction( SIGSEGV, &sa, 0 ); p = malloc( 1024 + pagesize - 1 ); p = ( char * )(( int )( p + pagesize - 1 ) & ~( pagesize - 1 )); mprotect( p, 1024, PROT_READ ); /* read only */ fprintf( stdout, "addr = %pH\n", p ); p[0x123] = 'a'; /* write!! */ return 0; } void handler( int signo, siginfo_t * siginfo,void * dummy ) { ( void )signo; ( void )dummy; fprintf( stdout, "addr = %pH, code = %s\n", siginfo -> si_addr, ( siginfo -> si_code == SEGV_MAPERR ) ? "SEGV_MAPERR" : "SEGV_ACCERR" ); exit( 1 ); }
4.6.2 スタックオーバーフロー
基本的にバッファオーバーフローと同じ実装なので対応は難しくないのですが、スタックオーバーフロー独特の厄介な問題があります。
一般的にスタックオーバーフローが発生する要因として、再帰呼び出しが多いと思います。下記プログラムを実行してみてください。
/*
gcc signal_test_stackover.c -o signal_test_stackover -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <signal.h> void stackover_func( ); void signal_handler( int signum ); int main( ) { struct sigaction sa; sa.sa_handler = signal_handler; sigaction( SIGSEGV, &sa, 0 ); stackover_func( ); return 0; } void stackover_func( ) { stackover_func( ); } void signal_handler( int signum ) { fprintf( stdout, "sig : [%d]\n", signum ); exit( 1 ); }
実行すると分かると思いますが、ハンドラ内のプリント文が出力されません。どうやらハンドラに入っていないようですが、異常終了メッセージからSIGSEGVシグナルを受け取っていることは確認できます。
これはシグナルハンドラとプロセスのスタックの関係にあります。スタックオーバーフローはその名の通り、プロセスに割り当てられているスタックを食いつぶしたことによって発生します。一方シグナルハンドラも、その処理自体はプロセスのスタックに積まれていますが、上記の通りスタックが食いつぶされた状況ではシグナルハンドラを実行することができません、確保する場所がないためです。結果、シグナルが到着してもハンドラが呼ばれないので、デフォルトの動きとして終了します。
このような場合の対処として代替シグナルスタックを使い、シグナル用に代わりのメモリ領域を用意します。実際にシグナルが発行されると、そのメモリ上でハンドラなどが実行されます。
int sigaltstack(const stack_t *ss, stack_t *oss);
詳細はman sigaltstackで見てもらいたいのですが、要はシグナルスタック用のメモリ領域をヒープから取得する事で、スタックとは別の位置で実行するようにするものです。この機能を使用する事で、スタックオーバーフローが発生した時もシグナルハンドラを実行できるようになります。
/*
gcc signal_test_sigstack.c -o signal_test_sigstack -W -Wall -g
*/ #include <stdio.h> #include <stdlib.h> #include <signal.h> void stackover_func ( ); void signal_handler ( int signum ); int main( ) { stack_t ss; struct sigaction sa; ss.ss_sp = malloc( SIGSTKSZ ); ss.ss_size = SIGSTKSZ; ss.ss_flags = 0; sigaltstack( &ss, 0 ); sa.sa_handler = signal_handler; sa.sa_flags = SA_ONSTACK; sigaction( SIGSEGV, &sa, 0 ); stackover_func( ); return 0; } void stackover_func( ) { stackover_func( ); } void signal_handler( int signum ) { fprintf( stdout, "sig : [%d]\n", signum ); exit( 1 ); }
