Recherche de LD_PRELOAD

Cette note a été écrite en 2014, mais je viens de subir la répression au hub et elle n'a pas vu la lumière. Pendant l'interdiction, je l'ai oublié, mais maintenant je l'ai trouvé dans des brouillons. Je pensais que c'était pour supprimer, mais peut-être que quelqu'un est utile.



En général, une petite lecture d'administration du vendredi sur le sujet de la recherche du LD_PRELOAD "inclus".

1. Une petite digression pour ceux qui ne sont pas familiers avec la substitution de fonction


Le reste peut passer directement à l' étape 2 .

Commençons par l'exemple classique:

#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); } } 

Compilez sans aucun indicateur:

 $ gcc ./ld_rand.c -o ld_rand 

Et, comme prévu, nous obtenons 5 nombres aléatoires inférieurs à 100:

 $ ./ld_rand 53 93 48 57 20 

Mais supposons que nous ne disposions pas du code source du programme et que nous devons changer le comportement.

Créons notre propre bibliothèque avec notre propre prototype de fonction, par exemple:

 int rand(){ return 42; } 

 $ gcc -shared -fPIC ./o_rand.c -o ld_rand.so 

Et maintenant, notre choix aléatoire est tout à fait prévisible:

 # LD_PRELOAD=$PWD/ld_rand.so ./ld_rand 42 42 42 42 42 

Cette astuce est encore plus impressionnante si nous exportons d'abord notre bibliothèque via

 $ export LD_PRELOAD=$PWD/ld_rand.so 

ou pré-exécuter

 # echo "$PWD/ld_rand.so" > /etc/ld.so.preload 

puis exécutez le programme en mode normal. Nous n'avons pas changé une seule ligne dans le code du programme lui-même, mais son comportement dépend maintenant d'une petite fonction dans notre bibliothèque. De plus, au moment d'écrire ces lignes , le faux rand n'existait même pas.

Qu'est-ce qui a poussé notre programme à utiliser de faux rand ? Passons en revue les étapes.
Au démarrage de l'application, certaines bibliothèques chargées contiennent les fonctions nécessaires au programme. Nous pouvons les voir en utilisant ldd :

 # 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) 

Cette liste peut varier en fonction de la version du système d'exploitation, mais il doit y avoir un fichier libc.so. C'est cette bibliothèque qui fournit les appels système et les fonctions de base, telles que open , malloc , printf , etc. Notre rand en fait également partie. Assurez-vous de ceci:

 # nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$" 000000000003aef0 T rand 

Voyons si l'ensemble des bibliothèques changera lors de l'utilisation de LD_PRELOAD

 # 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) 

Il s'avère que la variable définie LD_PRELOAD force notre ld_rand.so à se charger, même si le programme lui-même n'en a pas besoin. Et, puisque notre fonction rand est chargée plus tôt que rand depuis libc.so , alors elle gouverne la balle.

Ok, nous avons réussi à remplacer la fonction native, mais comment s'assurer que sa fonctionnalité est préservée et que certaines actions sont ajoutées. Nous modifions notre aléatoire:

 #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(); } 

Ici, en tant qu'additif, nous n'imprimons qu'une seule ligne de texte, puis créons un pointeur vers la fonction rand d' origine. Pour obtenir l'adresse de cette fonction, nous avons besoin de dlsym - c'est une fonction de la bibliothèque libdl qui trouvera notre rand dans la pile des bibliothèques dynamiques. Après quoi, nous appellerons cette fonction et retournerons sa valeur. En conséquence, nous devrons ajouter "-ldl" lors de la construction:

 $ 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 

Et notre programme utilise le rand "natif", après avoir effectué quelques actions indécentes.

2. Recherche de farine


Connaissant la menace potentielle, nous voulons découvrir que la précharge a été exécutée. Il est clair que la meilleure façon de détecter est de le placer dans le noyau, mais je m'intéressais exactement aux définitions dans l'espace utilisateur.

Ensuite, les solutions de détection et leur réfutation vont aller de pair.

2.1. Commençons par un simple


Comme mentionné précédemment, vous pouvez spécifier la bibliothèque à charger à l'aide de la variable LD_PRELOAD ou en l'écrivant dans le fichier /etc/ld.so.preload . Créons deux détecteurs les plus simples.

La première consiste à vérifier la variable d'environnement définie:

 #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"); } 

La seconde consiste à vérifier l'ouverture du dossier:

 #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"); } 

Charger des bibliothèques:

 $ 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) [+] 

Ci-après, [+] indique une détection réussie.
Par conséquent, [-] signifie détection de contournement.

Quelle est l'efficacité d'un tel détecteur? Voyons d'abord la variable d'environnement:

 #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) [-] 

De même, on se débarrasse du chèque ouvert :

 #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) [-] 

Oui, d'autres méthodes d'accès au fichier peuvent être utilisées ici, comme open64 , stat , etc., mais, en fait, les mêmes 5 à 10 lignes de code sont nécessaires pour les tromper.

2.2. Aller de l'avant


