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

ケーパビリティは、特権をより細かな断片に分割しようというものである。プロセスに対して特権を細かく分けて与えることによって、安全性を高めることができる。

ケーパビリティとは

UNIX系OSにおいてはrootユーザに絶対的な特権が与えられている。特権がなければできないことがある反面、特権がありさえすれば何でもできてしまう。この状況が問題をはらんでいることは、各種のセキュリティホールの存在によって明らかである。

Linuxケーパビリティは、こうした特権にまつわる問題(の少なくともいくらか)を解決するための機構だ。基本的な考え方は従来一つだった特権をいくつかに分割し、それぞれの権限のうちの本当に必要なものだけを選択的に保持するようにするというものだ。そして分割された特権の断片のそれぞれのことをケーパビリティと呼ぶ。

この仕組みは、特権のうちの必要のない部分を手放すための手段を提供するという形で機能する。つまり、もともと特権のなかったところにケーパビリティを与えるためのものではない。順序としては、rootでログインするか、あるいはsuid rootプログラムを実行することによって特権が与えられ、そこから必要のない権限を適当なタイミングで放棄していくということになる。また、あるプロセスでexecしたときには、ケーパビリティは継承されるが、元のケーパビリティを上まわるケーパビリティを得ることはできない。

ケーパビリティバウンディングセット

ところで、あるプロセスの持つケーパビリティがそこから起動された(fork & execされた)プロセスにすべて引き継がれるのかというと、必ずしもそうではない。普通に配布されているカーネルにおいては、プログラムが特権をもってexecされたときに、システムで指定されたケーパビリティのセットのみが与えられる。このセットのことを特にケーパビリティバウンディングセットと呼ぶ。通常のカーネルにおいては、ケーパビリティバウンディングセットはいわばシステム全体の機能を制限するためのマスクとして働くわけである。

ケーパビリティバウンディングセットにどのようなケーパビリティが含まれているかは/proc/sys/kernel/cap-boundによって参照することができる。

    # cat /proc/sys/kernel/cap-bound
    -257

この値は、プロセスに与えられるケーパビリティの一つ一つに応じたビットのON/OFFで決まる。すべてのケーパビリティが与えられる状態では(非常に分かりにくいのだが)すべて1を表す-1(0xFFFFFFFF)になるし、ケーパビリティが一切与えられない状態では0(0x00000000)になる。上の実行例について言うと、下位から数えて第8ビットを除いてONという状態(0xFFFFFEFF)である。第8ビットに対応するケーパビリティはCAP_PCAPと呼ばれるものであり、特権プロセスからはCAP_PCAPが除去されるとともに、CAP_PCAPを必要とする操作が不可能となる。

また、上の状態のケーパビリティバウンディングセットからさらにCAP_CHOWNというケーパビリティを取り除きたいという場合を考えてみよう。CAP_CHOWNは第0ビットに対応するものであるから、第8ビットと第0ビットをOFFにした-258(0xFFFFFEFE)を書き込むことで目的を実現できる。

    # echo -258 > /proc/sys/kernel/cap-bound

注意が必要なのは、一度ケーパビリティバウンディングセットから外したケーパビリティを再び元に戻すことはできないということである。つまり、ケーパビリティを段階的に手放すことは可能だが、その逆はできない(唯一、initプロセスだけが「逆」をできるのだが、通常のinitはそのような指示を受け付けてくれない)。

ケーパビリティの種類

バージョン2.4.21のカーネルでは、全部で29個のケーパビリティが定義されている。第0ビットに対応するものから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で、各々の詳細についてはバージョン1.54以降のLinux man pagesに含まれるcapabilities(7)で解説されているほか、include/linux/capability.hの中にも説明がある。

なお、cap-boundの値から現在のケーパビリティバウンディングセットを把握するには次のような小さなスクリプトを用意しておくと便利だ。ここではRubyで記述したが他の言語でもそう難しくないだろう。

    #!/usr/bin/ruby

    CAP_BOUND = '/proc/sys/kernel/cap-bound'
    CAPABILITY = %w(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)

    File.open(CAP_BOUND, 'r') do |f|
      cap_bset = f.gets.to_i
      CAPABILITY.each_with_index do |c, n|
        puts "#{cap_bset[n] == 1 ? '+' : '-'}#{c}"
      end
    end

上のスクリプトは各ケーパビリティの状態を一行に一つずつ表示する。最初の文字が「+」なら有効、「-」なら無効を表す。また、この出力をファイルにリダイレクトし、必要に応じて「+」と「-」を調整した上で次のスクリプトに入力すると、ケーパビリティバウンディングセットを指定した通りに設定することができる(もちろん「-」のケーパビリティを「+」にすることはできない)。

    #!/usr/bin/ruby

    CAP_BOUND = '/proc/sys/kernel/cap-bound'
    CAPABILITY = %w(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_bset = 0xFFFFFFFF
    ARGF.readlines.each do |x|
      x.strip!
      next if x.empty?
      if /^([-+])(\w+)$/ =~ x
        n = CAPABILITY.index($2)
        if n
          cap_bset ^= (1 << n) if $1 == '-'
        else
          STDERR.puts 'unknown capability: ' + $2
          exit 1
        end
      else
        STDERR.puts 'invalid input line: ' + $2
        exit 1
      end
    end

    File.open(CAP_BOUND, 'w') do |f|
      f.puts [cap_bset].pack('I').unpack('i').to_s
    end

いくつかの運用例

ケーパビリティバウンディングセットを使用する例をいくつか示しておこう。

CAP_CHOWN、CAP_DAC_OVERRIDE、CAP_DAC_READ_SEARCHを外す
たとえrootであっても自分の持ち物ではないファイルに対して読み書きなどをすることができなくなる。ただし、このような設定をしてしまうとシステムに誰もログインできなくなってしまうので注意が必要だ。
CAP_NET_ADMINを外す
ネットワークインタフェースの状態(up/downなど)を変えたり、新たにネットワークインタフェースを設定することができなくなる。またルーティングテーブルの内容も変更できなくなる。
CAP_SYS_RAWIOを外す
iopl(2)やioperm(2)が使用できなくなる。これによってsvgalibを使っているプログラムやkonなどは動作しなくなる。
CAP_SYS_MODULEを外す
ローダブルモジュールのロードやアンロードができなくなる。これによってモジュールを使った攻撃に耐性がつくが、CAP_SYS_MODULEを手放すとケーパビリティバウンディングセットに一切アクセスできなくなってしまう。

このような操作に関する注意点としては、すでに起動されてしまっているプロセスのケーパビリティが変更されるのではないということが挙げられる。ケーパビリティバウンディングセットの内容はプログラムがexecされるときに適用されるため、たとえば最初の例では、すでに特権をもって起動されているシェルがあれば、そのシェル上でリダイレクトすることによって他者のファイルを読み書きすることができる(もちろんケーパビリティバウンディングセットの変更後に起動されたシェルについては問題なく制限される)。

---
(2/3):プロセスケーパビリティ
(3/3):プロセスケーパビリティを調整する