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() {
$ 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, ®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); } }
Nous vérifions:
$ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-]
+ 0-0 = 5Merci beaucoup
Charles HubainChokepointValdikssPhilippe teuwenderhassdont les articles, les codes sources et les commentaires ont fait bien plus que moi pour faire apparaître cet article ici.