Diese Notiz wurde im Jahr 2014 verfasst, aber ich bin gerade an der Drehscheibe unter Druck geraten, und sie hat das Licht nicht gesehen. Während des Verbots habe ich es vergessen, aber jetzt habe ich es in Entwurfskopien gefunden. Ich dachte, es wäre zu löschen, aber vielleicht ist jemand hilfreich.

Im Allgemeinen eine kleine Freitag Admin Lesung zum Thema der Suche nach dem "enthaltenen"
LD_PRELOAD .
1. Ein kleiner Exkurs für diejenigen, die mit Funktionssubstitution nicht vertraut sind
Der Rest kann direkt mit
Schritt 2 fortfahren .
Beginnen wir mit dem klassischen Beispiel:
#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand (time(NULL)); for(int i=0; i<5; i++){ printf ("%d\n", rand()%100); } }
Kompiliere ohne irgendwelche Flags:
$ gcc ./ld_rand.c -o ld_rand
Und wie erwartet erhalten wir 5 Zufallszahlen kleiner als 100:
$ ./ld_rand 53 93 48 57 20
Angenommen, wir haben nicht den Quellcode des Programms und müssen das Verhalten ändern.
Erstellen wir eine eigene Bibliothek mit unserem eigenen Funktionsprototyp, zum Beispiel:
int rand(){ return 42; }
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
Und jetzt ist unsere zufällige Wahl ziemlich vorhersehbar:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand 42 42 42 42 42
Dieser Trick sieht noch beeindruckender aus, wenn wir unsere Bibliothek zuerst durch exportieren
$ export LD_PRELOAD=$PWD/ld_rand.so
oder vorab ausführen
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
Führen Sie dann das Programm in dem normalen Modus aus. Wir haben keine einzige Zeile im Code des Programms selbst geändert, aber sein Verhalten hängt jetzt von einer winzigen Funktion in unserer Bibliothek ab. Außerdem existierte der falsche
Rand zum Zeitpunkt des Schreibens nicht einmal.
Was hat unser Programm dazu gebracht, Fake
Rand zu verwenden ? Lass uns die Schritte durchgehen.
Beim Start der Anwendung werden bestimmte Bibliotheken geladen, die die für das Programm erforderlichen Funktionen enthalten. Wir können sie mit
ldd sehen :
# ldd ./ld_rand linux-vdso.so.1 (0x00007ffc8b1f3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe3da8af000) /lib64/ld-linux-x86-64.so.2 (0x00007fe3daa7e000)
Diese Liste kann je nach Betriebssystemversion variieren, es muss jedoch eine
libc.so- Datei
vorhanden sein . Es ist diese Bibliothek, die Systemaufrufe und Grundfunktionen wie
open ,
malloc ,
printf usw. bereitstellt. Unser
rand ist auch einer von ihnen. Stellen Sie sicher, dass:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$" 000000000003aef0 T rand
Mal sehen, ob sich die Bibliotheken ändern, wenn
LD_PRELOAD verwendet wird # LD_PRELOAD=$PWD/ld_rand.so ldd ./ld_rand linux-vdso.so.1 (0x00007ffea52ae000) /scripts/c/ldpreload/ld_rand.so (0x00007f690d3f9000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f690d230000) /lib64/ld-linux-x86-64.so.2 (0x00007f690d405000)
Es stellt sich heraus, dass die gesetzte Variable
LD_PRELOAD unser
ld_rand.so zum Laden zwingt, obwohl das Programm es selbst nicht benötigt. Und da unsere
rand- Funktion früher als
rand aus
libc.so geladen wird , regiert sie den Ball.
Ok, wir haben es geschafft, die native Funktion zu ersetzen, aber wie wir sicherstellen, dass ihre Funktionalität erhalten bleibt und einige Aktionen hinzugefügt werden. Wir modifizieren unseren Zufall:
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> typedef int (*orig_rand_f_type)(void); int rand() { /* */ printf("Evil injected code\n"); orig_rand_f_type orig_rand; orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand"); return orig_rand(); }
Hier drucken wir als „Additiv“ nur eine Textzeile und erstellen dann einen Zeiger auf die ursprüngliche
Rand- Funktion. Um die Adresse dieser Funktion zu erhalten, benötigen wir
dlsym - dies ist eine Funktion aus der
libdl- Bibliothek, die unseren
rand im Stapel dynamischer Bibliotheken findet. Danach rufen wir diese Funktion auf und geben ihren Wert zurück. Dementsprechend müssen wir beim
Erstellen "-ldl" hinzufügen:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand Evil injected code 66 Evil injected code 28 Evil injected code 93 Evil injected code 93 Evil injected code 95
Und unser Programm verwendet den "nativen"
Rand , nachdem es einige unanständige Aktionen ausgeführt hat.
2. Mehlsuche
In Kenntnis der potenziellen Bedrohung möchten wir feststellen, dass die
Vorspannung ausgeführt wurde. Es ist klar, dass der beste Weg, dies zu erkennen, darin besteht, es in den Kernel zu schieben, aber ich war an genau den Definitionen im Benutzerraum interessiert.
Als nächstes werden die Lösungen für die Erkennung und deren Widerlegung paarweise ausgeführt.
2.1. Beginnen wir mit einem einfachen
Wie bereits erwähnt, können Sie die zu ladende Bibliothek mithilfe der Variablen
LD_PRELOAD oder durch Schreiben in die Datei
/etc/ld.so.preload angeben . Lassen Sie uns zwei einfachste Detektoren erstellen.
Die erste besteht darin, die eingestellte Umgebungsvariable zu überprüfen:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> int main() { char* pGetenv = getenv("LD_PRELOAD"); pGetenv != NULL ? printf("LD_PRELOAD (getenv) [+]\n"): printf("LD_PRELOAD (getenv) [-]\n"); }
Die zweite ist das Öffnen der Datei zu überprüfen:
#include <stdio.h> #include <fcntl.h> int main() { open("/etc/ld.so.preload", O_RDONLY) != -1 ? printf("LD_PRELOAD (open) [+]\n"): printf("LD_PRELOAD (open) [-]\n"); }
Bibliotheken laden:
$ export LD_PRELOAD=$PWD/ld_rand.so $ echo "$PWD/ld_rand.so" > /etc/ld.so.preload $ ./detect_base_getenv LD_PRELOAD (getenv) [+] $ ./detect_base_open LD_PRELOAD (open) [+]
Nachfolgend zeigt [+] eine erfolgreiche Erkennung an.
Dementsprechend bedeutet [-] Bypass-Erkennung.
Wie effektiv ist ein solcher Detektor? Schauen wir uns zunächst die Umgebungsvariable an:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <dlfcn.h> char* (*orig_getenv)(const char *) = NULL; char* getenv(const char *name) { if(!orig_getenv) orig_getenv = dlsym(RTLD_NEXT, "getenv"); if(strcmp(name, "LD_PRELOAD") == 0) return NULL; return orig_getenv(name); }
$ gcc -shared -fpic -ldl ./ld_undetect_getenv.c -o ./ld_undetect_getenv.so $ LD_PRELOAD=./ld_undetect_getenv.so ./detect_base_getenv LD_PRELOAD (getenv) [-]
Ebenso werden wir den
offenen Scheck los:
#define _GNU_SOURCE #include <string.h> #include <stdlib.h> #include <dlfcn.h> #include <errno.h> int (*orig_open)(const char*, int oflag) = NULL; int open(const char *path, int oflag, ...) { char real_path[256]; if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0){ errno = ENOENT; return -1; } return orig_open(path, oflag); }
$ gcc -shared -fpic -ldl ./ld_undetect_open.c -o ./ld_undetect_open.so $ LD_PRELOAD=./ld_undetect_open.so ./detect_base_open LD_PRELOAD (open) [-]
Ja, hier können andere Methoden für den Zugriff auf die Datei verwendet werden, z. B.
open64 ,
stat usw. Tatsächlich werden jedoch dieselben 5-10 Codezeilen benötigt, um sie zu täuschen.
2.2. Weitermachen
Oben haben wir
getenv () verwendet , um den Wert von
LD_PRELOAD abzurufen , aber es gibt auch einen einfacheren Weg, um zu
ENV- Variablen zu gelangen. Wir werden keine Zwischenfunktionen verwenden, sondern auf das Umgebungsarray
** verweisen, in dem eine Kopie der Umgebung gespeichert ist:
#include <stdio.h> #include <string.h> extern char **environ; int main(int argc, char **argv) { int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { printf("LD_PRELOAD (**environ) [+]\n"); return 0; } } printf("LD_PRELOAD (**environ) [-]\n"); return 0; }
Da wir hier Daten direkt aus dem Speicher lesen, kann ein solcher Anruf nicht abgefangen werden, und unser
undetect_getenv stört die Bestimmung des Eindringens nicht mehr.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ LD_PRELOAD (**environ) [+]
Es scheint, dass das Problem gelöst wurde? Ich fange gerade erst an.
Nach dem Start des Programms ist der Wert der Variable
LD_PRELOAD im Speicher für Cracker nicht mehr erforderlich. Sie können ihn also lesen und löschen, bevor Anweisungen ausgeführt werden. Das Bearbeiten eines Arrays im Speicher ist natürlich zumindest ein schlechter Programmierstil, aber kann dies wirklich jemanden davon abhalten, der uns nicht wirklich alles Gute wünscht?
Dazu müssen wir unsere eigene Fake-Funktion
init () erstellen, in der wir das installierte
LD_PRELOAD abfangen und an unseren Linker übergeben:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <unistd.h> #include <dlfcn.h> #include <stdlib.h> extern char **environ; char *evil_env; int (*orig_execve)(const char *path, char *const argv[], char *const envp[]) = NULL; // init // // - void evil_init() { // LD_PRELOAD static const char *ldpreload = "LD_PRELOAD"; int len = strlen(getenv(ldpreload)); evil_env = (char*) malloc(len+1); strcpy(evil_env, getenv(ldpreload)); int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { // LD_PRELOAD unsetenv(env); break; } } } int execve(const char *path, char *const argv[], char *const envp[]) { int i = 0, j = 0, k = -1, ret = 0; char** new_env; if(!orig_execve) orig_execve = dlsym(RTLD_NEXT,"execve"); // LD_PRELOAD for(i = 0; envp[i]; i++){ if(strstr(envp[i], "LD_PRELOAD")) k = i; } // LD_PRELOAD , if(k == -1){ k = i; i++; } // new_env = (char**) malloc((i+1)*sizeof(char*)); // , LD_PRELOAD for(j = 0; j < i; j++) { // LD_PRELOAD if(j == k) { new_env[j] = (char*) malloc(256); strcpy(new_env[j], "LD_PRELOAD="); strcat(new_env[j], evil_env); } else new_env[j] = (char*) envp[j]; } new_env[i] = NULL; ret = orig_execve(path, argv, new_env); free(new_env[k]); free(new_env); return ret; }
Wir führen durch, überprüfen:
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ.c -o ./ld_undetect_environ.so $ LD_PRELOAD=./ld_undetect_environ.so ./detect_environ LD_PRELOAD (**environ) [-]
2.3. / proc / self /
Der Speicher ist jedoch nicht der letzte Ort, an dem Sie
LD_PRELOAD- Spoofing erkennen
können . Es gibt auch
/ proc / . Beginnen wir mit dem offensichtlichen
/ proc / {PID} / environ .
Tatsächlich gibt es eine universelle Lösung für
Undetect ** Environ und
/ proc / self / environ . Das Problem ist das "falsche" Verhalten von
unsetenv (env) .
richtige Option void evil_init() {
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ_2.c -o ./ld_undetect_environ_2.so $ (LD_PRELOAD=./ld_undetect_environ_2.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
Angenommen, wir haben es nicht gefunden und
/ proc / self / environ enthält „problematische“ Daten.
Versuchen Sie zuerst mit unserer vorherigen "Verkleidung":
$ (LD_PRELOAD=./ld_undetect_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD LD_PRELOAD=./ld_undetect_environ.so
cat verwendet dasselbe
open () , um die Datei zu öffnen, daher ähnelt die Lösung der bereits in Abschnitt 2.1 durchgeführten, aber jetzt erstellen wir eine temporäre Datei, in die wir die wahren Speicherwerte ohne Zeilen
kopieren, die
LD_PRELOAD enthalten.
#define _GNU_SOURCE #include <dlfcn.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <limits.h> #include <errno.h> #define BUFFER_SIZE 256 int (*orig_open)(const char*, int oflag) = NULL; char *soname = "fakememory_preload.so"; char *sstrstr(char *str, const char *sub) { int i, found; char *ptr; found = 0; for(ptr = str; *ptr != '\0'; ptr++) { found = 1; for(i = 0; found == 1 && sub[i] != '\0'; i++){ if(sub[i] != ptr[i]) found = 0; } if(found == 1) break; } if(found == 0) return NULL; return ptr + i; } void fakeMaps(char *original_path, char *fake_path, char *pattern) { int fd; char buffer[BUFFER_SIZE]; int bytes = -1; int wbytes = -1; int k = 0; pid_t pid = getpid(); int fh; if ((fh=orig_open(fake_path,O_CREAT|O_WRONLY))==-1) { printf("LD: Cannot open write-file [%s] (%d) (%s)\n", fake_path, errno, strerror(errno)); exit (42); } if((fd=orig_open(original_path, O_RDONLY))==-1) { printf("LD: Cannot open read-file.\n"); exit(42); } do { char t = 0; bytes = read(fd, &t, 1); buffer[k++] = t; //printf("%c", t); if(t == '\0') { //printf("\n"); if(!sstrstr(buffer, "LD_PRELOAD")) { if((wbytes = write(fh,buffer,k))==-1) { //printf("write error\n"); } else { //printf("writed %d\n", wbytes); } } k = 0; } } while(bytes != 0); close(fd); close(fh); } int open(const char *path, int oflag, ...) { char real_path[PATH_MAX], proc_path[PATH_MAX], proc_path_0[PATH_MAX]; pid_t pid = getpid(); if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); snprintf(proc_path, PATH_MAX, "/proc/%d/environ", pid); if(strcmp(real_path, proc_path) == 0) { snprintf(proc_path, PATH_MAX, "/tmp/%d.fakemaps", pid); realpath(proc_path_0, proc_path); fakeMaps(real_path, proc_path, soname); return orig_open(proc_path, oflag); } return orig_open(path, oflag); }
Und diese Phase ist abgeschlossen:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
Der nächste naheliegende Ort ist
/ proc / self / maps . Es macht keinen Sinn, sich darauf einzulassen. Die Lösung ist absolut identisch mit der vorherigen: Kopieren Sie die Daten aus der Datei abzüglich der Zeilen zwischen
libc.so und
ld.so.2.4. Option von Chokepoint
Mir hat diese Lösung wegen ihrer Einfachheit besonders gut gefallen. Vergleichen Sie die Adressen von Funktionen, die direkt aus
libc geladen wurden, mit der Adresse "NEXT".
#define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #define LIBC "/lib/x86_64-linux-gnu/libc.so.6" int main(int argc, char *argv[]) { void *libc = dlopen(LIBC, RTLD_LAZY); // Open up libc directly char *syscall_open = "open"; int i; void *(*libc_func)(); void *(*next_func)(); libc_func = dlsym(libc, syscall_open); next_func = dlsym(RTLD_NEXT, syscall_open); if (libc_func != next_func) { printf("LD_PRELOAD (syscall - %s) [+]\n", syscall_open); printf("Libc address: %p\n", libc_func); printf("Next address: %p\n", next_func); } else { printf("LD_PRELOAD (syscall - %s) [-]\n", syscall_open); } return 0; }
Wir laden die Bibliothek mit Interception
"open ()" und überprüfen:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so $ ./detect_chokepoint LD_PRELOAD (syscall - open) [+] Libc address: 0x7fa86893b160 Next address: 0x7fa868a26135
Die Widerlegung stellte sich als noch einfacher heraus:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> extern void * _dl_sym (void *, const char *, void *); void * dlsym (void * handle, const char * symbol) { return _dl_sym (handle, symbol, dlsym); }
# LD_PRELOAD=./ld_undetect_chokepoint.so ./detect_chokepoint LD_PRELOAD (syscall - open) [-]
2.5. Syscalls
Es scheint, dass das alles ist, aber immer noch Flunder. Wenn wir einen Systemaufruf direkt an den Kernel richten, umgeht dies den gesamten Abfangprozess. Die folgende Lösung ist natürlich architekturabhängig (
x86_64 ). Versuchen wir,
ld.so.preload zu implementieren, um die Öffnung zu erkennen.
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #define BUFFER_SIZE 256 int syscall_open(char *path, long oflag) { int fd = -1; __asm__ ( "mov $2, %%rax;" // Open syscall number "mov %1, %%rdi;" // Address of our string "mov %2, %%rsi;" // Open mode "mov $0, %%rdx;" // No create mode "syscall;" // Straight to ring0 "mov %%eax, %0;" // Returned file descriptor :"=r" (fd) :"m" (path), "m" (oflag) :"rax", "rdi", "rsi", "rdx" ); return fd; } int main() { syscall_open("/etc/ld.so.preload", O_RDONLY) > 0 ? printf("LD_PRELOAD (open syscall) [+]\n"): printf("LD_PRELOAD (open syscall) [-]\n"); }
$ ./detect_syscall LD_PRELOAD (open syscall) [+]
Und dieses Problem hat eine Lösung. Auszug aus dem
Menschen :
ptrace ist ein Tool, mit dem ein übergeordneter Prozess den Ablauf eines anderen Prozesses beobachten und steuern, seine Daten und Register anzeigen und ändern kann. In der Regel wird diese Funktion verwendet, um Haltepunkte in einem Debugging-Programm zu erstellen und Systemaufrufe zu verfolgen.
Der übergeordnete Prozess kann die Ablaufverfolgung starten, indem er zuerst die Funktion fork (2) aufruft. Anschließend kann der resultierende untergeordnete Prozess PTRACE_TRACEME ausführen, gefolgt von (normalerweise) der Ausführung von exec (3). Andererseits kann der übergeordnete Prozess das Debuggen des vorhandenen Prozesses mit PTRACE_ATTACH starten.
Bei der Ablaufverfolgung stoppt der untergeordnete Prozess jedes Mal, wenn ein Signal empfangen wird, auch wenn dieses Signal ignoriert wird. (Die Ausnahme ist SIGKILL, was auf die übliche Weise funktioniert.) Der übergeordnete Prozess wird benachrichtigt, wenn wait (2) aufgerufen wird. Anschließend kann er den Inhalt des untergeordneten Prozesses anzeigen und ändern, bevor er gestartet wird. Danach kann das Kind über den übergeordneten Prozess weiterarbeiten. In einigen Fällen wird das an ihn gesendete Signal ignoriert oder stattdessen ein anderes Signal gesendet.
Daher besteht die Lösung darin, den Prozess zu verfolgen, ihn vor jedem Systemaufruf anzuhalten und den Thread bei Bedarf an die Trap-Funktion umzuleiten.
#define _GNU_SOURCE #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <limits.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/reg.h> #include <sys/user.h> #include <asm/unistd.h> #if defined(__x86_64__) #define REG_SYSCALL ORIG_RAX #define REG_SP rsp #define REG_IP rip #endif long NOHOOK = 0; long evil_open(const char *path, long oflag, long cflag) { char real_path[PATH_MAX], maps_path[PATH_MAX]; long ret; pid_t pid; pid = getpid(); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0) { errno = ENOENT; ret = -1; } else { NOHOOK = 1; // Entering NOHOOK section ret = open(path, oflag, cflag); } // Exiting NOHOOK section NOHOOK = 0; return ret; } void init() { pid_t program; // program = fork(); if(program != 0) { int status; long syscall_nr; struct user_regs_struct regs; // if(ptrace(PTRACE_ATTACH, program) != 0) { printf("Failed to attach to the program.\n"); exit(1); } waitpid(program, &status, 0); // SYSCALLs ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD); while(1) { ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); if(WIFEXITED(status) || WIFSIGNALED(status)) break; else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80) { // syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL); if(syscall_nr == __NR_open) { // NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK); // if(!NOHOOK) { // // regs ptrace(PTRACE_GETREGS, program, 0, ®s); // Push return address on the stack regs.REG_SP -= sizeof(long); // ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP); // RIP evil_open regs.REG_IP = (unsigned long) evil_open; // ptrace(PTRACE_SETREGS, program, 0, ®s); } } ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); } } exit(0); } else { sleep(0); } }
Wir prüfen:
$ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-]
+ 0-0 = 5Vielen Dank
Charles HubainChokepointValdikssPhilippe Teuwenderhassderen Artikel, Quellcodes und Kommentare viel mehr als ich getan haben, um diesen Artikel hier erscheinen zu lassen.