Protection du code par vérification d’intégrité    Enregistrer au format PDF

Le principe de la protection par code checksum est de vérifier que la somme de ces opcodes n’a pas été modifiée, autrement dit, que le programme tourne avec son code original.


par S3cur3D

En ouvrant un éxécutable avec un éditeur hexadécimal tel hexedit, on peut avoir le bytecode de notre programme. Autrement dit, le code complet en hexadécimal ou en décimal (ce qui donne les caractères que l’on peut voir en ouvrant un éxécutable avec un éditeur texte, qui est la correspondance ascii du bytecode). Le code complet n’étant qu’une succession de nombres (les opcodes), il nous est tout à fait possible d’en faire la somme.

Pour ce faire, nous allons, à l’aide d’un programme simple en assembleur, tout d’abord vous montrer comment il est possible de contourner facilement des instructions de comparaison (cmp, cmpl) puis comment insérer un code de vérification de la somme des opcodes.

Exemple

Nous allons étudier le programme suivant, toujours codé en assembleur AT&T pour Unix. Il n’est autre qu’un classique Hello, World, légèrement modifié pour illustrer notre point et également codé en version longue (d’une part pour compliquer un peu le code et décourager les crackers débutants, c’est une bonne habitude à prendre, et d’autre part car les instructions telles xor ou inc sont bien plus rapides que mov, mais ce sont des détails 😉 ) :

  1.     .data   #declaration du segment des variables statiques initialisées
  2.  
  3.       bonjour: .string "Hello, World !\n"
  4.  
  5.       non_affiche: .string "Ce message ne peut pas être affiché\n"
  6.  
  7.     .text #declaration du segment code
  8.  
  9.       .global _start
  10.        _start:
  11.  
  12.          xorl %eax,%eax   #Affichage de Hello, World !
  13.          movb $4, %al
  14.          xorl %ebx,%ebx
  15.          inc %ebx
  16.          movl $bonjour,%ecx
  17.          xorl %edx,%edx
  18.          mov $15,%edx
  19.          int $0x80
  20.  
  21.          xorl %eax,%eax   #On mets eax à 0, puis on compare 1 et al, ce qui est donc toujours faux
  22.          cmp $1,%al
  23.          jne exit   #Ce Jump if Not Equal sera donc en théorie toujours réalisé
  24.  
  25.        naffiche:   #Affiche Ce message ne peut pas être affiché
  26.          xorl %eax,%eax
  27.          movb $4, %al
  28.          xorl %ebx,%ebx
  29.          inc %ebx
  30.          movl $non_affiche,%ecx
  31.          xorl %edx,%edx
  32.          mov $36,%edx
  33.          int $0x80
  34.  
  35.        exit:   #sortie
  36.          xorl %eax,%eax
  37.          xorl %ebx,%ebx
  38.          inc %eax
  39.          int $0x80

Télécharger

Pour ceux qui ne connaissent pas très bien l’assembleur, l’essentiel de ce code est expliqué dans la partie concernant le faux désassemblage. A noter que, comme indiqué, nous avons amplifié le code, par exemple, on aurait pu coder le label de sortie de façon plus simple :

  1.        exit:   #sortie
  2.          mov $1,%eax
  3.          mov $0,%ebx
  4.          int $0x80

Télécharger

Voici les sorties de ce programme en éxécution normale :

   $ gcc test.s -c -o test.o && ld test.o && ./a.out
   Hello, World !
   $

Comme prévu, le programme ne pouvant pas passer par le label naffiche (question de logique informatique), on a seulement un Hello, World ! classique qui s’affiche. Bien entendu, avec quelques connaissances légères en cracking, on sait facilement détourner le flux de ce programme. A l’aide d’un éditeur hexadécimal, on va donc modifier

   3C 01 (cmp $1,%al)
   75 15 (jne +15 <exit>)

en

   3C 00 (cmp $0,%al)
   75 15 (jne +15 <exit>)

ou

   3C 01 (cmp $1,%al)
   74 15 (je +15 <exit>)

Je pense que vous avez compris le but de la manoeuvre : soit on compare %al à 0, ce qui est toujours vrai et on n’utilise pas l’instruction Jump if Not Equal to exit, ou on compare %al à 1, ce qui est toujours faux et on n’utilise pas l’instruction Jump if Equal to exit). Vérifions le résultat de cette modification :

   $ hexedit a.out
   $./a.out
   Hello, World !
   Ce message ne peut pas être affiché
   $

