Shellskripte aus der Stümper-Liga – Folge 19: Shebang
Bash Bashing
Am Anfang waren das Doppelkreuz und das Ausrufezeichen – zumindest Shellskripte beginnen meist immer noch so. Der Erfinder Dennis Ritchie hat damit viel Leid über Anwender gebracht.
Am Anfang waren das Doppelkreuz und das Ausrufezeichen – zumindest Shellskripte beginnen meist immer noch so. Der Erfinder Dennis Ritchie hat damit viel Leid über Anwender gebracht.
Wenn ein Benutzer interaktiv Programme aufruft, entfaltet die Shell mehr Magie, als es zunächst den Anschein hat. Das gilt umso mehr, wenn es sich um Skripte handelt. Die Bash ist so allgegenwärtig, dass sich mancher gar nicht mehr bewusst macht, dass auch sie aus Code besteht, der einem Ablauf folgt.
Mit dem Entgegennehmen der Tasten hat die Bash selbst nur wenig am Hut. Das erledigt der Terminaltreiber, der entweder lokal an der Konsole oder über Umwege wie SSH oder einen X-Client hinweg ein Pseudoterminal beschickt. Das leitet die Eingaben an die interaktive Shell, die vorher noch einen Prompt in Richtung Terminal geschickt hat.
Die Shell wartet, bis sie ein Zeilenende empfängt, und interpretiert dann den bis dahin erhaltenen String nach der Bash-Syntax. Handelt es sich um ein internes Kommando wie eine Schleife mit »while«
, eine Bedingung mit »if«
oder eine Zuweisung mit »=«
, kann sich das Shell-Executable direkt darum kümmern. Gleiches gilt für die vielen Shell-Builtins wie »ulimit«
oder »history«
oder selbst definierte Shellfunktionen.
Triff keiner dieser Fälle zu, dann vermutet die Bash, dass der Anwender ein externes Programm starten möchte. Das muss sie aber erst einmal finden. Dazu iteriert sie über den Inhalt der Umgebungsvariablen »PATH«
, den sie vorher noch nach Doppelpunkten aufgetrennt hat.
Jedes Element öffnet sie als Pfad und sucht dort nach einer Datei mit dem Namen des eingegebenen Kommandos, das auch das Execute-Flag gesetzt hat. Aktuelle Versionen der Bash verwalten dafür einen Cache, um nicht bei jedem Aufruf den kompletten Pfad im realen Dateisystem durchsuchen zu müssen, was aber kaum etwas an der beschriebenen Vorgehensweise ändert.
Wird der Kommando-Interpreter fündig, überlässt er die weitere Arbeit zunächst dem Kernel, indem er den Systemaufruf »execve()«
in einem neuen Prozess aktiviert. Er übergibt der Funktion den gefundenen vollständigen Pfad, den eingetippten Kommandonamen und, falls es noch Argumente auf der Kommandozeile gibt, die Argumente. Dort öffnet der Kernel die Datei und prüft exakt die ersten beiden Bytes. Zeigen diese an, dass es sich um ein echtes Binary handelt, etwa im ELF-Format, startet er es direkt. Die Shell wartet unterdessen, bis das Programm terminiert, und verarbeitet anschließend den nächsten Befehl.
Lauten die ersten beiden Bytes jedoch »#!«
, gelangt der Kontrollfluss auf einigen verschlungenen Pfaden schließlich zu »linux/fs/binfmt_script.c«
im Kernel [1], Listing 1 zeigt einen vereinfachten Code. Nun rollt ein komplizierter Mechanismus an: Die Zeilen 10 bis 19 begrenzen die »#!«
-Zeile mit einer Ende-Markierung und entfernen davor eventuell auftretende Whitespaces. Zeile 20 eliminiert Leerzeichen, die direkt dem Shebang folgen, da Unix-Schöpfer Dennis Ritchie dieses Verhalten in einer E-Mail von 1980 explizit erlaubt hat [2].
Bleibt nach dem Trimmen in Zeile 21 kein sinnvoller String mehr übrig, gibt es keinen Interpreter-Namen und die Funktion meldet in Zeile 22 einen Fehler. Ist das nicht der Fall, steht nun in »i_name«
der zu nutzende Interpreter, meist »/bin/sh«
. Die Zeilen 24 bis 29 picken sich dann exakt ein Argument aus der verbleibenden Zeile raus – wenn es existiert – und speichern es in »i_arg«
. Den Rest der ersten Zeile ignoriert der Kernel.
Schließlich ruft die Funktion sich selbst rekursiv mit dem so extrahierten Kommando-Interpreter innerhalb des Kernels wieder auf und fügt den ursprünglichen Kommandonamen und die Argumente wieder an. Steht beispielsweise in »/tmp/runme«
die Anfangszeile »#!/foo/bar --myarg«
, gilt »PATH=/tmp«
, und tippt der Anwender »runme alpha beta«
ein, führt der Kernel tatsächlich den Aufruf »execve("/foo/bar", "bar", "--myarg", "/tmp/runme", "alpha", "beta")«
aus. Scheitert jedoch der Kernel darin, auf diese Weise einen Interpreter zu bestimmen, nimmt die Bash die Ausführung in eigene Hände und vermutet, dass die gefundene Datei gültigen Shellcode enthält, öffnet sie und interpretiert ihren Inhalt.
Listing 1
Kernel parst Shebang
01 static int
02 load_script(struct linux_binprm *bprm, struct pt_regs *regs) {
03 const char *i_arg, *i_name;
04 char *cp;
05 struct file *file;
06
07 if ((bprm->buf[0] != '#') || (bprm->buf[1] != '!'))
08 return -ENOEXEC;
09
10 if ((cp = strchr(bprm->buf, '\n')) == NULL)
11 cp = bprm->buf+BINPRM_BUF_SIZE-1;
12 *cp = '\0';
13 while (cp > bprm->buf) {
14 cp--;
15 if ((*cp == ' ') || (*cp == '\t'))
16 *cp = '\0';
17 else
18 break;
19 }
20 for (cp = bprm->buf+2; (*cp == ' ') || (*cp == '\t'); cp++);
21 if (*cp == '\0')
22 return -ENOEXEC; /* No interpreter name found */
23 i_name = cp;
24 i_arg = NULL;
25 for ( ; *cp && (*cp != ' ') && (*cp != '\t'); cp++);
26 while ((*cp == ' ') || (*cp == '\t'))
27 *cp++ = '\0';
28 if (*cp)
29 i_arg = cp;
30 [...]
31 }
Umfang: 2 Heftseiten
Preis € 0,99
(inkl. 19% MwSt.)
Alle Rezensionen aus dem Linux-Magazin
Im Insecurity Bulletin widmet sich Mark Vogelsberger aktuellen Sicherheitslücken sowie Hintergründen und Security-Grundlagen. mehr...