Comment jeter un œil à l’intérieur des fichiers binaires à partir de la ligne de commande Linux



Vous êtes face à un fichier dont vous ignorez la nature ? La commande « file » sous Linux vous permettra d’identifier rapidement son type. S’il s’agit d’un fichier binaire, vous pourrez approfondir son analyse grâce à une suite d’outils complémentaires. Cet article vous guidera à travers l’utilisation de certains de ces outils.

Détection des Types de Fichiers

Les fichiers possèdent généralement des caractéristiques intrinsèques qui permettent aux applications de déterminer leur nature, et donc comment traiter les données qu’ils contiennent. Il serait absurde de tenter d’ouvrir un fichier PNG avec un lecteur MP3 ; c’est pourquoi il est essentiel pour un fichier de posséder une forme d’identification.

Cette identification peut se manifester par quelques octets de signature, placés au début du fichier, indiquant clairement son format et son contenu. Dans d’autres cas, le type de fichier est déduit de l’agencement interne des données, une structure appelée architecture de fichier.

Certains systèmes d’exploitation, comme Windows, se fient principalement à l’extension du fichier. On pourrait qualifier cela de naïf ou de confiant, mais Windows considère qu’un fichier avec l’extension DOCX est effectivement un document de traitement de texte. Linux, en revanche, procède différemment, comme vous le constaterez. Il exige une preuve et examine le contenu du fichier pour l’obtenir.

Les outils présentés ici étaient préinstallés sur les distributions Manjaro 20, Fedora 21 et Ubuntu 20.04 que nous avons utilisées pour cet article. Débutons notre investigation en utilisant la commande « file ».

Manipulation de la Commande « file »

Nous avons un assortiment de fichiers de différents types dans notre répertoire courant : des documents, du code source, des exécutables et des fichiers texte.

La commande « ls » affichera le contenu du répertoire, et l’option « -hl » (tailles lisibles par l’humain, liste détaillée) indiquera la taille de chaque fichier :

ls -hl

Essayons d’identifier la nature de certains de ces fichiers :

file build_instructions.odt
file build_instructions.pdf
file COBOL_Report_Apr60.djvu

Les trois types de fichiers sont correctement identifiés. Dans la mesure du possible, « file » donne des informations supplémentaires. Par exemple, le fichier PDF est signalé comme étant au format de version 1.5.

Même si l’on renomme le fichier ODT avec une extension arbitraire, par exemple « XYZ », il sera toujours correctement identifié, aussi bien dans l’explorateur de fichiers que par la commande « file ».

Dans l’explorateur de fichiers, l’icône appropriée est attribuée. En ligne de commande, la commande « file » ignore l’extension et examine le contenu pour déterminer le type :

file build_instructions.xyz

L’utilisation de « file » sur des fichiers multimédias (images, musique) fournit des informations sur leur format, leur encodage, leur résolution, etc.

file screenshot.png
file screenshot.jpg
file Pachelbel_Canon_In_D.mp3

Il est intéressant de noter que « file » n’interprète pas un fichier texte par son extension. Ainsi, un fichier avec l’extension « .c » contenant du texte standard et non du code source ne sera pas considéré comme un véritable fichier de code source C :

file function+headers.h
file makefile
file hello.c

« file » identifie correctement le fichier d’en-tête (« .h ») comme faisant partie d’une collection de fichiers de code source C, et reconnaît le « makefile » comme un script.

Utilisation de « file » avec les Fichiers Binaires

Les fichiers binaires sont plus opaques que les autres. Les images peuvent être visualisées, les fichiers audio peuvent être écoutés, et les documents peuvent être ouverts avec le logiciel approprié. Les fichiers binaires représentent un défi plus important.

Par exemple, les fichiers « hello » et « wd » sont des exécutables binaires, donc des programmes. Le fichier nommé « wd.o » est un fichier objet. Quand le code source est compilé, un ou plusieurs fichiers objets sont créés. Ils contiennent le code machine que l’ordinateur exécutera, ainsi que des informations pour l’éditeur de liens. L’éditeur de liens combine les fichiers objets et toutes les bibliothèques dont le programme dépend. Le résultat est un fichier exécutable.

