Linux Kernel Capability(Linuxカーネルケーパビリティ)に関して(2020年版) - Part1

こんにちは。SIOS OSSエバンジェリスト/セキュリティ担当の面 和毅です。

過去にLinux Intrusion and Detection Systems(LIDS)のパッケージメンテナもやっていた経緯があり、Linux Kernel Capability(ケーパビリティ)について纏めた記事を執筆していました(@IT連載:権限を最小化するLinuxカーネルケーパビリティIPA 情報セキュリティ技術動向調査(2011 年下期)) 。

記事執筆時点から凡そ14年が経過し、Linux Kernel Capability (所謂Linux Capability)も諸々変化してきていて、コンテナなどのアクセス制御にも使われるようになっているので、改めてこちらの記事で2020年(Linux Kernel 5.7)時点のLinux Kerenel Capabilityを纏めてみます。


2020/07/20 更新:プログラムをrootで実行した場合のケーパビリティを追記しました。


Linux Kernel Capability(POSIXケーパビリティ由来)とは

Linux Kernel Capabilityを話すには、まずPOSIXケーパビリティの話からということで、その辺を一通りおさらいしておきます。

通常のUnix系OSでは、プロセスが一般ユーザ権限か特権(root権限)で動くかしかありませんでした。例えばApacheやnginxのサービスが1024番未満の所謂「特権ポート」をプロセスで使用する場合や、ntpd等のシステムの時刻を設定する等の際には、実行プロセスに特権を与える必要があります。

しかし、プロセスに特権をすべて与えてしまうと、プロセスに脆弱性があった場合に全ての特権が取られてしまう可能性があります。この問題は随分前から指摘されており、これを解決する方法として「ケーパビリティ(POSIXケーパビリティ)」という提案がPOSIXのドラフト1003.1eとして提出されていました。

これは、「特権」を更に細分化した「ケーパビリティ」と呼ばれる単位で取り扱う事ができるようにし、プロセスに必要最小限のケーパビリティを与えて、必要な処理だけを行わせようというコンセプトのものです。これにより、プロセスに脆弱性が発見されて悪用されたとしても、そのプロセスに与えられた必要最小限のケーパビリティしか悪用されないため、被害を局所化(コンパートメント化)することが出来ます。

このPOSIXケーパビリティは、Linux上では「Linuxカーネルケーパビリティ」としてカーネル2.4から採用されています。2020年6月現時点での最新のカーネル(5.7)でのLinux Kernel Capabilityはlinux/include/uapi/linux/capability.hで定義されており、抜粋すると

ケーパビリティ名 概略
CAP_CHOWN 0 ファイルのUIDとGIDを任意に変更する
CAP_DAC_OVERRIDE 1 ファイルのDAC(任意アクセス制御)をバイパスする
CAP_DAC_READ_SEARCH 2 ファイルのDACのREAD/SEARCHをバイパスする
CAP_FOWNER 3 プロセスのファイルシステムIDがファイルのUIDに一致する必要がある操作をバイパスする
CAP_FSETID 4 ファイルが変更された時にset-UIDとset-GIDの許可ビットをクリアしない
CAP_KILL 5 シグナルを送信する際の権限チェックをバイパスする
CAP_SETGID 6 プロセスのGIDに対する任意の操作を行う
CAP_SETUID 7 プロセスのUIDに対する任意の操作を行う
CAP_SETPCAP 8 ファイルケーパビリティがサポートされていない場合は、ケーパビリティセットに含まれる任意のケーパビリティを、他のプロセスに付与できる。
CAP_LINUX_IMMUTABLE 9 FS_APPEND_FSとFS_IMMUTABLE_FLを設定する
CAP_NET_BIND_SERVICE 10 1024番未満のポート番号をバインド出来る
CAP_NET_BROADCAST 11 ソケットのブロードキャストとマルチキャストの待受を行う
CAP_NET_ADMIN 12 各種のネットワーク関係の操作を実行する
CAP_NET_RAW 13 RAWソケットとPACKETソケットを使用する
CAP_IPC_LOCK 14 メモリーのロックを行う
CAP_IPC_OWNER 15 IPCオブジェクトに対しての操作で、権限チェックをバイパスする
CAP_SYS_MODULE 16 カーネルモジュールのロード・アンロードを行う
CAP_SYS_RAWIO 17 I/Oポート操作を実行する
CAP_SYS_CHROOT 18 chrootを呼び出す
CAP_SYS_PTRACE 19 ptraceを使うことが出来る
CAP_SYS_PACCT 20 acct(プロセスアカウントのオン・オフを切り替える)を呼び出す
CAP_SYS_ADMIN 21 mount,swapon等のシステム管理用の操作を実行する
CAP_SYS_BOOT 22 rebootを呼び出す
CAP_SYS_NICE 23 nice値の変更を行う
CAP_SYS_RESOURCE 24 setrlimitを呼び出しリソース制限を上書きできる
CAP_SYS_TIME 25 システムクロックを変更できる
CAP_SYS_TTY_CONFIG 26 仮想端末に関して特権が必要な操作を利用できる
CAP_MKNOD 27 mknodを使用してスペシャルファイルの作成が出来る
CAP_LEASE 28 ファイルリースを設定する
CAP_AUDIT_WRITE 29 AUDITログに書き込みが出来る
CAP_AUDIT_CONTROL 30 KernelのAUDIT機能の切り替えが出来る
CAP_SETFCAP 31 ファイルケーパビリティを設定する
CAP_MAC_OVERRIDE 32 強制アクセス制御(MAC)を上書きする。SMACK用に実装されている。
CAP_MAC_ADMIN 33 強制アクセス制御(MAC)を上書きする。SMACK用に実装されている。
CAP_SYSLOG 34 syslogに関して特権が必要な操作を利用できる
CAP_WAKE_ALARM 35 システムのWakeupトリガーを有効に出来る
CAP_BLOCK_SUSPEND 36 システムのサスペンドをブロックできる
CAP_AUDIT_READ 37 監査ログをnetlinkソケット経由で読み出すことが出来る

