Ich sehe was, was du nicht siehst

Posted by jonas on Saturday, January 17, 2015

Um die Privatsphäre unserer User zu verbessern, setzen wir bei uns schon seit längerer Zeit die mount-Option hidepid ein, die dafür sorgt, dass User nur ihre eigenen Prozesse sehen können, aber nicht die Prozesse anderer User. Die entsprechende Funktionalität ist zwar erst ab dem Linux-Kernel 3.2+ verfügbar; Red Hat hat sie aber in die Kernelversionen 2.6.18 bzw. 2.6.32 zurückportiert, die bei Red Hat Enterprise Linux und damit auch bei CentOS 5 und 6 zum Einsatz kommen. So weit, so gut.

Als uns nun einer unserer User schrieb, er habe einen Tor-Prozess gestartet und würde jenen aber nicht in seiner Prozessliste sehen, dachten wir offen gesagt zunächst an ein PEBCAK-Problem - konnten uns aber kurz darauf davon überzeugen, dass dem tatsächlich so ist, und konnten das mit einer eigenen Installation auch validieren. Hier mal ein Blick auf die Prozessliste des Users tortest, ausgeführt als root:

[root@andromeda ~]# ps afux | grep ^tortest
tortest  22373  0.0  0.0 104104  1908 ?        S    18:54   0:00  |   |           \_ sshd: tortest@pts/0
tortest  22374  0.3  0.0 111104  4712 pts/0    Ss+  18:54   0:00  |   |               \_ -bash
tortest  16808  0.0  0.0   4104   248 ?        S     2014   5:04  |   |   \_ /command/svscan /home/tortest/service
tortest  16833  0.0  0.0   3932   204 ?        S     2014   0:00  |   |       \_ supervise tor
tortest  22444 10.8  0.5  86760 41288 ?        S    18:54   0:01  |   |       |   \_ /home/tortest/bin/tor -f /home/tortest/etc/tor/torrc
tortest  16834  0.0  0.0   3932   200 ?        S     2014   0:00  |   |       \_ supervise log
tortest  16836  0.0  0.0   4076   264 ?        S     2014   0:00  |   |       |   \_ multilog t ./main

So weit, so gut; der Tor-Prozess mit der PID 22444 ist gut erkennbar. Aus Usersicht sieht’s aber so aus.

[tortest@andromeda ~]$ ps fx
  PID TTY      STAT   TIME COMMAND
22374 pts/0    Ss     0:00 -bash
26011 pts/0    R+     0:00  \_ ps fx
16808 ?        S      5:04 /command/svscan /home/tortest/service
16833 ?        S      0:00  \_ supervise tor
16834 ?        S      0:00  \_ supervise log
16836 ?        S      0:00  |   \_ multilog t ./main

Huh..? Wie man sieht, ist eigentlich alles da, der Tor-Prozess hingegen nicht (und wer aufmerksam hinschaut, stellt auch fest, dass der erste Prozess die bash ist, und neben Tor also auch der sshd-Prozess fehlt, obwohl jener unter der betreffenden User-ID läuft). Ein testweises mount -o remount,rw,hidepid=0 ließ die betreffenden Prozesse wieder in der als User generierten Prozessliste erscheinen; es war also klar, dass das Problem irgendwie mit hidepid zusammenhängt. Aber was ist an sshd und tor denn anders? Beruhigenderweise wollte auch den umstehenden Anwesenden so ad hoc erstmal nichts dazu einfallen.

Mit etwas Recherche ließ sich zunächst schon mal eine Merkwürdigkeit feststellen. Wenn wir mal ins Proc-Verzeichnis des supervise-Prozesses von Tor reinschauen, so sieht das erwartungsgemäß so aus:

[root@andromeda ~]# ls -l /proc/16833 | head -6
insgesamt 0
dr-xr-xr-x 2 tortest tortest 0 17. Jan 19:03 attr
-rw-r--r-- 1 tortest tortest 0 17. Jan 19:03 autogroup
-r-------- 1 tortest tortest 0 17. Jan 19:03 auxv
-r--r--r-- 1 tortest tortest 0 17. Jan 19:03 cgroup
--w------- 1 tortest tortest 0 17. Jan 19:03 clear_refs

Das Proc-Verzeichnis von Tor selbst hingegen sieht so aus:

[root@andromeda ~]# ls -l /proc/22444 | head -6
insgesamt 0
dr-xr-xr-x 2 tortest tortest 0 17. Jan 19:03 attr
-rw-r--r-- 1 root    root    0 17. Jan 19:03 autogroup
-r-------- 1 root    root    0 17. Jan 19:03 auxv
-r--r--r-- 1 root    root    0 17. Jan 19:03 cgroup
--w------- 1 root    root    0 17. Jan 19:03 clear_refs

Huh..? Wenn der User tortest hier einen Prozess startet - wie kann das denn bitte sein, dass dann fast der gesamte Inhalt im zugehörigen Proc-Verzeichnis root gehört? Recherche war angesagt, und für die Erläuterung muss ich nochmal etwas ausholen. Also.

