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.
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 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.
Liste basique d'instructions utiles a la compréhension d un programme en assembleur est courte.
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).
Les méthodes d'accès a la memoire (Data accessing methods) (as / GNU) :
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 :
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)
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
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.
Pour faire un appel system il faut :
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
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".
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 :
# 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 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:
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 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.
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.
help (h) : affiche l'aide
Le controle de l'execution :
Analyser :
Commandes utiles :
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 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.
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).