となっています(詳しくはCapabilitiesのMan pageを参照して下さい。

ケーパビリティセットと、実効ケーパビリティの計算方法

スレッドケーパビリティセット

各スレッドは、以下の3種類のケーパビリティセット(上述のケーパビリティの組み合わせ)を持ちます。

  1. 許可(permitted): スレッドが持つことになっている実効ケーパビリティの限定的なスーパーセット。
  2. 継承可能(inheritable): execve()前後で保持されるケーパビリティセット
  3. 実効(effective): カーネルがスレッドの権限(permission)をチェックする時に使用するケーパビリティセット

この中で、Kernelは実際には実効(effective)ケーパビリティセットから、必要となるケーパビリティをプロセスが持っているかどうかを確認して、アクセス制御に利用しています。

ファイルケーパビリティについて

2.6.24移行のカーネルで実装されたもので、実行ファイルにsetcapを用いてケーパビリティセットを対応付けることが出来ます。ファイルケーパビリティセットは拡張属性(SELinux等の強制アクセス制御のラベル保存にも使用されており、それぞれのファイルに設定されている)に保存されていて、execve後のスレッドのケーパビリティセットを決定する要素の一つとなっています。

このファイルケーパビリティにも、3つのケーパビリティセットが定義されています。

  1. 許可(permitted): スレッドの継承可能(Inheritable)ケーパビリティに関わらず、そのスレッドに自動的に許可されるケーパビリティ
  2. 継承可能(inheritable): このセットと、スレッドの継承可能(inheritable)ケーパビリティのANDが取られて、execve後に有効となる継承可能ケーパビリティが決定される。
  3. 実効(effective): これは集合ではなく1ビットの情報である。このビットがセットされていると、execve中にそのスレッドの新しい許可(permitted)ケーパビリティが全て実効ケーパビリティ集合においてもセットされる。このビットがセットされていない場合、execve後には新しい許可ケーパビリティのどれも新しい実効ケーパビリティ集合にセットされない。次の節で説明する計算方法によりわかるが、ファイルにケーパビリティをsetcapなどで割り当てる際には、いずれかのケーパビリティに対して実効フラグを有効と指定する場合、許可フラグや継承可能フラグで有効になっている他の全てのケーパビリティについても実効フラグを有効としなくてはならない。

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

ケーパビリティバウンディングセット(capability bounding set)を用いて、execve時に獲得できるケーパビリティを制限することが出来ます。バウンディングセットは、以下のように使用されます。

  • execve実行時に、ケーパビリティバウンディングセットとファイルの許可ケーパビリティセットのANDを取ったものが、そのスレッドの許可ケーパビリティセットに割り当てられる。つまり、ケーパビリティバウンディングセットは、実行ファイルの許可ケーパビリティに対して制限を賭けることが出来ます。
  • ケーパビリティバウンディングセットは、スレッドがcapsetにより自身の継承可能セットに追加可能なケーパビリティの母集団を制限します。あるケーパビリティがスレッドに許可されていても、バウンディングセットに含まれていなければ、スレッドはそのケーパビリティを継承可能セットに追加できないため、許可セットにケーパビリティをその先持ち続けることが出来ません。

バウンディングセットからケーパビリティを削除しても、スレッドの継承可能セットからはそのケーパビリティは削除されません。しかし、この先そのケーパビリティをスレッドの継承可能セットに追加することができなくなります。

Ambientケーパビリティについて

Linux Kernel 4.3から追加になったケーパビリティセットです。このケーパビリティセットは特権の無いプログラムがexecveで実行された際に保存されるケーパビリティセットになります。Ambientケーパビリティセットは許可又は継承の両方で設定されていない場合にはケーパビリティは存在しないというルールに従います。

ambientケーパビリティセットはprctlで変更することが出来ます。Ambientケーパビリティは許可(permitted)或いはinheritable(継承可能)ケーパビリティが低い際には、自動的に低くなります。

setUIDやsetGIDビットによりUIDやGIDが変更されるプログラムでは、Ambientセットはクリアされます。Ambientケーパビリティはexecveが呼び出された際に許可(permitted)セットから呼び出され、effectiveセットとして与えられます。Ambientケーパビリティがプロセスの許可(permitted)とeffectiveケーパビリティをexecveで増やす方向に動く場合は、ld.soで記載されているsecure-executionモードは発動しません。

ケーパビリティの計算方法

execve実行時に、カーネルはプロセスの新しいケーパビリティを、以下のアルゴリズムを使用して計算します。


           P'(ambient)     = (file is privileged) ? 0 : P(ambient)

           P'(permitted)   = (P(inheritable) & F(inheritable)) |
                             (F(permitted) & P(bounding)) | P'(ambient)

           P'(effective)   = F(effective) ? P'(permitted) : P'(ambient)

           P'(inheritable) = P(inheritable)    [i.e., unchanged] [つまり、変更されない]

           P'(bounding)    = P(bounding)       [i.e., unchanged] [つまり、変更されない]

ここで各変数の意味は以下のとおりになります。


P: execve(2) 前のスレッドのケーパビリティセットの値 
P': execve(2) 後のスレッドのケーパビリティセットの値 
F: ファイルケーパビリティセットの値 

以前(15年ほど前)に初めてケーパビリティの調査を行った際には、システム全体で共通のケーパビリティバウンディングセットを用いており、LIDS(Linux Intrusion Detection System)でもシステム共通のケーパビリティバウンディングセットをいじっていました。そのため、ケーパビリティバウンディングセットを修正すると、システム全体のプロセスに影響するので、細かなケーパビリティの調整が大変でした。しかし、Linux Kernel 2.6.25以降は各々のスレッドが個別にケーパビリティバウンディングセットを持つようになっています。

また、Linux Kernel 4.3以降では「Ambient」ケーパビリティセットも加わったため、更に複雑になっています。この辺は実際に動作を見て確認することにしたいと思います。

プログラムをrootで起動した場合のケーパビリティ

UID 0(root)がプログラムを実行した場合と、Set-User-ID(SUID)ビットがついているプログラムが実行された場合には、特別な扱いを行います。

set-user-IDモードのビットが立っていてrootでプログラムを起動した場合の、ファイルケーパビリティの扱いは以下のようになります。

  1. プロセスの実UID、或いは実効UID(EUID)が0の場合には、ファイルケーパビリティの
    • Inheritable(継承)
    • Permitted(許可)
    に関しては無視されます。その変わり、ファイルケーパビリティのそれらの全てのセットが1(有効)ということになります。
  2. EUIDが0の場合、またファイルケーパビリティのEffective(実効)ビットが1になっているケーパビリティに関しては1(有効である)と定義します。

UID 0で実行した場合、或いはSUIDビットがついているプログラムを実行した場合には、これらのファイルケーパビリティの値を持って、前述のexecした際のケーパビリティセット類が決定されます。

上記の例外として、SUIDビットが立っているバイナリにケーパビリティが付与されている場合があります。この場合には、プロセスのケーパビリティを計算する際のファイルケーパビリティは上述のように全てのセットが1(有効)という事にはならず、バイナリに与えられたファイルケーパビリティによって計算されます。

ケーパビリティの確認

ここまでがケーパビリティの説明です。わかりにくい所もあると思いますので、以降で実際にケーパビリティを調節したりして確認してみましょう。

ケーパビリティ確認の準備

以下、システムはDebian 10(buster)を使用します。Linux Kernelは5.7.2を使用しています。

  1. 準備として、まず(入っていない場合には)「libcap2-bin」パッケージをインストールします。
  2. 
    root@localhost:~# apt install libcap2-bin
    
  3. また、ケーパビリティを扱いやすい「libcap-ng」パッケージをインストールします。
    
    root@localhost:~# apt install libcap-ng0 libcap-ng-dev libcap-ng-utils
    
  4. ファイルシステムの拡張属性を見たいため、拡張属性を扱う「attr」パッケージをインストールします。
    
    root@localhost:~# apt install attr
    

ケーパビリティ確認

  1. まずは、プロセスにどの様なケーパビリティセットが設定されているかを確認してみましょう。プロセスに設定されているケーパビリティセット類は、/proc/[プロセスID]/task/[スレッドID]/statusを見ることで確認できます。例えば、initプロセス(PID=1)を見てみると
    
    root@localhost:~# cat /proc/1/task/1/status 
    Name:	systemd
    Umask:	0000
    --省略--
    CapInh:	0000000000000000
    CapPrm:	0000003fffffffff
    CapEff:	0000003fffffffff
    CapBnd:	0000003fffffffff
    CapAmb:	0000000000000000
    --省略--
    

    となっています。それぞれ16進数で表されていますので、上述の場合は2進数に変換すると

    
    CapInh: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
    CapPrm: 0000 0000 0000 0000 0000 0000 0011 1111 1111 1111 1111 1111 1111 1111 1111 1111
    CapEff: 0000 0000 0000 0000 0000 0000 0011 1111 1111 1111 1111 1111 1111 1111 1111 1111
    CapBnd: 0000 0000 0000 0000 0000 0000 0011 1111 1111 1111 1111 1111 1111 1111 1111 1111
    CapAmb: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
    

    となります。右端から38番目のビットまでが立って(1になって)います。ケーパビリティを説明した表の中で、「何ビット目か」という所で考えると(右端は0とカウントする)、ちょうど37(CAP_AUDIT_READ)までのビットが全て立っている事がわかります。

    これらのケーパビリティセットですが、平たく言えばどの様にスレッド内で子スレッド等に継承されていくかを定義するためのもので、プロセス/スレッドが実際に特定のケーパビリティを持っているか否かをLinux Kernelが比較する際にはCapEffを使用します。

  2. また、systemd-timesyncサービスのケーパビリティセットを確認してみましょう。PIDが335なので
    
    root@localhost:/proc/335/task/335# cat status
    Name:	systemd-timesyn
    Umask:	0022
    ---省略---
    CapInh:	0000000002000000
    CapPrm:	0000000002000000
    CapEff:	0000000002000000
    CapBnd:	0000000002000000
    CapAmb:	0000000002000000
    ---省略---
    
    となっています。それぞれ16進数で表されていますので、上述の場合には2進数に変換すると
    
    CapInh:	00000010000000000000000000000000
    CapPrm: 00000010000000000000000000000000
    CapEff: 00000010000000000000000000000000
    CapBnd: 00000010000000000000000000000000
    CapAmb: 00000010000000000000000000000000
    

    となります。右端から26番目のビットが立って(1になって)います。ケーパビリティを説明した表の中で、「何ビット目か」という所で考えると(右端は0とカウントする)、ちょうど25(CAP_SYS_TIME)のビットが立っていることがわかります。

  3. 基本的には前述の/proc以下の情報で全てのプロセスのケーパビリティセットを見る事ができますが、getpcaps "プロセスID番号"で、そのプロセスに設定されているケーパビリティをよりわかりやすく見る事が出来ます。例えばinitプロセス(1)を見てみると
  4. 
    root@localhost:~# ps ax |grep init
         1 ?        Ss     0:01 /sbin/init
       783 pts/0    S+     0:00 grep init
    root@localhost:~# getpcaps 1
    Capabilities for `1': = 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,cap_block_suspend,cap_audit_read+ep
    root@localhost:~# 
    

    となり、0-37までの全てのケーパビリティを持っていることがわかります。

  5. また、systemd-timesyncを見てみると
    
    root@localhost:~# ps ax|grep systemd-timesync
       335 ?        Ssl    0:00 /lib/systemd/systemd-timesyncd
      1645 pts/0    S+     0:00 grep systemd-timesync
    root@localhost:~# getpcaps 335
    Capabilities for `335': = cap_sys_time+eip
    root@localhost:~# 
    

    となり、systemd-timesyncはcap_sys_timeのケーパビリティのみを持っていることがわかります。

  6. ここで、systemd-timesyncがどこでcap_sys_timeのケーパビリティのみ持つようになったのかを確認すると、systemd-timesyncサービスの起動スクリプト"/usr/lib/systemd/system/systemd-timesyncd.service"で
    
    #  SPDX-License-Identifier: LGPL-2.1+
    #
    #  This file is part of systemd.
    #
    ---省略---
    [Service]
    AmbientCapabilities=CAP_SYS_TIME
    CapabilityBoundingSet=CAP_SYS_TIME
    ExecStart=!!/lib/systemd/systemd-timesyncd
    ---省略---
    

    となっており、プロセスが起動する際にCAP_SYS_TIMEをAmbientケーパビリティとケーパビリティバウンディングセットに設定していることがわかります。

ケーパビリティ実験

ここで実際に、ケーパビリティを調整するとどうなるのかを確認してみましょう。Debianではlibcap2-binパッケージに入っている「capsh」を使用します。capshですが、ケーパビリティバウンディングセットを変更することで、お手軽にケーパビリティのテストが出来るツールです。rootで起動するツールになります。
  1. capsh --printで、現時点でのプロセス(bash)のケーパビリティの設定を見る事が出来ます。
    
    root@localhost:~# capsh --print
    Current: = 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,cap_block_suspend,cap_audit_read+ep
    Bounding set =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,cap_block_suspend,cap_audit_read
    Securebits: 00/0x0/1'b0
     secure-noroot: no (unlocked)
     secure-no-suid-fixup: no (unlocked)
     secure-keep-caps: no (unlocked)
    uid=0(root)
    gid=0(root)
    groups=0(root)
    

    --dropを付けると、そのプロセス(bash)のケーパビリティ内で、ケーパビリティバウンディングセットから指定したケーパビリティを削除します。例えば、allで全てのケーパビリティを削除すると

    
    root@localhost:~# capsh --drop=all --print
    Current: = 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,cap_block_suspend,cap_audit_read+ep
    Bounding set =
    Securebits: 00/0x0/1'b0
     secure-noroot: no (unlocked)
     secure-no-suid-fixup: no (unlocked)
     secure-keep-caps: no (unlocked)
    uid=0(root)
    gid=0(root)
    groups=0(root)
    

    となり、ケーパビリティバウンディングセットに何もセットされていない状態が作り出せますので、このプロセス(bash)からexecするプログラムに関してはケーパビリティセットが0になります。

  2. 次に、"capsh --"コマンドを実行すると、コマンドを実行したbashから、指定された新しいケーパビリティバウンディングセットをその他のケーパビリティセットと一緒に用いて計算された、諸々のケーパビリティセットを与えられたbashがexecされます。特にケーパビリティバウンディングセットから削除するケーパビリティを指定しないで実行してみます。
    
    root@localhost:~# ps ax|grep bash
       809 pts/0    Ss     0:00 -bash
    ---省略---
      2516 pts/1    S      0:00 /bin/bash
    
    bashのPIDは2516なので、PID=2516のケーパビリティセットを確認します。
    
    root@localhost:~# cat /proc/2516/status 
    Name:	bash
    Umask:	0022
    --snip--
    CapInh:	0000000000000000
    CapPrm:	0000003fffffffff
    CapEff:	0000003fffffffff
    CapBnd:	0000003fffffffff
    CapAmb:	0000000000000000
    --snip--
    
    となっており、全てのケーパビリティを持っていることがわかります。
  3. 次に、ケーパビリティを減らしてみます。--drop="cap_net_raw"など、ケーパビリティバウンディングセットから落としたいケーパビリティをオプションとして渡してあげる形になります。今回はテストのため、「全てのケーパビリティを落とす」ということで--drop=allをやってみます。
    
    root@localhost:~# capsh --drop=all --
    root@localhost:~# ps ax|grep bash
       809 pts/0    Ss     0:00 -bash
    --snip--
      2921 pts/1    S      0:00 /bin/bash
    root@localhost:~# cat /proc/2921/status
    Name:	bash
    Umask:	0022
    --snip--
    CapInh:	0000000000000000
    CapPrm:	0000000000000000
    CapEff:	0000000000000000
    CapBnd:	0000000000000000
    CapAmb:	0000000000000000
    --snip--
    
    となり、全てのケーパビリティセットが0にされた状態になっています。
  4. 前述の状態で、"nc"コマンドを用いて"nc -kvl localhost -p 80"として、ncが80番で待ち受けるようにします。
    
    root@localhost:~# nc -kvl localhost -p 80
    Can't grab 0.0.0.0:80 with bind : Permission denied
    root@localhost:~# 
    
    となり、80番のPortをバインディング出来ません。これは前述のケーパビリティで説明した「CAP_NET_BIND_SERVICE」が0にされているため、1023番以下のポートにバインディングする権限が無くなっているためです。そのため、例えば8080番などであれば
    
    root@localhost:~# nc -kvl localhost -p 8080
    listening on [any] 8080 ...
    
    とポートをバインディングすることが出来ますが、1023番以下の所謂特権ポートは使用できなくなっています。
  5. 一旦、bashをexitして、今度はCAP_NET_BIND_SERVICE以外をケーパビリティバウンディングセットから外した状態を作りましょう。capsh --drop "cap_xx,cap_yy"のように、削除するケーパビリティをカンマ区切りで与えることで、ケーパビリティバウンディングセットから削除するケーパビリティを指定できます。一々名前を指定するのが面倒な場合には、前述の表の「何ビット目か」の数字を指定しても構いません。
    
    root@localhost:~# capsh --drop="0,1,2,3,4,5,6,7,8,9,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37" --
    root@localhost:~# ps ax|grep bash
       809 pts/0    Ss     0:00 -bash
    --snip--
      3087 pts/1    S      0:00 /bin/bash
    root@localhost:~# cat /proc/3087/status
    Name:	bash
    Umask:	0022
    --snip--
    CapInh:	0000000000000000
    CapPrm:	0000000000000400
    CapEff:	0000000000000400
    CapBnd:	0000000000000400
    CapAmb:	0000000000000000
    --snip--
    
    となります。16進数に直すと
    
    CapInh: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
    CapPrm: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0100 0000 0000
    CapEff: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0100 0000 0000
    CapBnd: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0100 0000 0000
    CapAmb: 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000
    

    となり、CAP_NET_BIND_SERVICEだけ1になっていることがわかります。

    この状態で、"nc"コマンドを用いて"nc -kvl localhost -p 80"とすると

    
    root@localhost:~#  nc -kvl localhost -p 80
    listening on [any] 80 ...
    
    
    となり、ssコマンドでポートを見ても
    
    tcp                LISTEN              0                    1                                                                                           0.0.0.0:80                                                0.0.0.0:*                      users:(("nc",pid=3173,fd=3))                                                   
    

    と、80番ポートを使用できていることがわかります。

  6. また、このCAP_NET_BIND_SERVICEだけが与えられているbashから、他のユーザのファイルに書き込みを行おうとしても、CAP_DAC_OVERRIDE等のケーパビリティが無いため、操作は失敗します。
    
    root@localhost~# echo "Test" > /home/sios/testfile
    bash: /home/sios/testfile: Permission denied
    root@localhost:~#
    

この様に、ケーパビリティを使用することによって、プロセスに必要以外の特権を与えることを防ぐことが出来、万が一プロセスに脆弱性が見つかって悪用されたとしても、悪用される範囲を限定できる(所謂サンドボックス化出来る)事がわかりました。

ケーパビリティの詳細

ケーパビリティの定義

前節まででケーパビリティを実際に触って確認を行いました。それでは、何でケーパビリティを設定することにより、特権を分割して与えることが出来るのかを、Linux Kernelのソースコードから簡単に見ていきたいと思います。

ケーパビリティの定義は、linux/uapi/include/linux/capability.hファイルに


typedef struct __user_cap_data_struct {
        __u32 effective;
        __u32 permitted;
        __u32 inheritable;
} __user *cap_user_data_t;

と定義されており、これをincludeする形でlinux/include/linux/capability.hファイルで


struct cpu_vfs_cap_data {
        __u32 magic_etc;
        kernel_cap_t permitted;
        kernel_cap_t inheritable;
        kuid_t rootid;
};

とファイルケーパビリティも定義されています。

その他、ケーパビリティに関する定義などは凡そ上述の2ファイル内に存在しますので、興味のある方は確認されると良いかと思います。

ケーパビリティの確認

ケーパビリティを設定することで何故一部の特権が使えるようになるのかは、システムコールのソースコードを確認すると理解できると思います。

例えば、時間管理のケーパビリティCAP_SYS_TIMEがどこで扱われているかを確認してみます。

  1. システムコールは(例外も有りますが)SYSCALL_DEFINEマクロで定義されています。時間を調整する際に呼び出されるシステムコールのadjtimexは、kernel/time/time.c中の

    
    #ifdef CONFIG_64BIT
    SYSCALL_DEFINE1(adjtimex, struct __kernel_timex __user *, txc_p)
    {
    	struct __kernel_timex txc;		/* Local copy of parameter */
    	int ret;
    
    	/* Copy the user data space into the kernel copy
    	 * structure. But bear in mind that the structures
    	 * may change
    	 */
    	if (copy_from_user(&txc, txc_p, sizeof(struct __kernel_timex)))
    		return -EFAULT;
    	ret = do_adjtimex(&txc);
    	return copy_to_user(txc_p, &txc, sizeof(struct __kernel_timex)) ? -EFAULT : ret;
    }
    #endif
    

    で定義されています。この中でdo_adjtimex()を呼び出しています。

  2. do_adjtimex()はkernel/time/timekeeping.c中で定義されています。

    
    /**
     * do_adjtimex() - Accessor function to NTP __do_adjtimex function
     */
    int do_adjtimex(struct __kernel_timex *txc)
    {
    	struct timekeeper *tk = &tk_core.timekeeper;
    	struct audit_ntp_data ad;
    	unsigned long flags;
    	struct timespec64 ts;
    	s32 orig_tai, tai;
    	int ret;
    
    	/* Validate the data before disabling interrupts */
    	ret = timekeeping_validate_timex(txc);
    	if (ret)
    		return ret;
    --snip--
    

    で定義されています。この中でtimekeeping_validate_timex()を呼び出しています。

  3. timekeeping_validate_timex()はkernel/time/timekeeping.c中で定義されています。

    
    /**
     * timekeeping_validate_timex - Ensures the timex is ok for use in do_adjtimex
     */
    static int timekeeping_validate_timex(const struct __kernel_timex *txc)
    {
    	if (txc->modes & ADJ_ADJTIME) {
    		/* singleshot must not be used with any other mode bits */
    		if (!(txc->modes & ADJ_OFFSET_SINGLESHOT))
    			return -EINVAL;
    		if (!(txc->modes & ADJ_OFFSET_READONLY) &&
    		    !capable(CAP_SYS_TIME))
    			return -EPERM;
    	} else {
    		/* In order to modify anything, you gotta be super-user! */
    		if (txc->modes && !capable(CAP_SYS_TIME))
    			return -EPERM;
    --snip--
    

    という判定をしています。この中で、capable(CAP_SYS_TIME)により、プロセスがCAP_SYS_TIMEケーパビリティを持っているかを確認しているというわけです。

  4. この様に、ケーパビリティを与えることで、本来特権が必要とされる操作が出来るということは、「動作する際に必要とされるシステムコール中で、権限チェックとして呼び出されている中で必要とされるケーパビリティが与えられているので、その操作が行える」という事を意味しています。

まとめ

Linuxケーパビリティを用いると、本来特権の全ての権限を必要としないプログラムに対して、制限された(本当に必要な)権限だけを分割して与えることが出来ます。ケーパビリティはOSレベルでの不要な権限を排除するという意味で、非常に使えるものですので、ご存じない方は是非この機会に触れて頂ければと思います。


セキュリティ系連載案内


OSSに関するお困りごとは サイオス OSSよろず相談室まで

サイオスOSSよろず相談室 では、OSSを利用する中で発生する問題に対し、長年培ってきた技術力・サポート力をもって企業のOSS活用を強力に支援します。Red Hat Enterprise Linux のほか、CentOS をご利用されている環境でのサポートも提供いたします。

前へ

Github Actionsを使ったOWASP Zap Baseline スキャンの説明

次へ

Linux Kernel Capability(Linuxカーネルケーパビリティ)に関して(2020年版) - Part2