Introduction à la programmation assembleur sous linux

Cours d'assembleur
NASM, un ami qui vous veut du bien.
  1. Sommaire
Préface
Introduction
      Base de chiffres
      Les processeurs
      Les registres
      Les mnémoniques
      Le format ELF
      Les syscalls
      La Libc
      Etude de cas
Spécifications
      Déclaration des variables
      Inclure un fichier
      Adresses spéciales
      Définition de constantes
      Répétition d'instructions
      Opérateurs
      SEG et WRT
      Annuler l'optimisation
      Expressions critiques
      Labels locaux
      Définition de structures
      Les comparaisons
Macros
      Macros simples
      Macros sur plusieurs lignes
Analyse de codes
      Macro ENUM
      Recup de argc
      Code polymorphique
Debuggers
      Ndisasm
      GBD
      Strace
      Application
Conclusion
Références
Remerciements
  1. Préface
Je suis tombé sur un article écrit il y a plusieurs années sur la programmation assembleur. Je n'avais jamais pris le temps de le mettre sur mon blog mais je vais enfin pouvoir partager cette expérience avec toi publique :).
Vous me direz, mais à quoi ça sert de savoir programmer en assembleur ? Et je vous répondrez que l'assembleur est un langage très proche du langage machine. De ce fait, en sachant programmer en assembleur vous comprendrez beaucoup mieux les mécanismes qui régissent le fonctionnement de votre cpu, et de ses périphériques.
D'un point de vu pratique nous allons, ensemble, apprendre l'assembleur pour, dans une suite d'articles prochains, comprendre les principes du reversing et peut-être commencer à programmer nos premiers virus.
Nous allons travailler sur un i386 (comme 95% des PC) sous linux (debian sid). J'ai préféré travailler avec NASM car il adopte la syntaxe intel ce qui, pour un passage d'os à windows, pourrait être pratique.
Avant de commencer je voudrais casser quelques mythes, l'assembleur n'est pas plus dur à assimiler que le C, on peut l'utiliser pour faire des programmes de haut niveau (sockets, environnement graphique ...). Si l'assembleur n'est pas aussi utiliser que le C c'est principalement dûe au fait que le code n'est pas portable et qu'il est plus fastidieux à créer.
Si vous êtes déjà familier avec le fonctionnement d'un cpu et que vous avez déjà les bases en assembleur, passez l'introduction.
Bien, nous pouvons maintenant rentrer dans le vif du sujet. Entrez dans mon monde.
  1. Introduction
  • Base de chiffres :Il y a la base décimale, celle que nous connaissons tous : en base 10 (de 0 à 9)

    La base binaire : en base 2 (de 0 à 1)

    110012 = 1 x 24 + 1 x 23 + 0 x 22 + 0 x 21 + 1 x 20
                  
    = 16 + 8 + 1 = 25

    On appel chaque “chiffre”, 0 ou 1 dans le cas présent, un bit (à ne pas confondre avec Byte qui est un octet).

    La base héxadécimale : en base 16 (de 0 à F)
    On compte ainsi en héxadécimal.
    0=0; 1=1; 2=2; 3=3; 4=4; 5=5; 6=6; 7=7; 8=8; 9=9; A=10; B=11; C=12; D=13; E=14; F=15.

    2AF16 = 2 x 162 + 10 x 161 + 15 x 160           = 512 + 160 + 15 = 687
    L'intérêt de l'héxadécimal est qu'il permet de représenter plus simplement des valeurs binaires. Comme le binaire est en base 2 et l'héxadécimal en base 16 il est simple de représenter 4 bits (24 = 16).

    Par exemple : 1110 1001 0110 1110
                          E       9       6       E
    Il faut également savoir qu'un ensemble de 8 bits s'appel un octet. De là vos fichiers en ko, Mo, Go.
  • Les processeurs :Un processeur est un composant électrique composé, pour ce qui nous intéresse, de registres, pour stocker des informations temporairement. D'une horloge envoyant des pulsations toutes les microsecondes ou nanosecondes. Lorsque vous achetez un processeur de type 1Ghz cela représente la vitesse de cette horloge ; pour savoir en secondes la durée d'un cycle de votre horloge faîtes 1/f où f est la fréquence (soit 1/1e9 = 1 ns pour notre cas). Les autres composants électronique se basent sur cette horloge pour effectuer leurs tâches.
    Parlons maintenant de l'organisation de la mémoire – non, pas celle de votre disque dur ; la mémoire du processeur et dans certains cas de la RAM –. Chaque octet est repéré par un nombre unique dans la mémoire de votre processeur. Mais le processeur traitant des quantités incroyable de données, il ne traite que rarement de simples octets. C'est pour cela que l'on a nommé des ensembles d'octets.
    word
    2 octets
    double word
    4 octets
    quad word
    8 octets
    paragraph
    16 octets
    Toutes les données en informatique sont des nombres. Par exemple les caractères sont sous forme de chiffre. On a seulement créé une table de correspondance entre les valeurs décimales et les caractères.

    Actuellement, la plus grande différence entre les différents types de processeurs se situe donc au niveau de la taille des registres et de la rapidité de l'horloge mais pas seulement.
  • Les registres :Nous travaillerons, comme je l'ai déjà dit, sur un processeur de type 80386. Ce qui veut dire que les registres auront une taille de 32 bits.
    Les registres sont des espaces mémoires nous permettant de stocker des valeurs de 32 bits. Grâce à eux nous pourrons accéder à une donnée précise, récupérer la valeur de retour d'une fonction, passer des arguments à une fonction, etc...
    Ils sont organisés de cette façon :
    EAX
    EBX
    ECX
    EDX
    registre général
    registre général
    registre général
    registre général
    ESI
    EDI
    EBP
    EIP
    ESP
    offset mémoire
    offset mémoire
    offset mémoire gardant l'adresse de la fonction
    offset mémoire du code
    offset mémoire de la pile
    Les registres généraux servent de foure-tout. Tandis que les registres d'offset pointent généralement sur une adresse mémoire utile. Vous apprendrez au fur et à mesure quel registre utiliser pour quoi faire. Il existe d'autres registres plus spécifiques.
  • La pile :Sur les processeurs de type x86, la pile est un outil permettant de stocker des données de façon temporaire car rapide d'accès. La pile est dite LIFO (Last In First Out), ce qui veut dire que la première chose mise dans la pile sera la dernière chose enlevée de la pile. C'est donc pour cela que l'on a l'habitude de s'imaginer la pile comme des assiettes empilées les unes sur les autres, où chaque assiette représenterait une donnée, puis que l'on désempilerait de haut en bas.
    Les mnémoniques pour empiler et désempiler sont respectivement “push” et “pop”. Un petit code pour illustrer ça :
    push val1 ; val1 = 10
    push val2 ; val2 = 20
    
    pop val1 ; val1 = 20
    pop val2 ; val2 = 10
    Vous verrez plus loin que la pile est un outil indispensable dans la programmation assembleur.
  • Les mnémoniques :Toute instruction NASM peut être résumée ainsi :
    label : mnémonique opérandes ;commentaire

    Il n'est pas obligatoire de retrouver tous les éléments sur la même ligne. Les mnémoniques représentent les instructions assembleur, ce qui nous permet de programmer. Par exemple.
    addition:       add eax,4 ; j'ajoute 4 à eax et je stock le résultat dans eax
    Il existe beaucoup de mnémoniques, pour un listing complet allez voir le manuel de NASM.
  • Le format ELF :Sous linux les éxécutables ont le format ELF pour Executable and Linking Format. Ce format offre un découpage modulaire de l'en-tête de l'éxécutable sous cette forme : (Représentation très simplifiée)
    ELF Header
    Program Header Table
      Segment #1
      Segment #2
      . . .
      Section Header Table
        Section 1
        . . .
        Section n
    Nous utiliserons trois segments dans notre code :
    .data
    .bss
    .text
    Sert aux variables de tailles fixes. Variables initialisées. Sert aux variables de tailles inconnues. Variables non initialisées. C'est là que se trouve le code du programme.
    Pour plus d'informations sur le format ELF :
    cat /usr/include/elf.h
  • Les syscall :
    En assembleur sous linux, il existe une mnémonique permettant de faire appel au noyau. C'est à dire que grâce à cette mnémonique on peut demander au noyau d'effectuer une action pour nous. La mnémonique en question se nomme int pour “interruption”. Pour faire appel au kernel il faut lui passer l'argument 0x80.
    Nous verrons son utilisation dans un code expliquer et commenter.

    Pour une liste assez exhaustive des différents syscalls : http://www.lxhp.in-berlin.de/lhpsyscal.html
  • La libc :
    Il est également possible d'utiliser les fonctions de la libc sous certaines conditions. Cela nous permet de faire pas mal de choses simplement.
  • Etude de cas :
    Nous allons étudier là deux petits programmes tout ce qu'il y a de plus banal, j'ai nommé des “hello world”.

    Le premier utilisant les syscalls. Le syscall permettant d'écrire (write) est le 4.
    Il prend en paramètres :
    edx : la longueur de la chaîne à affichée
    ecx : un pointeur sur le début de la chaîne
    ebx : le handle où l'on veut écrire (en l'occurence 1 pour l'écran)
    eax : le numéro du syscall à appeler
    ; NASM -f elf hello_world.asm
    ; ld hello_world.o -o hello_world
    
    segment .data
            hello db        "Hello World !", 0xa      ; 0xa équivaut à \0
            len     equ     $ - hello           ; taille de la chaîne (strlen(hello))
    
    segment .text
            global  _start
    
    _start:
            mov     edx, len           ; edx = longueur de la chaîne a afficher
            mov     ecx, dword hello   ; ecx pointe sur l'adresse du début de la chaîne
            mov     ebx, 1             ; file handle, ou l'on écrit (STDOUT)
            mov     eax, 4             ; sys_write
            int     0x80               ; call kernel
            
            mov     eax,1                   ; sys_exit
            xor     ebx,ebx                 ; ebx = 0 (soit exit(0) en c)
            int     80h                     ; call kernel
    Au début on défini les variables dans les bons segments (rappelez-vous, variables de longueurs fixes dans data ; variables de longueurs non définies dans bss). len contient la taille de la chaîne hello, nous verrons plus tard ce que représente la ligne qui définie cette variable. global sert à définir le point d'entrée du code (_start quand on compile avec ld et main quand on compile avec gcc). Ensuite le remplissage des paramètres pour appeler sys_write. dword est un mot clé pour dire que la variable est de type doubleword. Puis on quitte proprement avec un sys_exit.

    Le second utilisant la fonction printf() de la libc.
    Pour utiliser la libc il faut lier le .o avec gcc. Pour appeler une fonction de la libc il faut empiler les arguments dans la pile (push) dans l'ordre inverse qu'ils sont demandés par la fonction. Un exemple :
    int fprintf(FILE *stream, const char *format, ...); 

    Il faudra donc empiler le format en premier puis le stream soit :
    push format
    push stream
    call fprintf
    pop eax ; valeur de retour de la fonction

    extern permet de dire au compilateur que l'on va appeler une fonction extérieure à notre programme.
    Il faut nettoyer la stack après chaque appel à une fonction de la libc.
    ; NASM -f elf hello_printf.asm
    ; gcc hello_printf.o -o hello_printf
    
    global main
            extern printf   ; on déclare la fonction printf comme externe
    
    section .data
            msg     db      "Hello, World",0Dh,0Ah,0
    
    section .text
            main:
            push dword msg  ; pointe vers l'adresse du début de la chaîne à afficher
            call printf                 ; printf()
            pop eax                     ; on nettoie la stack
            ret
    Dans la programmation NASM les mot-clés du type dword, byte ... servent à spécifier que l'on pointe sur l'adresse de la variable et non pas sur son contenu. Comme dans notre cas :
    dword msg
  1. Spécifications
    Nous allons maintenant apprendre les bases nécessaires pour comprendre un code NASM : la signification des mots clés, la déclaration des variables, l'étude de quelques mnémoniques ...
  • Déclaration des variables :
    Comme nous l'avons déjà vu, il existe deux grands types de variables en assembleur : les variables non-initialisées et les variables initialisées déclarées respectivement dans les sections .bss et .data. NASM nous permet de définir à l'aide de lettres la taille de l'espace mémoire que nous allouons à ces données.
    La section .data :
    db   0x55                       ; un octet : 0x55
    db   0x55, 0x56, 0x57           ; trois octets
    db   'a', 0x55                  ; deux octets (a = 0x41)
    db   'hello', 13, 10,'$'        ; ça marche aussi avec les chaînes de caractères
    dw   0x1234                     ; deux octets : 0x34 0x12
    dw   'a'                        ; 0x41 0x00
    dw   'abc'                      ; 0x41 0x42 0x43 0x00
    dd   0x12345678                 ; quatre octets : 0x78 0x56 0x34 0x12
    dd   1.234567e20                ; nombre à virgule de précision float
    dq   1.234567e20                ; nombre à virgule de double précision float
    dt   1.234567e20                ; nombre à virgule de précision float étendue
    La précision des nombres de type “float” désigne le nombre de chiffres, après la virgule, gardés en mémoire.
    La section .bss :
    resb    255     ; REServe 255 octets
    resb    1       ; REServe 1 octets
    resw    1       ; REServe 1 Word (1 Word = 2 octets )
    resd    1       ; REServe 1 Double word (soit 4 octets)
    resq    1       ; REServe 1 float à double précision
    rest    1       ; REServe 1 float à précision étendue
  • Inclure un fichier :
    Le mot réservé INCBIN permet d'inclure un fichier binaire dans votre source.
    incbin   "file.dat"             ; inclue un fichier entier
    incbin   "file.dat", 1024       ; inclue le fichier sans les 1024 premiers octets
    incbin   "file.dat", 1024,512   ; inclue le fichier sans les 1024 premiers octets
                                    ; et jusqu'à 512 octets
  • Adresses spéciales :Les jetons $ et $$ désignent eux des adresses spécifiques : $ représente l'adresse de l'instruction par rapport au début du code et $$ l'adresse de l'instruction par rapport au début de la section. Ainsi pour créé une boucle infinie ou pourrait faire
    jmp $
  • Définition de constantes : EQU :EQU permet de définir une constante dont la valeur ne pourra donc pas changée. Par exemple :
    message         db      'hello, world'
    msglen          equ     $-message
    msglen représente la taille de message soit 12 octets.

  • Répétition d'instructions : TIMES :
    Le préfixe TIMES fait que l'instruction est assemblée plusieurs fois. L'avantage de cette instruction est qu'elle peut permettre d'effectuer plusieurs fois un ensemble d'action plus ou moins complexes.
    buffer: db 'hello, world'
            times 64-$+buffer db ' '
    Cet exemple va donc réserver 64 octets pour y stocker buffer.
    On peut utiliser TIMES pour répéter n'importe quelle mnémonique.

  • Opérateurs :NASM, tout comme le C accepte des opérateurs arithmétiques forts utiles. A la différence que les opérateurs Nasm ne sont utilisables que dans le préprocesseur (calcul d'adresse, macros ...), cf : expressions critiques.

    | : OR

    ^ : XOR

    & : AND

    << et >> : Décalage binaire

    + et - : addition, soustraction

    *, /, //, % et %%
    +, -, ~ : opérateurs s'applicant sur un argument
    Tout comme en C et en électronique, cet opérateur effectue un OR bit par bit à vos données. Tout comme en C et en électronique, cet opérateur effectue un XOR bit par bit à vos données. Tout comme en C et en électronique, cet opérateur effectue un AND bit par bit à vos données. Comme en C cet opérateur permet de copier le nombre de bits (spécifier par la deuxième opérande) de la première opérande en commençant par le bit de poid faible. Tout comme en C, ces opérandes permettent de faire une addition ou une soustraction. * permet de faire une multiplication
    / permet de faire une division non signée
    // permet de faire une division signée
    % permet de faire un modulo non signé
    %% permet de faire un modulo signé
    Lorsque vous mettez un + ou un – devant une seul variable vous rendez respectivement la variable en signe positif ou en signe négatif.
    ~ quant à lui agit comme une porte NOT en électronique.

    Comme il est essentiel de comprendre le fonctionnement de ces opérateurs nous allons développer un peu les opérateurs agissant au niveau binaire :
    L'opérateur OR.
    Bit 1
    Bit 2
    Bit 1 | Bit 2
    0
    0
    0
    0
    1
    1
    1
    0
    1
    1
    1
    1

    L'opérateur AND.
    Bit 1
    Bit 2
    Bit 1 & Bit 2
    0
    0
    0
    0
    1
    0
    1
    0
    0
    1
    1
    1

    L'opérateur XOR.
    Bit 1
    Bit 2
    Bit 1 ^ Bit 2
    0
    0
    0
    0
    1
    1
    1
    0
    1
    1
    1
    0

    L'opérateur NOT.
    Bit
    ~Bit
    0
    1
    1
    0

    L'opérateur de décalage binaire.
    Prenons l'exemple de 42 << 2 :
    42 en binaire.


    0
    0
    1
    0
    1
    0
    1
    0
    Le résultat de 42 << 2 soit 168 en décimal.
    0
    0
    1
    0
    1
    0
    1
    0
    0
    0
  • SEG et WRT :Dans un programme en assembleur, il existe différents types de segments, dont les plus connues sont CS/DS/SS pour Code Segment, Data Segment et Stack Segment. Ceux-ci permettent d'atteindre des adresses lointaines. Ainsi en mode 16 bits, ces segments sont éparpillés car ne pouvant être placés dans la même zone mémoire tandis qu'en mode 32 bits, les segments sont accessibles dans la même zone mémoire.
    SEG et WRT permettent d'accéder à l'adresse d'une donnée lointaine. Ce qui n'est que très rarement utile en mode 32 bits. Les seuls différences de ces mnémoniques sont au niveau de la syntaxe et au niveau du résultat obtenu.
    Quelques exemples :
    mov ax, seg donnee
    mov es, ax
    mov bx, donnee          ; ES:BX contient un pointeur valide vers donnee
    La mnémonique SEG récupère donc l'offset de donnee. Pour cela elle utilise un “base segment” par défaut pour obtenir un offset exploitable.
    Ce que l'on peut spécifier grâce à la mnémonique WRT :
    mov ax, base_segment
    mov es, ax
    mov bx, donnee wrt base_segment ; ES:BX contient un pointeur different mais tout aussi fonctionnel vers donnee
    Ici base_segment représente donc le “base segment” :).
    NASM permet de faire des appels et des sauts vers des labels lointains sous la forme :
    call segment:offset     ; ou segment et offset sont des valeurs numériques
       
    On peut donc à l'aide de SEG et WRT réaliser le calcul immédiatement :
    call (seg procedure):procedure                  ; les parentheses ne sont 
    call weird_seg:(procedure wrt weird_seg)        ; pas obligatoire en pratique.
  • Annuler l'optimisation : STRICT :Lorsque l'on passe à NASM l'argument “-On”, où n est un chiffre spécifiant le niveau d'optimisation, NASM effectue une optimisation du code. Il est possible que pour certaines instructions vous ne vouliez pas que l'optimisation ai lieu. C'est pour cela que STRICT a été inventé.
    Par exemple avec l'optimisation :
    push dword 33           ; 66 6A 21
    push strict dword 33    ; 66 68 21 00 00 00
  • Expressions critiques :Une limitation de NASM est qu'il assemble le code en deux phases minimum (sans optimisation) ; pas comme tasm ou d'autres. La première phase (préprocesseur) consiste pour NASM à récupérer la taille globale du programme, des segments de données ... Ainsi lors de la deuxième phase (runtime), la génération de l'exécutable, NASM connaît toutes les adresses des données dont fait référence le code source.
    Donc ce que NASM ne peux prendre en compte sont les instructions du code source faisant référence à une donnée déclarée après.
    Par exemple :
      times (label-$) db 0
    label: db 'Where am I ?'
    NASM ne pourra compiler ce code car la ligne déclarant label est après celle lui allouant de l'espace mémoire.
    Ce concept a été appelé expression critique. Il existe différentes instructions qui peuvent être restreintent par ces expressions critiques tel que TIMES, RES*, EQU ...
  • Labels locaux :
    Le point permet à NASM de déclarer des labels locaux, comme on le voit dans l'exemple. En plus, NASM permet de faire référence à un label en-dehors du domaine local.
    label1  ; some code
    .loop           
                            ; some more code
            jne .loop
            ret
    
    label2  ; some code
    .loop           
                            ; some more code
            jne .loop
            ret
            
    label3  ; some code
                            ; some more code
            jmp label1.loop
    Ainsi le premier jne .loop ira directement à label1.loop, le deuxième jne .loop ira à label2.loop et le dernier jmp label1.loop ira au début du code (label1.loop).
  • Définition de structures : STRUC :Pour définir une structure on fait appel au préprocesseur par le biais du mot-clé STRUC. Il ne prend qu'un argument. STRUC n'est qu'une macro qui effectue en réalité plusieurs EQU.
    struc mytype 
            .long:   resd 1
            .word:   resw 1
            .byte:   resb 1
            .str:    resb 32
    endstruc
    On déclare donc la structure mytype avec des variables locales de types différents : mytype.long, mytype.word, mytype.byte et mytype.str.
  • Les comparaisons : CMP :Les langages de programmation habituels tels que le C utilisent des structures de contrôle du flux d'éxécution du programme (if, while, switch) ce qui n'existe pas en assembleur. Pour pallier à cela on a recoure à la mnémonique CMP qui effectue une simple soustraction et stock le résultat dans un registre spécial, le registre FLAGS.

    Pour les entiers non-signés il y a deux bits importants dans le registre FLAGS : le zero (ZF) et la portée (CF)
    cmp vleft, vright
    La soustraction entre vleft et vright est effectuée et
    - si vleft = vright, alors ZF est mis à 1 et CF est mis à 0
    - si vleft > vright, alors ZF est mis à 0 et CF est mis à 0
    - si vleft < vright, alors ZF est mis à 0 et CF est mis à 1

    Pour les entiers signés il y a trois bits importants dans le registre FLAGS : le zero (ZF), la surcharge (OF) et le signe (SF)
    - si vleft = vright, alors ZF est mis à 1
    - si vleft > vright, alors ZF est mis à 0 et SF = OF
    - si vleft < vright, alors ZF est mis à 0 et SF != OF

    Les valeurs que prennent ces bits servent ensuite à éguiller le flux d'éxécution du code à l'aide de mnémoniques d'aiguillage dont voici un bref tableau récapitualtif :
    JZ
    JNZ
    JO
    JNO
    JS
    JNS
    JC
    JNC
    JP
    JNP
    Commute seulement si ZF = 1
    Commute seulement si ZF = 0
    Commute seulement si OF = 1
    Commute seulement si OF = 0
    Commute seulement si SF = 1
    Commute seulement si SF = 0
    Commute seulement si CF = 1
    Commute seulement si CF = 0
    Commute seulement si PF = 1
    Commute seulement si PF = 0

    L'instruction JMP saute quelque soit la valeur du registre FLAGS. On peut spécifier une certaine portée à nos mnémoniques d'aiguillage pour minimiser la taille du code par exemple :
    - SHORT permet de sauter à 128 octets vers le haut ou vers le bas du code.
    - NEAR permet de sauter n'importe où dans le segment courant.
    - FAR permet de sauter dans n'importe quel autre segment.

    De plus les processeurs de type 80x86 offrent la possibilités de rendre ces tests plus simples par le biais de mnémoniques d'aiguillage plus adaptées :
    Entiers signés
    Entiers non signés
    JE                     Commute si vleft = vright
    JNE                   Commute si vleft != vright
    JL, JNGE           Commute si vleft < vright
    JLE, JNG           Commute si vleft <= vright
    JG, JNLE           Commute si vleft > vright
    JGE, JNL           Commute si vleft >= vright
    JE                   Commute si vleft = vright
    JNE                 Commute si vleft != vright
    JB, JNAE         Commute si vleft < vright
    JBE, JNA         Commute si vleft <= vright
    JA, JNBE         Commute si vleft > vright
    JAE, JNB         Commute si vleft >= vright

    Et pour finir un petit bout de code exemple :
       cmp eax, 0xffffffff         ; si eax != -1
       jne thenblock               ; on saute vers thenblock
       mov DWORD PTR [esp], 0x1    ; on empile les arguments pour le call
       call ptrace                 ; on appel la fonction
       jmp next                    ; on passe à la suite
    thenblock :  
       mov DWORD PTR [esp], 0x8048564 ; on empile d'autres arguments
       call ptrace                    ; on appel la fonction
    next :                            ; on continue l'éxécution
    Il ne faut pas oublier que le registre FLAGS est modifiable via d'autres mnémoniques.
  1. Macros
    NASM comporte un outil très puissant, j'ai nommé le processeur de macros. Cet outil nous permet de définir des instructions plus ou moins complexes réutilisables de façon assez simple une fois que l'on a maîtriser la syntaxe. Dans ce chapitre nous allons apprendre à utiliser les macros déjà disponibles et à créer nos macros.
  • Les macros simples :
    Les macros simples permettent par le biais du préprocesseur de définir de nouvelles macros tout comme en C. Le mot clé permettant cette prouesse est “%define” et ses dérivés.
    %define ctrl 0x1F &                     ; définition de ctrl = 0x1F &
    %define param(a,b) ((a)+(a)*(b))        ; définition de param()
    
    mov byte [param(2,ebx)], ctrl 'D'       ; = mov byte [(2)+(2)*(ebx)], 0x1F & 'D'
    La macro %define ne subit pas la limitation des expressions critiques.
    %define a(x) 1+b(x)
    %define b(x) 2*x
    
    mov ax, a(8)    ; mov ax, 1+2*8
    Les noms des macros sont sensibles à la casse. C'est pour cela qu'il y a “%idefine” qui permet de définir un ensemble de noms pour la macro.
    %define foo bar         ; foo
    %idefine foo bar        ; foo, FOO, fOo, foO, ...
    Il est également possible de surcharger les arguments d'une macro :
    %define foo(x) 1+x
    %define foo(x, y) 1+x*y
    
    mov ax, foo(4)          ; ax = 1+4
    mov ax, foo(4,5)        ; ax = 1+4*5
    Parlons un peu de “%xdefine”. Cette instruction permet de déclarer une macro en fonction de son contexte et non pas comme “%define” qui elle déclare une macro en fonction de la valeur au moment de l'appelle à celle-ci. Un exemple :
    %define isTrue 1
    %define isFalse isTrue
    %define isTrue 0
    
    val1: db isFalse    ; val1 = 0
    
    %define isTrue 1
    
    val2: db isFalse    ; val2 = 1
    %xdefine isTrue 1
    %xdefine isFalse isTrue
    %xdefine isTrue 0
    
    val1: db isFalse        ; val1 = 1
    
    %xdefine isTrue 1
    
    val2: db isFalse        ; val2 = 1

    Dans le cas de “%define”, val1 prend la valeur 0 car lorsque l'on appel la macro “isFalse”, “isTrue” vaut 0 ; de même pour val2.
    Pour “%xdefine” val1 vaut 1 car lorsque l'on déclare “isFalse”, “isTrue” vaut 1. Ainsi “isFalse” vaudra toujours 1 quelque soit la valeur de “isTrue“ plus loin dans le code.

    Il existe une autre instruction pour créer des macros : “%assign”. Celle-ci n'est utilisée que pour les macros d'une ligne ne prenant aucuns arguments et de valeur numérique. On la retrouve souvent lorsqu'il faut effectuer une incrémentation ou décrémentation dans une macro.

    Pour détruire une macro on utilise le mot clé “%undef”
  • Les macros sur plusieurs lignes :
    La définition d'une macro de plusieurs lignes passe par le mot-clé “%macro” avec cette syntaxe :
    %macro nom_de_macro nb_arguments
    ; un peu de code
    %endmacro
    Où nom_de_macro est le nom de la macro et nb_arguments un nombre représentant le nombre d'arguments passés à la macro. Un exemple :
    %macro silly 2
            %2: db %1
    %endmacro
    Où %2 représente le deuxième argument et %1 le premier argument.
    silly 'hello_world', hello      ; hello: db 'hello_world'
    De plus on peut déclarer un intervalle d'arguments et des valeurs par défaut :
    %macro silly 2+ ; 2 arguments ou plus
    %endmacro
    
    %macro foobar 1-3 eax, [ebx+2]  ; au minimum 1 argument
                                    ; et pas plus de 3 arguments
    %endmacro
    Pour la macro foobar, lorsque les arguments %2 et %3 ne sont pas déclarés, %2 = eax et %3 = [ebx+2].
    L'astérisque (*) permet de déclarer un nombre infini d'arguments.

    L'argument %0 est le nombre d'arguments passés à la macro.

    L'instruction %rotate permet de faire tourner les arguments de la macro de x vers la gauche ou la droite respectivement si x est positif ou si x est négatif.
    %macro multipush 1-*   ; 1 arguments minimum
        %rep %0            ; boucle tant qu'il y a des arguments
           push %1         ; on push l'argument courant
           %rotate 1       ; on passe à l'argument suivant
        %endrep            
    %endmacro
  1. Analyse de Code
    Maintenant que nous possédons les notions théoriques essentielles (et un peu plus quand même :D) nous allons étudier quelques codes nasm. Histoire de voir à quoi cela peut nous servir en pratique.
  • Macro ENUM :
    Un petit code source de mammon_ tiré du Assembly Programming Journal. Cette macro permet de créer une énumération de variables associées à des valeurs numériques. Tout comme le fait le mot-clé enum en C.
    ;Summary: A NASM macro emulating the C 'ENUM" command
    ;Assembler: NASM
    ;by mammon_ && modified by Flyers
    %macro ENUM 2-*     ;Usage: ENUM int SYMBOLS
    %assign i %1        ;  where int is the number to begin enumeration at [0]
    %rep %0 -1          ;  SYMBOLS is a list of Symbols to define
      %2 EQU i          ;Example: ENUM 0, TRUE, FALSE
      %assign i i+1     ;  this EQUates TRUE to 0 and FALSE to 1
      %rotate 1         ;Example: ENUM 11, JACK, QUEEN, KING
    %endrep             ;  this EQUs JACK to 11, QUEEN to 12, KING to 13
    %endmacro
    Ce code ne devrait pas vous être si dur à assimiler maintenant mais je vais vous aider.
    On commence par créer la macro avec au minimum deux paramètres.
    On initialise i à la valeur du premier élément à énumérer (soit le premier argument de la macro).
    On attribue ensuite à chaque argument (-1 pour ne pas prendre en compte le premier argument) la valeur de i que l'on incrémente. On peut résumer cela par une boucle “for ( i = %1; i < %0 -1; i++)”.
  • Recup de argc :
    Nous allons étudier un bout de code qui s'occupe de récupérer argc (vous savez c'est la variable contenant le nombre d'arguments passés au programme en ligne de commande), qui le transforme en caractère et qui l'affiche à l'écran.
    Avant de commencer il faut que vous ayez quelques connaissances indispensables :

    - La pile, lorsqu'un programme est lancer sous linux, ressemble à ça :
    argc
    [dword] compteur d'arguments (integer)
    argv[0]
    [dword] nom du programme (pointer)
    argv[1]
    ...
    argv[argc-1]
    [dword] arguments du programme (pointers)
    NULL
    [dword] fin des arguments (integer)
    env[0]
    env[1]
    ...
    env[n]
    [dword] variables d'environnement (pointers)
    NULL
    [dword] fin des variables d'environnement (integer)
    Mais il faut savoir que ce schémas ne s'applique que lorsqu'on ne lie pas notre programme avec la libc cad lorsque l'on n'utilise pas gcc. Quand la libc est utilisée il reste dans la pile une valeur de retour. Il faut donc la nettoyée comme on le fait dans le code.

    - En informatique on a créé des tables de caractères qui sont des correspondances entre des caractères et des valeurs numériques. La plus vieille est la table de caractère ASCII. On remarque dans celle-ci que le nombre 0 est représenter par la valeur 48 en décimal soit 0x30 en héxadécimal. C'est donc en ajoutant 0x30 à la valeur numérique que j'obtiens la string correspondante. Il y a un problème majeur à cette astuce : je ne peux afficher que les valeurs ASCII des nombres allant de 0 à 9 seulement. Donc le programme souffre d'un bug connu (dommage pour les bidouilleurs qui auraient cru faire une trouvaille).

    - Il faut se souvenir que le registre esp pointe sur le haut de la pile.

    Voila, maintenant le code en question commenter et j'espère compréhensible :
    ; nasm -f elf argc.asm
    ; ld argc.o -o argc
    ; pour ne pas passer par la libc
    ; gcc argc.o -o argc
    ; pour utiliser la libc
    ; by Flyers
    
    segment .text:
            global _start   ; gcc : global main
            
    _start:                 ; gcc : main:
            pop eax         ; ld : argc ; gcc : valeur de retour de la libc
    ;       pop eax         ; gcc : argc
    ;       pop eax         ; argv[0]
    ;       pop eax         ; the first real arg 
    
            push 0x0A30             ; on push 30 pour convertir argc en ASCII 
                                    ; et 0A pour la fin de chaîne
            add     [esp],eax       ; on ajoute ce que contient la stack à argc
            mov     edx, 2          ; on affichera 2 bytes (argc+\0)
            mov     ecx, esp        ; la chaîne à afficher
            mov     ebx,1           ; file handle, ou l'on écrit (STDOUT)
            mov     eax,4           ; sys_write
            int     80h             ; call kernel
            
            ; pour éviter que le programme ne segfault à la fin on appel sys_exit
            mov     eax,1
            xor     ebx,ebx
            int     80h
  • Code s'auto-modifiant :
    Un code s'auto-modifiant est un code capable de se modifier lui même lors de l'éxécution. L'étude d'un tel code peut être une bonne introduction à la programmation de virii polymorphiques. Pour créé un code s'auto-modifiant, il faut être capable de jongler entre les adresses des différentes instructions car on ne peut spécifier d'adresse statique lorsque le programme est en éxécution, on utilise donc des adresses relatives comme vous allez le voir.

    Karsten Scheibler a écrit un très bon article dont voici une brève traduction :

    L'idée principale : il existe un syscall, sys_mprotect, qui permet de modifier les flags pour (presque) toutes les pages. Une page est la plus petite unité dans la gestion de la mémoire virtuelle. Sur les processeurs x86, la taille de la page est de 4Ko. Mais il n'est pas nécessaire de faire appel à ce syscall pour donner à la section .bss les droits en éxécution car sur les processeurs x86, une page avec les droits de lecture a également les droits en éxécution et la section .bss est en lecture/écriture. Mais ce syscall risque de devenir obsolète avec l'apparition du NX-flag sur les nouveaux processeurs.

    Les deux premiers exemples copient un bout de code dans la section .bss puis l'éxécute. Parce que nous avons les droits en lecture/écriture/éxécution dans cette zone mémoire, le programme peut s'auto-modifier. Le premier exemple (code1_start) copie un simple hello_world (utilisant sys_write), mais avant de l'éxécuter nous modifions quelques valeurs dans le code (le début et la taille de la chaîne à afficher). Le deuxième (code2_start) effectue une vraie auto-modification. L'instruction “rep stosb” écrase les quatre premiers “inc ebx” avec des “nop”, ainsi la chaîne à afficher à l'écran contient 04h au lieu de 08h attendu. Le troisième exemple (endless), quant à lui, modifie du code dans la section .text en y ajoutant les droits en lecture/écriture/éxécution via sys_mprotect.

    Note : Si vous voyez un 08h au lieu de 04h à l'écran, vous devriez connaître un drôle de comportement des codes s'auto-modifiants. Sur les processeurs clone du Pentium, la modification de la queue n'est pas prise en compte tout de suite. Il faut en quelque sorte rafraîchir la queue, pour cela un simple jmp suffit (essayez avec un “jmp” juste après le “rep stosb”).
    ;****************************************************************************
    ;****************************************************************************
    ;*
    ;* USING SELF MODIFYING CODE UNDER LINUX
    ;*
    ;* written by Karsten Scheibler, 2004-AUG-09
    ;*
    ;****************************************************************************
    ;****************************************************************************
    
    global _start
    
    ;****************************************************************************
    ;* some assign's
    ;****************************************************************************
    %assign SYS_WRITE                       4
    %assign SYS_MPROTECT                    125
    
    %assign PROT_READ                       1
    %assign PROT_WRITE                      2
    %assign PROT_EXEC                       4
    
    ;****************************************************************************
    ;* data
    ;****************************************************************************
    section .bss
                                            alignb  4
    modified_code:                          resb    0x2000
    
    ;****************************************************************************
    ;* smc_start
    ;****************************************************************************
    section .text
    _start:
    
            ;calcul l'adresse de la section .bss, elle doit se trouvée entre
            ;deux pages (x86: 4KB = 0x1000)
            ;NOTE: Dans cet exemple c'est inutile car chaque segment est aligné
            ;      en fonction des pages et nous ne l'utilisons qu'une fois, 
            ;      donc nous savons qu'il est entre deux pages, mais si vous avez
            ;      plus d'une section .bss dans votre programme vous ne pouvez
            ;      en être sûr.
    
            mov     dword ebp, (modified_code + 0x1000)
            and     dword ebp, 0xfffff000
    
            ;change les flags de la section en lecture/écriture/éxécution,
            ;NOTE: Sur les processeurs x86 cela est inutile comme cela a 
            ;      déjà été vu plus haut.
    
            mov     dword eax, SYS_MPROTECT
            mov     dword ebx, ebp
            mov     dword ecx, 0x1000
            mov     dword edx, (PROT_READ | PROT_WRITE | PROT_EXEC)
            int     byte  0x80
            test    dword eax, eax
            js      near  smc_error
    
            ;éxécute le code non modifié
    
    code1_start:
            mov     dword eax, SYS_WRITE
            mov     dword ebx, 1
            mov     dword ecx, hello_world_1
    code1_mark_1:
            mov     dword edx, (hello_world_2 - hello_world_1)
    code1_mark_2:
            int     byte  0x80
    code1_end:
    
            ;copie le code vers la bonne page (dont l'adresse est encore dans ebp)
    
            mov     dword ecx, (code1_end - code1_start)
            mov     dword esi, code1_start
            mov     dword edi, ebp
            cld
            rep movsb
    
            ;on y ajoute le code héxa de l'instruction 'ret', comme cela
            ;nous pourrons utiliser call
            
            mov     byte  al, [return]
            stosb
    
            ;change quelques valeurs dans le code: l'adresse de début du texte 
            ;et sa taille
    
            mov     dword eax, hello_world_2
            mov     dword ebx, (code1_mark_1 - code1_start)
            mov     dword [ebx + ebp - 4], eax
            mov     dword eax, (hello_world_3 - hello_world_2)
            mov     dword ebx, (code1_mark_2 - code1_start)
            mov     dword [ebx + ebp - 4], eax
    
            ;finalement on l'appel
    
            call    dword ebp
    
            ;copie le deuxième exemple
    
            mov     dword ecx, (code2_end - code2_start)
            mov     dword esi, code2_start
            mov     dword edi, ebp
            rep movsb
    
            ;fait quelque chose de vraiment méchant: edi pointe juste après
            ;l'instruction 'rep stosb', donc cela va réellement modifier le code
    
            mov     dword edi, ebp
            add     dword edi, (code2_mark - code2_start)
            call    dword ebp
    
            ;modifie le code dans la section .text
    
    endless:
            ;ajoute les droits à la section .text pour la modifiée
    
            mov     dword eax, SYS_MPROTECT
            mov     dword ebx, smc_start
            and     dword ebx, 0xfffff000
            mov     dword ecx, 0x2000
            mov     dword edx, (PROT_READ | PROT_WRITE | PROT_EXEC)
            int     byte  0x80
            test    dword eax, eax
            js      near  smc_error
    
            ;affiche le message à l'écran
    
            mov     dword eax, SYS_WRITE
            mov     dword ebx, 1
            mov     dword ecx, endless_loop
            mov     dword edx, (hello_world_1 - endless_loop)
            int     byte  0x80
    
            ;ici, les instructions empêchant la boucle sans fin
    
            mov     dword ecx, (smc_end_1 - smc_end)
            mov     dword esi, smc_end
            mov     dword edi, endless
            rep movsb
    
            ;et on recommence
    
            jmp     short endless
    
    ;****************************************************************************
    ;* code2
    ;****************************************************************************
    
            ;ici les adresses des instructions dont on rajoute les 
            ;codes héxa dans notre code 
    
    return:
            ret
    no_operation:
            nop
    
            ;ici du vrai code s'auto-modifiant, s'il est bien 
            ;copier dans .bss et edi bien charger, ebx doit contenir
            ;0x4 au lieu de 0x8
    
    code2_start:
            mov     byte  al, [no_operation]
            xor     dword ebx, ebx
            mov     dword ecx, 0x04
            rep stosb
    code2_mark:
            inc     dword ebx
            inc     dword ebx
            inc     dword ebx
            inc     dword ebx
            inc     dword ebx
            inc     dword ebx
            inc     dword ebx
            inc     dword ebx
            call    dword [function_pointer]
            ret
    code2_end:
                                            align 4
    function_pointer:                       dd      write_hex
    
    ;****************************************************************************
    ;* write_hex
    ;****************************************************************************
    write_hex:
            mov     byte  bh, bl
            shr     byte  bl, 4
            add     byte  bl, 0x30
            cmp     byte  bl, 0x3a
            jb      short .number_1
            add     byte  bl, 0x07
    .number_1:
            mov     byte  [hex_number], bl
            and     byte  bh, 0x0f
            add     byte  bh, 0x30
            cmp     byte  bh, 0x3a
            jb      short .number_2
            add     byte  bh, 0x07
    .number_2:
            mov     byte  [hex_number + 1], bh
            mov     dword eax, SYS_WRITE
            mov     dword ebx, 1
            mov     dword ecx, hex_text
            mov     dword edx, 9
            int     byte  0x80
            ret
    
    section .data
    hex_text:               db      "ebx: "
    hex_number:             db      "00h", 10
    
    ;****************************************************************************
    ;* some text
    ;****************************************************************************
    endless_loop:           db      "No endless loop here!", 10
    hello_world_1:          db      "Hello World!", 10
    hello_world_2:          db      "This code was modified!", 10
    hello_world_3:
    
    ;****************************************************************************
    ;* smc_error
    ;****************************************************************************
    section .text
    smc_error:
            xor     dword eax, eax
            inc     dword eax
            mov     dword ebx, eax
            int     byte  0x80
    
    ;****************************************************************************
    ;* smc_end
    ;****************************************************************************
    section .text
    smc_end:
            xor     dword eax, eax
            xor     dword ebx, ebx
            inc     dword eax
            int     byte  0x80
    smc_end_1:
    ;*********************************************** linuxassembly@unusedino.de *
    Vous avez toutes les connaissances théoriques pour comprendre le fonctionnement du code en général. Si vous voulez le comprendre plus en détail, allez voir les mnémoniques que vous ne comprenez pas dans le manuel de nasm ou essayez de calculer les adresses relatives en imaginant que les labels représentent des adresses fixes.
  1. Debuggers
    Pour découvrir les sources de bugs dans un code compiler on utilise des debuggers. Il en existe de deux sortes : les debuggers passifs et les debuggers actifs. Les debuggers passifs sont également appelés désassembleurs car ils ne font qu'afficher le code source de l'éxécutable. Ainsi, ils sont passifs du fait qu'ils n'éxécutent pas une ligne de code. Les debuggers actifs, eux, éxécutent le code tout en permettant grâce à certains signaux système de bloquer l'éxécution du programme.
  • Ndisasm :Cet outil est le désassembleur Nasm. Créé par les développeurs de notre compilo favori, il affiche en sortie du code nasm (cad avec la syntaxe intel). Bien que le code de sortie puisse paraître différent du fichier source.
    Une petite astuce : si vous voulez que le code désassembler ressemble un peu plus à ce que vous avez coder utilisez l'option -b 32. Qui spécifie que le code sera désassemblé en utilisant des registres de 32 bits.
    ndisasm -b 32 hello_world | less
  • GDB :gdb est le debugger fournie par défaut avec Linux, il est à gcc ce qu'est ndisasm à nasm. Bien qu'il soit beaucoup plus développé. gdb est un debugger ce qui veut dire qu'il est capable de lancer un processus, de le bloquer, de le désassembler ...
    Nous allons voir comment effectuer quelques opérations de bases avec gdb via un exemple.
    Nous allons donc chercher à debugger un programme simple qui s'occupe de nous donner la moyenne de plusieurs nombres contenus dans un tableau.
    #include <stdio.h>
    
    int main(void) {
            int a[10] = {1, 58, 45, 87, 78, 98, 15, 56, 78, 21};
            printf ("Moyenne du tableau a : %d", moyenne(a, sizeof(a)));
            return 0;
    }
    
    int moyenne(int tableau[], int taille) {
            int total = 0, moyenne, i;
            for( i=0; i<taille; i++)
                    total += tableau[i];
            return total/taille;
    }
    Pour compiler ce code nous allons utiliser l'option -g de gcc permettant d'avoir des informations de debuggage plus précises : gcc moyenne.c -g -o moyenne.
    flyers@Cyfik:~$ gdb moyenne
    
    (gdb) run
    Starting program: /home/flyers/moyenne 
    Moyenne du tableau a : 10027686
    Program exited normally.
    
    (gdb) break main
    Breakpoint 1 at 0x8048394: file moyenne.c, line 12.
    On place un breakpoint à l'adresse de la fonction main() : 0x8048394. Et on relance le programme.
    (gdb) run
    Starting program: /home/flyers/moyenne 
    
    Breakpoint 1, main () at moyenne.c:12
    12              int a[10] = {1, 58, 45, 87, 78, 98, 15, 56, 78, 21};
    
    (gdb) print a
    $1 = {0, 134518396, -1073742984, 134513293, -1208116144, -1073742972, 
      -1073742952, 134513787, -1073742804, -1208116128}
    On affiche notre tableau qui n'est pas encore initialisé (c'est pour ça qu'il contient des valeurs fantaisistes).
    (gdb) next
    13              printf ("Moyenne du tableau a : %d", moyenne(a, sizeof(a)));
    (gdb) print a
    $2 = {1, 58, 45, 87, 78, 98, 15, 56, 78, 21}
    Après initialisation, il est tout beau le tableau :)
    (gdb) step
    moyenne (tableau=0xbffffb60, taille=40) at moyenne.c:18
    18              int total = 0, moyenne, i;
    next permet de passer à l'instruction suivante mais pas à sauter vers la fonction appelée tandis que step sert justement à cela. On saute dans la fonction moyenne()
    (gdb) display tableau[i]
    1: tableau[i] = 1
    
    (gdb) display total
    2: total = -1073742896
    display permet d'afficher les valeurs des variables de façon actualisée.
    (gdb) next
    19              for( i=0; i<taille; i++)
    2: total = -1208115591
    1: tableau[i] = -1208116128
    Après une bonne vingtaine de next (lorsque l'on valide une ligne vide, gdb refait l'instruction précédente), on se rend compte que des valeurs bizarres sont ajoutées à la variable total. Cela est dûe à la variable taille qui est à la base du fait que l'on effectue trop d'itérations, ce qui fait que la variable tableau[i] prend des valeurs au hasard dans la pile et celles-ci sont ajoutées à la valeur de la variable total.
    L'initialisation de la variable taille est donc foireuse. En effet, sizeof ne renvoie pas le nombre d'éléments contenues dans le tableau mais la taille en octet de celui-ci ce qui donne donc 40 (10 * int où int = 4 sur notre x86).
    (gdb) quit
    The program is running.  Exit anyway? (y or n) y
    Ainsi si l'on modifie la ligne
    moyenne(a, sizeof(a))
    par
    moyenne(a, sizeof(a)/sizeof(*a))
    notre programme fonctionnera parfaitement avec une variable taille contenant 10.

    gdb permet d'utiliser de nombreuses commandes, ce qui risque d'être fastidieux à apprendre et à taper surtout pour les gros programmes à debugger c'est pour cela qu'il existe des abbréviations de ces commandes (ainsi que l'auto-complétion cf: touche “TAB”) dont voici un petit récapitulatif :
    help
    h
    Accéder à l'aide
    run
    r
    Lancer le processus
    next
    n
    Ligne suivante dans le code
    step
    s
    Sauter dans une fonction
    break
    b
    Mettre en place un breakpoint
    print
    p
    Afficher une variable
    quit
    q
    Quitter gdb
    frame
    f
    Affiche des informations sur la page de pile courante
    backtrace
    bt
    Affiche la pile d'appels
    info
    i
    Donne des informations (info sans arguments pour savoir sur quoi)
    disable
    dis
    Désactiver momentanément un breakpoint
    enable
    en
    Activer un breakpoint
    delete
    d
    Supprimer un breakpoint
    continue
    c
    Continuer le programme après un breakpoint
    disassemble
    disass
    Désassemble tout ou partie du code.

    La commande set permet de définir toute sorte de paramètres (dans gdb ou dans le programme en cours de debuggage). Pour voir tout ce que l'on peut définir : “h set“. Une petite astuce pour nous autre asmongueurs :) à la sauce intel : gdb de base désassemble le code dans la syntaxe gas ce qui n'est pas très compréhensible pour nous, qu'à cela ne tienne, la commande “set disassembly-flavor intel” réparera cet affront et faira en sorte que le code désassemblé sera dans la syntaxe intel.
  • Strace :Strace est un debugger actif permettant de suivre tous les appels et les signaux émis par le programme. Cet utilitaire est très utile pour commencer l'étude du fonctionnement d'un binaire au sein du système.
    Par exemple, si l'on strace notre hello_world de début d'article, on ne verra que les appels aux syscall write et exit. Tandis que le strace de hello_printf nous montreras tous les appels et signaux de la libc lors du lancement du programme ainsi que lors de la sortie. Je vous laisse tester par vous mêmes :).
  • Application :Nous allons maintenant appliquer les notions que nous avons apprises ci-avant. Pour cela on va tenter d'outre-passer une protection de type anti-ptrace. Le code contenant la protection “ptraced.c”:
    #include <sys/ptrace.h>
    
    int main(){
    if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) exit(1);
    puts("FOO");
    }
    Ainsi donc, si le programme est ptracé alors le programme quitte. Sinon il nous affiche la chaîne “FOO”.
    flyers@Cyfik:~$ gdb ptraced
    
    (gdb) r
    Starting program: /home/flyers/Data/Code/tmp/ptraced
    
    Program exited with code 01.           # Le code a bien repéré qu'on le ptrace
    
    (gdb) set disassembly-flavor intel
    
    (gdb) disass main
    Dump of assembler code for function main:
    0x080483f4 :   push   ebp
    0x080483f5 :   mov    ebp,esp
    0x080483f7 :   sub    esp,0x18
    0x080483fa :   and    esp,0xfffffff0
    0x080483fd :   mov    eax,0x0
    0x08048402 :   sub    esp,eax
    0x08048404 :   mov    DWORD PTR [esp+12],0x0
    0x0804840c :   mov    DWORD PTR [esp+8],0x1
    0x08048414 :   mov    DWORD PTR [esp+4],0x0
    0x0804841c :   mov    DWORD PTR [esp],0x0
    0x08048423 :   call   0x80482f8 <_init+56>
    0x08048428 :   cmp    eax,0xffffffff          # c'est ici que le code vérifie qu'on
                                                  # le ptrace
    0x0804842b :   jne    0x8048439               # si eax != -1 on saute à 0x8048439 
    0x0804842d :   mov    DWORD PTR [esp],0x1
    0x08048434 :   call   0x8048318 <_init+88>
    0x08048439 :   mov    DWORD PTR [esp],0x8048564
    0x08048440 :   call   0x80482e8 <_init+40>
    0x08048445 :   leave  
    0x08048446 :   ret    
    End of assembler dump.
    
    (gdb) b *0x08048428          # un breakpoint juste avant le jne
    Breakpoint 1 at 0x8048428: file ptraced.c, line 4.
    
    (gdb) r
    Starting program: /home/flyers/Data/Code/tmp/ptraced 
    
    Breakpoint 1, 0x08048428 in main () at ptraced.c:4
    4               if (ptrace(PTRACE_TRACEME, 0, 1, 0) == -1) exit(1);
    
    (gdb) p $eax
    $1 = -1
    
    (gdb) set $eax=0      # on change la valeur de eax pour faire croire au programme qu'il
                          # n'est pas ptracé
    
    (gdb) c
    Continuing.
    FOO
    
    Program exited with code 06.     # et ça marche !
    (gdb) 
    Comme le montre cette session, la méthodologie est tout d'abord de regarder (si on le peut) où se trouve la protection puis de trouver la méthode la plus adaptée pour l'outre-passer.
  1. Conclusion
    Vous devriez maintenant posséder toutes les notions pour faire vos propres programmes mais également pour comprendre ceux des autres. N'oubliez pas que si vous ne comprenez pas une mnémonique, le manuel nasm est là pour vous aidez ; si vous ne comprenez pas un syscall, il y a la liste des syscalls et enfin, si votre problème est impénétrable, pensez à nos amis debuggers :)
  2. Références
  3. Remerciements
    Un grand merci à tous ceux qui m'ont aidés à écrire cet article et à mieux comprendre l'assembleur. J'ai nommé :
    edcba
    Karsten Scheibler
    kaze
    mammon_
    neil
    viriiz

Par Flyers

No comments:

Post a Comment

Merci de votre commentaire. Celui-ci est en cours de modération.

Most seen