シグナルハンドラの制約
UNIXなどPOSIX準拠のOSでは、割り込みや例外を抽象化した「シグナル」と呼ばれる仕組みを用いてプロセスに(非)同期イベントが通知されます。ユーザが[Ctrl]-[C]キーを押してプログラムを中断しようとしたり(SIGINT)、整数オーバーフローが発生したり(SIGFPE)すると、それらのイベントに対応するシグナルがカーネルからプロセスに対して通知されるのです。プログラマは、これらのシグナルを受信した時に特定の動作を行わせる「シグナルハンドラ」を書くことができます。しかし、シグナルハンドラで行える処理には制約があり、これを無視したコードを書くと脆弱性につながる恐れがあります。
今回はシグナルハンドラの制約に関するルールを見てみましょう。
結論から言うと、シグナルハンドラ内で安全に(未定義の動作にならずに)行える処理は以下に限られます。
- 非同期シグナル安全(async-signal-safe)な関数の呼び出し
- volatile sig_atomic_t型変数の読み書き
- シグナルハンドラ内の自動変数の読み書き
ではコード例を見ながら解説しましょう。次のコードには複数の問題が存在するため、実行環境によってはプログラマの意図せぬ動作をする恐れがあります。
enum { MAXLINE = 1024 }; char *info = NULL; void handler(int signum) { fprintf(stderr, info); free(info); info = NULL; } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { /* エラー処理 */ } info = (char*)malloc(MAXLINE); if (info == NULL) { /* エラー処理 */ } while (1) { fprintf(stderr, info); } free(info) return 0; }
非同期シグナル安全な関数のみ呼び出す
シグナルハンドラから fprintf()を呼び出すのは危険です。fprintf()が非同期シグナル安全な関数ではないからですが、なぜ非同期シグナル安全でない関数を呼び出すのが問題になるのかは後述します。
シグナルハンドラからfree()を呼び出すのも危険です。main()でfree()を呼び出している最中に割り込みシグナルを受信した場合、ヒープ領域を管理するデータ構造が破壊されるかもしれません。さらに、main()のfree()直後でシグナルを受信すると、二重解放の脆弱性を引き起こす可能性もあります。
また、volatile sig_atomic_t型でない変数infoをシグナルハンドラが読み取る処理も危険です。
以下の修正コードでは、シグナルハンドラ内では sig_atomic_t型変数への代入しか行わないようにすることで、上述の問題を回避しています。
enum { MAXLINE = 1024 }; volatile sig_atomic_t eflag = 0; char *info = NULL; void handler(int signum) { eflag = 1; } int main(void) { if (signal(SIGINT, handler) == SIG_ERR) { /* エラー処理 */ } info = (char*)malloc(MAXLINE); if (info == NULL) { /* エラー処理 */ } while (!eflag) { fprintf(stderr, info); } fprintf(stderr, info); free(info); info = NULL; return 0; }
シグナルハンドラ内で非同期シグナル安全でないライブラリ関数などを呼び出すと、なぜ脆弱性の原因となるのか。その理由としては、(a)これらの関数が内部でグローバル変数など静的データ構造を利用するから、(b)mallocやfreeを内部で呼び出すから、(c)標準入出力関数の一部であるから、などが挙げられます。malloc()はその代表格です。malloc()は割り当て済みヒープ領域をリンクリストで管理しており、そのリストの書き換えの最中にシグナルハンドラからmalloc()を呼び出すと、リンクリストに対するアトミックでない変更が生じてしまい、結果としてプログラムの動作が予測不能になる可能性があります。printf()系関数も内部でmalloc()を呼び出す実装になっていることがあり、同様のリスクがあります。
非同期シグナル安全な関数の一覧は sigaction(2)のマニュアルなどに掲載されています。このうち、主なものについて以下の表にまとめました。
_Exit() | _exit() | abort() | accept() |
access() | aio_error() | aio_return() | aio_suspend() |
alarm() | bind() | cfgetispeed() | cfgetospeed() |
cfsetispeed() | cfsetospeed() | chdir() | chmod() |
chown() | clock_gettime() | close() | connect() |
creat() | dup() | dup2() | execle() |
execve() | fchmod() | fchown() | fcntl() |
fdatasync() | fork() | fpathconf() | fstat() |
fsync() | ftruncate() | getegid() | geteuid() |
getgid() | getgroups() | getpeername() | getpgrp() |
getpid() | getppid() | getsockname() | getsockopt() |
getuid() | kill() | link() | listen() |
lseek() | lstat() | mkdir() | mkfifo() |
open() | pathconf() | pause() | pipe() |
poll() | posix_trace_event() | pselect() | raise() |
read() | readlink() | recv() | recvfrom() |
recvmsg() | rename() | rmdir() | select() |
このように、シグナルハンドラの制約は見落としがちなので注意が必要です。
Sendmailの脆弱なシグナルハンドラの実装を突き、ヒープメモリの管理データ構造を悪用することで攻撃を成立させる仕組みについて、参考情報にあげたMichal Zalewski氏の論文に詳しい解説があります。興味のある方はそちらも併せてご参照ください