Site WWW de Laurent Bloch
Slogan du site

ISSN 2271-3905
Cliquez ici si vous voulez visiter mon autre site, orienté vers des sujets moins techniques.

Pour recevoir (au plus une fois par semaine) les nouveautés de ce site, indiquez ici votre adresse électronique :

Le problème de la sécurité informatique :
Décoder un logiciel de cyberattaque
Article mis en ligne le 18 janvier 2023
dernière modification le 19 janvier 2023

par Laurent Bloch

Pour déminer une cyberattaque : des compétences en programmation

Les cyberattaques récentes mettent en évidence la question de la sécurité informatique, et ce mal vient avec un bien : rappeler à tout le monde l’importance de la programmation informatique. Il ne manque en effet pas d’oracles pour annoncer la fin de la programmation grâce à l’intelligence artificielle : il n’y a guère qu’un demi-siècle que j’entends cette prophétie, dont je laisse la responsabilité à ceux qui la divulguent. Mais lorsqu’il s’agit de décrypter un logiciel dangereux pour savoir comment le désamorcer, là, rien à faire, il faut des compétences en programmation, et même des compétences approfondies.

Le programme malfaisant qu’il s’agit de comprendre afin de l’empêcher de nuire a été écrit dans un langage de programmation compréhensible par un être humain. Entendons-nous : par un être humain qui aura appris ce langage, ce qui lui aura pris des mois, et pour une connaissance approfondie, des années. Le texte de ce programme tel qu’il a été écrit, dans un langage compréhensible (enfin, relativement compréhensible), est ce que l’on appelle le programme source, qui pour être exécuté doit être traduit en langage machine, le résultat de cette traduction est ce que l’on appelle le programme binaire.

Seulement voilà : les cyberattaquants n’ont en général pas l’amabilité de fournir à leurs victimes le texte de leur programme source. Le consultant en cybersécurité qui intervient pour le déminage doit d’abord trouver sur la scène du crime le vecteur d’attaque, c’est-à-dire le même programme sous forme binaire, et, pour y comprendre quelque chose, reconstituer le programme source à partir du binaire, effectuer la traduction inverse, si l’on veut, ce qui est très difficile, et demande des compétences extrêmes, non seulement en programmation mais aussi en architecture des ordinateurs.

En fait, si le programme a été écrit initialement dans un langage de niveau d’abstraction élevé, tel que le langage C ou C++ par exemple, ou si la vulnérabilité exploitée par l’attaquant se trouve dans un logiciel écrit dans un tel langage, reconstituer le programme source est une opération complexe au résultat incertain. Il est plus facile de désassembler le programme, c’est-à-dire de traduire le code binaire en langage assembleur, un langage qui correspond mot à mot (si j’ose dire) au langage machine, mais sous une forme lisible par un humain compétent. C’est cette démarche que nous allons expliquer ci-dessous, au moyen d’un exemple simpliste mais supposément pédagogique.

Un petit aparté à l’intention des lecteurs qui hésiteraient pour le choix d’une orientation professionnelle : les études qui mènent à ce type de compétence sont longues et ardues, mais celui qui s’en sera donné la peine ne risquera guère le chômage ni la misère. Le type d’intervention dont il est question ici se paie entre 1000 et 2000 euros la journée, et l’activité des cyberattaquants ne semble pas en voie d’extinction.

Anatomie d’un programme

Pour comprendre la démarche d’analyse d’un programme binaire, qu’il soit destiné à une cyberattaque ou à tout autre chose, il faut comprendre comment il est construit. Soit un programme simple en langage C, nommé test-disasm.c, qui se contente d’afficher un texte, et dont voici le code source :

  1. /*
  2.   Programme simple
  3. */
  4.  
  5. #include <stdlib.h>
  6. #include <stdio.h>
  7.  
  8. void afficher(void);
  9.  
  10. int main(void)
  11. {
  12.   afficher();
  13.   return EXIT_SUCCESS;
  14. }
  15.  
  16. void afficher(void)
  17. {
  18.   printf("printf peut afficher ceci.\n");
  19.   printf("            --------\n");
  20. }

Télécharger

Observons que le programmeur, pour rendre la lecture du programme plus intelligible, a donné un nom explicite à la fonction afficher. Remarquons aussi que le programme fait appel à deux bibliothèque externes, stdlib et stdio, qui vont ajouter au code écrit explicitement des sous-programmes (bien plus longs et bien plus compliqués que ce que l’on peut lire ici) pour accomplir des interactions avec le système d’exploitation, par exemple pour afficher des textes à l’écran.

On peut transformer ce programme source en programme exécutable par la commande gcc test-disasm.c, mais décomposons les opérations.

