HOME情報セキュリティ資料・報告書・出版物調査・研究報告書情報セキュリティ技術動向調査(2011 年下期)

本文を印刷する

情報セキュリティ

情報セキュリティ技術動向調査(2011 年下期)

1. capsicum

面 和毅

1.1. 概要

 FreeBSD9.0からは、新たなセキュリティ機構としてケーパビリティを利用するcapsicumが実装された。ここでは、一般的なケーパビリティと、capsicumの使用方法を説明する。

1.2. capabilityについて(POSIXケーパビリティとLinuxでの実装)

 通常Unixでは、プロセスは一般ユーザの権限で動くか、特権(root権限)で動くかの2種類となる。たとえば、Apacheなどのサービスが1024番未満のいわゆる「特権ポート」をプロセスで使用する際や、pingやsnortなどのように生(raw)ソケットをイーサネットデバイスに対してオープンし、生のIPデータトラフィックを見る時、あるいはntpdなどでシステムの時刻を設定する際には、プロセスに特権が必要になる。
 しかし、プロセスに特権をすべて与えてしまうと、動作しているプロセスに脆弱性があった場合に、不正な操作ですべての特権が取られてしまう可能性がある。
 この問題は随分前から指摘されており、これを解決する方法として「ケーパビリティ(POSIXケーパビリティ)」という提案がPOSIXのドラフト1003.1eとして提出されていた[1]
 これは、特権を更に細分化した「ケーパビリティ」と呼ばれる単位で取り扱う事ができるようにし、プロセスに必要最小限のケーパビリティを与えて、必要な処理だけを行わせようというものになる。これにより、プロセスに脆弱性が発見されて悪用されたとしても、そのプロセスに必要な最小限のケーパビリティしか悪用されないため、被害を局所化(コンパートメント化)できるようになる。
 このPOSIXケーパビリティは、Linux上では「Linuxカーネルケーパビリティ」としてカーネル2.4から採用されている。
 最新のカーネル3.2でのPOSIXケーパビリティを次に示す。


LinuxにおけるCapability

  • CAP_CHOWN
  • CAP_DAC_OVERRIDE
  • CAP_DAC_READ_SEARCH
  • CAP_FOWNER
  • CAP_FSETID
  • CAP_KILL
  • CAP_SETGID
  • CAP_SETUID
  • CAP_SETPCAP
  • CAP_LINUX_IMMUTABLE
  • CAP_NET_BIND_SERVICE
  • CAP_NET_BROADCAST
  • CAP_NET_ADMIN
  • CAP_NET_RAW
  • CAP_IPC_LOCK
  • CAP_IPC_OWNER
  • CAP_SYS_MODULE
  • CAP_SYS_RAWIO
  • CAP_SYS_CHROOT
  • CAP_SYS_PTRACE
  • CAP_SYS_PACCT
  • CAP_SYS_ADMIN
  • CAP_SYS_BOOT
  • CAP_SYS_NICE
  • CAP_SYS_RESOURCE
  • CAP_SYS_TIME
  • CAP_SYS_TTY_CONFIG
  • CAP_MKNOD
  • CAP_LEASE
  • CAP_AUDIT_WRITE
  • CAP_AUDIT_CONTROL
  • CAP_SETFCAP
  • CAP_MAC_OVERRIDE
  • CAP_MAC_ADMIN
  • CAP_SYSLOG
  • CAP_WAKE_ALARM

 それぞれのケーパビリティに関する詳しい説明は、Linuxカーネルソース内の/usr/src/linux/include/capability.hファイルに、コメントとして説明が載っている。
 Linuxカーネルケーパビリティを前提にしたプログラムは、既にいくつもリリースされている。 例えば、DNSの実装で有名なbindはソースコード中で、OSがLinuxだった場合に、プロセスのケーパビリティを一度初期化してから、必要最低限のCAP_NET_BIND_SERVICE(ポートバインディングに必要)や、CAP_SYS_CHROOT(chroot化するのに必要)を付加している(コード1)。

コード1
---bin/named/unix/os.c---

#ifdef HAVE_LIBCAP
        cap_t curcaps;
        cap_value_t capval;
        char strbuf[ISC_STRERRORSIZE];
        int err;
