profs の hidepid オプションの値

procfs のマウントオプションに hidepid というオプションがあることを知った。
man で確認すると 0~3 の値を取るらしい。

hidepid=n (since Linux 3.3)
       This option controls who can access the information in /proc/[pid] directories.  The argument, n, is one of the following values:

       0   Everybody may access all /proc/[pid] directories.  This is the traditional behavior, and the default if this mount option is not specified.

       1   Users may not access files and subdirectories inside  any  /proc/[pid]  directories  but  their  own  (the  /proc/[pid]  directories  themselves  remain  visible).   Sensitive  files  such  as
           /proc/[pid]/cmdline  and  /proc/[pid]/status  are  now  protected against other users.  This makes it impossible to learn whether any user is running a specific program (so long as the program
           doesn't otherwise reveal itself by its behavior).

       2   As for mode 1, but in addition the /proc/[pid] directories belonging to other users become invisible.  This means that /proc/[pid] entries can no longer be used to discover  the  PIDs  on  the
           system.   This  doesn't  hide the fact that a process with a specific PID value exists (it can be learned by other means, for example, by "kill -0 $PID"), but it hides a process's UID and GID,
           which could otherwise be learned by employing stat(2) on a /proc/[pid] directory.  This greatly complicates an attacker's task of gathering information about running processes (e.g., discover‐
           ing whether some daemon is running with elevated privileges, whether another user is running some sensitive program, whether other users are running any program at all, and so on).

man には書かれていないが実はカーネル v5.8 からは 4 という値があって、ptrace 可能な PID のみを返すようにできるらしい。なぜ3を飛ばして4なのか…(歴史的経緯は追っていない)。
また、subset=pid オプションも追加されていて、PID以外のディレクトリやファイル(例えば /proc/sysrq-trigger など)も隠せるようになっている。コンテナ環境に /proc をそのままマウントしてしまうと様々な Attack Surafaces を生み出してしまうが、これで安全にマウントできて便利っぽい。これについては後述する。

hidepid を設定した場合の挙動の確認

デフォルト(つまり hidepid=0)では普通に別ユーザーのプロセスが確認できる。

ubuntu@sandbox:~$ ps aux | head -n 3
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.2 225720  9472 ?        Ss   Aug19   0:07 /sbin/init
root         2  0.0  0.0      0     0 ?        S    Aug19   0:00 [kthreadd]

hidepid=1 で procfs をマウントすると別ユーザーのプロセスは確認できない。

ubuntu@sandbox:~$ sudo mount /proc -o remount,hidepid=1
ubuntu@sandbox:~$ ps aux | head -n 3
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
ubuntu    1549  0.0  0.1  76820  7704 ?        Ss   Aug19   0:00 /lib/systemd/systemd --user
ubuntu   27136  0.0  0.1  23260  5376 pts/1    Ss   16:33   0:00 bash

/proc を確認すると PID 自体は見えるが、その配下のファイルやディレクトリは確認できない。

ubuntu@sandbox:~$ ls -l /proc/ | head
total 0
dr-xr-xr-x  9 root             root              0 Aug 14 14:45 1
dr-xr-xr-x  9 root             root              0 Aug 22 17:06 10
dr-xr-xr-x  9 root             root              0 Aug 22 17:06 1090
dr-xr-xr-x  9 daemon           daemon            0 Aug 22 17:06 1093
dr-xr-xr-x  9 root             root              0 Aug 22 17:06 1098
dr-xr-xr-x  9 root             root              0 Aug 22 17:06 1099
dr-xr-xr-x  9 root             root              0 Aug 22 17:06 11
dr-xr-xr-x  9 root             root              0 Aug 22 17:06 1106
dr-xr-xr-x  9 messagebus       messagebus        0 Aug 22 17:06 1108

ubuntu@sandbox:~$ ls /proc/1
ls: cannot open directory '/proc/1': Operation not permitted

続いて hidepid=2 でマウントしてみる。man にあるように自分のプロセスの PID の以外は見えなくなっている。

ubuntu@sandbox:~$ sudo mount /proc -o remount,hidepid=2
ubuntu@sandbox:~$ ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
ubuntu    1549  0.0  0.1  76820  7704 ?        Ss   Aug19   0:00 /lib/systemd/systemd --user
ubuntu   27136  0.0  0.1  23260  5376 pts/1    Ss   16:33   0:00 bash
ubuntu   27287  0.0  0.0  37796  3236 pts/1    R+   17:08   0:00 ps aux

ubuntu@sandbox:~$ ls -l /proc/ | head
total 0
dr-xr-xr-x  9 ubuntu ubuntu  0 Aug 14 14:45 1549
dr-xr-xr-x  9 ubuntu ubuntu  0 Aug 22 17:08 27136
dr-xr-xr-x  9 ubuntu ubuntu  0 Aug 22 17:09 27292
dr-xr-xr-x  9 ubuntu ubuntu  0 Aug 22 17:09 27293
dr-xr-xr-x  2 root   root    0 Aug 14 19:04 acpi
-r--r--r--  1 root   root    0 Aug 22 17:09 buddyinfo
dr-xr-xr-x  4 root   root    0 Aug 14 19:04 bus
-r--r--r--  1 root   root    0 Aug 22 17:09 cgroups
-r--r--r--  1 root   root    0 Aug 22 17:09 cmdline

ただし、kill -0 $PID でプロセスが存在していることは確認できる。

