Introduction au Reverse Engineering

logo tux ouvrier

Exemples de Reverse Engineering

Bypass d'un programme

Le premier exemple est de montrer comment réussir à outrepasser (bypasser) une authentification en adaptant le code hexadécimal. Prenons le code suivant (login.c) :

#include 
#include 

int main(int argc, char *argv[]) {
    char pwd[10];
    printf("Password: ");
    scanf("%s", &pwd);
    if(!strcmp(pwd, "biniou")){
        fprintf(stdout,"Success!!\n");
    }
    else{
        fprintf(stderr,"Password invalid!\n");
    }
    return 0;
}

Une fois compilé (gcc -o login login.c), vous pouvez l'exécuter et constater le comportement suivant : Si vous tapez n'importe quel suite de caractères sauf biniou, un message d'erreur s'affiche. Par contre, si vous saissisez biniou, vous obtenez un message de confirmation.

Nous allons chercher à modifer le fichier binaire de deux manières :


Pour modifier le binaire, il convient de l'examiner : Lancer hte sur le binaire, et choissisez le mode d'affichage elf/image (avec F6).

Chercher le bloc intitulé "! ; function main (global)"

Ensuite, chercher la partie code faisant un test suivi d'un jnz : Il suffit de modifier le JNZ en JZ pour réaliser le premier point, et remplacer le JNZ pour réaliser le second.


Pour cela :

  1. Editer la ligne (F4) en remplaçant le code 75 associé au JNZ par 74, et sauvegarder (F2). Vous pouvez quitter (F10) et relancer le programme : N'importe quelle chaîne de caractères vous renverra un Success, sauf biniou qui vous renverra un message d'erreur.
  2. Pour faire en sorte que n'importe quel chaîne de caractères renvoie un message de réussite, il vous faut réaliser la même procédure, mais remplacer la partie 75XX par 9090. Relancer le programme : n'importe quelle chaîne de caractères vous affichera un message de succès!

Modifier un binaire sans avoir accès au code source peut ainsi paraître facile, mais il ne faut pas oublier que cet exemple ne contient qu'un simple if. Avec des vrais programmes, le code assembleur devient beaucoup plus compliqué à analyser.

Fuzzing d'un serveur

Pour le deuxième exemple, nous allons réaliser un fuzzing sur un serveur contenant une faille susceptible de générer un buffer overflow (exécution de code arbitraire par dépassement de tampon).

Pour cela, nous avons pris le serveur avec le code suivant (serveur gérant mal les buffers d'émission et de réception) :

#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h
#include stdio.h
#include stdlib.h
#include unistd.h
#include errno.h
#include string.h
#include sys/types.h

int main(int argc, char *argv[]){
    int listenfd = 0, connfd = 0;
    struct sockaddr_in serv_addr; 
    
    char buf[256];
    const char *mdp = "biniou";

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&serv_addr, '0', sizeof(serv_addr));
    memset(buf, '0', sizeof(buf)); 

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(8888); 

    bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); 
    listen(listenfd, 10); 

    while(1){
      connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); 
      
      // Receive more than the size of the buffer...
      int cn = recv(connfd, buf, 1000,0);
      printf("Received from client : \n%s", buf);
      if(!strcmp(buf, mdp)){
	printf("Password correct!!");
	// Send more than the size of the buffer...
	send(connfd, buf, 1000,0);
      } 
      else {
	printf("\nFailed!!!\n\n");
	send(connfd, buf, 1000,0);
      }	
      close(connfd);
      sleep(1);
    }
}

Pour réaliser le fuzzing, un script python fait très bien l'affaire :

#!/usr/bin/python

import time
import socket


buffer = ['a']
bytes = 25

# fill an array of buffer
while len(buffer) <= 25:
	buffer.append(buffer[0]*bytes)
	bytes = bytes + 25


for data in buffer:
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.connect(('127.0.0.1', 8888))
	print "\n\nFuzzing with " + str(len(data)) + " bytes",
	s.send(data + '\r\n')
	
	print "Received from Server : \n",
	
	print s.recv(len(data))
	s.close()
	time.sleep(3)

Le client va envoyer des séries d'octets en augmentant par bloc de 25 toutes les 3 secondes : 1, 25, 50, 75... 200, 225, 250, 275, ...

Le serveur, contient un buffer qui sert d'émission et de réception, d'une taille définie à 256 octets. Le problème est qu'il reçoit et envoit sur 1000 octets, ce qui est un gros problème : Dès que le client va envoyer plus de 256 octets, les données seront écrites sur un emplacement mémoire non allouée, ce qui provoquera une erreur de segmentation du serveur!

Pour pouvoir tester, il faut compiler le serveur, le lancer, et lancer le client fuzzing en python.

On remarque que le client fuzzing s'arrête après avoir envoyé 275 octets, car le buffer du serveur n'en contient que 256. Le serveur est ainsi stoppé, avec une erreur de segmentation : On essaye d'écrire un emplacement non alloué dans la mémoire à travers le buffer de réception.

On a ainsi mis en avant une erreur de conception du serveur, grâce à un fuzzing.

logo nsa la delorean de retour vers le futur logo anonymous | we are legion

Roads? Where we’re going, we don’t need roads.; Back to the future, Dr. Emmett Brown