Chapitre 3. L'analyse de fichier binaire

Table des matières

Théorie
Intro
La programmation de bas niveau
L'analyse dynamique
Syscall / Library Monitor
Comment marche un debugger (gdb)
Les commandes de bases de gdb a connaitre
Exercice: Modifier le flux d'execution d'un programme
L'analyse statique
Exercice: Modifier un programme pour en faire un keygen
Défense
Quelques protections basiques contre l'analyse statique et dynamique
Aller plus loin avec l'analyse binaire: elfsh, ...
Etude de cas reel: Analyse d'un binaire retrouve apres une attaque sur le twiki du lse

Théorie

Intro

L'analyse de fichier binaire est une connaissance importante pour toute personne souhaitant accroitre ses connaissances en securite informatique car elle permet de connaitre comment fonctionne un programme de 'l'exterieur' sans en avoir les sources. Lors d'audit de securite vous pouvez etre amene a devoir decouvrir comment un intru est parvenu a corrompre un systeme. Dans les differentes traces que laisse un attaquant on retrouve souvent les binaires qu'il a utilises lors de son attaque, il est donc essentiel de savoir les comprendre, pour savoir quelles techniques il a utilise et donc pouvoir colmater la faille. L'analyse de fichier binaire est un domaine tres complexe, nous n'aborderons donc que les bases qui vous permetront d'en decouvrir plus par vous meme.