Le fichier « watch.exe » est un exécutable binaire compilé pour Windows :

file wd
file wd.o
file hello
file watch.exe

En commençant par le dernier, « file » nous révèle que « watch.exe » est un exécutable PE32+, un programme console pour la famille de processeurs x86 sous Microsoft Windows. PE signifie Portable Executable, un format qui a des versions 32 et 64 bits. PE32 est la version 32 bits et PE32+ est la version 64 bits.

Les trois autres fichiers sont identifiés comme ELF (Executable and Linkable Format), un standard pour les fichiers exécutables et les fichiers d’objets partagés (comme les bibliothèques). Nous examinerons le format d’en-tête ELF sous peu.

Vous remarquerez que les deux exécutables (« wd » et « hello ») sont identifiés comme objets partagés de la Linux Standard Base (LSB), et le fichier objet « wd.o » comme un LSB relocalisable. L’absence du terme « exécutable » est notable.

Les fichiers objets sont relocalisables, c’est-à-dire que le code qu’ils contiennent peut être chargé à n’importe quelle adresse en mémoire. Les exécutables sont répertoriés comme des objets partagés car l’éditeur de liens les crée à partir de fichiers objets, en conservant cette caractéristique.

Cela permet l’Address Space Layout Randomization (ASLR) de charger les exécutables en mémoire à des adresses aléatoires. Les exécutables standard ont une adresse de chargement inscrite dans leurs en-têtes, qui impose leur emplacement en mémoire.

L’ASLR est une technique de sécurité. Le chargement d’exécutables à des adresses prévisibles les rend vulnérables aux attaques. En effet, leurs points d’entrée et les emplacements de leurs fonctions sont connus des attaquants. Les exécutables indépendants de la position (Position-Independent Executables, ou PIE) chargés à des adresses aléatoires évitent cette vulnérabilité.

Si nous compilons notre programme avec le compilateur gcc et l’option « -no-pie », nous créerons un exécutable conventionnel.

L’option « -o » (fichier de sortie) nous permet de nommer l’exécutable :

gcc -o hello -no-pie hello.c

Utilisons « file » sur le nouvel exécutable et voyons les différences :

file hello

La taille de l’exécutable est identique (17 ko) :

ls -hl hello

Le binaire est maintenant identifié comme un exécutable standard. Ceci est uniquement à des fins de démonstration. Si vous compilez vos applications de cette façon, vous perdez les bénéfices de l’ASLR.

Pourquoi un Exécutable est-il si Volumineux ?

Notre programme « hello » fait 17 ko, ce qui n’est pas énorme, mais tout est relatif. Le code source, lui, ne fait que 120 octets :

cat hello.c

Qu’est-ce qui gonfle le binaire, alors qu’il ne fait qu’afficher une chaîne de caractères dans le terminal ? Il y a bien un en-tête ELF, mais il ne fait que 64 octets pour un binaire 64 bits. Il y a donc autre chose :

ls -hl hello

Commençons par scanner le binaire avec « strings », pour avoir une idée de son contenu. Nous limiterons la sortie avec « less » :

strings hello | less

Il y a de nombreuses chaînes de caractères dans le binaire, outre le « Hello, Geek world! » de notre code source. La plupart sont des étiquettes pour les régions du binaire, ainsi que les noms et les informations de liaison des objets partagés. Cela inclut les bibliothèques et leurs fonctions, dont dépend le binaire.

La commande « ldd » nous montre les dépendances d’objets partagés d’un binaire :

ldd hello

La sortie affiche trois entrées, dont deux indiquent un chemin de répertoire (la première n’en a pas) :

