InjecSO : Injection de .so sous Linux

14 février 2009 – 22:37

L’injection de librairie dynamique est une technique principalement orientée reverse-engineering qui consiste à introduire et exécuter du code dans un processus actif du système d’exploitation. Le code en question s’exécute alors dans le contexte du processus cible, et peut accéder aux mêmes ressources. Attention, je parle bien d’injection dynamique, c’est à dire qu’elle peut avoir lieu n’importe quand après le lancement du processus, et non pas juste à son lancement comme avec la technique utilisant LD_PRELOAD (qui en plus ne fonctionne pas avec les programmes setuid).

Après avoir lu de nombreux articles sur l’injection de DLL sous Windows, j’ai commencé à en avoir assez à ne pas trouver d’équivalent sous Linux.  Le peu d’information que j’ai trouvé pour Linux datent d’au moins 5 ans, et se révèlent non adaptables aux distributions récentes. C’est pourquoi je me suis lancé dans l’idée de réaliser ce genre d’outil moi même. Ainsi, après m’être heurté à plusieurs obstacles, j’ai finalement réussi à développer un outil fonctionnel : InjecSO.

Cet outil marche très bien sur ma machine qui est une Ubuntu 8.10, avec un noyau 2.6.27, sans outil de protection de la mémoire comme PaX (la technique utilisée ne marchera pas si la pile n’est pas exécutable), et avec une Libc de version 2.8.90. Je n’ai pas eu l’occasion de tester sur d’autres machines, mais je pense qu’il devrait fonctionner aussi bien, les deux seuls obstacles étant l’implémentation de la librairie standard et les éventuels patches appliqués au noyau.

InjecSO in a nutshell

Voyons dans un premier temps comment utiliser InjecSO dans un cas simple.

Téléchargement

Télécharger InjecSO au format tar.gz

Présentation rapide

InjecSO se présente sous la forme de deux outils.

  • injecso est le cœur du programme utilisé pour l’injection de code. Il s’agit d’un programme écrit en C qui prend 3 arguments : le pid du processus cible, le chemin absolu de la librairie à injecter, et l’adresse de la fonction __libc_dlopen_mode() dans l’espace mémoire du processus cible. Cette dernière est une fonction spéciale de la libc qui rend l’injection possible et est décrite précisément dans la deuxième partie de l’article. Localiser cette adresse précise dans le processus cible est faisable mais rébarbatif ; c’est pourquoi j’ai développé un deuxième outil pour se faciliter la vie.
  • injecso.sh est un script Bash qui a justement pour but de calculer cette adresse de façon automatique. Il ne prend donc que deux paramètres : le pid et le nom de la librairie à injecter. Il appelle automatiquement le programme précédent en lui fournissant le paramètre manquant. De plus, le chemin de la librairie peut être relatif car l’outil calcule le chemin absolu automatiquement.

J’entends déjà des remarques venir :  «Pourquoi ne pas avoir tout intégré dans un seul programme ?». La réponse est simple : cela aurait été faisable, mais m’aurait nécessité beaucoup plus de temps pour au final arriver au même résultat. Le calcul de l’adresse nécessite d’analyser la mémoire du processus et le code de la libc, et il se trouve qu’il existe déjà des outils qui font cela très bien sous Linux. Ainsi, injecso.sh ne fait qu’exploiter ces ressources pour calculer rapidement l’adresse de la fonction voulue. Je suis conscient que cela a ses avantages et ses inconvénients ; en particulier, le script nécessite que vous ayez certaines dépendances d’installées, dont readelf et perl (pour parser la sortie produite par ces outils). Je ne pense pas que cela soit une exigence trop forte, puisque ces outils sont en général présents sur beaucoup de systèmes, et sont au pire facilement installables surtout si votre distribution comprend un système de paquets.

Compiler le programme

Décompressez l’archive et utilisez le Makefile pour compiler l’exécutable :

$ tar xzvf injecso-1.0tar.gz
$ make

Exemples d’utilisation

L’outil est capable d’injecter n’importe quelle librairie dans n’importe quel type de processus, pour peu que vous ayez les droits suffisants (n’espérez pas injecter du code dans un processus appartenant à root si vous ne l’êtes pas vous-même). Comme exemple, prenons un processus simple tel que l’éditeur de texte vi et une librairie dynamique qui affiche « Hello World! » nommée libhelloworld.so. Voici le code de libhelloworld.c :