#endif

        /*%
         * We don't need most privileges, so we drop them right away.
         * Later on linux_minprivs() will be called, which will drop our
         * capabilities to the minimum needed to run the server.
         */
        INIT_CAP;

        /*
         * We need to be able to bind() to privileged ports, notably port 53!
         */
        SET_CAP(CAP_NET_BIND_SERVICE);

        /*
         * We need chroot() initially too.
         */
        SET_CAP(CAP_SYS_CHROOT);

 これにより、Linux上でケーパビリティによる制御を有効にしている場合には、bindにセキュリティホールが見つかり悪用されたとしても、攻撃者はすべての特権ではなく一部のケーパビリティ(CAP_NET_BIND_SERVICEやCAP_SYS_CHROOT)しか取得できないため、被害を局所化することが可能になっている。

1.3. capsicumについて

(1) capsicumとFreeBSD 9.0

 一方、FreeBSDでは、FreeBSD 9.0から、capsicumと呼ばれるケーパビリティ制御機構が実装された。capsicumは、LinuxでのCapabilityと違い、ソースコードレベルでのCapabilityの操作を主体に考えた実装となっており、各プログラムのソースコード中で、プログラムが使用するファイルディスクリプタをすべて用意した後に、そのプログラムを「ケーパビリティモード」と呼ばれる状態に移行し、それ以降はそのプログラムがディスクリプタを新たに作成できないようにする事で、そのプログラム上で想定外のアクセスによる不正アクセスが発生した場合でも、使用できるリソースを規制するというものである。
 capsicumによって提供されるセキュリティは、ユーザ側で何か変更をするというものではなく、プログラム提供者がcapsicumの機能を利用するようにソースコードを変更していくというものになる。そのため、ユーザ側にとっては、特に使い勝手が変わることなく、今までよりも高セキュリティになったシステムを使用できる事になる。
 一方開発者側にとっても、capsicum対応にはロジック変更が必要無いため、実装することはそれほど難しくない。
 実際、FreeBSD開発者会議ではすでに、capsicumをどのライブラリやコマンドに適用させるかの議論まで進んでおり、google chromeのオープンソース版であるchromiumなどでは既に採用されている。


(2) capsicumを有効にする

 9.0-Releaseの時点では、capsicumを使用するにはカーネルを再構築する必要がある。このためには、/sys/[アーキテクチャ]/conf/以下にCAPSというファイルをリスト2のように作成し、リスト3の手順でCAPSファイルを含んだカーネルを構築/インストールして再起動する。

リスト2
include GENERIC

ident           CAPS

# enable Capsicum
options         CAPABILITIES
options         CAPABILITY_MO

リスト3
cd /usr/src/
make KERNCONF=CAPS buildkernel
make KERNCONF=CAPS installkernel

(3) capsicumの使い方

 capsicumでは、ファイルディスクリプタを介してケーパビリティ(許可する操作を設定した、特別なファイルディスクリプタ)を作成する。この際に使用できるケーパビリティには、次のようなものがある。


capsicumにおけるcapability

  • CAP_READ
  • CAP_WRITE
  • CAP_MMAP
  • CAP_MAPEXEC
  • CAP_FEXECVE
  • CAP_FSYNC
  • CAP_FTRUNCATE
  • CAP_SEEK
  • CAP_FCHFLAGS
  • CAP_FCHDIR
  • CAP_FCHMOD
  • CAP_FCHOWN
  • CAP_FCNTL
  • CAP_FPATHCONF
  • CAP_FLOCK
  • CAP_FSCK
  • CAP_FSTAT
  • CAP_F CAP_EXTATTR_DELETE
  • CAP_EXTATTR_GET
  • CAP_EXTATTR_LIST
  • CAP_EXTATTR_SET
  • CAP_ACL_CHECK
  • CAP_ACL_DELETE
  • CAP_ACL_GET
  • STATFS
  • CAP_FUTIMES
  • CAP_CREATE
  • CAP_DELETE
  • CAP_MKDIR
  • CAP_RMDIR
  • CAP_MKFIFO
  • CAP_LOOKUPCAP_ACL_SET
  • CAP_ACCEPT
  • CAP_BIND
  • CAP_CONNECT
  • CAP_GETPEERNAME
  • CAP_GETSOCKNAME
  • CAP_GETSOCKOPT
  • CAP_LISTEN
  • CAP_PEELOFF
  • CAP_SETSOCKOPT
  • CAP_SHUTDOWN
  • CAP_SOCK_ALL
  • CAP_MAC_GET
  • CAP_MAC_SET
  • CAP_SEM_GETVALUE
  • CAP_SEM_POST
  • CAP_SEM_WAIT
  • CAP_POLL_EVENT
  • CAP_POST_EVENT
  • CAP_IOCTL
  • CAP_TTYHOOK
  • CAP_PDGETPID
  • CAP_PDWAIT
  • CAP_PDKILL
  • CAP_MASK_VALID

 capsicumが有効になったシステムでは、各プログラム中でcap_enter()を呼び出すと、プログラムはケーパビリティモードと呼ばれるモードに移行される。
