Logo address

オープン FD とセキュリティ

目次

最近 Russ Cox による Plan 9 のセキュリティ上のバグが 9fans にアナウンスされた*。これは cron が dev/caphash をオープンした時のファイルを閉じないまま、そのディスクリプタを子プロセスに渡していることから発生するセキュリティ上の危険性を問題にしている。ここではこれを、もう少し補足しよう。

注*: [9fans] security vulnerability - cron and /dev/caphash (2007年5月17日)

オープンモード

Plan 9 には、ファイルのオープン時に指定できる、セキュリティ上の問題点を回避するのに便利なオプションが 2 つ存在する。ORCLOSE と OCEXEC である。

ORCLOSE

close されたら削除する
この有り難みはよくわかる。
一時ファイルを作って、作業中に異常終了した場合のことを考えてみたらよい。
その時、作成された一時ファイルが自動的に削除されるのだから。

OCEXEC

いわゆる close-on-exec のオプションで、exec または execl が実行されたときにファイルを閉じる

Plan9 の場合には

	fd = open("foo", OREAD|OCEXEC);
	...
	execl(....)

UNIX の場合には

	fd = open("foo", O_RDONLY)
	fcntl (fd, F_SETFD, FD_CLOEXEC)
	....
	execl(....)
である。

これは何を意図しているか?
明示的に

	fd = open(....);
	...
	close(fd);
	exec(...);
で構わないはずだが、あえて OCEXEC を導入するのは、fd 問題で発生するセキュリティホールが多いからか?

mount

mount(2) には mount() が使用する fd について
The file descriptor fd is automatically closed by a successful mount call.
すなわち
	fd = open("/srv/foo",ORDWR)
	mount(fd,...)
で自動的に fd が close される。
まあ、確かに fd はマウントが完了すれば用なしだから。しかし mount が失敗したらどうなるのだろう? fd は close されないことになるが...

BUGS
Mount will not return until it has successfully attached to
the file server, so the process doing a mount cannot be the
one serving.

オープン FD の検査

ファイルディスクリプタ fd を close しないまま、exec を実行すると、その fd が子プロセスに渡る。これをオープン fd と言う。子プロセスはオープン fd のファイルを自由に操る事ができる。オープン fd はプロセスのオーナーが変わるケースでセキュリティ問題に発展する。

筆者の su のように、 su を実行するユーザと、それによって生成されたプロセスを操るユーザが同一人物であれば問題は発生しない。しかし、他のユーザが操るような場合には問題が発生する。このようなニーズはホストオーナーによって生成されるサービスプロセスで発生する。この代表的なケースが telnetd や、 cron や、筆者の mon である。

アクセス制御を掛けなくてはならないファイルのオープン fd はセキュリティを破る。特に

	open("/dev/caphash",...)

	open("/mnt/factotum/ctl",...)
は大きな問題をもたらすだろう。

一般的に言えば、uid が変わった場合、子プロセスに引き継がせても良い fd は 0,1,2 だけである。

Plan 9 の場合にはオープン fd は容易に検証できる。子プロセスから

	ls /fd
を実行してみればよい。
term% ls /fd
/fd/0
/fd/0ctl
/fd/1
/fd/1ctl
/fd/2
/fd/2ctl
/fd/3
/fd/3ctl
term%
この場合はオープン fd は 0,1,2 だけである。最後の 3 は ls が開いた fd であり、親プロセスのものではない。実際 fd=2 は
term% cat /fd/2ctl
  2 w  M 1892 (0000000000000001 0 00)  8192    25834 /dev/cons
term%
のように、その実体は /dev/cons であるが fd=3 は存在しないことが分かる。
term% cat /fd/3ctl
cat: can't open /fd/3ctl: '/fd/3ctl' file does not exist
term%

他方

	ls -l /fd
の場合には次のようになる。
term% ls -l /fd
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/0
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/0ctl
---w------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/1
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/1ctl
---w------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/2
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/2ctl
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/3
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/3ctl
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/4
--r-------- d 0 arisawa arisawa 0 Aug 10  2005 /fd/4ctl
term% cat /fd/3ctl
cat: can't open /fd/3ctl: '/fd/3ctl' file does not exist
term% cat /fd/4ctl
cat: can't open /fd/4ctl: '/fd/4ctl' file does not exist
term%
今度は 3 と 4 が余計に現れたのは
	ls -l /fd
によってディレクトリ /fd のオープンと、その中に存在する個々のファイルが順にオープン、クローズされて行くからである。

次に筆者の su を見る。

term% su
su# ls /fd
/fd/0
/fd/0ctl
/fd/1
/fd/1ctl
/fd/2
/fd/2ctl
/fd/3
/fd/3ctl
/fd/4
/fd/4ctl
su# cat /fd/3ctl
  3 rw M   11 (0000000000000007 0 00)  8192        0 /mnt/factotum/ctl
su# cat /fd/4ctl
cat: can't open /fd/4ctl: '/fd/4ctl' file does not exist
su#
つまり su は factotum を開きっぱなしにして子プロセスに渡している。su はサービス用に作成されたものではないので、通常の使い方をしている限りセキュリティ上の問題にはならないが、好ましくはないであろう。サービス用に使用される可能性があるからだ。su の最新版はこの問題点は潰されている。

Pegasus で使用されている筆者の mon はどうであろうか?

term% mon rc
term% ps
none           7087    0:00   0:00      188K Pread    ps
term% ls /fd
/fd/0
/fd/0ctl
/fd/1
/fd/1ctl
/fd/2
/fd/2ctl
/fd/3
/fd/3ctl
term% cat /fd/2ctl
  2 w  M 1892 (0000000000000001 0 00)  8192    27050 /dev/cons
term% cat /fd/3ctl
cat: can't open /fd/3ctl: '/fd/3ctl' file does not exist
term%
OK である。

rfork(RFCFDG)

マニュアル rfork(2) には RFCFDG について、
If set, the new process starts with a clean file descriptor table
とある。RFCFDG はファイル記述子のテーブルを完全にクリアするので、かなり特殊な使い方である。
fd の 0,1,2 が必要であれば、明示的に開かなくてはならない。例えば
	if((rfork(RFPROC|RFCFDG|RFENVG|RFNAMEG))==0){
		putenv("prompt", "#: ");
		fd = open("/dev/cons", ORDWR);
		dup(fd,0);
		dup(fd,1);
		dup(fd,2);
		execl("/bin/rc","rc",nil);
	}
	waitpid();
のようにする。

文献

オープン fd のセキュリティに関する URL がネットにあったので、ついでに紹介する。