Pour etudier ces programmes plusieurs methodes d'analyse existent :

  • L'analyse statique :

    Cela consiste a analyser un programme sans l'executer en utilisant comme outils des desassembleurs (gdb, IDA), des decompileurs (asm2C), des analyseurs de code source ou de simples outils comme `grep` ou `strings`.

  • L'analyse dynamique :

    Cela consiste a etudier un programme au cours de son execution en utilisant un debugger, des `tracer`, des machines virtuelles modifies, des analyeurs logiques ou des sniffers reseaux.

  • L'analyse dite "black-box" :

    Cette technique permet d'etudier un programme sans connaitre sont fonctionnement interne, juste en regardant comment il reagit et quels sont les resultats des differentes entrees et sorties.

  • L'analyse dite "post-mortem" :

    On regarde simplement les resultats de l'execution du programme, comme les differents logs, les changements dans les fichiers, dans la date d'acces des fichiers, les donnees que l'on peut retrouver dans la memoire... C'est souvent la seule methode utilisable dans le cadre de l'etude d'un incident. Cette technique est problematique car elle suit la loi OOV (Order Of Volatility), qui definit un ordre de grandeur de temps de validite des donnees (quelques secondes pour la RAM, jusqu'a plusieurs annees pour un CD-ROM) que l'on doit resepecter pour savoir quelles donnees recuperer en premier pour esperer en avoir le maximum intact, il faut donc souvent du materiel special pour pouvoir faire de bonne sauvegarde ce qui n'est pas le cas en general des sauvegardes fournies par la plupart des administrateurs. Nous n'etudierons pas cette technique dans ce cours.

La programmation de bas niveau

Nous allons vous rappeller les bases necessaires d'assembleur pour la comprehension des prochains cours. Nous avons naturellement choisis l'assembleur x86 puisque s'est sous cette plateforme que nous allons etuidier les failles logicielles, nous etudierons ce language sous linux.

Les registres

Les registres a but general : eax, ebx ,ecx ,edx, edi, esi. Les registres speciaux : ebp, esp, eip, eflags. Certains de ces derniers registres peuvent être utilises comme des `general purpose register` mais il sont plus rapides pour certaines opérations c'est pour cela qu'on les nomme `special purpose register`. De meme certains registres communs ou `general purpose register` peuvent être dans certains cas utilisés comme registres spéciaux car certaines instructions en sont dépendantes, c'est a dire que l'instruction nécessite la présence de ces variables dans certains registres.

Un registre est de 32 bits (4 bytes sur x86). On peut accéder a diffèrentes parties du registre.

        -------------------------------%eax---------------------------
        ______________________________________________________________
        |               |               |              |             |
        |               |               |   %ah        |   %al       |
        |               |               |              |             |
        |               |               |              |             |
        |_______________|_______________|______________|_____________|
                                        --------------%ax------------
	  

%eax est un dword soit 4 bytes, %ax est le least significant half de eax (la partie basse de eax), il est utilisé pour traiter deux bytes. %al est le LSB (least significant Byte) de %ax il est utilise pour traiter un byte. %ah est le MSB (most significant Byte) de %ax il permet de modifier la partie haute de %ax.

Les instructions

Liste basique d'instructions utiles a la compréhension d un programme en assembleur est courte.

  • Déplacement dans la mémoire :
    • mov : Permet de déplacer le contenu d'un registre dans un autre
    • lea : Permet de déplacer la valeur pointee a un emplacement mémoire donne dans un registre
    • push : Ajoute une valeur sur la pile et decremente la stack
    • pop : Extrait une valeur de la stack et incrémente la stack
  • Instructions de contrôle :
    • cmp : Compare deux registre
    • call : Appelle une fonction
    • int: Demande d'interruption logicielle
    • ret : Retour a la fonction appelante
    • je (jump if equal)
    • jne (jump if not equal)
    • jg (jump if greater) (jump if second value is greater)
    • jge (jump if greater or equal)
    • jl (jump if less)
    • jle (jump if less or equal)
    • jmp (jump !)
  • Instructions arithmetiques :
    • add / sub / mul / div : Modifie la valeur a un registre
    • inc : Incrémentation unitaire d'un registre
    • dec : Decrementation unitaire d'un registre
  • Instructions logiques :
    • and / or / xor / not
  • Instructions diverses :
    • nop : ne fait rien (x90)

Le typage : comme nous l'avons vu précédemment, un registre peut être découpe pour que l'on utilise 4, 2 ou 1 Byte. Les instructions vues précédemment peuvent etre suffixee pour spécifier le nombre de byte a utiliser. Par exemple pour mov: movb (utilise qu'un seul byte : movb $0xFF, %al), mov ou movw (utilise 2 bytes ou un word (16 Bits) : mov $0xFFFF, %ax), movl (utilise 4 bytes ou un dword (double word, 32 Bits) : mov $0xFFFFFFFF, %eax).

Accès à la memoire

Les méthodes d'accès a la memoire (Data accessing methods) (as / GNU) :

  • Immediate mode (valeur directe precedee d'un $) : mov $42, %eax
  • Register addressing mode (contenu d'un registre) : mov %ebx, %eax
  • Indirect addresing mode (valeur pointee par registre) : mov (%eax), %ecx
  • Direct addressing mode : mov 0x4242, %ebx (=mov $0x15552, %eax mov (%eax), %ebx )
  • Indexes addressing mode (adresse calculee) : le multiplier représente en general la taille d'une variable et l'index permet d'avancer dans le tableau situe a l'adresse de base, Address_or_offset(%base_or_offset, %index, %multiplier) : movl string_start(,%ecx,1), %eax
  • Base pointer addresing mode (equivalent a l'indirect addressing avec un offset, on s'en servira très fréquemment pour accéder a une valeur de la stack) : movl 4(%eax), %ebx

La C Calling Convention

Le mecanisme d'appel de fonction en C est soumis a des regles specifiques a la C calling convention. Linux et NetBSD utilisent la convention d'appel system V. Windows utilise stdcall, majoritairement semblable. La stack est l'élément de base qui permet l'exécution de la C-Calling Convention, elle permet de spécifier les variables locales et la valeur de retour de la fonction.

La stack ou la pile est un emplacement en memoire ou l'on va stocker des valeurs, ces valeurs s'empilent l'une au dessus de l'autre, de plus quand on ajoute un élément a la stack son adresse diminue de la taille de cet élément (downward), la stack commence donc a une adresse haute et finit a une adresse plus basse. Pour stocker un élément dans la stack ou récupérer un élément on utilise les instructions push et pop.

	|BF002236|    	0x000214	
	|BF001121|	0x000210

	mov $2, %eax
	push  %eax	

	|BF002236|	0x000214
	|BG001121|	0x000210
	|00000002|	0x000206

	pop %eax     /* %eax = 2 */

	|BF002236|	0x000214
	|BF001121|	0x000210
	  

Pour accéder a la stack on peut aussi utiliser l'adressage indirect, on utilise ce mode d'adressage avec le registre esp qui pointe sur le haut de la stack, et ebp qui pointe sur la base de la stack.

	|	 |	0x000000	---> ebp
	|~~~~~~~~|
	|BF002236|	0x000214	
	|BF001121|	0x000210	---> esp

	mov (%esp), %eax  /* %eax = BF001121 */
	  

L'instruction pop %eax fait en realite un mov (%esp), %eax puis add $4, %esp.

L'utilisation de la stack, de l'ebp et de l'esp va permettre l'appel de fonction et le retour dans la fonction appelante dans de bonnes conditions. C'est a dire que l'on va créer une nouvelle stack pour la fonction appelante mais qu'il faut aussi pouvoir restaurer la stack de la fonction appellante au moment du retour et lui donner la valeur de retour de la fonction appelle.

Voici comment se passe la C Calling Convention :

  • Push de tous les arguments de la fonction dans l'ordre inverse (le 1er argument sera en haut de la stack).
  • appel de la fonction : call fait un Push de l'adresse de retour (adresse de la prochaine instruction après le call) et modifie eip pour qu'il pointe sur la première instruction de la fonction appellée.
  • La fonction appellee fait un push ebp pour sauvegarder l'adresse de base de l'ancienne stack dans la nouvelle stack, et un mov %esp, %ebp (esp pointe sur le haut de la stack actuelle qui va devenir la base de la nouvelle stack on la place donc dans ebp pour pouvoir référencer cette nouvelle stack). Ebp se trouve donc après les arguments de la fonction, après l'adresse de retour et après la sauvegarde de l'ancien ebp. Après ebp va donc se trouver les arguments locaux a la fonction. Ebp permet de décrire une stack frame.
  • La fonction réserve de la place pour les variables locales. Si elle a besoin de stocker un dword elle (gcc) fera sub $8, %esp. On soustrait 8 a %esp ce qui permet de laisser de la place en haut de la stack puisque les adresses hautes de la stack diminuent. Puisque ces variables sont relatives a la stack frame de cette fonction on dit qu'elles sont locales car une fois l'ancienne stack restauree on ne pourra plus y accéder.
    	
    	Parameter #N 		--- N*4+4(%ebp)
    	Parameter  2 		--- 12(%ebp)
    	Parameter  1 		--- 8(%ebp)
    	Return	Adress 		--- 4(%ebp)
    	old 	ebp    		--- (%ebp)
    	Local Variable 		--- -4(%ebp)
    	Local Variable 2 	--- -8(%esp) and (%esp)
    		
  • Quand la fonction a fini son exécution elle déplace la valeur de retour dans eax. Elle restore la stack frame (Pour avoir accès a l'adresse de retour). Elle retourne le contrôle a la fonction appelante, en placent la valeur de la prochaine instruction, qui avait éte sauvegardee, dans eip.
    	
    	mov 	$0, %eax 	--- eax contient la valeur de retour 0
    	mov	%ebp, %esp	--- restore la stack frame
    	pop	%ebp		--- récupère l'ancien ebp
    	ret			--- retourne le contrôle a la fonction appelante
     
       Note : L'instruction leave correspond a :
            mov %ebp, %esp
    	pop %ebp
    	        
  • %eax contient la valeur de retour. On (gcc) fait un add 4*n, %esp ou n est le nombre de paramètres de la fonction appellee pour retrouver la stack comme avant l'appel.

Après l'appel d'une fonction les registres peuvent être overwrites, il faut donc penser a les sauvegarder (pushall) et les restaurer (popall) après.

RÉFÉRENCE: http://wwwlinuxbase.org/spec/refspecs : System V Application Binary Interface - Intel386 Architecure Processor Supplement.

Les appels systemes sous BSD/Linux

Pour faire un appel system il faut :

  • mettre le numéro de l'appel system dans eax (/usr/include/asm/unistd.h pour les valeurs).
  • préparer les arguments :
    • Sur linux : les arguments sont mis dans les registres (ebx pour le premier, ecx pour le second, edx ...)
    • Sur bsd : il faut les mettre sur la stack dans l'ordre inverse. D'autre part l'interuption doit avoir lieu dans une fonction spécifique, il faut donc rajouter une valeur quelconque sur la stack avant de faire l'interuption pour faire croire qu'il y a eu un appel de fonction
  • effectuer l'instruction int $0x80 qui est une interruption logicielle qui va donner la main au kernel pour qu'il execute l'appel systeme.

En C :
int main()
{
  exit(0);
}

asm linux:
.globl _start
_start:
  mov $1,%eax
  mov $0x0,%ebx
  int $0x80

asm bsd:
.globl _start
_start:
  mov $1,%eax
  push $0x0
  push %eax
  int $0x80
	  

Ecrire un programme en assembleur

Un programme en assembleur est fait de differentes sections qui commencent toujours par un '.'. C'est une directive pour l'assembleur, aussi appellee "assembler directive" ou "pseudo opérations".

  • .section .data : cette section est faite pour la declaration de donnees initialisees comme des chaines de characteres ou des tailles de buffer.
  • .section .bss : cette section est faite pour la declaration de variables globales de taille voulue.
  • .section .text : c'est dans cette section que va etre ecrit le code source du programme. On y liste d'abord le nom des fonctions grace a des symboles qui vont permettre au kernel de savoir ou elles se trouvent, il doit toujours avoir une fonction _start dans un programme pour que le kernel sache ou l'execution du programme commence.
  • .globl _start : previent que l'assembleur doit exporter le symbol _start après l'assemblage (_start est ici un symbole special qui définit le début du programme).
  • _start: définit le label start qui va etre suivi des instructions du debut du programme.

Il existe deux types de syntaxe pour l'assembleur x86: la syntaxe Intel utilisee sous windows / dos et sous unix dans certains cas notamment avec l'assembleur nasm, et la syntaxe AT&T est utilisee par défaut sous unix notamment par as (gcc / gdb) et objdump. Leurs principales différences est l'ordre des opérandes qui est inverse et le fait que la syntaxe Intel n'utilise pas de préfixe ou de suffixe (http://www.w00w00.org/files/articles/att-vs-intel.txt).

Exemple (Intel / AT&T): mov eax, 4 / movl $4, %eax

Nous allons faire un programme qui ecrit HelloWorld sur la sortie standard. Nous devons donc :

  • Appeler l'appel systeme write avec comme parametres la sortie standard, la chaine que l'on veut ecrire et sa taille.
  • Appeler l'appel systeme exit avec comme parametre la valeur de retour, pour que le programme quitte proprement (sans segfault).

# syntaxe AT&T
# pour compiler
# as hello_as.s -o hello_as.o
# ld hello_as.o -o hello_as

.section .data			# cette section permet de declarer des donnees initialisees

   hello:			# definit le nom de notre chaine
  .string  "Hello World!\n"	# .string definit le type de la chaine puis on definit sont contenue

.section .text			# cette section permet de lister les fonctions du programme et ses instructions

  .global _start		# exporte la fonction start

  _start:			# debut de la fonction start

  # mise en place des arguments et appel de write:
 
 	mov $4, %eax		# on met la valeur 4 dans le registre eax, cette valeur correspond
				# a l'appel systeme write	
	mov $1, %ebx		# on met la valeur 1 dans le registre ebx, cette valeur correspond
				# au premiere argument de write, ici la sortie standard
	mov $hello, %ecx	# on met l'adresse de la chaine hello dans le registre ecx, cette
				# valeur correspond au deuxieme argument de write, ici l'adresse de
				# la chaine que l'on veut ecrire
	mov $13, %edx		# on met la valeur 13 dans le registre edx, cette valeur correspond
				# au troisieme argument de write le nombre de bytes a ecrire
	int $0x80		# on effectue l'interruption logiciel qui va lancer l'appel systeme


  # mise sen place des arguements et appel d'exit:

	mov $1, %eax		# on met la valeur 1 dans eax, cette valeur correspond a l'appel systeme exit
	mov $0, %ebx		# on met la valeur 0 dans ebx, cette valeur correspond au deuxieme argument
				# d'exit, la valeur de retour
	int $0x80		# on effectue l'appel systeme

########################################################################################################
# syntaxe Intel
# pour compiler
# nasm -f elf hello_nasm.asm
# ld -o hello_nasm hello_nasm.o

section .data			# cette section permet de declarer des donnees initialises

  hello:  db 'Hello World!', 10 # definie le nom de notre chaine, db indique que le type
  				# de chaine (une suite de byte), puis on definie la chaine ',10'
				# permet d'ajouter le charactere 10 de la table ascii ("\n") a la fin de la chaine 

section .text			# cette section permet de lister les fonctions du programme et ces instructions 

  global	_start		# exporte la fonction start

  _start:			# debut de la fonction start
  
  # mise en place des arguments et appel de write:

	mov eax,4		# on met la valeur 4 dans le registre eax, cette valeur correspond
				# a l'appel systeme write	
	mov ebx,1		# on met la valeur 1 dans le registre ebx, cette valeur correspond
				# au premiere argument de write, ici la sortie standard
	mov ecx, hello		# on met l'adresse de la chaine hello dans le registre ecx,
				# cette valeur correspond au deuxieme argument de write, ici l'adresse
				# de la chaine que l'on veut ecrire
	mov edx, 13		# on met la valeur 13 dans le registre edx, cette valeur correspond au
				# troisieme argument de write le nombre de byte a ecrire
  
  # mise sen place des arguements et appel d'exit:

	int 80h			# on effectue l'interruption logiciel qui va lancer l'appel systeme
	
	mov eax,1		# on met la valeur 1 dans eax, cette valeur correspond a l'appel systeme exit
	mov ebx,0		# on met la valeur 0 dans ebx, cette valeur correspond au deuxieme arguement d'exit, 
	int 80h			# on effectue l'appel systeme
	    

L'analyse dynamique

L'analyse dynamique d'un programme est le fait d'analyser le code execute par un programme, pour cela il faut stopper le programme a certains moments de son execution et regarder l'etat du contexte du programme, par exemple regarder la valeur des differents registres ou des differentes variables a un instant 't'. Le fait d'executer un programme est bien sur dangereux surtout dans le cas d'analyse de malware. Il faut donc creer une "sandbox" qui est un environnement d'execution controle. Plusieurs techniques peuvent etre utilisees pour confiner un programme. La plus simple est celle du "sacrificial lamb" on execute le programme sur une machine non connectee a un reseau et sans donnees importantes dessus.

Mais il existe d'autres techniques qui utilisent des machines virtuelles modifiees le plus souvent, implementees en software, l'avantage est de pouvoir controler l'execution du programme et d'interagir avec lui et de pouvoir creer des fichiers `undoable` ou rediriger les logs a l'exterieur de la VM, il existe meme des VM avec fonctionnalites avancees comme ReVirt qui enregistre toutes les interruptions et les entrees externes (clavier, reseaux) combine a un systeme qui enregistre les fichiers a l'etat initial permet permet de `replay` toutes les instructions de la machine et de voir comment les donnees sont modfiees au cours de l'attaque. Le probleme est qu'un environnement virtuel est souvent facillement reconnaissable:

  • en regardant les identifiants materiel par exemple
  • en executant certaines instructions qui ont mal ete emulees et en comparant les resultats
  • ou encore en verifiant les temps d'acces (par exemple deux fichiers qui paraissent contigus dans la VM peuvent etre assez eloignes sur le media physique)

D'autres environnements pour confiner un programme existent comme les chroot et les jails (chroot limite l'acces au file system mais pas au processus contrairement a jails) qui ont l'avantage de demander moins de ressources mais le desavantage de ne pas etre totalement ferme.

Nous allons d'abord voir comment faire de l'analyse dynamique en `monitorant` les appels systemes ou les appels au libraires puis nous verrons comment marche et comment utiliser un debugger.

Syscall / Library Monitor

Syscall Monitor :

`strace` (Linux, FreeBSD, Solaris), `truss` (solaris) : Cela permet de connaitre les appels systeme fait par le programme, c'est souvent suffisant pour comprendre la majorite de ses actions. strace utilise /proc et ptrace().

Exemple, utiliser `strace` pour lire sur l'entree et la sortie standard de ssh: (strace -f -p ssh_pid -e trace=read,write -e write=3 -e read=5). Les syscall monitor peuvent aussi etre utilise pour confiner un programme c'est le cas de janus, une sandbox qui permet de creer des regles par rapport aux appels systeme, au depart janus etait ecrit en userland et etait vulnerable au `race conditions`, il a donc ete passe en kernel land et marche a peu pres comme `systrace`.

`systrace` permet comme strace de lister les appelles systeme d'un programme mais il permet aussi comme janus de definir des regles sur les syscalls pour controler le programme execute. Pour installer systrace sous linux vous avez besoin de patcher le kernel, le recompiler et installer les outils de controle user-land.

`systrace` possede 3 modes :

Police generating mode: "systrace -A command", cela permet de creer un fichier avec les regles par default que le programme a besoin pour s'executer.

Police enforcing mode: "systrace -a command", cela permet de confiner un programme en suivant les regles definies.

Interactive mode: "systrace command", execute le programme et ses regles si elles existent puis demande la permission avant d'executer chaque appel systeme. Systrace est aujourd'hui utilise sous OpenBSD pour compiler des programmes d'origine exterieure (pour les portages), car un portage a ete modifie pour qu'il se connecte sur machine exterieure. Avec l'utilisation de systrace on est prevenu directement qu'un programme effectue un appel systeme `connect`.

Une autre methode existe pour faire du confinement grace aux syscalls, c'est le syscall spoofing.

Le probleme de la technique precedente est qu'on sait ce que le programme veut faire mais qu'on ne voit pas le resultat des ces actions. Par exemple on peut changer la syscall fork par une autre comme getpid qui renvoie toujours 0 ansi le programme croit qu'il fork mais ce n'est pas le cas ce qui permet de l'analyser plus simplement. C'est a peu pres ainsi que marche le systeme alcatraz qui permet d'isoler un process et une fois que le process est fini, alcatraz demande a l'utilisateur s'il doit marquer ou non les changements.

Le danger du confinement grace aux appels systemes est que dans le cas d'une implementation user-level, le programme de confinement peut etre attaque de differentes manieres. Meme dans le cas d'une implementation kernel-land la plus part des `system call censor` ne sont pas capables d'analyser les differentes threads d'un programme, ce qui peut poser des problemes puisqu'une thread non analysee peut effectuer des taches dangereuses.

Library call monitor:

ltrace (Linux, BSD) sotruss (Solaris), en general ces programmes peuvent a la fois montrer les appel systemes et les appels des librairires, mais par default il ne montre que les appels des differentes librairies.

On peut utiliser aussi les library call monitor pour confiner un programme: on regarde la liste des fonctions appellees grace a nm (liste les section d'un exec) ou objdump (liste les symboles d'un executable). puis on utilise LD_PRELOAD qui est une variable d'environnement qui permet de precharger une librairie partage que l'on a cree et dont les fonctions vont etre appellees avant celles par default. On peut par exemple dans le cas d'un programme qui utilise strcmp pour comparer un mot de passe saisi et l'original, ecrire une fonction strcmp qui affiche la valeur des parametres passees pour pouvoir retrouver son mot de passe.

Comment marche un debugger (gdb)

Pour pouvoir stopper un programme, avoir des infos sur le moment ou on la stopper et continuer son execution apres l'analyse ou apres avoir modifie certaines variables pour voir les reactions du programme, il faut utiliser un debugger.

Puisque nous travaillons dans un environnement GNU nous allons vous expliquer de maniere succinte comment marche et comment l'on peut se servir de GDB.

GDB permet d'executer un programme ou de s'attacher a un programme en cours d'execution, pour pouvoir stopper le programme au moment souhaite GDB utilise ptrace pour surveiller l'action du programme fils qu'il execute, il faut aussi placer des ponts d'arret 'breakpoints'. C'est en fait a l'appel INT3 que GDB reagit pour pouvoir s'arreter a l'adresse voulue GDB va donc inserer dans le programme un INT3 a l'endroit voulu, puis executer le programme au moment ou GDB va rencontrer l'INT3 il va arreter le programme puis au moment de continuer l'execution GDB va remplacer l'INT3 par l'opcode inital pour que l'execution du programme se deroule correctement. C'est ce qu'on appelle des breakpoints software. L'avantage de cette technique c'est qu'elle peut etre utilisee sur une multide de processeurs et d'architecture mais les processeurs X86 permettent aussi d'utiliser des breakpoints hardware qui utilise les registre DR0-DR7.

Pour que GDB puisse vous donner un maximum d'informations sur un programme il utilise les symboles contenus dans l'executable. S'il est compile en utilisant des options de debug (-gX -ggdb avec gcc) les symboles vous donneront un maximum d'informations jusqu'a pouvoir savoir a quelles parties du source du programme correspond la partie en cours d'execution, dans le cas d'un binaire strippe la majorite des symboles sont enleves, l'analyse dynamique est donc compromise et doit souvent etre completee par une analyse statique qui permet de recreer une table de symboles par contre le cas d'un programme crypte ou compresse il n'y a rien a faire pour passer ses 'protections' puisque le programme va forcemment se decrypter avec de s'executer, c'est un avantage sur l'analyse statique le desavantage etant que l'on a pas une vue d'ensemble d'un programme puisque par exemple si le programme execute une partie de son code que sous certaines conditions (par exemple s'il est root) on ne vera pas ses parties s'executer a moins de reussir a trouver les conditions a remplir pour changer le flux de l'execution.

D'autres outils contribuent a la bonne reussite d'une analyse dynamique vous pouvez entre autre utiliser strace qui va vous lister les appels systemes et les signaux envoyes par le programme, strace ansi que ptrace et ktrace permettent souvent une premiere analyse neamoins tres informative sur le deroulement d'un programme execute.

Les commandes de bases de gdb a connaitre

help (h) : affiche l'aide

Le controle de l'execution :

  • break (b) nom_fonction : pose un breakpoint apres le prologue et les variables locales de le la fonction
  • break (b) *0x08040301 : pose un breakpoint a l'adresse donnee
  • run (r) parametres : lance l'execution du programme avec les parametres donnes
  • step (s) : execution ligne a ligne si le programme a ete compile avec -ggdb
  • stepi (si) : execution instruction par instruction
  • continue (c) : continue l'execution jusqu'au prochain breakpoint
  • attach pid : force gdb a suivre le processus donne

Analyser :

  • disassemble (disas) : dessassemble la fonction courante
  • info (i) : donne la liste des informations disponibles
    • info registers (i r) : affiche tous les registres
    • info breakpoints (i b) : affiche tous les breakpoints
    • info locals (i lo) : affiche les variables locales si compile avec -ggdb
  • print (p) $esp: affiche un registre
  • x/NBR format address: examine les NBR premieres occurrences a l'adresse, avec le format donne (x : hexadecimal, s : chaine de caracteres, c : caracteres)
  • set *Adr = val : modifie dans la memoire le programme

Commandes utiles :

  • x/100x $esp : afficher la stack courante
  • p nom_fonction : affiche l'adresse d'une fonction (pour afficher l'adresse d'une bibliotheque dynamique, il faut executer le debut du programme car c'est a l'execution que le kernel les charge en memoire)

Exercice: Modifier le flux d'execution d'un programme

Nous fournissons un binaire qui execute une boucle infinie, le but est de le faire sortir proprement de cette boucle en utilisant gdb

L'analyse statique

L'analyse statique ou "reverse engineering", permet d'analyser un programme sans l'executer. Cela permet donc d'analyser des binaires qui sont fait pour un autre systeme, de plus cela permet de comprendre un programme a 100% puisque vous pouvez analyser une partie du programme qui ne s'execute que sous certaines conditions. En pratique l'analyse statique peut etre tres difficile notamment dans le cas de programmes cryptes, ou proteges par divers mecanismes. Il faut pouvoir comprendre facilement un code en assembleur, et il faut de l'experience pour pouvoir reconnaitre des parties de code souvent communes. Neanmois l'analyse d'exploit de worms ou de virus est tres interessante et c'est une connaissance peu commune chez les developpeurs.

Nous allons analyser de simples binaires pour comprendre comment il fonctionne et reperer les etapes de base telle que les passages d'argument ou la calling convention, pour pouvoir se concentrer sur ce que fait vraiment le programme. Sous linux vous pouvez utiliser objdump (syntax as) ou ndisas ( dans le package de nasm, il utilise donc la syntax intel) pour desassembler un programme. Sous windows des dessassembleurs puissants existent tel que IDA qui vous permet de dessasembler le code d'architecture tres differentes et qui vous permet de creer un organigramme de votre programme ce qui permet de le comprendre beacoup plus rapidement, ou W32Dasm qui montre les fonctions appellees et les fonctions appelantes, les utilisations possibles de chaines de caracteres, et qui permet egalement de faire de l'analyse dynamique.

Exercice: Modifier un programme pour en faire un keygen

Nous vous fournissons un binaire qui demande un nom et un serial et verifie la validite de ces donnees. Vous devez tout d'abord afficher le mon de passe qui correspond a votre login. Puis devez trouver comment modifier ce programme pour qu'il accepte n'importe quel serial. Vous devrez nous rendre par mail (secu@lse.epita.fr) avant mardi 21 a 23h42 la source d'un un petit crack (fopen, fseek et fwrite) pour patcher le programme. Enfin vous devrez nous envoyer le programme modifie en un keygen (un programme qui permet de generer une clef valide pour un nom donne).