#include <stdlib.h>
#include <stdio.h>

void __attribute__ ((constructor)) hello_world(void);

void hello_world(void){
  printf("Hello World!n");
}

La directive __attribute__ ((constructor)) indique au compilateur qu’il devra ajouter cette fonction à la liste des fonctions à appeler au chargement de la libairie. Si vous êtes familier du monde Windows, c’est plus ou moins l’équivalent de la directive DllMain(). Compilez le code avec :

$ gcc libhelloworld.c -o libhelloworld.so -shared -fPIC

Vous pouvez placer cette librairie ou bon vous semble, le plus simple étant de la mettre dans le même dossier qu’InjectSO. Lancez vi, récupérez son pid avec la commande pidof vi et lancez l’injection !

$ ./injecso.sh $(pidof vi) ./libhelloworld.so
[+] Found __libc_dlopen_mode at 0xb7e44210
[+] Launching: injecso 9796 libhelloworld.so 0xb7e44210
[+] Attaching...
[+] Waiting for process...
[+] Copying shellcode to 0xbfa0b02e...
[+] Setting eip and esp...
[+] Detaching...

Regardez à présent la fenête où vous avez lancez vi… Vous devriez voir un petit Hello World! en haut de la console :)

Vous pouvez aussi bien injecter votre librairie dans des plus gros processus comme par exemple Firefox — pour les applications graphiques, le message s’affichera dans la console ayant l’ancé le programme —, et complexifier votre librairie. Les possibilités n’ont de limite que votre imagination : création d’un client ou serveur, dump de la mémoire, log d’événements, hooking de fonctions de la PLT… Notez que votre librairie peut à son tour appeler d’autres librairies (dynamiques ou pas) sans aucun problème.

Remarque concernant les threads

Il est important de noter qu’InjecSO ne crée aucun thread dans le processus cible. A la différence des outils d’injection de code sous Windows qui effectuent un appel à CreateRemoteThread(), le code injecté est directement exécuté dans le contexte courant du processus, qui est sauvegardé avant l’injection puis restauré. Si le code de votre librairie est gros ou effectue des opérations gourmandes en cycles CPU, le processus cible en sera ralenti. Cette remarque ‘est particulièrement valable si votre librairie effectue des entrées/sorties disque ou réseau (si vous comptez coder un serveur…). C’est pourquoi dans ces cas il est préférable de créer un nouveau thread, en utilisant par exemple la libairie pthread.

Détails d’implémentation

Cette section décrit en détails comment InjecSO est implementé. Je commence par faire un tour d’horizon des techniques utilisées, puis je détaille le code de l’outil.

ptrace() et dlopen()

Il faut reconnaître que sous Linux, l’attirail disponible pour injecter des librairies est très limité, en tout cas beaucoup plus que sous Windows. A vrai dire, il n’y a tout simplement pas d’appel de fonction tel que  OpenProcess() et CreateRemoteThread(), donc manipuler un processus devient beaucoup plus délicat. Le seul outil dont nous disposons est ptrace().  Il s’agit d’un appel système qui est utilisé majoritairement pour le débogage de processus. Il est relativement simple à utiliser ; on commence par s’attacher au processus à tracer, qui se bloque. On peut alors récupérer son état, ses registres et sa mémoire d’un processus, et les modifier. Une fois les opérations de tracage terminées, on se détache, et le processus reprend son cours. Nous allons voir comment InjecSO utilise cet appel système par la suite ; en attendant je vous renvoie au manuel si vous voulez en savoir plus.

Pour charger une librairie dynamique sous Unix, on utilise la fonction dlopen() qui est plus ou moins l’équivalent de LoadLibraryA() sous Windows. Cette fonction prend en paramètre le nom de la librairie à charger, ainsi qu’un flag qui indique la manière dont doivent être résolus les symboles. Cela n’a guère d’importance pour notre application, aussi nous spécifierons arbitrairement que les symboles doivent tous être résolus au chargement.

Un premier problème : dlopen()

