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
|
|
それぞれのケーパビリティに関する詳しい説明は、Linuxカーネルソース内の/usr/src/linux/include/capability.hファイルに、コメントとして説明が載っている。
Linuxカーネルケーパビリティを前提にしたプログラムは、既にいくつもリリースされている。 例えば、DNSの実装で有名なbindはソースコード中で、OSがLinuxだった場合に、プロセスのケーパビリティを一度初期化してから、必要最低限のCAP_NET_BIND_SERVICE(ポートバインディングに必要)や、CAP_SYS_CHROOT(chroot化するのに必要)を付加している(コード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ファイルを含んだカーネルを構築/インストールして再起動する。
include GENERIC ident CAPS # enable Capsicum options CAPABILITIES options CAPABILITY_MO |
cd /usr/src/ make KERNCONF=CAPS buildkernel make KERNCONF=CAPS installkernel |
(3) capsicumの使い方
capsicumでは、ファイルディスクリプタを介してケーパビリティ(許可する操作を設定した、特別なファイルディスクリプタ)を作成する。この際に使用できるケーパビリティには、次のようなものがある。
capsicumにおけるcapability
|
|
capsicumが有効になったシステムでは、各プログラム中でcap_enter()を呼び出すと、プログラムはケーパビリティモードと呼ばれるモードに移行される。
このケーパビリティモードでは次のような制限が加わる
。
- 新たなファイルディスクリプタを取得することができなくなる。
- 図1に示すような、グローバルなファイルシステムにアクセスできなくなる。
- 既に作成されたファイルディスクリプタを介して与えられたケーパビリティをベースにして、新たに別のケーパビリティを作成できる。
- 3.の新たなケーパビリティは、権限を拡大することはできず、縮小することのみが可能である。
- プログラムがケーパビリティモードに入っている際にforkされたプロセスはケーパビリティモードに入っており、ケーパビリティが引き継がれる。
図1:グローバル ネームスペース
プログラムがこのケーパビリティモード中で使用できるケーパビリティを定義するため、capsicumでは、cap_new()を使用する。
例として、サンプルプログラム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)。
#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" |
参考資料
- 「権限を最小化するLinuxカーネルケーパビリティ」
http://www.atmarkit.co.jp/fsecurity/rensai/lids03/lids01.html - 「FreeBSD Daily Topics 新セキュリティ「Capsicum」」
http://gihyo.jp/admin/clip/01/fdt/201111/08 - 「IPA セキュアプログラミング講座」 (旧)第6章 セキュアC/C++プログラミング(「6-1. バッファオーバーラン ~その1・こうして起こる~」)
http://www.ipa.go.jp/security/awareness/vendor/programmingv1/b06_01.html