Traduisons ce programme source en assembleur par la commande gcc -S test-disasm.c, qui donne le texte de test-disasm.s ci-dessous. On remarque la syntaxe particulièrement biscornue de l’assembleur Intel x86 ; heureusement d’autres architectures, dont j’espère beaucoup pour l’avenir, telle l’architecture libre RISC-V, offrent des syntaxes plus lisibles.

  1.         .file   "test-disasm.c"
  2.         .text
  3.         .globl  main
  4.         .type   main, @function
  5. main:
  6. .LFB6:
  7.         .cfi_startproc
  8.         endbr64
  9.         pushq   %rbp
  10.         .cfi_def_cfa_offset 16
  11.         .cfi_offset 6, -16
  12.         movq    %rsp, %rbp
  13.         .cfi_def_cfa_register 6
  14.         call    afficher
  15.         movl    $0, %eax
  16.         popq    %rbp
  17.         .cfi_def_cfa 7, 8
  18.         ret
  19.         .cfi_endproc
  20. .LFE6:
  21.         .size   main, .-main
  22.         .section        .rodata
  23. .LC0:
  24.         .string "printf peut afficher ceci."
  25. .LC1:
  26.         .string "            --------"
  27.         .text
  28.         .globl  afficher
  29.         .type   afficher, @function
  30. afficher:
  31. .LFB7:
  32.         .cfi_startproc
  33.         endbr64
  34.         pushq   %rbp
  35.         .cfi_def_cfa_offset 16
  36.         .cfi_offset 6, -16
  37.         movq    %rsp, %rbp
  38.         .cfi_def_cfa_register 6
  39.         leaq    .LC0(%rip), %rax
  40.         movq    %rax, %rdi
  41.         call    puts@PLT
  42.         leaq    .LC1(%rip), %rax
  43.         movq    %rax, %rdi
  44.         call    puts@PLT
  45.         nop
  46.         popq    %rbp
  47.         .cfi_def_cfa 7, 8
  48.         ret
  49.         .cfi_endproc
  50. .LFE7:
  51.         .size   afficher, .-afficher
  52.         .ident  "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"
  53.         .section        .note.GNU-stack,"",@progbits
  54.         .section        .note.gnu.property,"a"
  55.         .align 8
  56.         .long   1f - 0f
  57.         .long   4f - 1f
  58.         .long   5
  59. 0:
  60.         .string "GNU"
  61. 1:
  62.         .align 8
  63.         .long   0xc0000002
  64.         .long   3f - 2f
  65. 2:
  66.         .long   0x3
  67. 3:
  68.         .align 8
  69. 4:

Télécharger

On remarque au début de certaines sections du code assembleur la présence de l’instruction endbr64 (en clair Terminate Indirect Branch in 64 bit) : elle figure dans le jeu d’instructions des processeurs d’architecture Intel récents pour protéger le programme contre les attaques de type Return-oriented Programming (ROP) ou Jump/Call-oriented Programming (JOP/COP), qui consistent à insérer dans un sous-programme une adresse de retour frauduleuse afin d’accéder à une adresse de code normalement inaccessible.

Traduisons le code assembleur en langage machine par la commande gcc -c test-disasm.s, qui donne le fichier test-disasm.o, que l’on peut examiner par la commande od -x test-disasm.o (l’affichage est en hexadécimal, en binaire ce seraient des pages et des pages) :

Plus intéressant, on peut afficher face à face le texte en assembleur et le texte binaire, en utilisant le programme objdump par la commande :

qui répond :

test-disasm.o n’est pas un programme complet, il lui manque printf, qui est un programme de bibliothèque. Voici comment obtenir un programme complet, qui ressemblera au texte ci-dessus, avec des ajouts :

À partir d’un exécutable, reconstituer le texte source

Après avoir compris les étapes successives du processus de traduction d’un programme source en programme binaire exécutable, telles qu’exposées par les sections ci-dessus, nous pouvons étudier la démarche inverse, qui consiste, à partir d’un programme binaire exécutable suspect récupéré sur la scène du crime, à reconstituer un programme source dans un langage à peu près compréhensible. Il est relativement facile de produire le texte assembleur, nettement moins évident de reconstituer le programme C. Cette opération de traduction inverse se nomme aussi désassemblage. Outre le déminage de logiciels malfaisants, le désassemblage peut également servir à la rétro-ingénierie d’un logiciel dont on souhaite reproduire les fonctionnalités pour son propre compte.

J’ai fait des essais avec deux désassembleurs : la version gratuite d’IDA Pro, conçu et réalisé par Ilfak Guilfanov (dont Wikipédia nous apprend qu’issu d’une famille de Tatars de la Volga, il s’est installé en Belgique), et Ghidra, logiciel libre réalisé par la National Security Agency (NSA), un éditeur de bonne réputation.

IDA Pro fournit le code assembleur, mais la version gratuite ne permet pas de reconstituer le texte C, que donnerait la version payante.

Fenêtre IDA Pro 8.2

Ghidra est d’une mise en œuvre un peu plus laborieuse qu’IDA Pro, l’affichage est assez touffu mais complet, et on peut reconstituer le texte d’un programme C, qui est bien sûr différent du programme d’origine, mais censé produire le même résultat.

Fenêtre Ghidra 10.2.2