Mois de l'assembleur x86, semaine 4 - Fonctions et appels système
Le mois de Juin prend une saveur particulière cette année sur le blog du Hackfest, en effet, quatre articles sur la programmation assembleur seront publiés, un à chaque semaine, et le mois sera couronné d’une présentation HackerSpace sur le sujet. Espérons que vous y apprendrez quelque chose! Voici le dernier article de cette série sur le sujet. Les articles de la semaine 1, semaine 2 et semaine 3 sont aussi disponibles.
Nous y voillà, le dernier article de la série sur la programmation assembleur. Aujourd’hui, nous parlerons des appels système ainsi que de l’utilisation des fonctions rendues disponibles par les librairies C. En toute fin, nous allons voir comment programmer nos propres fonctions en assembleur et comment il est possible de faire appel aux nouvelles fonctions que nous avons préparés.
Les “System calls”
Tous les systèmes d’exploitation offrent une interface de programmation avec le système dans le but que celle-ci soit utilisée par les programmeurs pour faciliter le travail de ces derniers. Cette interface, par l’entremise d’appels au système (system calls) offre la chance au programmeur d’utiliser les différentes fonctionnalités du systèmes d’exploitation sur lequel il programme sans avoir à écrire lui-même le code servant à interfacer le matériel et son programme. Vous aurez donc compris qu’un programmeur utilisant les appels au système rend donc, automatiquement, son code dépendant du système d’exploitation pour lequel il développe. La figure suivante présente une shématisation de l’interaction entre le programmeur et le système d’exploitation.
Vous aurez donc compris que le système d’exploitation expose ses fonctionnalités à travers les appels faits au système. Ainsi, tout ce qui concerne la gestion de la mémoire en passant par l’accès aux différents périphériques peut être fait par l’utilisation des “system calls”.
Utilisation des system calls
Jusqu’à maintenant, les bouts de code que nous avons développés ne pouvaient avoir aucun interaction avec l’utilisateur. Ces programmes ne pouvaient être observés qu’à l’aide d’un débugeur. Nous allons maintenant faire usage des appels systèmes dans le but de permettre l’affichage d’information à la console lors de l’exécution d’une application. La figure suivante montre un exemple d’application utilisant deux system call. Le premier permet d’écrire le texte “Hello World \n” à la console alors que le deuxième permet de terminer l’exécution de l’application de manière normale.
.section .data message: .ascii "Hello World\n" .section .text .globl _start _start:
#Préparation du system call movl $4, %eax movl $1, %ebx movl $message, %ecx movl $12, %edx int $0x80 # Appel du system call #Préparation du system call movl $1, %eax movl $0, %ebx int $0x80 # Appel du system call |
Sur Linux, un appel au système est fait en 3 étapes:
-
Choix de l’appel devant être fait
-
Passage des paramètres
-
Appel sous forme d’intéruption
Le choix de l’appel devant être fait utilise toujours le registre eax. On doit inscrire le numéros de la fonctions que l’on désire appeler dans le registre eax avant de faire l’appel à la fonction en question. Dans le cas actuel, le premier appel de fonction système utilise le numéro 4 (la fonction write). Cette fonction demande 3 paramètres:
-
Un descripteur de fichier
-
Un message ou buffer devant être écrit
-
La taille du buffer devant être écrit
Les paramètres, vous l’aurez compris, sont passés à partir des registres ebx, ecx et edx. Notez, qu’il est nécéssaire dans le cas actuel de spécifier la taille du buffer.
La troisième étape consiste à provoquer l’appel sur la fonction système désirée. Dans le cas actuel, on utilise l’interruption 0x80 pour indiquer au kernel qu’on désire appeler une fonction système.
Le deuxième appel système fait dans l’application utilise la fonction 1 (la fonction exit) afin de sortir de l’application. Jusqu’à maintenant, nous avons utilisé cette fonction sans se questionner sur l’objectif de celle-ci. Cette fonction ne demande qu’un seul paramètre, le code de retour. Ce code est passé comme paramètre à l’aide du registre ebx. Dans le cas actuel, cet appel au système peut très bien être comparé à l’utilisation de “return 0;” à la fin d’un programme C.
Vous pouvez maintenant compiler et exécuter l’application. Vous devriez voir apparaître le message “Hello World” sur la console.
$ as -gstabs --32 -o systemCalls.o systemCalls.s $ ld -m elf_i386 -o systemCalls systemCalls.o $ ./systemCalls Hello World |
Dans l’exemple précédent, aucune valeur de retour n’est nécéssaire. Cependant, dans l’éventualité où une valeur était retournée au code appelant, cette valeur serait positionnée dans le registre eax à la sortie de l’exécution. Vous savez donc maintenant comment utiliser les “system calls”. Pour ceux qui aimeraient consulter une liste complète des différents appels pouvant être fait au système sous Linux, vous pouvez consulter le lien: http://syscalls.kernelgrok.com/ Attention, pour ceux qui voudraient travailler en mode 64 bits, cette liste ne peut pas être utilisées puisque plusieurs modifications importantes ont été faites lors du passage en 64 bits. Vous pouvez trouver une explication des principales différences entre la programmation ASM linux 32 et 64 bits à l'adresse: http://www.exploit-db.com/papers/13136/ Aussi, en ce qui me concerne, la meilleure source de renseignement sur les appels systèmes 64 bits demeure simplement la lecture des fichiers "H" du kernel.
Maintenant, bien que nous soyons en mesure de faire l’utilisation des system calls, dans plusieurs situations, il peut être intéressant d’utiliser des fonctions présentes dans les librairies C. Il est possible d’utiliser l’ensemble des fonctions C à l’intérieur d’une application ASM. Comme nous travaillons avec du code 32 bit, dans l’éventualité où vous utilisez un système 64 bits, vous devrez faire l’installation des outils 32 bits requis pour être en mesure de faire fonctionner vos applications. La figure suivante présente les commandes qui devraient être faites pour installer les outils manquant sur Ubuntu 12.04 64 bits afin de faire la compilation de programmes assembleur 32 bits.
# apt-get install ia32-libs # apt-get install gcc-multilib # ldconfig |
Suite à ces trois commandes, vous devriez avoir tout ce donc vous avez besoin pour continuer avec la suite de cet article.
La figure suivante reprend l’exemple précédent cette fois en utilisant la fonction C printf à la place de la fonction système write.
.section .data message1: .asciz "Hello %s" message2: .asciz "World \n"
.section .text .globl _start _start: pushl $message2 pushl $message1 call printf addl $8, %esp movl $1, %eax movl $0, %ebx int $0x80 |
Comme vous pouvez voir, plusieurs nouvelles instructions sont présentes dans ce bloc de code. Ces nouvelles instructions sont liées au fonctionnement de la pile d’exécution de l’application; la “stack”. La pile contient des données necéssaires au bon fonctionnement de l’application. Ainsi, le bloc de code:
pushl $message2 pushl $message1 call printf addl $8, %esp |
est en réalité équivalent à l’appel de la fonction printf tel que:
char * message2 = “World\n”; printf(“Hello %s”, message2); |
Notez que l’ordre des paramètres envoyés à la fonction printf est inversé! Pour comprendre pourquoi, il suffit de regarder plus en détail le fonctionnement de la pile. Laissons de côté le code précédent un moment pour voir le fonctionnement de la pile...
La pile
Pour que la pile fonctionne, un pointeur doit “pointer” sur l’élément du dessus de la pile. Dans un processeur x86, le registre esp joue le role de “pointeur de haut de pile. Ce registre doit donc pointer sur le dessus de la pile afin de s’assurer du bon fonctionnement des différentes instructions faisant varier la pile. La figure suivante montre un exemple de pile ainsi que le registre esp qui pointe sur le dessus de celle-ci.
Dans l’éventualité où un élément est ajouté sur la pile, la valeur de registre ESP sera modifiée afin que le registre esp pointe toujours sur le haut de la pile. La figure suivante montre l’ajout d’un élément sur la pile.
Ainsi, le registre esp ne pointe plus sur l’élément original mais bien sur le nouvel élément fraichement ajouté dans la pile. En assembleur x86, nous utiliserons les instruction “push” et “pop” pour ajouter et retirer des éléments de la pile. Pour observer le comportement de la pile lors des variations, il n’y a rien de mieux qu’un exemple! Compiler le code suivant et exécuter ce dernier dans un debugueur. Lors de l’exécution, prenez le temps d’observer la valeur du registre esp (ou rsp si vous êtes en 64 bits). Prenez aussi le temps d’observer la variation des valeurs présentes dans les différents registres impliqués.
.section .data message: .asciz "On aime tous la stack\n"
.section .text .globl _start _start:
movl $50, %eax pushl %eax movl $100, %ebx pushl %ebx movl $150, %ecx pushl %ecx
pushl $1000 popl %eax popl %ebx popl %ecx
popl %edx pushl $message popl %eax
pushl %eax call printf addl $4, %esp
movl $1, %eax movl $0, %ebx int $0x80 |
Notez, que pour compiler ce programme vous devrez procéder à lier la librairie C avec votre application. Les deux commande requises sont:
$ as -gstabs --32 NomFichier.s -o NomFichier.o $ ld -dynamic-linker /lib/i386-linux-gnu/ld-2.15.so -lc -m elf_i386 NomFichier.o -o NomFichier |
Il est important de savoir que vous devrez remplacer “/lib/i386-linux-gnu/ld-2.15.so” par l’emplacement de votre propre linker dynamique.
À l’observation du comportement de ce programme, vous devriez, principalement, constater deux éléments clés. Le premier, la pile “grandit” vers le bas. En ce sens que, lorsqu’un élément est ajouté sur la pile, la valeur du pointeur de pile esp diminue. Inversement, lorsqu’un élément est retiré de la pile, esp augmente. Deuxièmement, le premier élément mis sur la pile est le dernier à en être retiré. Vous pourriez très bien utiliser la pile dans le but d’inverser une chaîne de caractères. À titre d’exemple, le programme présenté à la figure suivante utilise le concept de la pile dans le but d’inverser le message présent dans la variable message1 vers la chaîne message2. Si l’exemple précédent ne vous a pas fait comprendre le principe de la stack, celui-ci devrait faire l’affaire :) Notez que vous aurez peut-être une petite recherche à faire au sujet du registre “al” pour bien comprendre cet exemple...
.section .data message1: .asciz "We Love The Stack" message2: .asciz "We Hate The Stack" output: .asciz "%s\n" .section .text .globl _start _start: movl $message1, %esi movb (%esi), %al #Positionnement des caractères sur la pile pushContinue: pushl %eax inc %esi movb (%esi), %al cmp $0, %al jne pushContinue movl $message2, %esi #Retrait des caractères de la pile et #positionnement de ceux-ci dans message2 popContinue: popl %eax movb %al, (%esi) inc %esi movb (%esi), %bl cmp $0, %bl jne popContinue #Impression des résultats pushl $message1 pushl $output call printf addl $8, %esp pushl $message2 pushl $output call printf addl $8, %esp #Fin du programme movl $1, %eax movl $0, %ebx int $0x80 |
De retour aux appels de fonctions...
Avant de bifurquer dans l’explication du fonctionnement de la stack, nous en étions aux appels de fonctions C. Ainsi, si on prend uniquement le code de l’appel de la fonction printf tel que:
pushl %eax call printf addl $4, %esp |
Comme vous pouvez voir, un “push” est fait avant d’appeler la fonction C. Ce push sert à ajouter les paramètres de la fonctions sur la pile. Lors de l’appel de la fonction, celle-ci obtiendra les information à partir de la pile. Mais attention, la fonction appelée n’effectue pas de “pop” sur les éléments ajoutés par le code appellant. Pour cette raison, l’instruction addl est utilisée après l’appel de la fonction dans le but de rétablir l'alignement de la pile. Si vous n’êtes pas tout à fait certain de comprendre cette mécanique, je vous propose un autre exemple. Compiler et exécuter le code suivant à l’aide d’un debogueur.
.section .data print2Param: .asciz "il y a %d pommes dans le sac.\n" print1Param: .asciz "Bonjour tout le monde.\n" .section .text .globl _start _start: pushl $print1Param call printf addl $4, %esp pushl $50 pushl $print2Param call printf addl $8, %esp movl $1, %eax movl $0, %ebx int $0x80 |
L’observation du fonctionnement de cet application devrait vous permettre de bien comprendre le principe des appels de fonction C à partir de code assembleur. Vous remarquerez que, en raison du fonctionnement de la pile, les paramètres sont passés en ordre inverse sur la pile lors des appels (on a normalement printf(StringAvecFormat, variableAAjouterDansFormat)). Aussi, la valeur utilisée lors de la commande “addl” suivant un appel de fonction varie en fonction du nombre d’octets ajouté sur la pile lors de l’appel de la fonction. Le graphique suivant montre l’évolution de la stack pendant l’exécution de l’application (en dehors de l’appel de la fonction printf).
Comme vous pouvez voir, les valeurs “restent dérrière”. Celle-ci seront “effacées”, en réalité, lors du prochain ajout sur la pile.
Voilà! Vous connaissez maintenant les bases du langage assembleur x86 ainsi que quelques techniques que vous pouvez utiliser pour appeler des fonctions C et les fonctions du système! Quel mois ;)
On se revoit au HackerSpace pour voir quel genre d’utilité tout ça peut bien avoir :)