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.

relationOS

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:

  1. Choix de l’appel devant être fait


  2. Passage des paramètres


  3. 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:

  1. Un descripteur de fichier


  2. Un message ou buffer devant être écrit


  3. 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.

pile1

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.

pile2

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).


stack

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 :)