A première vue, ptrace() et dlopen() constituent de bonnes bases pour notre injection. Seulement, ce n’est pas si simple : il se trouve que la fonction dlopen() n’est pas une fonction standard, mais se situe dans une librairie séparée nommée libdl… qui n’est pas toujours chargée par tous les processus. Autrement dit, un processus lambda ne possède pas forcément le moyen de charger une librairie dynamique, car la fonction qui permet de charger des librairies se trouve justement dans une librairie dynamique !

C’est là qu’on peut se dire «OK, mais alors comment fait un processus quand il veut charger une librairie ?». Réponse : c’est le programmeur qui spécifie au moment de la compilation et de l’édition des liens qu’il vaut lier son programme avec libdl qui sera alors chargée à son lancement. Sauf que dans notre cas, nous ne sommes pas forcément le développeur du programme cible, et nous ne voulons de toute manière pas modifier le code du programme…

La solution : __libc_dlopen_mode()

En cherchant de la documentation sur les détails d’implémentation de dlopen(), j’ai finalement trouvé un papier datant de 2003 [1] qui explique que les fonctions de libdl sont pour la plupart des stubs qui appellent des fonctions qui se trouvent en réalité dans la libc. Rappelons que la libc est chargée dans quasiment tous les processus, donc cette découverte parraît très intéressante. Selon le papier, dlopen() appelle en fait _dl_open(). Après vérification, je me rend compte que ce n’est pas/plus le cas, du moins sur ma machine. Mais il semblerait qu’il y ait une fonction similaire avec un nom assez proche : __libc_dlopen_mode(). Voici la mise en évidence en images :

$ pidof bash
10712 9864 8911
$ cat /proc/10712/maps | grep libc
b7d61000-b7eb9000 r-xp 00000000 08:08 51577   /lib/tls/i686/cmov/libc-2.8.90.so
b7eb9000-b7ebb000 r--p 00158000 08:08 51577   /lib/tls/i686/cmov/libc-2.8.90.so
b7ebb000-b7ebc000 rw-p 0015a000 08:08 51577   /lib/tls/i686/cmov/libc-2.8.90.so
$ readelf -s -D /lib/tls/i686/cmov/libc-2.8.90.so | grep dlopen
 2188 744: 0011d210   156    FUNC GLOBAL DEFAULT  11 __libc_dlopen_mode
 2188 966: 0011d210   156    FUNC GLOBAL DEFAULT  11 __libc_dlopen_mode

Dans un premier temps, on récupère les pid d’un processus quelconque, ici Bash, on obtient le chemin complet de la libc (ici /lib/tls/i686/cmov/libc-2.8.90.so) et on utilise readelf pour afficher les symboles dynamiques de la librairie. Résultat : il y a bien une fonction qui a l’air similaire à dlopen().

Mais que fait cette fonction, et quel est son prototype ? Pour cela, le plus simple est de récupérer le code source de la librairie standard et de le parcourir. C’est ce que j’ai donc fait, et je suis finalement tombé sur cela :

extern void *__libc_dlopen_mode  (__const char *__name, int __mode);

Comparons cela au prototype original de dlopen() :

void *dlopen(const char *filename, int flag);

Hum… cela paraît très similaire, pour ne pas dire identique ! Je m’empresse donc de coder un petit programme en C qui appelle cette fonction, et m’aperçois alors que la libraire est bien chargée, comme avec dlopen() ! Super, nous pouvons donc nous contenter de cette fonction.

Deuxième problème : randomization des adresses

Ok, nous avons maintenant un nom de fonction pour charger la librairie. Cependant, pour pouvoir l’appeler dans le processus cible, il nous faut son adresse. Comment l’obtenir, sachant qu’elle se trouve dans l’espace mémoire du processus cible ? Nous savons que la fonction réside dans la libc ; pour déterminer son adresse nous pouvons utiliser le même programme que précédamment, readelf. Lors de notre dernière commande, cet outil nous a indiqué que la fonction se situe à l’offset 0x0011d210 dans l’image mémoire de la librairie. Comment obtenir l’adresse globale à partir de cet offset ? Simplement en additionnant cet offset avec l’adresse de base à laquelle est chargée la libc. Mais quelle est l’adresse de base de la libc ? Observons le résultat de la commande ldd :

$ ldd /bin/bash | grep libc
        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7ee4000)