ubuntu@sandbox:~$ kill -0 1
bash: kill: (1) - Operation not permitted
ubuntu@sandbox:~$ kill -0 2
bash: kill: (2) - Operation not permitted
ubuntu@sandbox:~$ kill -0 123456
bash: kill: (123456) - No such process

subset=pid で PID 以外のファイルも隠す

hidepid オプションとは異なるが、確認したので一応メモ。前述した通り、カーネルv5.8以降で subset=pid というオプションが追加されている。

ssm-user@ip-10-0-1-113:~$ sudo mount /proc -o remount,hidepid=2,subset=pid
ssm-user@ip-10-0-1-113:~$ ls /proc/
1073  960  992  self  thread-self

ssm-user@ip-10-0-1-113:~$ cat /proc/uptime
cat: /proc/uptime: No such file or directory

ssm-user@ip-10-0-1-113:~$ echo c > /proc/sysrq-trigger
bash: /proc/sysra-trigger: No such file or directory

コンテナを作るときはセキュリティ上の問題があるので特定のファイルを ReadOnly にしたり bind mount したりする必要があるが、このオプションが代替となっていきそうだ。

実装を見てみる

カーネルのコード的にはどのような実装になっているか確認してみる(5.8.3でみています)。
inode_oprations.permission のコールバック関数 proc_pid_permission() という関数でチェックをしている。

static int proc_pid_permission(struct inode *inode, int mask)
{
	struct proc_fs_info *fs_info = proc_sb_info(inode->i_sb);
	struct task_struct *task;
	bool has_perms;

	task = get_proc_task(inode);
	if (!task)
		return -ESRCH; has_perms = has_pid_permissions(fs_info, task, HIDEPID_NO_ACCESS);
	put_task_struct(task);

	if (!has_perms) {
		if (fs_info->hide_pid == HIDEPID_INVISIBLE) {
			/*
			 * Let's make getdents(), stat(), and open()
			 * consistent with each other.  If a process
			 * may not stat() a file, it shouldn't be seen
			 * in procfs at all.
			 */
			return -ENOENT;
		}

		return -EPERM;
	}
	return generic_permission(inode, mask);
}

この関数では has_pid_permissions() を呼び出している。

has_perms = has_pid_permissions(fs_info, task, HIDEPID_NO_ACCESS);
/*
 * May current process learn task's sched/cmdline info (for hide_pid_min=1)
 * or euid/egid (for hide_pid_min=2)?
 */
static bool has_pid_permissions(struct proc_fs_info *fs_info,
				 struct task_struct *task,
				 enum proc_hidepid hide_pid_min)
{
	/*
	 * If 'hidpid' mount option is set force a ptrace check,
	 * we indicate that we are using a filesystem syscall
	 * by passing PTRACE_MODE_READ_FSCREDS
	 */
	if (fs_info->hide_pid == HIDEPID_NOT_PTRACEABLE)
		return ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS);

	if (fs_info->hide_pid < hide_pid_min)
		return true;
	if (in_group_p(fs_info->pid_gid))
		return true;
	return ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS);
}

hidepid の値の定義はこれ。

/* definitions for hide_pid field */
enum proc_hidepid {
	HIDEPID_OFF	  = 0,
	HIDEPID_NO_ACCESS = 1,
	HIDEPID_INVISIBLE = 2,
	HIDEPID_NOT_PTRACEABLE = 4, /* Limit pids to only ptraceable pids */
};

HIDEPID_NOT_PTRACEABLE は前述した v5.8 から入っている値。ptrace 可能な PID のみにアクセスできる。

if (fs_info->hide_pid < hide_pid_min) のところを見てみる。if (fs_info->hide_pid < HIDEPID_NO_ACCESS) なので1より小さければ(つまり0であるかをみている) true になる。
has_permsfalse のときには hidepid の値によって返ってくるエラーが異なる。

	if (!has_perms) {
		if (fs_info->hide_pid == HIDEPID_INVISIBLE) {
			/*
			 * Let's make getdents(), stat(), and open()
			 * consistent with each other.  If a process
			 * may not stat() a file, it shouldn't be seen
			 * in procfs at all.
			 */
			return -ENOENT;
		}

		return -EPERM;
	}

このように hidepid=2 のときは -ENOENT が返るが、hidepid=1 の場合は -EPERM が返る。
これは hidepid=1 が PID 自体は見えるのに対して hidepid=2 の場合は PID 自体も見えなくなるのと一致している。

ubuntu@sandbox:~$ sudo mount /proc -o remount,hidepid=1
ubuntu@sandbox:~$ stat /proc/1/cmdline
stat: cannot stat '/proc/1/cmdline': Operation not permitted

ubuntu@sandbox:~$ sudo mount /proc -o remount,hidepid=2
ubuntu@sandbox:~$ stat /proc/1/cmdline
stat: cannot stat '/proc/1/cmdline': No such file or directory

ちなみに ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)hidepid=1,2 のときのアクセスチェックをしているが、 PTRACE_MODE_READ_FSCREDSPTRACE_MODE_READ | PTRACE_MODE_FSCREDS を指す。
つまり /proc/[pid]/stat などのファイルへの読み込み権限と正しいUID/GIDを持っているかのチェックかな。ptrace API 便利。

has_pid_permissions()proc_pid_readdir()pid_getattr() などでも呼び出されており、これらも file_operationsinode_oprations のコールバック関数として使用されているようだ。

References