このケーパビリティモードでは次のような制限が加わる 。

  1. 新たなファイルディスクリプタを取得することができなくなる。
  2. 図1に示すような、グローバルなファイルシステムにアクセスできなくなる。

  3. 図1:グローバル ネームスペース


  4. 既に作成されたファイルディスクリプタを介して与えられたケーパビリティをベースにして、新たに別のケーパビリティを作成できる。
  5. 3.の新たなケーパビリティは、権限を拡大することはできず、縮小することのみが可能である。
  6. プログラムがケーパビリティモードに入っている際にforkされたプロセスはケーパビリティモードに入っており、ケーパビリティが引き継がれる。

 プログラムがこのケーパビリティモード中で使用できるケーパビリティを定義するため、capsicumでは、cap_new()を使用する。
 例として、サンプルプログラム1を見てみよう 。


サンプルプログラム1
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>
#include <errno.h>
#include <sysexits.h>
#include <sys/capability.h>

main(void)
{
	int fd1, fd2, ret, len, cap1, cap2;
	char insert_txt[]="write_test";

	char content1[BUFSIZ];
	char content2[BUFSIZ];

	fd1 = open("READ_WRITE", O_RDWR);
	if (-1 == fd1)
		err(EX_NOPERM, "open error: %d", errno);

	fd2 = open("READ_ONLY", O_RDWR);
	if (-1 == fd2)
		err(EX_NOPERM, "open error: %d", errno);

	// add capability
	cap1 = cap_new(dup(fd1), CAP_READ | CAP_WRITE | CAP_SEEK ); // ----a.
	if (cap1 == -1)
		err(EX_NOPERM, "cap_new(1) error: %d", errno);
	close(fd1); // -----b. 

	cap2 = cap_new(dup(fd2), CAP_READ | CAP_SEEK );
	if (cap2 == -1)
		err(EX_NOPERM, "cap_new(1) error: %d", errno);
	close(fd2);

	// enter capability mode 
	cap_enter();

 	ret = write(cap1, insert_txt, 10); // --- c.
	if (-1 == ret)
		err(EX_NOPERM, "read error: %d", errno);
 	len = read(cap1, content1, BUFSIZ);
	if (-1 == len)
		err(EX_NOPERM, "read error: %d", errno);

	printf("%s\n", content1);

 	ret = write(cap2, insert_txt, 10); // --- d.
	if (-1 == ret)                     // --- d.
		err(EX_NOPERM, "read error: %d", errno); // --- d.

 	len = read(cap2, content2, BUFSIZ);
	if (-1 == len)
		err(EX_NOPERM, "read error: %d", errno);

	printf("%s\n", content2);

	return;
}

 まずプログラム中のa.のように、取得済みのファイルディスクリプタに対して、どのケーパビリティを使用することが出きるかを宣言しておく。この際には、cap_new(2)ではdup()で複製したファイルディスクリプタを用いて、ケーパビリティの宣言をするのが通例である。ケーパビリティ宣言後は、ケーパビリティが宣言されているファイルディスクリプタを使用するため、元のファイルディスクリプタは使用しないので、b.のようにclose()しておく。
 cap_enter()によってケーパビリティモードに移行した後は、こうしてケーパビリティを与えられたファイルディスクリプタに対してc.のようにread/writeなどの処理を行う。
この際に、CAP_WRITEが与えられていないファイルディスクリプタ(cap2)に対して、d.のようにwriteの処理を行うと、次のように「ケーパビリティが足りない」というエラーが出力される。