Nous utilisons cette commande sur Bash, et filtrons la sortie pour n’afficher que ce qui nous intéresse. Notez que le chemin de la libc est ici différent de précédamment, mais cela n’a pas d’importance pour ce que je cherche à illustrer ici.  Ce qu’il faut noter ici, c’est que ldd nous affiche que la libc est chargée à l’adresse 0xb7ee4000. Mais il y a un léger hic… En effet, si on relance la même commande une deuxième fois…

$ ldd /bin/bash | grep libc
        libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7ef0000)

Oups. L’adresse a changée ! Pourquoi donc ? Parce que depuis relativement récamment (quelques années), Linux a introduit un système de randomization des adresses. Autrement dit, l’adresse à laquelle une librairie est chargée n’est pas constante et varie pour toutes les exécutions d’un programme donné. Autant dire que cela ne va pas nous faciliter la tâche pour calculer l’adresse de __libc_dlopen_mode()

Solution : /proc/<pid>/maps

Tout n’est pas perdu. Linux possède un système de fichier virtuel, nommé /proc/, qui va nous permettre de nous en sortir. En effet, lorsqu’un processus est créé, Linux crée un répertoire /proc/<pid>/ (<pid> étant le nom du processus en question) contenant plein d’informations. En particulier, le fichier /proc/<pid>/maps contient la liste de toutes les sections mappées mémoires dans l’espace du processus. Voyons ce que nous pouvons obtenir…

$ pidof bash
10712 9864 8911
$ cat /proc/10712/maps | grep libc
b7d61000-b7eb9000 r-xp 00000000 08:08 51577  /lib/tls/i686/cmov/libc-2.8.90.so
b7eb9000-b7ebb000 r--p 00158000 08:08 51577  /lib/tls/i686/cmov/libc-2.8.90.so
b7ebb000-b7ebc000 rw-p 0015a000 08:08 51577  /lib/tls/i686/cmov/libc-2.8.90.so

On s’aperçoit que la libc est mappée 3 fois dans l’espace mémoire de Bash : une fois en lecture et exécution, une fois en lecture seule, et une fois en lecture et écriture. En ce qui nous concerne, nous souhaitons exécuter une fonction, donc nous avons intérêt à choisir la section exécutable. Son adresse est 0xb7d61000 ; ajoutons à cela l’offset précédent (0x0011d210), et nous obtenons l’adresse de __libc_dlopen_mode() ! Sa valeur n’a pas d’importance dans cet exemple car nous voulons juste un moyen de la calculer automatiquement pour n’importe quel processus.

Injecso.sh : calcul de l’adresse

Si nous rassemblons tout ce que nous venons de voir, nous savons désormais calculer l’adresse de __libc_dlopen_mode() pour un processus donnée. Il ne reste plus qu’à mettre tout cela dans un script ; c’est le but d’injecso.sh. Le script a volontairement été raccourci ici pour n’afficher que les bouts intéressants.

#!/bin/bash
# Param renaming
pid=$1
lib=$2

Nous commençons par récupérer la ligne de /proc/<pid>/maps qui nous intéresse, et nous allons extraire d’une part l’adresse de base de la librairie, et son nom.

# Get the map of process and get the line
# that correspond to the executable section of libc
line=$(cat /proc/$pid/maps | grep "libcb" | grep r-x | head -n 1)
# Extract the base address of that section and the name of the library
libc_baseaddr=$(echo "$line" | cut -d "-" -f1)                # first field
libc_name=$(echo "$line" | perl -n -e '/(S+$)/ && print $1') # last field

Nous utilisons ensuite readelf pour extraire l’offset de __libc_dlopen_mode().

# Use readelf to find the offset of __libc_dlopen_mode
dlopen_offset=$(readelf -s -D $libc_name | grep __libc_dlopen_mode |
  head -n 1 | perl -n -e '/^s*S+s+S+s+(S+)/ && print $1') # 3rd field

L’adresse est obtenue en additionnant cet offset avec l’adresse de base.

# Compute the actual addresses
dlopen_addr=$(expr $(printf "%d" 0x$libc_baseaddr) 
  + $(printf "%d" 0x$dlopen_offset))
dlopen_addr_hex=$(printf "0x%x" $dlopen_addr)

echo "[+] Found __libc_dlopen_mode at $dlopen_addr_hex"
echo "[+] Launching: injecso $pid $lib $dlopen_addr_hex"

