Hackflash #5 : Ecrire un shellcode

Un shellcode est un petit programme dont il faut déclencher l'exécution, généralement en écrasant l'adresse de retour d'un programme dans la pile (la valeur de EIP empilée par le CALL du programme appelant) au moyen d'un buffer overflow.
Ecrire un shellcode
Qu'est-il attendu d'un shellcode, et quelles sont les contraintes qui imposent son écriture en assembleur ?
A la base, un shellcode est écrit en assembleur pour en optimiser la taille, mais aussi contrôler les valeurs des octets qui composent ses opcodes.
Généralement, le shellcode appelle une fonction du système, parmi les fameux syscalls.
La liste de syscalls, notamment le numéro par lequel ils sont identifiés lors d'un appel depuis le shellcode, diffère selon que l'architecture est 32 bits ("x86") ou 64 bits. Une liste ramassée des syscalls, qui précise pour chacune son numéro selon l'architecture, se trouve ici.
Noter que ces syscalls sont parfois désignés avec le préfixe sys_, mais que ce n'est visiblement pas leur désignation officielle. Par exemple, il n'existe pas de syscall sys_write () : c'est write ().
Pour appeler un syscall depuis le shellcode, il faut procéder à une interruption 0x80 après avoir fourni le numéro du syscall dans AL, et ses arguments dans des registres toujours assignés aux arguments selon leur rang. Sur Intel :
Argument : arg[1] arg[2] arg[3] arg[4] arg[5] arg[6]
Registre : EBX ECX EDX ESI EDI EBP
Comme les opcodes du shellcode sont souvent destinés à être injectés via un argument lors de l'appel au programme dont il s'agit d'exploiter une faille, donc sous la forme d'une chaîne dans la ligne de commandes, ces opcodes ne doivent pas comporter un octet dont la valeur correspondrait au code ASCII d'un caractère auquel le mécanisme d'extraction des arguments donne un sens. Liste non exhaustive :
0x00Fin de la ligne de commandes
0x0aFin de la ligne de commandes
0x0dFin de la ligne de commandes
Par ailleurs, le shellcode ne peut consister qu'en une seule section .text. Autrement dit, toute donnée que le shellcode utilise doit être générée par le shellcode, ou alors récupérée par ce dernier en mémoire où elle pré-existe, comme par exemple dans la valeur d'une variable d'environnement.
Par exemple, pour disposer d'une chaîne ABC\n à afficher lors d'un appel à sys_write (), il faut trouver un moyen de stocker la chaîne, tout en évitant de mentionner son \n (0x0a). Une solution :
mov eax,0x08424140
add eax,0x02010101
push eax
En effet, dans les opcodes, on observe alors :
$ objdump rootme
...
 8048064:       b8 40 41 42 08          mov    $0x8424140,%eax
 8048069:       05 01 01 01 02          add    $0x2010101,%eax
 804806e:       50                      push   %eax
Pourquoi partir de 0x08 et lui ajouter 0x02 pour générer \n, et non pas de 0x09 et lui ajouter 0x01 ? Parce que 0x09 correspond à a tabulation, caractère dont le code ASCII ne doit donc pas être mentionné dans la ligne de commandes, donc dans les opcodes.
Le shellcode comprendra un appel à sys_write (), suivi d'un appel à sys_exit () pour terminer proprement :
  • Pour sys_write (), documenté ici :
    ssize_t write(int fd, const void *buf, size_t count);
    
    AL0x04 pour sys_write ()
    EBX1 pour stdout
    ECXESP après avoir empilé ABC\n
    EDX4, longueur de ABC\n
  • Pour exit (), documenté ici :
    void _exit(int status);
    
    AL0x01 pour exit ()
    EBX0 pour le code de retour
Ce qui donne :
section .text
global _start
_start:
	xor edx,edx
	mov dl,4			;EDX = longueur de "ABC\n"
	mov eax,$08424140
	add eax,$02010101
	push eax
	mov ecx,esp			;ECX = adresse de "ABC\n"
	xor ebx,ebx
	mov bl,1			;EBX = stdout
	xor eax,eax
	mov al,4			;EAX = 4
	int 0x80
	xor eax,eax
	mov al,1			;EAX = 1
	int 0x80
Les opcodes peuvent être récupérés en découpant le résultat retourné par OBJDUMP après assemblage, ici avec NASM :
$ nasm -f elf32 rootme.asm -o rootme.o

$ for i in $(objdump -d rootme.o | grep "^ " | cut -f2); do echo -n '\x'$i; done; echo
\x31\xd2\xb2\x04\xb8\x40\x41\x42\x08\x05\x01\x01\x01\x02\x50\x89\xe1\x31\xdb\xb3\x01\x31\xc0\xb0\x04\xcd\x80\x31\xc0\xb0\x01\xcd\x80
Hackflash #5 : Ecrire un shellcode