> gcc -o sample3 sample3.c
> ./sample3
sample3: read error: 93: Capabilities insufficient

 d.の箇所を削除して再度コンパイルして実行すると、次のように処理が成功する。


> ls -l READ_*
-rw-r--r--  1 omok  omok  29 Feb  6 00:56 READ_ONLY
-rw-r--r--  1 omok  omok   0 Feb  6 00:56 READ_WRITE
> more READ_WRITE 

> more READ_ONLY 
This is read-only test file.

> gcc -o sample3 sample3.c
> ./sample3

This is read-only test file.

> more READ_WRITE 
write_test
> more READ_ONLY 
This is read-only test file.
> ls -l READ_*
-rw-r--r--  1 omok  omok  29 Feb  6 00:56 READ_ONLY
-rw-r--r--  1 omok  omok  10 Feb  6 00:57 READ_WRITE

(4) capsicumを使用した例

 最後に、capsicumを使用した際に攻撃されるとどうなるかの例を、簡単にみてみよう。
サンプルとして、バッファーオーバーフローを起こす脆弱性のあるプログラムを作成する(サンプルプログラム2)。


サンプルプログラム2
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <err.h>
#include <errno.h>
#include <sysexits.h>
#include <sys/capability.h>

void copy_to_buff(char *input)
{
	char buf[8]; 

	strcpy(buf, input); 
}

void main(int argc, char *argv[])
{
	// enter capability mode
	cap_enter();

	if (argc > 1) {
		copy_to_buff(argv[1]);
		printf("Input %s\n", argv[1]);
	}
}

 プログラムの所有者はrootにし、さらにSticky bitを立てておく(通常ありえない設定であるが、あくまで攻撃のサンプルのため)。この状態で、攻撃用のコードを一般ユーザomokで実行すると、バッファーオーバーフローの脆弱性を使用して、rootユーザでシェルが開かれてしまう。


> ./sample aaa
Input aaa

> ls -l sample
-rwsr-xr-x  1 root  omok  4998 Feb  6 02:13 sample

> ./exploit
# id
uid=1001(omok) gid=1001(omok) euid=0(root) groups=1001(omok),0(wheel)
# whoami
root
# touch /root/exploit_rec
# ls -l /root/exploit_rec 
-rw-r--r--  1 root  wheel  0 Feb  6 02:16 /root/exploit_rec

 しかし、このプログラムが(バッファーオーバーフロー脆弱性のある場所の前に)ケーパビリティモードに移行していた場合にはどうなるか。サンプルプログラム2のように、copy_to_buff()を呼び出す前にcap_enter()を実行してケーパビリティモードに移行していれば、以降はグローバルなファイルシステムにアクセスできなくなるため、rootでシェルが開けなくなる。


> ./sample aaa
Input aaa

> ls -l sample
-rwsr-xr-x  1 root  omok  4998 Feb  6 02:13 sample

> ./exploit aaa
>                                       <------- exploitでシェルが開けなくなる

 このように、ファイルディスクリプタの割り当てとケーパビリティの割り当てなどの必要な処理を施した後に、ケーパビリティモードに移行しておけば、プログラムに想定外の脆弱性があっても、必要最低限の権限しか与えられていないため、被害を(完全には防げないものの)局所化出来ることが分かる。

1.4. まとめ

 ケーパビリティは、SELinuxなどのように動作の主体(Subject)/対象(Object)の両方に対してラベルを設定し、アクセス権を与えていく様なセキュリティ機構に比べて、Subjectに対してのみアクセス権を加えていくため、設定が(理論上、当然粗くなるが)簡単になる。また、Objectの変更に左右されないため、実際の運用環境のように、アクセス対象のファイルが個々のシステムによって生成/改変されていくような状況でも、Subjectは変更されにくいために、いちいち設定をカスタマイズ化/修正運用していく手間が少ないため、より実運用に向いているといえるだろう。
 このようにケーパビリティを利用してシステムを設計していけば、一般的なDAC(Discretionary Access Control)の仕組みを生かしつつ被害の局所化などでセキュリティを高められるため、ぜひ推奨したい。

以上

参考文献

[1]

"Summary about Posix.1e"
http://wt.tuxomania.net/publications/posix.1e/

参考資料

 

目次へ 次へ