Les Buffers Overflows Techniques et évolution par Raphaël GERMON

Les shellcodes

Un shellcode est un code injecté en mémoire vers lequel l'exécution d'un programme est dirigé. Il permet ainsi de réaliser une opération compromettant le système (typiquement ouvrir un shell distant). Il est représenté sous un ensemble de caractères qui représente du code binaire exécutable. Son exécution est déclenché lorsque l'adresse de retour d'une fonction est remplacé par l'adresse du shellcode injecté, généralement à l'aide d'un dépassement de tampon comme vu précédemment.


Ainsi, lorsque la routine est terminée, le microprocesseur, qui doit normalement exécuter les instructions situées à l'adresse de retour, exécute le shellcode.


Écriture de shellcodes

L'écriture de shellcodes est soumise à des contraintes. En effet, un shellcode est une chaîne de caractères qui va être injectée en mémoire car elle sera en dehors de l'espace normalement alloué. Or les chaînes de caractères, dans la plupart des langages de programmation, ont l'octet nul (0x00) comme marqueur de fin. Par exemple la fonction strcpy en C arrête la copie de chaine dès qu'elle rencontre cet octet. Un shellcode ne peut donc pas contenir d'octet 0x00, sinon, il ne sera pas entièrement copié.

L'écriture d'un shellcode demande alors de n'avoir recours à aucune instruction assembleur contenant un octet nul (à l'exception du dernier). La tâche étant ardue, les concepteurs de shellcodes importants écrivent initialement un « chargeur » de shellcode servant à transformer un code assembleur (pouvant contenir des octets nuls) en un code ne contenant pas d'octet nul (le code du chargeur devant à son tour être écrit sans caractère nul). Une technique classique consiste à transformer chaque octet du code par une opération « ou exclusif » (XOR) : cette opération est simple, réversible, et on peut généralement (quoique pas à coup sûr) trouver une « clé » à appliquer au « ou exclusif » permettant d'éviter les caractères nuls.

Dans ces deux cas, la technique est la même que précédemment : écrire un « décodeur » avec ces contraintes qui transforme (en mémoire) le véritable code malveillant encodé précédemment. L'écriture du c½ur du code malveillant (on parle de payload) est alors plus facile et indépendante du type de contraintes de la cible. Ainsi, il existe actuellement de véritables bibliothèques de construction de shellcodes permettant de l'assembler par type de fonction à réaliser et par contraintes (par "codeur" à intégrer).



Les OS modernes sont équipés de protection pour empêcher ce type d'exploit à l'aide de diverses techniques de protections. Il faut savoir que les premières ont été inclues sous Windows à partir de Windows XP SP2.


Exemple de shellcode

Voici un exemple classique d'une cinquantaine d'octets permettant d'exécuter (via un appel système à l'interruption 0x80 qui permet d'entrer en kernel mode et executer la fonction demandée) le programme /bin/sh.

Pour rappel, un appel système (en anglais, system call, abrégé en syscall) est une fonction fournie par le noyau d'un système d'exploitation. Suivant les Os nos syscall seront appelés différemment.

Exemple d'appel système fréquemment utilisé: open, write, read, close, chmod, chown ...

L'adresse des syscalls change selon si vous êtes en 32 ou 64 bits.

Ce shellcode est programmé avec des instructions ne contenant aucun caractère nul. Il ne fonctionne que sur architecture x86, sous Linux.

char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh"