Linuxケーパビリティ (3/3)

プロセスケーパビリティを操作するための具体的な方法を解説する。

cap_from_textスタイル

あるプロセスのケーパビリティがどうなっているのかはlibcapに含まれるgetpcapsを使って調べることができる。オプションで調べたいプロセスのプロセスIDを指定すると次のような出力が得られる。

    # getpcaps 12345
    Capabilities for `12345': =eip cap_chown,cap_sys_rawio-eip

この出力はcap_from_text(3)のスタイルに従っている。cap_from_textスタイルの一般的な書式は次の通りだ。

    [<capability>[,<capability>,...]]<action-list>  ...

ここでaction-listは次のような書式である。

    <action>[<flags>][<action>[<flags>]...]

上の実行例ではactionにあたるのが=-、flagsにあたるのがeip、capabilityにあたるのがcap_chowncap_sys_rawioと空文字列(=eipの前には空のcapabilityが指定されていると考える)である。また=eipcap_chown,cap_sys_rawio-eipがそれぞれひとかたまりのグループであり、間は空白で区切られる。複数のグループがある場合には左側のものから先に解釈する。なお、解釈をはじめる前の状態はケーパビリティがまったく与えられていない状態である。

capabilityには各種のケーパビリティか、あるいは特別な値としてallを指定できる。allはすべてのcapabilityを意味する。またcapabilityの部分を空にした場合にはallが指定されたものとみなされる。つまりall=eip=eipは同じ意味になる。

flagsにはケーパビリティセットを表す文字を指定する。指定できるのは次の三つ。

e
effectiveセット
i
inheritableセット
p
permittedセット

actionとして指定できるのは次の三つだ。

=
三種類のプロセスケーパビリティセットから=の左側で指定された各ケーパビリティを取り去り、その後に=の右側に指定されたケーパビリティセットに入れ直す。ただし実際にその通りに動作するのではなく、ルールに従って計算した結果を反映させる。
+
+の左側で指定された各ケーパビリティを+の右側のケーパビリティセットに入れる。
-の左側で指定された各ケーパビリティを-の右側のケーパビリティセットから取り去る。

いくつか例を示そう。

=
all=
cap_chown,...,cap_lease= (実際には29個のケーパビリティを指定する)
三つとも同じ意味を表しており、すべてのケーパビリティを各ケーパビリティセットから外して何も加えない、つまりすべてのケーパビリティを外した状態を表す。
all+p
すべてのケーパビリティをpermittedセットに入れることを意味する。
cap_fowner+p-i
CAP_FOWNERをpermittedセットに入れ、inheritableセットから外すことを意味する。
cap_fowner+pe-i
CAP_FOWNERをpermittedセットとeffectiveセットに入れ、inheritableセットから外すことを意味する。
cap_fowner=+pe
CAP_FOWNERをすべてのケーパビリティセットから外し、その上でpermittedセットとeffectiveセットに入れることを意味する。結果的には上のcap_fowner+pe-iと同じ。

以上のようなスタイルは状態を表すためにも使われるし、設定する内容を指示するためにも使われる。

プロセスケーパビリティの操作方法

きめこまかなケーパビリティの操作は、プログラム自身によって行われなければならない場合が多い。たとえばネットワーク系のサーバでは、起動時に1023以下のポートをbindする必要があるためCAP_NET_BIND_SERVICEが必要だが、このケーパビリティはbind後には必要なくなる。このため、bind後にはこのケーパビリティを手放すのが望ましいはずだ。こういう操作を行うためには、プログラム自体にケーパビリティをケアするようにするなコードが含まれていなければならない。

ところがケーパビリティに対応しているプログラムはまだそれほど多くはない。それではどうしようもないのかというと、そうでもなくて、あらかじめ必要ないことが分かっているケーパビリティについてならば、プログラムの外部から取り去ってしまうことが可能である。前述したようなサーバプロセスであれば、CAP_NET_BIND_SERVICEを取り去ってしまうことは不可能だが、たとえばCAP_NET_RAWはプロセスの一生を通して不要かもしれない。そうであればあらかじめCAP_NET_RAWを除いた状態でプログラムを起動するか、起動直後にでもCAP_NET_RAWを取り去ってしまうという対処が考えられる。

このようなことを実行するためにはlibcapに含まれているsetpcapsやexeccapを使うこともできるだろう。setpcapsは指定したプロセスのケーパビリティを外から操作するツールであり、execcapは指定したケーパビリティにした上でプログラムを起動するためのラッパーである(suid-rootではないので特権を得られないことに注意)。これらは次のように使用する。

    # getpcaps 12345
    Capabilities for `12345': =eip
    # setpaps '=eip cap_chown,cap_sys_rawio-eip' 12345
    [caps set to:
    =eip cap_chown,cap_sys_rawio-eip
    ]           
    # getpcaps 12345
    Capabilities for `12345': =eip cap_chown,cap_sys_rawio-eip

getpcapsはプロセスIDを指定して実行すると、現存のプロセスケーパビリティセットをcap_from_textスタイルで表示してくれる。setpcapsはターゲットのプロセスIDと設定したいプロセスケーパビリティセットをcap_from_textスタイルで与えることによって、そうすることが可能であればそのプロセスのケーパビリティセットを調整してくれる。このケースのように、他のプロセスのケーパビリティセットを調整しようとしたとき、調整の内容がどんなものでも受け入れられるわけではない。以下に示したルールのどれにも当てはまらなければ調整は受け入れられるが、そうでなかったらエラーになる。

  • A(inheritable) ⊆ T(inheritable) ∪ C(permitted)
  • A(permitted) ⊆ T(permitted) ∪ C(permitted)
  • A(effective) ⊆ A(permitted)

ここでA(name)は設定したいケーパビリティセットの内容を、T(name)はターゲットとなるプロセスのケーパビリティセットの内容を、C(name)は自プロセスのケーパビリティセットの内容をそれぞれ表すものとする。

注意: permittedセットからケーパビリティを外したら自力では復帰することはできないと説明した。しかしsetpcapsを使って実験を行っていて、ケーパビリティを復帰できてしまうことに気付いた人もいるだろう。ここには理解をさまたげる罠がある。なぜこのようなことが起きるのかというと、setpcapsはシェルの組み込みコマンドではないため必ずfork & execされるためだ。inheritableセットから外さない限り、execした段階でケーパビリティが与えられてしまう。また、自分のpermittedセットにあるケーパビリティであれば、他者のpermittedセットに分け与えることができるため、このような一見復帰できてしまったかのような現象が起きるわけだ。

こうしたことにまどわされたくなければzshを使うとよい。zshにはケーパビリティに関する組み込みのサポートがある。zmodload zsh/capを実行するとケーパビリティ操作のためのcapコマンドが使えるようになる。このコマンドは組み込みコマンドであるからfork & execを介すことがない。実験環境としてはより適していると言えるだろう。

以上の現象について実験してみた様子を以下に示しておくので参考にしてほしい。

    bash# getpcaps $$
    Capabilities for `988': =eip
    bash# setpcaps '=eip cap_chown-ep' $$  (cap_chownを手放す)
    [caps set to:
    =eip cap_chown-ep
    ]           
    [caps set on 988]
    bash# getpcaps $$
    Capabilities for `988': =eip cap_chown-ep
    bash# setpcaps '=eip' $$  (復帰を試みる)
    [caps set to:
    =eip        
    ]           
    [caps set on 988]
    bash# getpcaps $$
    Capabilities for `988': =eip  (cap_chownを復帰できた!)
    bash# exec zsh  (zshに切り替える)
    zsh# zmodload zsh/cap
    zsh# cap  (自プロセスのケーパビリティを確認)
    =eip        
    zsh# cap '=eip cap_chown-ep'  (cap_chownを手放す)
    zsh# cap
    =eip cap_chown-ep
    zsh# cap '=eip'  (復帰を試みる)
    cap: can't change capabilites: operation not permitted  (復帰失敗!)
    zsh# setpcaps '=eip' $$  (setpcapsを使ってやってみる)
    [caps set to:
    =eip        
    ]           
    [caps set on 988]
    zsh# cap
    =eip  (復帰できた!)

ケーパビリティを操作しているツールたち

最後にプロセスケーパビリティを操作することで、安全性を高めようとしているツールのいくつかを紹介しておこう。

cage
capexecとchrootを合わせたようなツールで、指定されたディレクトリにchrootし、さらにケーパビリティを調整した上でプログラムを起動することができる。
SecureCGI
指定したケーパビリティセットを設定した上で任意のCGIプログラムを起動するためのラッパー。設定するケーパビリティセットの内容は、Apache HTTPサーバの設定である環境変数にcap_from_textスタイルの文字列で指定する。また、通常このプログラムはsuidされるため、CGIプログラムはそのファイルの持ち主の権限で動作する。
ProFTPD
mod_capを組み込んでおくと、個々のコネクションを処理するために起動される子プロセスにおいてCAP_NET_BIND_SERVICEとCAP_CHOWN以外のケーパビリティを手放すようになる。
pure-ftpd
vsftpd
ProFTPDと同じような機能を持っている。
pam_limits.so
/etc/security/limits.confの中で、itemとしてcapabilitiesを指定するとvalueの部分にcap_from_textスタイルの設定が可能である。



(1/3):ケーパビリティとケーパビリティバウンディングセット
(2/3):プロセスケーパビリティ