Ci-dessus, nous avons utilisé getenv () pour obtenir la valeur de LD_PRELOAD , mais il existe également un moyen plus «bas niveau» d'accéder aux variables ENV . Nous n'utiliserons pas de fonctions intermédiaires, mais faisons référence au tableau ** environ , dans lequel une copie de l'environnement est stockée:

 #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; } 

Comme nous lisons ici les données directement de la mémoire, un tel appel ne peut pas être intercepté et notre undetect_getenv n'interfère plus avec la détermination de l'intrusion.

 $ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ LD_PRELOAD (**environ) [+] 

Il semblerait que le problème a été résolu? Ça commence tout juste.

Après le lancement du programme, la valeur de la variable LD_PRELOAD en mémoire n'est plus nécessaire pour les crackers, c'est-à-dire que vous pouvez la lire et la supprimer avant l'exécution des instructions. Bien sûr, éditer un tableau en mémoire est au moins un mauvais style de programmation, mais cela peut-il vraiment arrêter quelqu'un qui ne nous souhaite pas vraiment bonne chance?

Pour ce faire, nous devons créer notre propre fausse fonction init () , dans laquelle nous interceptons le LD_PRELOAD installé et le transmettons à notre éditeur de liens:

 #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; } 

Nous effectuons, vérifions:

 $ 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 /


Cependant, la mémoire n'est pas le dernier endroit où vous pouvez détecter l'usurpation d' identité LD_PRELOAD , il y a aussi / proc / . Commençons par l'évident / proc / {PID} / environ .

En fait, il existe une solution universelle pour undetect ** environ et / proc / self / environ . Le problème est le "mauvais" comportement de unsetenv (env) .

bonne option
 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); //  unset     for(int j = 0; environ[i][j] != '\0'; j++) environ[i][j] = '\0'; break; } } } 


 $ 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 $ 


Mais supposons que nous ne l'avons pas trouvé et que / proc / self / environ contient des données "problématiques".

Tout d'abord, essayez avec notre précédent «déguisement»:

 $ (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 utilise le même open () pour ouvrir le fichier, donc la solution est similaire à ce qui a déjà été fait dans la section 2.1, mais maintenant nous créons un fichier temporaire où nous copions les vraies valeurs de la mémoire sans lignes contenant LD_PRELOAD .

 #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); } 

Et cette étape est terminée:

 $ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $ 

Le prochain endroit évident est / proc / self / maps . Cela n'a aucun sens de s'y attarder. La solution est absolument identique à la précédente: copiez les données du fichier moins les lignes entre libc.so et ld.so.

2.4. Option depuis Chokepoint


J'ai particulièrement aimé cette solution en raison de sa simplicité. Comparez les adresses des fonctions chargées directement à partir de libc et l'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; } 

Nous chargeons la bibliothèque avec l'interception "open ()" et vérifions:

 $ export LD_PRELOAD=$PWD/ld_undetect_open.so $ ./detect_chokepoint LD_PRELOAD (syscall - open) [+] Libc address: 0x7fa86893b160 Next address: 0x7fa868a26135 

La réfutation s'est avérée encore plus simple:

 #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


Il semblerait que ce soit tout, mais ça reste un peu flet. Si nous dirigeons un appel système directement vers le noyau, cela contournera l'ensemble du processus d'interception. La solution ci-dessous dépend bien sûr de l'architecture ( x86_64 ). Essayons d'implémenter ld.so.preload pour détecter l'ouverture.

 #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) [+] 

Et ce problème a une solution. Extrait de l' homme :
ptrace est un outil qui permet à un processus parent d'observer et de contrôler le flux d'un autre processus, d'afficher et de modifier ses données et registres. En règle générale, cette fonction est utilisée pour créer des points d'arrêt dans un programme de débogage et suivre les appels système.

Le processus parent peut commencer le traçage en appelant d'abord la fonction fork (2), puis le processus enfant résultant peut exécuter PTRACE_TRACEME, suivi par (généralement) l'exécution de exec (3). D'un autre côté, le processus parent peut commencer à déboguer le processus existant à l'aide de PTRACE_ATTACH.

Lors du traçage, le processus enfant s'arrête chaque fois qu'un signal est reçu, même si ce signal est ignoré. (L'exception est SIGKILL, qui fonctionne de la manière habituelle.) Le processus parent en sera informé lors de l'appel de wait (2), après quoi il pourra afficher et modifier le contenu du processus enfant avant qu'il ne démarre. Après cela, le processus parent permet à l'enfant de continuer à travailler, en ignorant dans certains cas le signal qui lui est envoyé ou en envoyant un autre signal à la place).

Ainsi, la solution est de suivre le processus, de l'arrêter avant chaque appel système et, si nécessaire, de rediriger le thread vers la fonction trap.

 #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, &regs); // 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, &regs); } } ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); } } exit(0); } else { sleep(0); } } 

Nous vérifions:

 $ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-] 

+ 0-0 = 5

Merci beaucoup

Charles Hubain
Chokepoint
Valdikss
Philippe teuwen
derhass

dont les articles, les codes sources et les commentaires ont fait bien plus que moi pour faire apparaître cet article ici.

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


All Articles