はじめに
Advanced Programming in the UNIX Environment (3rd Edition) (以下APUEと呼びます)を読んで疑問に思ったこと、調べたことをメモしていきます。
今回はp.62からの「3.3 open
and openat
Functions」に書かれている。openat
関数がTOCTTOUエラーの対策になっている、という記述について、本文中に十分な解説がなかったため補足していきます。
目次
open
関数とopenat
関数について
書式は以下の通りです。
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);
openat
関数では、第一引数fd
で指定したファイル記述子に対応するディレクトリからの相対パスを第二引数path
に指定できる点がopen
関数と異なります。
ちなみに、本文ではファイルオープンのオプションとして
O_RDONLY
O_WRONLY
O_RDWR
O_EXEC
O_SEARCH
の5つが列挙されていますが、O_EXEC
とO_SEARCH
の2つはあまりサポートされていないようです。手元のmacOSでman 2 open
してもこの2つは出てきませんでした。(The GNU Libraryによれば、O_EXEC
はGNU/Hurdでしかサポートされていないらしい。)
TOCTTOUエラーとは
TOCTTOUとは、"time-of-check-to-time-of-use"の略です。race conditionの一種で、ある資源の状態をチェックしたあと、それを実際に使うまでに生じるタイムラグによって発生するエラーを指します。ファイルI/Oの文脈で使われることが多いようです。
本題:openat関数とTOCTTOUエラーの関連性
本文には、openat
関数が用意されている意味について、複数のスレッド(同じプロセスの下にあるスレッドはカレントディレクトリの情報を共有しているため、単純にopen
を使うとどのスレッドでもそのスレッドが属するプロセスのカレントディレクトリからのパスだと認識されます)がそれぞれ異なるディレクトリのファイルをオープンするような操作を容易にすることに加えて、TOCTTOUエラーを回避することができる、と書かれています。これについて、本文にはTOCTTOUエラーについての説明しかないので、openat
との関連を解説します。
具体例を挙げます。これは記事の最後にある参考文献を参考にしています。多くのUNIXライクなOSには、コマンドラインからメールを送信するsendmail
というコマンドがあります。このコマンドは、まずメールボックスの属性をチェックします。このチェックとは、例えばディレクトリがシンボリックではないか?などが含まれます。もしシンボリックリンクだとすると、リンク先のディレクトリが攻撃者からアクセスしやすい場所にあったときにそこから情報を盗まれたり、意図しないファイルへの書き込みをしてしまったりするためです。このチェックを終えると、コマンドはメールボックスにルートユーザとして新しいメッセージを書き込みます。
このプロセスが2段階の操作からなっていることがわかると思います。すなわち、このプロセスにはTOCTTOUエラーの余地があります。攻撃者は、メールボックスの属性のチェックを終えたあとに、メールボックスを/etc/passwd
へのシンボリックリンクに変更することができるかもしれません。もしこの攻撃が成功すると、メッセージは/etc/passwd
に書き込まれてしまい、攻撃者から見れば、許可されていないはずの/etc/passwd
というファイルへのアクセスが達成できてしまったことになります。
これがTOCTTOUエラーを利用した攻撃の一例です。次に、openat
関数がどのようにしてこの攻撃を防ぐかを見ていきます。
この攻撃を防ぐには、まずメールボックスのあるディレクトリをプログラム内でオープンしておき、そのファイル記述子をopenat
関数の引数として与えればよいです。こうすると、例えば攻撃者が別のプログラムを用いてメールボックスを削除し、それを別のディレクトリへのシンボリックリンクで置き換えるような操作を実行しても、openat
関数の引数として指定されたファイル記述子はもとの(ここでは削除された)ディレクトリを指し続けています。このときopenat
関数は失敗します。(つまり、意図しない書き込みは行われません。)このようにopenat
関数を使うと、TOCTTOUエラーを利用した攻撃を防ぐことができます。
最後にサンプルコードを示します。
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUFSIZE 4096
int main() {
int fd_d, fd_f;
// あらかじめディレクトリをオープンしておく
if ((fd_d = open("tocttou_test", O_DIRECTORY)) < 0) {
perror("open");
exit(1);
}
/* 攻撃者が別のプログラムから行う操作
if ((rmdir("tocttou_test")) < 0) {
perror("rmdir");
exit(1);
}
if ((symlink("/etc", "tocttou_test")) < 0) {
perror("symlink");
exit(1);
}
*/
//ファイルオープン
if ((fd_f = openat(fd_d, "mail.txt", O_RDWR|O_CREAT|O_APPEND, S_IRWXU)) < 0) { //もし別のプログラムによって"tocttou_test"が削除されていれば、openatは失敗する
perror("openat");
exit(1);
}
char buf[BUFSIZE] = "test message!\n";
if ((write(fd_f, buf, strlen(buf))) < strlen(buf)) {
perror("write");
exit(1);
}
close(fd_d);
close(fd_f);
exit(0);
}