Il ne reste plus qu’a passer les paramètres adéquats à notre injecteur.

# Launch InjecSO
$(dirname $0)/injecso $pid $(pwd)/$lib $dlopen_addr_hex

Voila, nous venons de faire le tour du script. Plutôt simple, la difficulté majeure étant de parser la sortie des différents outils utilisés.

Structure de l’injection

Bon, ce n’est pas tout, mais nous n’avons toujours pas vu comment se déroule l’injection ! C’est là que nous allons utiliser ptrace()… Comme je l’ai dit précédemment, cet appel système permet de modifier la mémoire et les registres d’un processus cible. Comment l’utiliser à fin d’injecter un appel à __libc_dlopen_mode() ? La solution la plus simple est d’injecter un shellcode qui va réaliser l’appel. Il y a plusieurs possibilités pour ce faire ; j’ai choisi la plus simple c’est à dire d’injecter le shellcode dans la pile. Notez que cela requirt que la pile soit exécutable, ce qui n’est pas toujours le cas.

De plus, il va nous falloir faire très attention car nous ne voulons pas crasher le processus cible. Comme notre shellcode sera susceptible de s’exécuter n’importe quand dans le processus, il doit rester « invisible ». Autrement dit, il faut que l’état du processeur avant et après l’exécution du shellcode soit « quasiment » le même. Pour cela, nous allons devoir sauvegarder les registres avant d’exécuter la charge utile du shellcode, et les restaurer ensuite.

Là encore, il y a plusieurs solutions pour résoudre ce problème ; je vais détailler celle que j’ai retenue pour injecSO. Voici un shema de la pile avant l’appel au shellcode :

 |               |
 |               |
 |               |
 |               |
 |               |
 +---------------+
 |    donnees    | <- esp
 |               |

Dans ce schéma, les adresses croissent vers le bas, mais rappelez-vous que la pile croît en sens inverse. Au moment de l’injection, nous allons effectuer les opérations suivantes en utilisant ptrace():

  • Sauvegarder l’adresse de l’instruction courante (contenue dans eip) sur la pile
  • Allouer le shellcode sur la pile
  • Faire pointer  eip et esp sur le shellcode

Voici donc la pile après l’injection :

 |               |
 +---------------+
 |   shellcode   | <- eip, esp
 |               |
 |               |
 |               |
 +---------------+
 |  ancien eip   |
 +---------------+
 |    donnees    |
 |               |

Pour que cela fonctionne, le shellcode devra avoir une structure particulière :

  • Quelques nops afin de compenser un éventuel décalage dans les adresses.
  • pushal afin de sauvegarder les registres sur la pile.
  • Charge utile du shellcode (en gros, push des arguments et call __libc_dlopen_mode )
  • popal pour restaurer les registres
  • addl $size, %esp ($size étant la taille du shellcode) afin de repositionner esp sur l’adresse de retour
  • ret qui dépilera et restaurera l’adresse de retour

L’injecteur va dans un premier temps générer le shellcode suivant ce modèle. Cependant, ce modèle est incomplet car le shellcode nécessite des paramètres qui ne seront connus qu’à l’exécution : l’adresse de __libc_dlopen_mode, le nom de la librairie à injecter, ainsi que la taille du shellcode (comprenant le nom de la librairie). Ainsi, l’injecteur va devoir compléter/patcher le shellcode à plusieurs endroits avant qu’il soit fonctionnel.

Le shellcode

Voici le code du shellcode utilisé ; je pense que les commentaires sont assez explicites :

  .text
  .globl shellcode_code
  .globl shellcode_code_end

shellcode_code:

  /* Some nops */
  nop
  nop
  nop
  nop
  nop
  nop
  nop

  /* Save all registers */
  pushal

  /* Get the name of the library into ebx */
  jmp       libname

call_dlopen:
  popl      %ebx

  /* 0x11111111 will be later replaced by
     the address of __lib_dlopen_mode */
  movl      $0x11111111, %eax
  pushl     $2      /* RTLD_NOW */
  pushl     %ebx    /* name of the library */
  call      *%eax   /* call __lib_dlopen_mode */

  /* Clean args on the stack */
  addl      $8, %esp

  /* Restore all registers */
  popal

  /* 0x12345678 will be later replaced by
     the size of the shellcode (+ delta) */
  addl      $0x12345678, %esp

  /* Return to where we were before */
  ret

