Christian Seiler: Verständnisfehler Set-UID

Beitrag lesen

Hallo Dennis,

Was mir übrigens noch einfällt: Wenn der User root (id 0) im Spiel ist, dann ändert sich das Verhalten der set*uid-Funktionen übrigens teils gewaltig, d.h. meine Erklärungen vom vorigen Posting bitte nur auf User <-> User, nicht auf Root <-> User o.ä. beziehen!

Dein ursprüngliches Problem ist, dass der Kernel das setuid-Bit nicht auf Scripte anwendet.

Das scheint übrigens eine Debian Security-Policy zu sein oder so etwas in der Art, zumindest nach dem was meine bisherigen Recherchen ergeben haben. Offensichtlich wird das aber von den meisten anderen Distributionen genauso gehandhabt.

Naja, ok, man kann das glaub ich über Capabilities ändern, aber an sich macht das der Kernel immer so, außer man redet's ihm explizit aus.

  1. "/bin/ls"
  2. { "ls", "-l", NULL }
  3. environ

Woher kommt das eigentlich, dass man dem Programm als ersten Parameter immer noch mal seinen eigenen Namen (ls) bzw. den Pfad zum eigenen Namen (/bin/ls) übergibt? Macht man das einfach, weil das so üblich ist und jeder das so macht, oder steckt da ein tieferer Sinn dahinter?

Ganz einfach: Gebe ich auf der Shell folgenden Befehl ein:

ls -l datei

Dann steht ja hinterher im argv des ls-Programms folgendes: { 'ls', '-l', 'datei', NULL } - halt exakt so, wie's auf der Kommandozeile eingegeben wurde modulo Interpretationen durch die Shell.

Das Problem ist: Der Kernel weiß an Hand von 'ls' nicht, welche Datei auszufüren ist. Daher muss man execve() den Pfad zum _ausführbaren_ Programm direkt übergeben, das er dann wie eine Datei einliest. Wenn die Argumente das 'ls' jedoch nicht enthielten, dann wäre die einzige Möglichkeit, argv wieder so hinzukriegen, wie's ist, indem man '/bin/ls' in argv[0] schreibt - dann wäre das aber nicht das, was der User eingegeben hat.

Der Sinn des ganzen ist: Wenn irgend ein Programm eine Fehlermeldung ausgibt, dann ist es Konvention, dass das Programm seinen eigenen Namen mit angibt. Woher bekommt das Programm den? Über argv[0]. Wenn in argv[0] nur die ausführbare Datei stünde, wäre das eher schlecht ("/bin/ls: Datei oder Verzeichnis nicht gefunden" würde nur unnötig verwirren).

Gut, man könnte sich fragen, wozu überhaupt argv[0] und dort nicht schon gleich den ersten Parameter reinschreiben (und auf das Fehlermeldungsgedöns pfeifen). Die einfache Antwort ist: Damit kann das Programm selbst feststellen, als *was* es aufgerufen wurde. Schau Dir z.B. bunzip2 an: ls -l which bunzip2 - Du wirst feststellen, dass das ein Symlink auf bzip2 ist. bzip2 stellt dann in der main-Funktion fest, ob argv[0] bunzip2 ist - wenn ja, dann reagiert es, als ob man bzip2 -d aufgerufen hätte. Dadurch kann man viele kleine Programme, die von der Programmlogik her fast das gleiche machen, zu einem einzigen Programm zusammenzufassen.

Danke für diese Sicherheits-Aufklärungen - zwar hab ich davon schon mal gehört und auch derweil hier im Forum drüber gelesen, aber dass dies bei so kleinen Programmen wie diesem hier schon so relevant wird, da denke ich als !C-Programmierer natürlich nicht dran ;-) Letztendlich läuft es aber doch immer darauf hinaus, dass Programme die man in seinem Script (oder Programm, je nach dem) verwendet weitere Programme, Libraries oder Scripte über Pfade aus Umgebungsvariablen nachladen wollen