Notre manipulation a donc parfaitement fonctionné et le cours du programme a été modifié, la présence d’un message qui n’aurait jamais pu être affiché en temps normal le prouve. Maintenant, nous allons protéger l’exécutable par code checksum. En réalité, on le voit bien dans les deux modifications précédentes que la somme des opcodes sera décrémentée de 1 après l’édition de l’exécutable. Voici le nouveau code protégé :

  1.     .data   #declaration du segment des variables statiques initialisées
  2.  
  3.       bonjour: .string "Hello, World !\n"
  4.       non_affiche: .string "Ce message ne peut pas être affiché\n"
  5.       tentative_crack: .string "Tentative de crack !\nAbandon...\n"
  6.  
  7.     .text   #déclaration du segment code
  8.  
  9.       .global _start
  10.        _start:
  11.  
  12.          jmp checksum   #On commence par effectuer Checksum
  13.          suite:   #on revient ici après le checksum s'il est positif
  14.  
  15.         xorl %eax,%eax
  16.         movb $4, %al
  17.         xorl %ebx,%ebx
  18.         inc %ebx
  19.         movl $bonjour,%ecx
  20.         xorl %edx,%edx
  21.         mov $15,%edx
  22.         int $0x80
  23.  
  24.         xorl %eax,%eax
  25.         cmp $1,%al
  26.         jne exit
  27.  
  28.       naffiche:
  29.         xorl %eax,%eax
  30.         movb $4, %al
  31.         xorl %ebx,%ebx
  32.         inc %ebx
  33.         movl $non_affiche,%ecx
  34.         xorl %edx,%edx
  35.         mov $36,%edx
  36.         int $0x80
  37.  
  38.       exit:
  39.         xorl %eax,%eax
  40.         xorl %ebx,%ebx
  41.         inc %eax
  42.         int $0x80
  43.  
  44.       checksum:   #fonction checksum
  45.         xorl %ebx,%ebx
  46.         mov $checksum,%ecx
  47.         sub $_start,%ecx
  48.         mov $_start,%esi
  49.       boucle:   #boucle d'addition des opcodes
  50.          lodsb
  51.          add %eax,%ebx
  52.          loop boucle
  53.          cmpl $5917,%ebx   #on a au préalable compté les opcodes et trouvé 5917
  54.          jne crack   #Si le résultat de la boucle n'est pas 5917, on passe à <crack>
  55.         jmp suite   #sinon on revient au début du programme
  56.  
  57.       crack:   #On avertit de la tentative de crack et on quitte
  58.         xorl %eax,%eax
  59.         movb $4, %al
  60.         xorl %ebx,%ebx
  61.         inc %ebx
  62.         movl $tentative_crack,%ecx
  63.         xorl %edx,%edx
  64.         mov $32,%edx
  65.         int $0x80
  66.         jmp exit

Télécharger

Il ne nous reste plus qu’à vérifier que ce code checksum marche réellement :

   $ gcc checksum.s -c -o checksum.o && ld checksum.o -o checksum && ./checksum
   Hello, World !
   $ hexedit checksum
   $ ./checksum
   Tentative de crack !
   Abandon...
   $

Tout s’est bien déroulé, la protection n’entrave pas le fonctionnement du programme et empêche toute modification de la somme des opcodes. Cette technique est réellement puissante, surtout quand elle est combinée à d’autres techniques comme le faux désassemblage, ce qui rend très dur pour l’éventuel reverser ou cracker de modifier le programme à sa guise (puisque rien que le positionnement d’un breakpoint terminera l’éxécution du programme). Il doit alors, soit combler ses modifications par des instructions sans importance mais qui feront la même somme au final, ce qui n’est pas facile, soit réussir à déjouer le faux assemblage puis à détourner la fonction checksum, ce qui est aussi ardu. Par conséquent, cette protection est de loin la plus efficace que nous vous ayons exposé ici. A vrai dire, la seule protection qui est plus efficace est la protection par chiffrement du code, que nous vous exposerons une fois la partie réseaux reconstruite.

Documentations publiées dans cette rubrique