blog · git · desktop · images · contact & privacy · gopher


Linux-Kunde, Teil 2: Warum kein SUID/SGID-Bit bei Skripten?

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?