こんにちは。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種類のケーパビリティセット(上述のケーパビリティの組み合わせ)を持ちます。
- 許可(permitted): スレッドが持つことになっている実効ケーパビリティの限定的なスーパーセット。
- 継承可能(inheritable): execve()前後で保持されるケーパビリティセット
- 実効(effective): カーネルがスレッドの権限(permission)をチェックする時に使用するケーパビリティセット
この中で、Kernelは実際には実効(effective)ケーパビリティセットから、必要となるケーパビリティをプロセスが持っているかどうかを確認して、アクセス制御に利用しています。
ファイルケーパビリティについて
2.6.24移行のカーネルで実装されたもので、実行ファイルにsetcapを用いてケーパビリティセットを対応付けることが出来ます。ファイルケーパビリティセットは拡張属性(SELinux等の強制アクセス制御のラベル保存にも使用されており、それぞれのファイルに設定されている)に保存されていて、execve後のスレッドのケーパビリティセットを決定する要素の一つとなっています。
このファイルケーパビリティにも、3つのケーパビリティセットが定義されています。
- 許可(permitted): スレッドの継承可能(Inheritable)ケーパビリティに関わらず、そのスレッドに自動的に許可されるケーパビリティ
- 継承可能(inheritable): このセットと、スレッドの継承可能(inheritable)ケーパビリティのANDが取られて、execve後に有効となる継承可能ケーパビリティが決定される。
- 実効(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でプログラムを起動した場合の、ファイルケーパビリティの扱いは以下のようになります。
- プロセスの実UID、或いは実効UID(EUID)が0の場合には、ファイルケーパビリティの
- Inheritable(継承)
- Permitted(許可)
に関しては無視されます。その変わり、ファイルケーパビリティのそれらの全てのセットが1(有効)ということになります。
- EUIDが0の場合、またファイルケーパビリティのEffective(実効)ビットが1になっているケーパビリティに関しては1(有効である)と定義します。
UID 0で実行した場合、或いはSUIDビットがついているプログラムを実行した場合には、これらのファイルケーパビリティの値を持って、前述のexecした際のケーパビリティセット類が決定されます。
上記の例外として、SUIDビットが立っているバイナリにケーパビリティが付与されている場合があります。この場合には、プロセスのケーパビリティを計算する際のファイルケーパビリティは上述のように全てのセットが1(有効)という事にはならず、バイナリに与えられたファイルケーパビリティによって計算されます。
ケーパビリティの確認
ここまでがケーパビリティの説明です。わかりにくい所もあると思いますので、以降で実際にケーパビリティを調節したりして確認してみましょう。
ケーパビリティ確認の準備
以下、システムはDebian 10(buster)を使用します。Linux Kernelは5.7.2を使用しています。
- 準備として、まず(入っていない場合には)「libcap2-bin」パッケージをインストールします。
- また、ケーパビリティを扱いやすい「libcap-ng」パッケージをインストールします。
root@localhost:~# apt install libcap-ng0 libcap-ng-dev libcap-ng-utils
- ファイルシステムの拡張属性を見たいため、拡張属性を扱う「attr」パッケージをインストールします。
root@localhost:~# apt install attr
root@localhost:~# apt install libcap2-bin
ケーパビリティ確認
- まずは、プロセスにどの様なケーパビリティセットが設定されているかを確認してみましょう。プロセスに設定されているケーパビリティセット類は、/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を使用します。
- また、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)のビットが立っていることがわかります。
- 基本的には前述の/proc以下の情報で全てのプロセスのケーパビリティセットを見る事ができますが、getpcaps “プロセスID番号”で、そのプロセスに設定されているケーパビリティをよりわかりやすく見る事が出来ます。例えばinitプロセス(1)を見てみると
- また、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のケーパビリティのみを持っていることがわかります。
- ここで、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ケーパビリティとケーパビリティバウンディングセットに設定していることがわかります。
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までの全てのケーパビリティを持っていることがわかります。
ケーパビリティ実験
ここで実際に、ケーパビリティを調整するとどうなるのかを確認してみましょう。Debianではlibcap2-binパッケージに入っている「capsh」を使用します。capshですが、ケーパビリティバウンディングセットを変更することで、お手軽にケーパビリティのテストが出来るツールです。rootで起動するツールになります。
- 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になります。
- 次に、”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--
となっており、全てのケーパビリティを持っていることがわかります。
- 次に、ケーパビリティを減らしてみます。–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にされた状態になっています。
- 前述の状態で、”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番以下の所謂特権ポートは使用できなくなっています。
- 一旦、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番ポートを使用できていることがわかります。
- また、この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がどこで扱われているかを確認してみます。
システムコールは(例外も有りますが)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()を呼び出しています。
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()を呼び出しています。
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ケーパビリティを持っているかを確認しているというわけです。
この様に、ケーパビリティを与えることで、本来特権が必要とされる操作が出来るということは、「動作する際に必要とされるシステムコール中で、権限チェックとして呼び出されている中で必要とされるケーパビリティが与えられているので、その操作が行える」という事を意味しています。
まとめ
Linuxケーパビリティを用いると、本来特権の全ての権限を必要としないプログラムに対して、制限された(本当に必要な)権限だけを分割して与えることが出来ます。ケーパビリティはOSレベルでの不要な権限を排除するという意味で、非常に使えるものですので、ご存じない方は是非この機会に触れて頂ければと思います。
セキュリティ系連載案内
- OSSセキュリティ技術の会による日経Linuxでの連載「IoT時代の最新SELinux入門」がITPro上で読めるようになりました。技術の会代表で第一人者である中村さん等による、最新のSELinuxの情報やコマンド類等も更新されているのでお薦めです。
- OSSセキュリティ技術の会によるThinkITでの連載「開発者のためのセキュリティ実践講座」がThinkIT上で開始しました。技術の会の中の人間で、最新の代表的なOSSセキュリティ技術を紹介していきます。
- OSSセキュリティ技術の会により、ThinkITでLinuxSecuritySummit 2018のレポートが紹介されています。
- OSSセキュリティ技術の会の面により、@ITで「OSS脆弱性ウォッチ」が連載されています。
- OSSセキュリティ技術の会の面により、@ITで「OpenSCAPで脆弱性対策はどう変わる?」が連載されています。
- OSSセキュリティ技術の会のメンバーにより、@ITで「Berkeley Packet Filter(BPF)入門」が連載されています。