Im Prinzip schon.

int main (int argc, char **argv) {

Was ist Unterschied von **argv zu *argv[]?

In dem Fall: Geschmackssache. Bei *argv[] weist Du den Compiler an, dass Deine Funktion explizit ein Array von Zeigern erwartet, bei **argv kann auch ein Zeiger auf einen Zeiger übergeben werden (nachdem beim *Zugriff* kein Unterschied zwischen Zeiger und Array gemacht wird, ist das *hier* vollkommen egal). Der Unterschied entsteht genau dann, wenn Du Variablen deklarierst, d.h.:

char bla_array[]; // geht nicht - ist auch unsinn  
char *bla_zeiger; // geht - ist halt nur uninitialisiert  
char foo_array[] = "hallo"; // geht  
char *foo_zeiger = "hallo"; // geht

Und dann ist noch die Frage, was Du hinterher mit den Variablen anstellen kannst:

bla_zeiger = "ciao"; // geht  
foo_array = "ciao"; // geht *nicht*  
foo_zeiger = "ciao"; // geht

Warum meckert der Compiler bei foo_array? Weil foo_array eben wirklich ein Array und kein Zeiger ist - foo_zeiger ist dagegen nur ein Zeiger auf ein "unbenanntes" Array, das irgendwo im Speicher sitzt. Man greift zwar *identisch* auf foo_array und foo_zeiger zu - allerdings sind Arrays halt doch nicht exakt das gleiche wie Zeiger in C.

Weitere Erklärungen gibt's im Standardwerk von Kernighan/Ritchie: Programmieren in C.

char *const p_env[] = {  [...] };
  char *const p_argv[] = { [...] };
  const char *p_prog = "/bin/bash";

Warum heißt es hier in den ersten beiden Fällen char const und im letzten Fall const char, also anders herum?

Es ist nicht "anders herum" - nur so halb.

const char *p_prog = "/bin/bash";  
  
p_prog = "foo"; // geht  
p_prog[0] = '!'; // geht *nicht*  
  
char *const p_prog2 = "/blub";  
p_prog2 = "bar"; // geht *nicht*  
p_prog2[0] = '!'; // kompiliert, allerdings gibt's Speicherzugriffsfehler  
                  // da man konstante Strings nicht ändern darf

Bei const char * ist also der *Inhalt* des Zeigers nicht veränderbar, bei char *const ist der Zeiger selbst nicht veränderbar.

Ok, was haben wir jetzt bei char *const p_env[]? Da bezieht sich das const klar auf die Variable - allerdings steht bei der Variable direkt auch ein [] - d.h. die Variable ist eigentlich ein Array (von Zeigern) - und dieses Array von Zeigern darf nicht geändert werden. Gut, Arrays in C etwas zuzuweisen geht sowieso nicht so wirklich, wie wir oben gesehen haben, aber den Inhalt kann man ja grundsätzlich ändern - außer es steht const davor.

Nun kann man sich fragen: Warum steht da kein const vor dem char - damit der *Inhalt* des Arrays auch nicht modifizierbar ist? *Sehr* gute Frage, keine Ahnung, wissen wohl nur die, die die API dazu entworfen haben - korrekterweise müsste das nämlich auch dabei stehen, d.h. const char *const name;

hier kann dann entweder die UID fest kodiert stehen wie in
meinem einfachen Beispiel oder eben eine Logik, die den
Pfad des ausgeführten Programms herausfindet und die UID
über stat() ausliest (wird relativ kompliziert, wenn man es
richtig machen will)

Im Prinzip ist es doch keine Gefahr, die UID hart zu kodieren, weil wenn es nicht die UID des Scriptes ist, scheitert der User-Wechsel ja sowieso, korrekt so weit?

Ja.

Viele Grüße,
Christian

--
"I have always wished for my computer to be as easy to use as my telephone; my wish has come true because I can no longer figure out how to use my telephone." - Bjarne Stroustrup