L’expression (*(void(*)()) shellcode)()
est une expression que vous retrouverez souvent lorsqu’il s’agira d’exécuter un shellcode dans un POC en C ou en C++. Pour comprendre cette expression complexe, il vous faut d’abord être à l’aise avec les expressions suivantes :
int* var0 // pointeur vers un entier
*var0 // accès au contenu référencé par var0
(int*) var0 // cast (transformation) de var0 en un pointeur d'entier
void f0() // déclaration d'une fonction ne renvoyant rien
Vous avez probablement l’habitude de manipuler des pointeurs d’objets. Plus précisément, des pointeurs vers des zones de la mémoire contenant des données. Mais, il est également possible de créer des pointeurs de fonction qui référencent des zones de la mémoire contenant du code exécutable.
void(*)()
void(*)()
représente un pointeur de fonction (vers une fonction ne renvoyant rien et prenant des paramètres non spécifiés). C’est l’équivalent du int*
pour un pointeur d’entier.
Voici un exemple comparant la déclaration d’une fonction et la déclaration d’un pointeur de fonction :
// Déclaration d'une fonction
// <type> <identificateur>(<paramètres>);
int function_name( int );
// Déclaration d'un pointeur de fonction
// <type> (*<identificateur>)(<paramètres>);
int (*pointer_name)( int );
Les parenthèses (* )
permettent de faire la différence entre la déclaration d’une fonction et un pointeur de fonction. Tout comme le prototype d’une fonction, le type de retour ainsi que le type des arguments doivent être indiqués.
(* … )()
(* pointeur )()
permet d’appeler la fonction référencée par pointeur
.
int add1(int var0){
...
}
int (*pointeur)( int );
pointeur = &add1;
(*pointeur)(99); // équivalent à : add1(99)
(*(void(*)()) shellcode)()
Nous pouvons à présent comprendre l’expression.
(void(*)()) shellcode
représente donc un cast (transformation) de la variable shellcode
en un pointeur de fonction.
Maintenant que nous avons notre pointeur, il ne reste plus qu’à appeler la fonction grâce à la syntaxe (* pointeur )()
, ce qui donne :
(* (void(*)()) shellcode )()
Mais pourquoi ?
Un shellcode est un code binaire exécutable destiné à lancer un shell. Il n’est donc pas possible de l’exécuter via un appel de fonction classique. Une des manières de tester un shellcode est donc de stocker le shellcode dans la mémoire et d’utiliser un pointeur de fonction pour l’exécuter. Vous l’aurez compris, dans notre exemple, shellcode
est un pointeur vers la zone mémoire contenant notre shellcode.
// exemple
#include <stdio.h>
unsigned char random[] = //randomness
unsigned char shellcode[] = //shellcode binary
int main(void){
((void(*)()) shellcode)();
}