Bonjour,
Suite à une discussion sur IRC avec Xia, je vous propose d'écrire votre premier programme (ou presque) en langage d'assemblage x86.
En fait nous allons faire mieux : nous allons désassembler un programme écrit en C, puis le ré-assembler juste pour le fun.
Je pars du principe que vous avez un environnement Linux.
Ouvrez votre éditeur favoris et rédigez le code suivant :
1 2 3 4 5 6 7 8 | % cat main.c #include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { printf("Hello, world!\n"); return EXIT_SUCCESS; } |
Vous l'aurez deviné : c'est du C.
Compilez le code source au moyen de la ligne suivante :
1 | % gcc -o main main.c -m32 |
Le drapeau -m32
spécifie que le code généré doit être exécutable sur une architecture 32-bit. Nous voulons désassembler et réassembler du code Intel 32-bit pour rester un maximum accessible (peut-être que des vieux de la vieille ont encore un processeur intel x86 ? )
Ceci fait, récupérons quelques informations sur notre programme :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | % file main main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=0xbb18920a5be16ccd8f707db06e5abf964f9bcf7c, not stripped % readelf -h main ELF Header: Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 Class: ELF32 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: EXEC (Executable file) Machine: Intel 80386 Version: 0x1 Entry point address: 0x8048320 Start of program headers: 52 (bytes into file) Start of section headers: 1964 (bytes into file) Flags: 0x0 Size of this header: 52 (bytes) Size of program headers: 32 (bytes) Number of program headers: 8 Size of section headers: 40 (bytes) Number of section headers: 31 Section header string table index: 28 |
Le binaire main
est structuré au format ELF. Si cet acronyme vous est étrangé et que vous souhaitiez en savoir plus, je vois renvoie sur l'introduction à la rétro-ingénierie des binaires.
Notre objectif : désassembler le fichier, puis le réassembler.
Il faut comprendre que notre fichier main
n'est en fait qu'une collection de structures destinées à décrire le fonctionnement du fichier (propriétés des segments mémoire à cartographier, ce qu'ils contiennent, des informations sur les symboles de débogage, etc).
Pour cela… On va écrire un désassembleur naïf en python. Et quand je dis naïf, je ne joue pas sur les mots !
En effet, celui-ci va prendre en entrée un fichier binaire et va générer en sortie un fichier source en langage d'assemblage, au format .asm.
Pour cela, on va utiliser python3 (coucou nohar ! coucou !) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | % cat naive_disassembler.py #!/usr/bin/env python3 import sys if len(sys.argv) < 2: print("Usage: %s <binary>" % (sys.argv[0])) sys.exit(-1) with open(sys.argv[1], "rb") as file_handle: binary_content = file_handle.read() file_handle.close() with open("%s.asm" % (sys.argv[1]), "w") as output_asm_file: output_asm_file.write("bits 32\n\n") for c in binary_content: output_asm_file.write("db 0x%02X\n" % (c & 0xff)) output_asm_file.close() print("Written %d bytes into output file!" % len(binary_content)) |
Démonstration :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ge0@samaritan ~/python/retrodisass % ./naive_disassembler.py Usage: ./naive_disassembler.py <binary> ge0@samaritan ~/python/retrodisass % ./naive_disassembler.py ./main Written 4868 bytes into output file! ge0@samaritan ~/python/retrodisass % head main.asm bits 32 db 0x7F db 0x45 db 0x4C db 0x46 db 0x01 db 0x01 db 0x01 db 0x00 |
C'est là que le "(Vous allez être déçu)" du sous-titre prend tout son sens ! On s'est contenté d'écrire une série débile d'instructions "db", qui signifient en fait data byte. En résumé, on déclare les octets un par un, sans écrire de mnémonique (exemple : mov eax, 0xdeadbeef
).
La première directive, "bits 32" au début du fichier, sert à indiquer que celui-ci contient en fait du code pour processeur Intel 32-bit. Cette directive n'est réellement utile que si nous avions écrit des instructions à l'aide de mnémoniques.
Pourquoi je m'arrête là ? Pour que vous puissiez pratiquer vous-mêmes sans trop d'effort. On ré-assemble le fichier ? Pour ce faire, on va utiliser nasm, un désassembleur puissant pour architectures intel ! (http://www.nasm.us)
1 | % nasm -f bin main.asm -o main_assembled |
L'option -f
permet de spécifier que le format de sortie sera du binaire pur. En effet, dans notre fichier .asm, nous avons plus ou moins fait un "dump" de notre binaire, en incluant ses en-têtes, ses symboles, son code… Le tout dans une joyeuse série idiote de "db". Mais ça fonctionne au réassemblage !
1 2 3 4 | % md5sum main_assembled d63e1c7dee081a3a1f5c210481ef928f main_assembled % md5sum main d63e1c7dee081a3a1f5c210481ef928f main |
Quel est l'intérêt, me demanderez-vous ? On pourrait imaginer un désassembleur plus intelligent, générant un fichier source qui décrirait de manière claire les en-têtes, les données, le code… Pour ensuite le modifier à souhait.
Paradoxal de faire ça quand on a entre les mains un logiciel "privateur", vous ne trouvez pas ? Et pourtant…
Bonus : un hello world en intel x86, avec commandes de fabrication fournies :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | % cat helloworld.asm bits 32 extern printf section .data ; section des données (format ELF) helloWorld db "Hello world", 10, 0 ; 10 = Line Feed, 0 = Null-Byte section .text ; section du code (format ELF) global main ; pour le linker main: push ebp ; prologue mov ebp, esp sub esp, 4 ; on alloue de l'espace mémoire au sein de la pile mov dword [esp], helloWorld ; dépose l'argument sur la pile call printf xor eax, eax ; return 0 leave ret ; épilogue / fin de la fonction |
Assemblons et lions (il n'y a pas de compilation à proprement parler ) :
1 2 3 4 5 | % nasm -f elf32 helloworld.asm -o helloworld.o ge0@samaritan ~/python/retrodisass % gcc -o helloworld helloworld.o -m32 % ./helloworld Hello world |
C'est tout. Pour le moment.