blog · git · desktop · images · contact
2010-06-30
Vor einiger Zeit hatte ich einen genaueren Blick auf den Shebang-Mechanismus von Linux geworfen. Mit diesem Wissen als Grundlage soll nun die Frage beantwortet werden, weshalb bei Skripten eigentlich SUID-Bit und SGID-Bit keinen Effekt haben.
Zur Erinnerung, der interessante Teil der Prozesserzeugung im Kernel
beginnt in der Funktion do_execve()
Danach wird anhand einer
Liste in der Funktion search_binary_handler()
entschieden, um
welche Art von ausführbarer Datei es sich handelt -- ist es eine
ELF-Datei,
ein Skript oder etwas ganz anderes?
Nun der Reihe nach im Bezug auf besagte Bits. Nach einigen anderen
Vorbereitungen wird in do_execve()
die Funktion
prepare_binprm()
aufgerufen. Dort wird nachgeschaut, ob diese
Bits beim Mountpoint der fraglichen Datei Beachtung finden sollen, und
falls ja, ob denn eines gesetzt ist. Das passiert zu einem recht frühen
Zeitpunkt und zwar noch bevor klar ist, welcher Handler sich später
einmal um die Datei kümmern wird -- sprich, vor dem Aufruf von
search_binary_handler()
. Zu Beginn werden also tatsächlich erst einmal
UID und GID der Skriptdatei übernommen. Gekürzter Ausschnitt:
int prepare_binprm(struct linux_binprm *bprm)
{
/* ... */
struct inode * inode = bprm->file->f_path.dentry->d_inode;
mode = inode->i_mode;
/* clear any previous set[ug]id data from a previous binary */
bprm->cred->euid = current_euid();
bprm->cred->egid = current_egid();
if (!(bprm->file->f_path.mnt->mnt_flags & MNT_NOSUID)) {
/* Set-uid? */
if (mode & S_ISUID) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->euid = inode->i_uid;
}
/* Set-gid? */
/*
* If setgid is set but no group execute bit then this
* is a candidate for mandatory locking, not a setgid
* executable.
*/
if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP)) {
bprm->per_clear |= PER_CLEAR_ON_SETID;
bprm->cred->egid = inode->i_gid;
}
}
/* ... */
}
Dabei fällt übrigens auf, dass das SGID-Bit nicht greift, wenn die Datei nicht explizit für die Gruppe ausführbar ist -- was schon eine gewisse Relevanz besitzt, da die Datei für einen Benutzer immernoch über die "Others"-Rechte ausführbar sein könnte. Aber das nur am Rande.
Diese Zuweisung der effektiven IDs findet also vor dem Aufruf von
search_binary_handler()
statt, in dieser Funktion springt dann aber
letztendlich doch der Handler für Shebang-Dateien an. Damit geht es
bekanntermaßen zu load_script()
. Wird hinter den Shebang-Zeichen
ein gültiger Interpreter gefunden, kommen wir zu der Stelle, an der sich
das Geheimnis lüften lässt. Wieder gekürzt:
static int load_script(struct linux_binprm *bprm,struct pt_regs *regs)
{
/* ... */
file = open_exec(interp);
bprm->file = file;
retval = prepare_binprm(bprm);
/* ... */
}
Es erfolgt hier also ein erneuter Aufruf von prepare_binprm()
. Vorher
wird aber "file" auf den Eintrag des Interpreters gesetzt und damit wird
diesmal die Binary des Interpreters abgefragt. Von hier aus kann es dann
natürlich rekursiv weitergehen, falls der vermeintliche Interpreter
wieder eine Datei mit Shebang ist. Damit könnte auch dieser aktuell
gesetzte Wert später eventuell wieder überschrieben werden.
Fazit: Bei Skripten spielen SUID- und SGID-Bit keine Rolle, sondern nur die des jeweiligen Interpreters.
Noch einmal im Überblick:
do_execve
|-- prepare_binprm # angewandt auf Skriptdatei
`-- search_binary_handler
`-- load_script
|-- prepare_binprm # angewandt auf Interpreter
`-- [Rekursion]
Was man so im Netz zu diesem Thema findet, hört sich an manchen Stellen so an, als seien SUID- und SGID-Bit bei Skripten "deaktiviert", sprich, "eigentlich könnte man die Bits schon nutzen, aber wir haben da keine Lust drauf und wollen euch den Spaß verderben." ;) Ich gebe zu, dass der Gedanke gar nicht so abwegig ist. Schließlich wird durch den Shebang-Mechanismus ein hoher Grad an Transparenz geschaffen. Wenn man die Datei nicht öffnet und sich ihren Inhalt anschaut, merkt man unter Umständen gar nicht, dass es überhaupt ein Skript ist. Doch: Es wird niemals ein Prozess für das Skript als solches erzeugt -- sondern immer nur für den Interpreter. Damit ist das durchaus in sich schlüssig und keine willkürliche Festlegung, finde ich.
– Nachtrag, 02.04.2012:
Durch ein Blogposting von meillo auf dieses alte Usenet-Posting von Bill Joy gestoßen. Es ist von 1981. Leider ist der Kontext, also der ganze Thread, wohl nicht mehr da oder ich finde ihn nicht. Jedenfalls schreibt Bill da etwas über das Shebang-Feature in 4.1bsd auf der VAX. Und: Dort wurden tatsächlich das SGID- und SUID-Bit von Skripten interpretiert!
– Nachtrag, 22.06.2014:
Und nochmal über meillo bin ich auf diesen Shebang-Artikel von Sven Maschek gekommen. Hier werden viele verschiedene Systeme betrachtet und auch der Grund angesprochen, weshalb es keine so gute Idee ist, die S*ID-Bits bei Skripten zu aktivieren: Der Kernel kann nur bewirken, dass der Interpreter mit erweiterten Rechten startet -- nicht aber das eigentliche Skript. Wenn der Kernel den Interpreter gestartet hat und dieser dann "hochfährt", kann man -- wenn man schnell genug ist -- das Skript möglicherweise austauschen. Tatsächlich ist dieses Szenario sogar in der Wikipedia beschrieben.
Es wird dort auch angesprochen, dass es Möglichkeiten gibt, diesen Angriff zu verhindern. Zumindest in dem Linux-Code, den ich mir angeschaut hatte, ist das aber nicht drin.
In Linux ist es so, dass es sich einfach ergibt, dass S*ID-Bits nicht bei Shebang-Files greifen. Es ist keine Logik im Code, die sich explizit um die S*ID-Handhabung bei Skripten kümmert, sondern es ist eben eher so ein Nebeneffekt. Aus historischer Sicht (und die Historie hatte ich beim ursprünglichen Schreiben dieses Blogpostings nicht beachtet) könnte es aber durchaus Absicht sein, dass der Code so ist, wie er jetzt ist.
Es ist auch irgendwo verständlich. Selbst, wenn der Kernel alles tut, um sicherzustellen, dass nur genau dieses eine Skript mit erweiterten Rechten versehen wird, läuft immer noch eine Shell und im Regelfall die GNU Bash dann mit diesen Rechten. Wie viele Angriffsmöglichkeiten, gegen die der Kernel gar nichts tun kann, würde das wohl eröffnen?