libname:
  call call_dlopen
shellcode_code_end:
/*
 * End of shellcode
 * The string corresponding to the library name
 * will be put here later
 */

Comme je le disais, le shellcode est encore incomplet. En particulier, il manque encore le nom de la librairie, qui sera ajouté à la fin. Pour récupérer son adresse relative, nous utilisons l’astuce du jmp/call, assez célèbre. Concernant l’adresse de __libc_dlopen_mode et la taille du shellcode, nous laissons des offsets bidon pour le moment que nous allons patcher ensuite. Voila justement le début du code de l’injecteur, qui a pour but d’assembler/patcher le shellcode :

int main(int argc, char **argv){
  char * shellcode;
  int shellcode_size, libname_size;
  char * ptr;
  int i;
  int pid;
  char * libname;
  int dlopen_addr;

  //Check parameters
  check_params(argc, argv, &pid, &libname, &dlopen_addr);

Ici, nous venons de récupérer les paramètres de l’injecteur, c’est à dire le pid, le nom de la librairie et l’adresse de __libc_dlopen_mode. Je ne pense pas que cette fonction soit sufisamment intéressante et complexe pour être détaillée ici.

  //Compute the size of the library name and deduce the shellcode size
  libname_size = strlen(libname);
  shellcode_size = (char *) shellcode_code_end - (char *) shellcode_code 
                   + libname_size + 1;

  //Allocate the shellcode buffer
  shellcode = malloc(shellcode_size);

La copie du shellcode commence ici, puis le nom de la librairie y est ajouté.

  //Copy the shellcode code into the buffer
  for(i = 0, ptr = (char *) shellcode_code;
      ptr != (char *) shellcode_code_end;
      ptr++, i++){
    shellcode[i] = *ptr;
  }

  //Copy the library name at the end of the shellcode
  for(ptr = libname; *ptr != 0; ptr++, i++){
    shellcode[i] = *ptr;
  }

Maintenant, c’est le moment de patcher le shellcode. Le début du shellcode ayant une taille fixe, nous connaissons précisément les offsets à patcher (il suffit d’assembler le shellcode une première fois et de compter).

  //Patch the shellcode by inserting the real address of __libc_dlopen_mode
  //(replace 0x11111111 by dlopen_addr)
  *((int *) &(shellcode[12])) = dlopen_addr;

  //Patch the shellcode to include its own size
  *((int *) &(shellcode[27])) = shellcode_size + DELTA_SHELLCODE_EIP_BAK;

Il y a un petit détail dont je n’ai pas parlé : l’expérience montre que le shellcode inséré tel quel juste après l’adresse de retour ne marche pas ; la fin du shellcode se retrouve écrasée pour une raison que je n’ai pas encore bien comprise. Pour éviter ça, j’ai introduit une petit décalage (delta) de quelques octets entre l’adresse de retour et la fin du shellcode, afin de garantir qu’elle ne sera pas touchée. Il faut juste prendre en compte ce décalage dans certains calculs, bref rien de très compliqué.

  //Inject!
  inject(pid, shellcode, shellcode_size);

Le shellcode est désormais prêt, place à l’injection !

L’injection de code

Voici enfin la routine qui effectue l’injection.

void inject(int pid, char * shellcode, int shellcode_size){

  long res;
  struct user_regs_struct regs;
  char * addr_shellcode;
  int i;

  //Attach to the process
  printf("[+] Attaching...n");
  res = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
  if(res == -1){
    perror("Attaching");
  }

Après s’être attaché au processus, il faut impérativement l’attendre, sans quoi il risque de ne pas être prêt.

  //Wait for the process
  printf("[+] Waiting for process...n");
  waitpid(pid, NULL, 0);

  //Set option for interrupted syscalls
  res = ptrace(PTRACE_SETOPTIONS, pid, NULL, PTRACE_O_TRACESYSGOOD);
  if(res == -1){
    perror("Setting ptrace option");
  }

Cette option n’est pas obligatoire, mais est préférable dans le cas où le processus a été interompu en plein milieu d’un appel système.

  //Get the registers of the process
  res = ptrace(PTRACE_GETREGS, pid, NULL, &regs);
  if(res == -1){
    perror("Getting registers");
  }

Nous avons les registres du processus, nous pouvons maintenant les manipuler comme nous voulons. Nous commençons par calculer l’adresse à laquelle le shellcode devra être chargé, puis nous sauvegardons l’adresse de retour sur la pile

  //Compute the address where the shellcode will be copied
  //We keep 4 bytes for old eip and a delta between this
  //and the end of the shellcode
  addr_shellcode = (char *) regs.esp - shellcode_size 
                    - DELTA_SHELLCODE_EIP_BAK - 4;

  //Save eip on the stack (esp orig - 4)
  res = ptrace(PTRACE_POKEDATA, pid, regs.esp-4, regs.eip);
  if(res == -1){
    perror("Saving eip");
  }

Il faut maintenant copier le shellcode. Attention : lors des transferts de données, ptrace() copie les octets 4 par 4. Il faut donc faire attention lors de l’itération, et ne pas oublier la fin du shellcode.

  //Copy shellcode
  printf("[+] Copying shellcode to 0x%x...n", (int) addr_shellcode);
  for(i = 0; i < shellcode_size/4; i++){
    res = ptrace(PTRACE_POKEDATA, pid, &addr_shellcode[i*4], 
         (int) *((int*) &shellcode[i*4])); //Copy 4 bytes each time
    if(res == -1){
      perror("Copying shellcode");
    }
  }
  if((shellcode_size % 4) != 0){
    res = ptrace(PTRACE_POKEDATA, pid, &addr_shellcode[i*4], 
         (int) *((int*) &shellcode[i*4])); //Copy the last 3- bytes if necessary
    if(res == -1){
      perror("Copying shellcode");
    }
  }

Le shellcode a été copié, il ne reste plus qu’à mettre à jour esp et eip en les faisant pointer sur le shellcode. En fait, nous décalons légèrement eip afin d’être sûr de tomber au milieu des nops.

  //Make eip and esp point to the shellcode
  printf("[+] Setting eip and esp...n");
  regs.eip = (int) addr_shellcode+2;
  regs.esp = (int) addr_shellcode;
  res = ptrace(PTRACE_SETREGS, pid, NULL, &regs);
  if(res == -1){
    perror("Setting eip and esp");
  }

Mission accomplie, plus qu’à se détacher pour libérer le processus.

  //Detach from the process
  printf("[+] Detaching...n");
  res = ptrace(PTRACE_DETACH, pid, NULL, NULL);
  if(res == -1){
    perror("Detaching");
  }

Pour comprendre en détails les arguments de chaque appel à ptrace(), je vous conseille fortement de lire le manuel.

Conclusion

Ca y est, nous venons d’arriver au bout de l’injection. J’espère que vous avez désormais une idée plus claire sur le fonctionnement général de l’injection de code sous Linux. Cela m’aura pris 3 jours pour arriver à un prototype fonctionnel, mais je ne suis vraiment pas déçu compte tenu du résultat. Si vous avez des remarques, n’hésitez pas !

Références

  1. 8 réponses à “InjecSO : Injection de .so sous Linux”

  2. Super boulot :-)

    Par JoE le 15 février 2009

  3. Geek :p

    Par Léo le 19 février 2009

  4. Parce que ceux qui regardent des mangas 24h/24 sont pas des geeks peut-être ? :D Personnellement je préfère faire des choses plus productives…

    Par Emilien Girault le 19 février 2009

  5. Chapeau mec ! Très instructif !

    Par dloic le 22 février 2009

  6. respect!

    Par plop le 9 mai 2009

  7. Ce blog n’est plus à jour, mais il recopie tes articles: http://hackingsecu.canalblog.com/

    Par strepoetlo le 12 décembre 2010

  8. Good job, cela dit pas tres original linjection de lib via ptrace. Tu nes surement pas le premier (ni le dernier) a reinventer cette technique.

    Par jvanegue le 31 janvier 2011

  9. Effectivement, j’ai juste trouvé utile de faire un tuto sur le sujet car à l’époque (2009) je n’avais trouvé que très peu de doc sur le sujet.

    Par Emilien Girault le 31 janvier 2011

Désolé, les commentaires sont fermés pour le moment.