Annonce

Réduire
Aucune annonce.

Analyse d'un code d'amorçage pour FreeBSD : pmbr - Bugs détectés (info exclusive)

Réduire
X
 
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Analyse d'un code d'amorçage pour FreeBSD : pmbr - Bugs détectés (info exclusive)

    Introduction

    Je désirais faire un papier sur l’assembleur depuis bien longtemps mais je ne trouvais pas sous quel angle aborder le sujet.

    Je ne voulais pas faire un énième tutoriel sur « hello world ». C’est alors que j’ai eu l’idée d’examiner un programme déjà fait et d’en détailler l’étude ici. Car, si ma pensée est juste, c’est plutôt de cette façon que la plupart d’entre vous vont y être confronté, c’est-à-dire en tâchant de craquer un programme ou bien lors d’un challenge. Et là, on se retrouve face à une liste de mnémoniques qu’il faut comprendre.

    J’y ai bien réfléchi et désassembler, par exemple, le plus simple des accessoires Windows aurait été d’une grande longueur. Alors, j’ai cherché des programmes plus courts, sans interface graphique. Je me suis dit qu’il fallait que je regarde dans les logiciels écrits en assembleur à la base. Cependant, il faut bien avouer que les programmes écrits dans ce langage se font rares car tout ou presque peut être rédigé en C ou en C++ sans véritables inconvénients par rapport à l’assembleur.

    Alors, j’ai juste regardé mon dernier article : https://hackademics.fr/forum/syst%C3...-d%C2%92effort

    Pour ceux qui l’ont lu, rappelez-vous :
    [email protected]:/usr/home/Icarus # gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 2 ada1
    partcode written to ada1p2
    bootcode written to ada1
    Avec cette commande, pmbr est écrit dans le premier secteur du disque ada1 tandis que gptzfsboot est écrit dans la partition N°2 de type freebsd-boot. Ces deux codes vont permettre l’amorçage du système depuis le BIOS (sans EFI). Il se trouve que pmbr a été écrit en assembleur et qu’il est très court (place disponible dans un secteur MBR oblige).

    pmbr ne fait qu’extraire les données du schéma de partitionnement GPT puis il localise la partition freebsd-boot avant de la charger en mémoire et d’exécuter le code qu’elle contient (ici gptzfsboot).

    Toujours est-il que je me suis lancé dans l’explication détaillé de programme. Je crois que c’est lorsque je suis arrivé à la septième page que je me suis dit que c’était trop long, trop compliqué pour tout expliquer dans un simple message mais j’ai quand même continué par pure curiosité personnelle.

    C’est là que, incidemment, je suis tombé sur un bug. J’ai contacté l’auteur, M. John Baldwin qui m’a confirmé en retour que mon analyse était juste. Il a également pointé une autre erreur que je n’avais pas vue. Puis, en tâchant de corriger le code, je me suis aperçu que d’autres bugs étaient présents. Au total, ce n’est pas moins de cinq erreurs que j’ai relevées (plus une approximation que je trouve dangereuse).

    Alors attention, tous ces bugs n’ont que peu d’incidences. Ce programme a été écrit en 2007 et est demeuré inchangé depuis. Il est probable que personne n’a été confronté à l’un de ces effets. Cela ne touche que les disques de plus de 2 Tio et dans deux cas précis (quoique liés) : si la partition freebsd-boot se situe au-delà de 2 Tio à partir du début du disque (et seulement dans quelques cas particuliers) ou si l’entête GPT dit « primaire » est endommagé. Nous verrons qu’il en existe une sauvegarde ainsi que des descripteurs de partitions mais situés à la fin du disque (donc au-delà de 2 Tio si le disque a une capacité supérieure).

    Techniquement, il s’agit d’erreurs d’arithmétique sur des données en 64 bits que l’on traite via des registres 32 bits. Je tâcherai de les détailler au mieux. Mais en attendant, il faut passer par quelques généralités : les modes de fonctionnement d’un processeur X86 64 bits, les particularités de l’assembleur version AT&T ainsi que la structure du schéma de partitionnement GPT.


    Modes de fonctionnement d’un processeur x86 64 bits

    Un tel processeur dispose de lignes de données 64 bits mais aussi de trois modes de fonctionnement distincts :
    • Le mode protégé 64 bits
    • Le mode protégé 32 bits
    • Le mode réel 16 bits
    Sans entrer dans une longue explication, les modes protégés utilisent une vision virtuelle, idéalisée de la mémoire, charge au SE de la mettre en place à l’aide de divers mécanismes présents dans le processeur. De plus, un système de contrôle d’accès aux ressources (dont la mémoire) et de niveaux de privilège permet d’éviter que les applications n’interfèrent avec les autres programmes et le système. Pour en savoir plus : https://fr.wikipedia.org/wiki/Mode_prot%C3%A9g%C3%A9

    Le mode réel, comme le nom l’indique, utilise directement les zones mémoires comme elles sont, sans contrôle ni filtre. Il s’agit du mode antique de fonctionnement des processeurs X86 directement hérité du 8086 lui-même. Il faut savoir que lorsqu’un processeur X86, quel qu’il soit, est redémarré, il bascule automatiquement dans ce mode. C’est donc lui qui va nous intéresser pour cet article car c’est bien de démarrage « legacy BIOS », dont il s’agit.

    Dans ce mode, oubliez vos gigaoctets de mémoire. Au temps du 8086, les PC avaient au plus 640 Kio de mémoire. Il n’est jamais venu à l’esprit des concepteurs qu’on dépasserait le Mio…
    Du coup, l’adressage mémoire a été défini sur 20 bits soit 2^20 octets au maximum ou encore 1024 Kio, autrement dit 1 Mio (il est possible de dépasser quelque peu ce maximum mais c’est ici hors sujet). On utilise deux registres 16 bits pour adresser la mémoire : le premier s’appelle le registre de segment et le second, le déplacement (offset).

    Les registres de segment disponibles ont des usages prédéterminés : CS est le segment où le code est exécuté (IP), SS celui de pile (SP), DS est le segment de base pour les données. Il y a d’autres registres de segment en réserve pour les données : ES, FS, GS.

    Par exemple : DS = 0x01000, AX = 0x3471 à DS:AX adresse = 0x13471 (segment x 16 + déplacement).
    Rien de très difficile, même si cela est un poil plus complexe que de l’assembleur en mode protégé car, dans ce dernier mode, il n’y pas de calcul pour générer une adresse mémoire.


    L’assembleur version AT&T

    Si j’aborde ce point, c’est parce que pmbr a été écrit avec cette syntaxe car c’est celle reconnue par l’utilitaire as. Le problème, c’est que je n’en ai pas l’habitude, ayant toujours programmé avec la syntaxe Intel, utilisée entre autres par NASM et FASM. Et je pense qu’il en est de même pour beaucoup de programmeurs.

    Dans la syntaxe AT&T, les registres sont préfixés par % mais surtout on est dans le modèle « mnémonique source, destination » alors que c’est l’inverse avec la syntaxe Intel. Beaucoup de mnémoniques ont un suffixe d’une lettre précisant la taille de la donnée sur laquelle on travaille : b = octet (byte, 8 bits), w = mot (word, 16 bits), l = double-mot (long, 32 bits), etc.
    On pourrait se dire que ces suffixes apportent une certaine clarté dans le code mais pas vraiment. Ainsi, les opérations mémoire ne sont pas toujours très claires :
    Code:
    movb NHRDRV,%dh            # NHRDRV = 0x475 -> MOV DH,[0x475]
    movw $DPBUF+DPBUF_SEC,%si  # MOV SI, DPBUF+DPBUF_SEC
    adcl $0,4(%si)             # ADC DWORD [SI+4],0

    Il y a d’autres différences encore mais relativement mineures. Voici la référence pour comprendre sa syntaxe : https://sourceware.org/binutils/docs...l#SEC_Contents


    Le schéma de partitionnement GPT

    Tout ce qu’il faut savoir se trouve ici : https://fr.wikipedia.org/wiki/GUID_Partition_Table
    Je vais néanmoins citer ce qui est important pour comprendre le fonctionnement de pmbr.

    Le schéma de partitionnement historique sur PC est MBR (Master Boot Record). Il est encore très utilisé. Dans ce schéma, toutes les informations sont contenues dans le premier secteur du disque (appelé aussi LBA 0). On y trouve le programme de démarrage ainsi que quatre descripteurs de partition. Chaque descripteur contient le type de partition ainsi que le début et la fin de celle-ci.

    GPT étend considérablement les possibilités offertes par MBR : on peut avoir jusque 2^32 - 1 partitions et on peut adresser des zones au-delà de 2 Tio dans le disque (ce qui est impossible avec MBR). Initialement prévu pour être utilisé de pair avec EFI, il est possible de l’employer pour amorcer un système avec BIOS. C’est ce que fait pmbr.

    Le premier secteur d’un disque GPT est appelé Protective MBR. Il s’agit en fait d’un faux partitionnement de type MBR. Il dispose d’une table de partition avec une seule entrée (ou descripteur) désignant l’utilisation d’un système de fichier inconnu pour tout le disque (ou ses deux premiers Tio si le disque est plus grand). Ce dispositif sert en cas d’utilisation d’outils de partitionnement anciens qui ne connaissent pas GPT. En général, comme ils ne comprennent pas le type de partition installé, ils décident (ou proposent) de ne rien faire, d’où le terme « Protective ».

    Le deuxième secteur (LBA 1) va receler l’entête GPT. Il commence obligatoirement par la chaîne de caractères « EFI PART » et contient diverses valeurs comme le numéro de secteur où se trouvent les premiers descripteurs de partition (les éventuels autres suivent ce secteur), le nombre de partitions dans le disque, la taille en octets d’un descripteur, etc.

    Les descripteurs de partition ont normalement une longueur de 128 octets. Le type de partition est codé sur 16 octets (c’est un GUID). On y trouve, entre autres choses et hormis le type, le début et la fin de la partition en termes de secteurs LBA du disque. Ces deux données sont codées sur 64 bits, ce qui autorise l’adressage de disque jusqu’à (si la taille de secteur reste à 512 octets) : 2^64 x 512 = 9444732965739290427392 octets, soit 8589934592 Tio ou encore 8192 Eio soit 8 Zio (à peu près 9,4 Zo). On peut dire qu’il y a de la marge…

    GPT prévoit un entête et une table de partition de secours au cas où l’entête primaire serait corrompu. Ce deuxième entête se situe dans le dernier secteur du disque tandis que la copie de la table de partition se situe quelques secteurs avant la fin du disque.


    Fonctionnement de pmbr

    Je vais décrire les opérations effectuées par pmbr mais de façon globale, sans entrer dans les détails. L'analyse complète du code fait 11 pages et est disponible avec le code source original de pmbr ici.

    En préambule, il est utile de dire ce que fait le BIOS pour amorcer un système d’exploitation : il dispose d’une liste de périphériques pour démarrer. Il va prendre le premier périphérique, tenter de démarrer dessus et si ça ne marche pas, passer au suivant dans la liste, etc.

    Lorsqu’il s’agit d’un disque dur, le BIOS va faire exactement ceci : charger son premier secteur (dit LBA 0) à l’adresse mémoire 0x07c00. Un secteur fait normalement 512 soit 0x200 octets. Le BIOS va ensuite regarder quels sont les deux derniers octets de ce secteur. S’il s’agit de 0x55, 0xaa, il considère que le secteur est amorçable et il lance l’exécution à 0x07c00 où se retrouve donc pmbr.
    • L’essentiel du code se copie à l’adresse 0x61a afin de disposer de plus de place pour travailler et transfère l’exécution à cette adresse.
    • Il valide le numéro du disque dur démarré (au sens BIOS : le premier est 0x80, le second est 0x81, etc.) qui est transmis par le BIOS via le registre %dl.
    • Il demande au BIOS combien ce disque a de secteurs afin de savoir où se trouve l’entête GPT de secours (le BIOS donne ce nombre sur 64 bits).
    • Il commande la lecture du deuxième secteur du disque (LBA 1) et vérifie si ce dernier comporte la signature « EFI PART » (recherche de l’entête GPT). Si la signature n’est pas correcte, il recherche la sauvegarde de l’entête GPT dans le dernier secteur du disque dur. S’il ne la trouve pas non plus, il stoppe avec le message « Invalid partition table ».
    • Un entête GPT valide étant en mémoire, il prend connaissance des trois paramètres suivants : numéro de secteur débutant la table de partition, taille d’un descripteur de partition en octets et nombre de partitions dans le disque.
    • Il charge en mémoire le premier secteur de la table de partition puis examine le premier descripteur afin de savoir s’il s’agit du type freebsd-boot. Si ce n’est pas le cas, il passe au second descripteur et ainsi de suite jusqu’à ce qu’il trouve ou arrive au dernier descripteur sans que ce soit le bon. Auquel cas, il affiche le message « Missing boot loader » et s’arrête. A noter que les descripteurs peuvent être stockés dans plusieurs secteurs consécutifs. Pour cette raison, à chaque descripteur à examiner, pmbr regarde si ce dernier est toujours dans le dernier secteur chargé. Si ce n’est pas le cas, il commande le chargement du secteur suivant.
    • Lorsque le descripteur ayant le type freebsd-boot est localisé, la partition est entièrement chargée à partir de l’adresse 0x7c00. Le programme s’occupe de placer en mémoire tous les secteurs concernés d’après les informations lues dans le descripteur correspondant, c’est-à-dire le premier secteur de cette partition et le dernier. Si lors du chargement, pmbr constate que cette partition excède 545 Kio en taille, il affiche le message « Boot loader too large » et stoppe son exécution.
    • Une fois que la partition est chargée entièrement, le contrôle lui est conféré par un saut à 0x7c00.
    Carte mémoire de pmbr après transfert :








    Les erreurs dans pmbr

    Cela commence au moment où l’entête primaire GPT ne serait pas valide et où il est question de charger l’entête de secours. On trouve le code suivant :
    Code:
    #
    # Try alternative LBAs from the last sector for the GPT header.
    #
    main.3:   movb $0,%dh                  # %dh := 0 (reading backup)
              movw $DPBUF+DPBUF_SEC,%si    # %si = last sector + 1
              movw $lba,%di                # %di = $lba
    main.3a:  decl (%si)                   # 0x0(%si) = last sec (0-31)
              movw $2,%cx
              rep
              movsw                        # $lastsec--, copy it to $lba
              jmp main.2a                  # Read the next sector

    Nous avons là pas moins de trois erreurs. Premièrement, le numéro de LBA est codé sur 64 bits, autrement dit 4 mots (words) mais on n’en copie que 2 avec movw $2,%cx. Ensuite, on ne décrémente que les 32 bits de poids faible du LBA, on ne tient pas compte d’une éventuelle retenue (par exemple, si on a initialement 0x0000 0002 0000 0000, on devrait obtenir 0x0000 0001 FFFF FFFF ; avec ce code, on arrive à 0x0000 0002 FFFF FFFF). Par ailleurs decl n’affecte pas le drapeau de retenue, donc ce n’est pas avec cette instruction qu’il faut décrémenter mais avec subl. On va retrouver ce dernier type d’erreur en deux autres endroits du code.

    Voici la portion de code corrigée :
    Code:
    #
    # Try alternative LBAs from the last sector for the GPT header.
    #
    main.3:    movb $0,%dh                 # %dh := 0 (reading backup)
               movw $DPBUF+DPBUF_SEC,%si   # %si = last sector + 1
               movw $lba,%di               # %di = $lba
    main.3a:   subl $1, (%si)              # 0x0(%si) = last sec (0-31)
               sbbl $0, 4(%si)
               movw $4,%cx
               rep
               movsw                       # $lastsec--, copy it to $lba
               jmp main.2a                 # Read the next sector

    Ensuite, le code de vérification du type de partition freebsd-boot, bien qu’apparemment fonctionnel comporte une approximation inquiétante :
    Code:
    #
    # Load a partition table sector from disk and look for a FreeBSD boot
    # partition.
    #
    load_part: movw $GPT_ADDR+GPT_PART_LBA,%si
               movw $PART_ADDR,%bx
               call read
    scan:      movw %bx,%si                # Compare partition UUID
               movw $boot_uuid,%di         # with FreeBSD boot UUID
               movb $0x10,%cl
               repe cmpsb
               jnz next_part               # Didn't match, next partition

    %cl est placé à 0x10 mais c’est %cx qui est utilisé comme compteur par l’instruction repe. Il faut croire que %ch est toujours à 0 à ce moment-là. Il aurait été plus correct d'utiliser : movw $0x10,%cx.

    Lorsque le descripteur de partition examiné révèle que la partition n’est pas de type freebsd-boot, on passe au descripteur suivant :
    Code:
    next_part:  decl GPT_ADDR+GPT_NPART          # Was this the last partition?
                jz err_noboot
                movw GPT_ADDR+GPT_PART_SIZE,%ax
                addw %ax,%bx                     # Next partition
                cmpw $PART_ADDR+0x200,%bx        # Still in sector?
                jb scan
                incl GPT_ADDR+GPT_PART_LBA       # Next sector
                adcl $0,GPT_ADDR+GPT_PART_LBA+4
                jmp load_part

    On retrouve ici le bug concernant l’arithmétique sur 64 bits. Incl comme decl ne génère pas de retenue (n’affecte pas le drapeau carry). Il faudrait la remplacer par addl $1, GPT_ADDR+GPT_PART_LBA.

    Enfin, exactement la même chose se retrouve dans la portion de code qui charge le programme de boot présent dans la partition freebsd-boot en mémoire :
    Code:
    #
    # We found a boot partition.  Load it into RAM starting at 0x7c00.
    #
                movw %bx,%di                   # Save partition pointer in %di
                leaw PART_START_LBA(%di),%si
                movw $LOAD/16,%bx
                movw %bx,%es
                xorw %bx,%bx
    load_boot:  push %si                       # Save %si
                call read
                pop %si                        # Restore
                movl PART_END_LBA(%di),%eax    # See if this was the last LBA
                cmpl (%si),%eax
                jnz next_boot
                movl PART_END_LBA+4(%di),%eax
                cmpl 4(%si),%eax
                jnz next_boot
                mov %bx,%es                    # Reset %es to zero
                jmp LOAD                       # Jump to boot code
    next_boot:  incl (%si)                     # Next LBA
                adcl $0,4(%si)
                mov %es,%ax                    # Adjust segment for next
                addw $SECSIZE/16,%ax           #  sector
                cmp $0x9000,%ax                # Don't load past 0x90000,
                jae err_big                    #  545k should be enough for
                mov %ax,%es                    #  any boot code. :)
                jmp load_boot

    Il faudrait remplacer incl (%si) par addl $1, (%si).



    Conclusions

    Ce travail fut long et laborieux mais très excitant. J’avoue cependant que c’est plus agréable de coder soi-même que de passer au crible le source des autres, fut-il aussi utile et prestigieux que pmbr.

    L’auteur a été averti de toutes les erreurs et j’espère qu’il en fera la correction pour la prochaine version.

    git : https://git.hackademics.fr/Icarus/An...mbr_de_FreeBSD
    Dernière modification par Icarus, 21 octobre 2018, 08h35.

  • #2
    Tudieu, au réveil, mon cerveau n'était pas prêt.
    Tu fais parti des magiciens de l'informatique de mon point de vue. Même si je comprends la teneur de ton papier, sa complexité m'empêche de comprendre les données en détails.

    J'y reviendrai plus tard, avec plus de neurones disponibles.

    Félicitations à toi en tout cas, car même si c'est largement hors de ce que je connais, cela reste abordable à la compréhension.

    Mon expérience en assembleur se limite à la recherche et l'exploitation de BUF Overflow, mais j'ai trouvé cela très interessant de pouvoir manipuler et d'exploiter un programme de cette manière.

    Je recherche d'ailleurs des tutos simples et vulgarisé sur l'exploitation des "HEAP" overflow si je ne dis pas de bétises.

    Respect, tu as un fan

    F0ngic
    Dernière modification par F0ngic, 21 octobre 2018, 09h42. Motif: mise en page

    Commentaire

    Chargement...
    X