# Shellcode

Un shellcode est une chaîne de caractères qui représente un code binaire exécutable. À l'origine destiné à lancer un shell ('/bin/sh' sous Unix ou command.com sous DOS et Microsoft Windows par exemple), le mot a évolué pour désigner tout code malveillant qui détourne un programme de son exécution normale. Un shellcode peut être utilisé par un hacker voulant avoir accès à une interface en ligne de commande.
Historiquement, les shellcodes ont été développés dans le cadre de la recherche en sécurité informatique pour démontrer les vulnérabilités des systèmes. Par exemple, ils ont été utilisés dans des attaques célèbres telles que le ver Morris en 1988.

## Fonctionnement d'un shellcode

Pour mieux comprendre le fonctionnement des shellcodes, il est utile de connaître les bases de l'architecture des systèmes informatiques, notamment la gestion de la mémoire et le fonctionnement des buffers.
Généralement, les shellcodes sont injectés dans la mémoire de l'ordinateur grâce à l'exploitation d'un dépassement de tampon. Dans ce cas, l'exécution du shellcode peut être déclenchée par le remplacement dans la pile (stack en anglais) de l'adresse normale de retour par l'adresse du shellcode injecté. 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 chaîne 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 certains contextes, les contraintes sont encore plus sévères : le jeu d'octets autorisés peut être encore plus réduit. Il est possible que la chaîne envoyée au programme vulnérable ait précédemment été filtrée pour ne faire passer que des caractères alphanumériques (ayant un code ASCII compris entre 32 et 127 par exemple). On trouve un autre exemple classique sur certains systèmes fonctionnant en interne en UTF-16 (cf Unicode) : il arrive qu'une chaîne de caractères codée sur 8 bits soit d'abord « étendue » en UTF-16 (en ajoutant un 0 un octet sur deux généralement) puis traitée. Dans ce cas, les instructions qui pourront être exécutées par l'attaquant seront forcément de la forme XX00 (en hexadécimal), ce qui complique énormément la tâche du concepteur de shellcode.
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).

## Détection de shellcodes

L'exploitant agissant généralement avec une certaine marge d'erreur lors de l'exploitation d'une faille dans la découverte de la bonne adresse mémoire (voir Dépassement de tampon), le shellcode est préfixé d'un maximum d'instructions nulpotentes (opcode NOP, codé 0x90 sur architecture 80x86) pour optimiser les chances d'exécution du shellcode. Ainsi, la première méthode de détection de shellcodes a été de repérer les séries de NOP en mémoire pour empêcher d'exécuter le shellcode qui suit les NOP. Cette méthode est cependant relativement coûteuse en temps de calcul.
D'autres méthodes se basent sur le fait qu'un shellcode contient souvent certaines chaînes de caractères spécifiques servant à ouvrir un shell comme /bin/sh. Un programme faisant référence à cette chaîne de caractères est donc suspect et il est alors possible d'empêcher son exécution.
Cependant, les hackers peuvent outrepasser ces méthodes basiques de détection en chiffrant cette chaîne de caractères, ou même en utilisant du code polymorphe.
Un autre moyen de détection est de regarder si le programme essaye d'accéder à une zone mémoire qui n'est pas attribuée au programme en cours.
Néanmoins, les techniques de prévention modernes incluent l'utilisation de solutions de sécurité avancées comme l'ASLR (Address Space Layout Randomization) et les DEP (Data Execution Prevention), qui compliquent l'exécution de shellcodes.

## Exemple de shellcode

Exemple de shellcode, largement publié (voir liens externes), d'une cinquantaine d'octets permettant d'exécuter (via un appel à l'interruption 0x80) le programme /bin/sh. Ce shellcode est programmé avec des instructions ne contenant aucun caractère nul. Il ne fonctionne que sur architecture 80x86, sous Linux.