linux-vdso.so : Le Virtual Dynamic Shared Object (VDSO) est un mécanisme du noyau qui permet d’accéder à des routines du noyau depuis un programme utilisateur. Cela évite la surcharge d’un changement de contexte entre les modes utilisateur et noyau. Les objets partagés VDSO respectent le format ELF (Executable and Linkable Format), ce qui permet de les lier dynamiquement au binaire à l’exécution. Le VDSO est alloué dynamiquement et utilise l’ASLR. La capacité VDSO est fournie par la GNU C Library si le noyau prend en charge l’ASLR.
libc.so.6 : La GNU C Library en tant qu’objet partagé.
/lib64/ld-linux-x86-64.so.2 : C’est l’éditeur de liens dynamique que le binaire doit utiliser. L’éditeur de liens interroge le binaire pour connaître ses dépendances. Il charge ces objets partagés en mémoire, prépare le binaire à l’exécution, trouve les dépendances et permet d’y accéder, puis lance le programme.

L’En-tête ELF

Nous pouvons examiner et décoder l’en-tête ELF avec l’outil « readelf » et l’option « -h » (en-tête de fichier) :

readelf -h hello

L’en-tête est interprété pour nous.

Le premier octet de tout binaire ELF est 0x7F en hexadécimal. Les trois octets suivants sont 0x45, 0x4C et 0x46. Le premier octet indique qu’il s’agit d’un binaire ELF. Et pour être plus clair, les trois octets suivants forment la chaîne « ELF » en ASCII :

Classe : Indique si le binaire est un exécutable 32 ou 64 bits (1=32, 2=64).
Données : Indique l’endianness utilisé. L’endianness détermine la manière dont les nombres multi-octets sont stockés. Dans le codage big-endian, le nombre est stocké avec ses bits les plus significatifs en premier. Dans le codage little-endian, c’est l’inverse.
Version : La version d’ELF (actuellement, c’est 1).
OS/ABI : Le type d’interface binaire d’application utilisé. Cela définit l’interface entre deux modules binaires, par exemple un programme et une bibliothèque partagée.
Version ABI : La version de l’ABI.
Type : Le type de binaire ELF. Les valeurs courantes sont ET_REL pour une ressource relocalisable (comme un fichier objet), ET_EXEC pour un exécutable compilé sans l’option -no-pie, et ET_DYN pour un exécutable compatible ASLR.
Machine : L’architecture du jeu d’instructions. Indique la plate-forme cible du binaire.
Version : Toujours défini à 1, pour cette version d’ELF.
Adresse du point d’entrée : L’adresse mémoire dans le binaire où l’exécution commence.

Les autres entrées sont des tailles et des nombres de régions et de sections du binaire, permettant de calculer leurs emplacements.

Un aperçu rapide des huit premiers octets du binaire avec « hexdump » affichera l’octet de signature et la chaîne « ELF » dans les quatre premiers octets du fichier. L’option « -C » (canonique) donne la représentation ASCII des octets à côté de leurs valeurs hexadécimales. L’option « -n » (nombre) permet de spécifier le nombre d’octets à afficher :

hexdump -C -n 8 hello

« objdump » et la Vue Détaillée

Si vous voulez une analyse granulaire, vous pouvez utiliser la commande « objdump » avec l’option « -d » (désassembler) :

objdump -d hello | less

Cela désassemble le code machine exécutable et l’affiche sous forme d’octets hexadécimaux, avec leur équivalent en langage assembleur. L’adresse du premier octet de chaque ligne est indiquée à gauche.

Ceci est utile si vous pouvez lire l’assembleur ou si vous êtes curieux de voir ce qui se passe en coulisses. La sortie est volumineuse, c’est pourquoi nous l’avons affichée avec « less ».

Compilation et Liaison

Il existe de nombreuses façons de compiler un binaire. Par exemple, le développeur peut choisir d’inclure ou non des informations de débogage. La façon dont le binaire est lié joue également un rôle dans son contenu et sa taille. Si un binaire partage des objets en tant que dépendances externes, il sera plus petit que s’il inclut statiquement ses dépendances.

La plupart des développeurs connaissent déjà les commandes présentées ici. Pour les autres, elles permettent d’explorer et d’analyser le contenu d’un binaire.