AFFICHER CET ARTICLE EN MODE PAGE ENTIERE
SOMMAIRE
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
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
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].
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
?
Prenons 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) |
Ce 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 |
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
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*/ |
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.
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.
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
----------*/ #define
BUFFER_LEN 4 int
main(int argc, char **argv)
_char *arg[] = { TARGET, "flip",
NULL }; |
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).
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.
-----------------------------------------------------------------------------------
[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