Prinzipiell kann ein User nach Belieben eigene Prozesse mit ptrace kontrollieren. Das bedeutet, ein Prozess kann den Zustand eines anderen Prozesses sowohl einsehen (also den Prozessstatus inklusive RAM-Inhalt) als auch verändern. Auf den ersten Blick mag das nicht allzu kritisch erscheinen - ist es aber doch. Man denke zum Beispiel an einen Browser, in dem man Passwörter gespeichert hat, die mit einem Master-Passwort gesichert sind. Will man sich dann auf einer Website einloggen, muss man logischerweise das Master-Passwort eingeben, um den Passwortspeicher zu “entsperren”; der Browser hält jenen anschließend im RAM. Er selbst muss die Passwörter ja logischerweise im Klartext wissen. Die Preisfrage ist aber: Will ich, dass beliebige andere meiner Prozesse - was weiß ich, ich habe ja unter meiner User-ID vielleicht noch einen Mailclient, einen IRC-Client und sonst noch irgendwelche Software laufen - also, will ich, dass jene anderen Prozesse nun einfach meinen Browser ptracen und somit zum Beispiel auf dessen entsperrten Passwortspeicher zugreifen können? Vermutlich, äh, eher nicht. Und dafür gibt es einen Syscall, den ein Prozess ausführen kann, um ptrace “du darfst hier nicht rein” sagen zu können:

prctl(PR_SET_DUMPABLE, 0);

Tools, die genau das tun, sind - neben Browsern - zum Beispiel: SSH. Tor. D’Oh!

Im Linux-Kernel findet man in fs/proc/base.c in proc_pid_make_inode, was da genau passiert: Die Einträge des zur entsprechenden Prozess-ID gehörenden Verzeichnisses werden nur dann dem User übereignet, wenn (task_dumpable(task)) zutrifft. Was entsprechend nicht mehr zutrifft, wenn der Task das aktiv verhindert hat. (Neuere Kernel können übrigens Yama als LSM wählen, um für das Problem unerwünschter ptraces eine generellere Lösung zu implementieren, die grob vereinfacht darauf basiert, dass nur ein Elternprozess einen Kindprozess mit ptrace kontrollieren kann, aber nicht mehr irgendwelche anderen Prozesse.)

Schauen wir - wiederum in fs/proc/base.c - was in has_pid_permissions geprüft wird:

static bool has_pid_permissions(struct pid_namespace *pid,
    			 struct task_struct *task,
	    		 int hide_pid_min)
{
    if (pid->hide_pid < hide_pid_min)
	    return true;
    if (in_group_p(pid->pid_gid))
	    return true;
    return ptrace_may_access(task, PTRACE_MODE_READ);
}

Also, ein Prozess in /proc/PID darf dann näher betrachtet werden, wenn der hidepid-Wert ausreichend gering ist, oder wenn der Betrachter Mitglied einer bestimmten Gruppe ist, die für hidepid als Ausnahmegruppe gilt, deren Mitglieder dann doch alle Prozesse sehen dürfen (so wie sonst nur root). Ist beides nicht gegeben, macht hidepid die Entscheidung davon abhängig, ob der User den betreffenden Prozess mit ptrace untersuchen darf, weil er das eben nur mit eigenen Prozessen machen darf - aber wenn jener Prozess eben mittels prctl(PR_SET_DUMPABLE, 0) genau das verhindert, hat das implizit auch zur Folge, dass bei Anwendung der hidepid-Mountoption der User den Prozess eben auch nicht mehr sehen kann. Nichtsdestotrotz kann er ihn sehr wohl noch mit den üblichen Signalen kontrollieren, wie ein einfaches Beispiel zeigt: Wir starten uns einen ssh-agent …

[jonas@andromeda ~]$ ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-hxfMI16010/agent.16010; export SSH_AUTH_SOCK;
SSH_AGENT_PID=16011; export SSH_AGENT_PID;
echo Agent pid 16011;

… der aber nicht in der Prozessliste erscheint, da er ptrace verbietet:

[jonas@andromeda ~]$ ps fx
  PID TTY      STAT   TIME COMMAND
12512 pts/4    Ss     0:00 -bash
16012 pts/4    R+     0:00  \_ ps fx

Nichtdestotrotz ist er da und auch steuerbar, wie man daran sieht, dass ein Killen des Prozesses problemlos möglich ist (das erste - funktionierende - kill liefert keine Ausgabe, was zeigt, dass der zu killende Prozess gefunden wurde; das zweite kill zeigt, dass das SIGTERM angekommen ist):

[jonas@andromeda ~]$ kill 16011
[jonas@andromeda ~]$ kill 16011
-bash: kill: (16011) - Kein passender Prozess gefunden

Die Crux an der Sache ist: Ändern können wir an dieser Situation derzeit erstmal leider nichts; auf hidepid zu verzichten, ist aus Privatsphäregründen leider keine schöne Option. Eine andere Lösung, die mit den offiziellen RHEL-/CentOS-Kerneln funktioniert, ist uns hier derzeit leider nicht bekannt. :-(