[PROLOG] 0x000, In Assembler we trust (French version)

September 26, 2022    edito malware prolog

Dans ce voyage au cœur des fichiers binaires et du code exécutable se trouve l’empereur de tous les langages informatique, la source première du dialogue avec nos CPU : l’assembleur.

Je ne vais pas produire ici sur ce blog des cours d’assembleur. Il en existe de nombreux et d’excellente facture sur internet. Je vais juste poser quelques rappels qui m’apparaissent essentiels pour la suite de notre voyage dans le Reverse Engineering des binaires.

Nature de l’assembleur

L’assembleur est un langage. Comme ce langage est spécifique et lié au type de CPU auquel il s’adresse, il en existe donc plusieurs types. Le nom de l’assembleur prend d’ailleurs le nom du CPU pour lequel il est destiné. Dans le cadre de notre apprentissage, nous allons limiter nos reverses à 2 familles de CPU : INTEL X64/32 et ARM (64 bit).

  • INTEL (aka x86) : nous allons principalement lire (et un peu écrire) du code Intel 32 bit (x86_32) et Intel 64 bit. On retrouve l’Intel 64-bit derrière les différents acronymes suivants : 'x64', 'x86_64', 'Intel64', 'AMD64'. Le choix de cette architecture de processeur nous permettra d’adresser les PC (sous Windows et sous Linux avec leurs différents formats de fichier binaire : ELF pour Linux, PE (32-bits) et PE+(64-bit) pour Windows)

  • ARM 64 : L’étude de code s’exécutant sur processeur ARM64 (souvent désigné par AArch64), nous permettra de reverser et de comprendre les applications et malwares compilés nativement pour les Mac M1 au format binaire exécutable MachO64.

L’assembleur est la “dernière” grammaire/abstraction/représentation qu’un humain peut raisonnablement utiliser pour écrire les instructions qu’il souhaite faire exécuter par le CPU. Ce code est ensuite traduit en hexa et en binaire. Et oui, vous pourriez directement programmer en binaire si vous aviez un temps infini ;-)

Tailles et unités

Il m’apparaît intéressant de rappeler ici quelques unités sur les informations que nous allons manipuler :

- BYTE - un Octet (8 bits) | Permet de stocker des valeurs entre 0-255 ou -128 à 127 

- WORD - Word (16 bits) | permet de stocker des valeurs entre 0 - 65535 ou -32768 à 32767 

- DWORD - Double word (32 bits) | Permet de stocker des valeurs de 0 - 232 

- QWORD - Quad word (64 bits) | Permet de stocker des valeurs de 0 0 - 2^64

Les registres des CPU x86 et x64

Chaque CPU dispose d’un ensemble de registres d’intérêt général, 8 pour x86 et 16 pour x86-64. Un registre est une zone mémoire particulière, intégrée au CPU, dont l’accès est ultrarapide et qui permet de stocker des données non-typées de manière (très) temporaire. C’est par ces/ses registres (mais pas uniquement) que le CPU reçoit et “transfert” les informations, les conserve temporairement et les transmet selon les instructions de son unité de contrôle (ECU).

En architecture 32-bits, les registres ont une capacité de stockage de 4 octets. Sur les CPU 64 bits, les registres ont une capacité de stockage de 8 octets.

RegistreNomSous-registre
RAXAccumulatorEAX(32), AX(16), AH(8), AL(8)
RBXBaseEBX(32), BX(16), BH(8), BL(8)
RCXCounterECX(32), CX(16), CH(8), CL(8)
RDXDataEDX(32), DX(16), DH(8), DL(8)
RSISourceESI(32), SI(16), SL(8)
RDIDestinationEDI(32), DI(16), DL(8)
RBPBase pointerEBP(32), BP(16), BPL(8)
RSPStack pointerESP(32), SP(16), SPL(8)
New registersNew registersR8D-R15D(32), R8W-R15W(16), R8B-R15B(8)

Note Les suffixes utilisés pour adresser les bits de faible poids des New registers sont :

  • B byte, 8 bits
  • W word, 16 bits
  • D double word, 32 bits

Nous reviendrons sur les registres très prochainement, afin de présenter leur convention d’usage et notamment sur les OS Linux et Windows 64-bits

A un même assembleur, 2 syntaxes :

Pour des raisons historiques, il existe 2 syntaxes possibles pour un même code assembleur : La syntaxe AT&T et la syntaxe INTEL. Comprenez bien : il s’agit du même assembleur (donc des mêmes instructions). Seules les conventions d’écriture changent.

Prenons un code qui en langage C serait:

int i = 62;
j = i;

Syntaxe INTEL

mov rax,0x3e
mov [ebp-8],rax

Syntaxe AT&T

movq $0x3e,%rax
movq %rax,-8(%ebp)

Les principales différences entre les 2 syntaxes sont résumées dans le tableau ci-dessous :

A titre personnel, j’ai une préférence pour la syntaxe Intel. Sachez néanmoins que nous allons beaucoup utiliser le débugger GDB, et que ce dernier utilise par défaut la syntaxe AT&T. Si comme moi vous voulez lui faire générer de l’Intel, c’est possible.

set disassembly-flavor intel

A ce stade, nous avons 2 notions essentielles avec lesquelles il vous faut vous familiariser : La stack et les conventions d'appel. Justement, c’est tout l’objet du billet suivant.