Techniques de Hacking

Transcription

Techniques de Hacking
0x400
Réseau
La communication et le langage ont énormément amélioré les capacités de l’espèce
humaine. Grâce à une langue commune, les êtres humains sont capables de transférer des connaissances, de coordonner des actions et de partager des expériences.
De manière similaire, les programmes peuvent devenir beaucoup plus puissants
lorsqu’ils ont la possibilité de communiquer avec d’autres au travers d’un réseau. La
véritable utilité d’un navigateur Web se trouve non pas dans le programme lui-même,
mais dans sa capacité à communiquer avec des serveurs Web.
Les réseaux sont tellement répandus que leur présence est parfois considérée comme
allant de soi. De nombreuses applications, comme le courrier électronique, le Web
et la messagerie instantanée, reposent sur l’existence d’un réseau. Chacune de ces
applications s’appuie sur un protocole réseau précis, mais chaque protocole emploie
les mêmes méthodes de transport générales.
De nombreuses personnes ne réalisent pas que les protocoles réseau puissent être
eux-mêmes vulnérables. Dans ce chapitre, vous apprendrez à faire communiquer
vos applications au travers de sockets et à gérer les vulnérabilités réseau classiques.
0x410 Modèle OSI
Lorsque deux ordinateurs communiquent l’un avec l’autre, ils doivent parler la
même langue. La structure de cette langue est décrite dans les couches du modèle
OSI. Il apporte des standards qui permettent aux dispositifs matériels, comme les
routeurs et les pare-feu, de se focaliser sur un aspect particulier des communications
et d’ignorer les autres. Ainsi, le routeur et le pare-feu peuvent se concentrer sur le
passage de données aux couches inférieures, en ignorant les couches supérieures
utilisées par les applications. Voici les sept couches OSI :
■■
Couche physique. Cette couche gère la connexion physique entre deux points.
Il s’agit de la couche la plus basse, dont le rôle principal est d’échanger des flux
de bits bruts. Elle est également responsable de l’activation, du maintien et de
la désactivation de ces communications.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 203
07/06/12 18:42
204
Techniques de hacking

■■
Couche liaison de données. Cette couche s’occupe du transfert des données
entre deux points. À l’opposé de la couche physique, qui se charge de transmettre des bits bruts, celle-ci fournit des fonctions de haut niveau, comme la
correction des erreurs et le contrôle de flux. Elle dispose également de procédures pour activer, maintenir et désactiver les connexions de données.
■■
Couche réseau. Cette couche joue le rôle d’intermédiaire. Son rôle principal
est de passer les informations entre les couches inférieures et les couches supérieures. Elle apporte les fonctions d’adressage et de routage.
■■
Couche transport. Cette couche fournit un transfert transparent des données
entre les systèmes. En offrant des communications de données fiables, elle évite
aux couches supérieures de s’occuper de la fiabilité ou de l’efficacité des transmissions de données.
■■
Couche session. Cette couche est responsable de l’établissement et du maintien
des connexions entre des applications réseau.
■■
Couche présentation. Cette couche s’occupe de la présentation de données aux
applications, en employant une syntaxe ou un langage qu’elles comprennent.
Cela permet de mettre en œuvre des fonctions comme le chiffrement ou la
compression des données.
■■
Couche application. Cette couche est concernée par la prise en charge des
exigences de l’application.
Lorsque des données sont échangées au travers de ces couches de protocoles, elles
sont envoyées en petits morceaux appelés paquets. Chaque paquet comprend les
implémentations de ces couches de protocoles. En partant de la couche application,
les données du paquet sont enveloppées par la couche présentation, qui englobe
la couche session, qui englobe la couche transport, etc. Ce processus se nomme
encapsulation. Chaque couche encapsulée contient un en-tête et un corps. L’entête contient les informations nécessaires au protocole de la couche, tandis que le
corps contient les données de cette couche. Le corps d’une couche contient l’intégralité du paquet des couches précédemment traversées, comme la peau d’un oignon ou
les contextes de fonction placés sur la pile d’un programme.
Par exemple, lorsque nous naviguons sur le Web, le câble et la carte Ethernet
constituent la couche physique ; ils se chargent de la transmission des bits bruts
d’une extrémité du câble à l’autre. Ensuite vient la couche liaison de données. Dans
l’exemple du navigateur Web, Ethernet compose cette couche, en apportant les
communications de bas niveau entre les ports Ethernet sur le réseau. Ce protocole
met en œuvre des communications entre des ports Ethernet, mais ils n’ont pas encore
d’adresses IP. Le concept d’adresses IP n’intervient que dans la couche suivante,
la couche réseau. Outre l’adressage, cette couche est responsable du transfert des
données d’une adresse à une autre. L’ensemble de ces trois couches inférieures est
capable d’envoyer des paquets de données d’une adresse IP à une autre. La couche
suivante est la couche transport, c’est-à-dire TCP pour le trafic Web. Elle apporte
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 204
07/06/12 18:42
0x400
Réseau
205
une connexion bidirectionnelle sans interruption au travers des sockets. Le terme
TCP/IP décrit l’utilisation de TCP sur la couche transport et d’IP sur la couche
réseau. Il existe plusieurs modèles d’adressage dans cette couche. Cependant, votre
trafic Web utilise probablement IP version 4 (IPv4), dont les adresses ont le format
familier XX.XX.XX.XX. IP version 6 (IPv6) est également présent dans cette couche,
avec un schéma d’adressage totalement différent. Puisque le premier est le plus
répandu, dans cet ouvrage IP fera toujours référence à IPv4.
Le trafic Web lui-même utilise HTTP (Hypertext Transfer Protocol) pour les
communications, qui se trouve dans la couche supérieure du modèle OSI. Lorsque
nous surfons sur le Web, le navigateur de notre réseau communique au travers d’Internet avec le serveur Web qui se trouve sur un réseau privé différent. Au cours
de ces échanges, les paquets de données sont encapsulés alors qu’ils se dirigent
vers la couche physique où ils sont passés à un routeur. Puisque celui-ci n’est en
rien concerné par le contenu réel des paquets, il doit simplement implémenter les
protocoles jusqu’à la couche réseau. Le routeur envoie les paquets sur Internet, qui
atteignent ensuite le routeur de l’autre réseau. Ce routeur encapsule le paquet avec
l’en-tête du protocole de la couche inférieure afin qu’il atteigne sa destination finale.
Ce processus est représenté dans l’illustration suivante.
Toute cette encapsulation des paquets constitue un langage complexe que les hôtes
Internet (et d’autres types de réseaux) utilisent pour communiquer les uns avec
les autres. Ces protocoles sont implémentés dans les routeurs, les pare-feu et les
systèmes d’exploitation des ordinateurs afin qu’ils communiquent. Les programmes
qui utilisent les fonctions réseau, comme les navigateurs Web et les clients de messagerie, doivent s’interfacer avec le système d’exploitation qui prend en charge les
communications réseau. Puisque les détails de l’encapsulation réseau sont du ressort
du système d’exploitation, le développement de programmes réseau consiste simplement à employer l’interface réseau qu’il offre.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 205
07/06/12 18:42
206
Techniques de hacking

0x420Sockets
Une socket est un mécanisme standard pour communiquer sur le réseau par l’intermédiaire du système d’exploitation. Une socket peut être vue comme l’extrémité
d’une connexion, tout comme une prise sur le central téléphonique d’un opérateur.
Mais ces sockets ne sont qu’une abstraction qui s’occupe de tous les détails pratiques
du modèle OSI décrit précédemment. Pour le programmeur, une socket permet
d’envoyer et de recevoir des données au travers du réseau. Ces données sont transmises à la couche session (5), au- dessus des couches inférieures (gérées par le
système d’exploitation), qui se charge du routage. Il existe différents types de sockets
qui déterminent la structure de la couche transport (4). Les plus répandues sont les
sockets par flots (stream) et les sockets par datagrammes (datagram).
Les sockets en mode flot apportent des communications bidirectionnelles fiables
semblables à un appel téléphonique. L’un des correspondants initie la connexion,
puis, une fois celle-ci établie, les deux personnes peuvent communiquer. Par ailleurs,
il existe une confirmation immédiate du fait que les mots prononcés ont réellement
atteint leur destinataire. Les sockets par flots utilisent un protocole de communication standard nommé TCP (Transmission Control Protocol), présent dans la couche
transport (4) du modèle OSI. Dans les réseaux informatiques, les données sont généralement transmises en petits morceaux appelés paquets. La conception de TCP est
telle que les paquets de données arriveront sans erreur et dans l’ordre, comme les
mots atteignent leurs destinataires dans l’ordre dans lequel ils ont été prononcés.
Les serveurs Web, les serveurs de messagerie et les applications clientes associées
communiquent par des sockets TCP.
Les sockets par datagrammes sont le deuxième type le plus répandu. Les communications avec ces sockets ressemblent plus à l’envoi d’une lettre qu’à un appel
téléphonique. La connexion est à sens unique et non fiable. Si nous postons plusieurs
lettres, nous ne sommes pas certains qu’elles arriveront dans l’ordre d’envoi, ni même
que les destinataires les recevront. Si les services postaux sont plutôt fiables, ce
n’est pas le cas d’Internet. Les sockets en mode datagramme utilisent non pas TCP,
mais un autre protocole standard de la couche de transport (4) appelé UDP (User
Datagram Protocol). Comme son nom l’indique, il peut servir à créer des protocoles
personnalisés. Ce protocole est très basique et léger et fournit quelques garanties.
Il ne s’agit pas d’une connexion réelle, juste d’une méthode de base pour transmettre des données d’un point à un autre. Avec les sockets de type datagramme, le
surcoût lié au protocole est très faible, mais celui-ci ne fait pas grand-chose. Si votre
programme doit être certain qu’un paquet a été reçu par l’autre extrémité, celle-ci
doit être programmée de manière à renvoyer un accusé de réception. Dans certains
cas, la perte d’un paquet est acceptable. Les sockets par datagrammes et UDP sont
généralement utilisés dans les jeux en réseau et les transmissions multimédias en
continu, car les développeurs peuvent adapter les communications en fonction des
besoins sans être pénalisés par le surcoût propre à TCP.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 206
07/06/12 18:42
0x400
Réseau
207
0x421 Fonctions pour les sockets
En C, les sockets se comportent de manière très comparable aux fichiers, car elles
utilisent des descripteurs de fichiers pour s’identifier. Elles sont tellement semblables
aux fichiers que nous pouvons utiliser les fonctions read() et write() pour recevoir et envoyer des données sur des descripteurs de sockets. Cependant, plusieurs
fonctions sont spécifiquement conçues pour la manipulation des sockets. Leurs
prototypes sont définis dans /usr/include/sys/sockets.h.
socket(int domain, int type, int protocol)
Crée une nouvelle socket. Retourne un descripteur de fichiers pour la socket ou
–1 en cas d’erreur.
connect(int fd, struct sockaddr *remote_host, socklen_t addr_length)
Connecte une socket (indiquée par le descripteur de fichier fd) à un hôte distant.
Retourne 0 en cas de succès et –1 en cas d’erreur.
bind(int fd, struct sockaddr *local_addr, socklen_t addr_length)
Lie une socket à une adresse locale afin qu’elle puisse attendre des connexions
entrantes. Retourne 0 en cas de succès et –1 en cas d’erreur.
listen(int fd, int backlog_queue_size)
Attend des connexions entrantes et place en file d’attente les demandes de
connexions, au maximum backlog_queue_size. Retourne 0 en cas de succès
et –1 en cas d’erreur.
accept(int fd, sockaddr *remote_host, socklen_t *addr_length)
Accepte une connexion entrante sur une socket liée. Les informations d’adresse
de l’hôte distant se trouvent dans la structure remote_host, dont la taille est
indiquée par *addr_length. Cette fonction retourne un nouveau descripteur qui
identifie la socket connectée ou –1 en cas d’erreur.
send(int fd, void *buffer, size_t n, int flags)
Envoie n octets de *buffer sur la socket fd. Retourne le nombre d’octets envoyés
ou –1 en cas d’erreur.
recv(int fd, void *buffer, size_t n, int flags)
Reçoit n octets de la socket fd dans *buffer. Retourne le nombre d’octets reçus
ou –1 en cas d’erreur.
Lorsqu’une socket est créée à l’aide de la fonction socket(), nous devons préciser son
domaine, son type et le protocole. Le domaine fait référence à la famille de protocoles de la socket. Les communications peuvent respecter divers protocoles, allant
du protocole Internet standard servant à naviguer sur le Web aux protocoles radio
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 207
07/06/12 18:42
208

Techniques de hacking
amateur comme AX.25 (lorsque vous faites partie des vrais nerds). Ces familles de
protocoles sont définies dans bits/socket.h, qui est automatiquement inclus par
sys/ socket.h.
Extrait de /usr/include/bits/socket.h
/* Protocol families. */
#define PF_UNSPEC 0 /* Unspecified. */
#define PF_LOCAL 1 /* Local to host (pipes and file-domain). */
#define PF_UNIX
PF_LOCAL /* Old BSD name for PF_LOCAL. */
#define PF_FILE
PF_LOCAL /* Another nonstandard name for PF_LOCAL.
#define PF_INET
2 /* IP protocol family. */
#define PF_AX25
3 /* Amateur Radio AX.25. */
#define PF_IPX
4 /* Novell Internet Protocol. */
#define PF_APPLETALK 5 /* Appletalk DDP. */
#define PF_NETROM 6 /* Amateur radio NetROM. */
#define PF_BRIDGE 7 /* Multiprotocol bridge. */
#define PF_ATMPVC 8 /* ATM PVCs. */
#define PF_X25
9 /* Reserved for X.25 project. */
#define PF_INET6 10 /* IP version 6. */
...
*/
Comme nous l’avons mentionné précédemment, il existe plusieurs types de sockets,
même si les modes flot et datagramme sont les plus répandus. Ces types sont
également définis dans bits/socket.h. (Les chaînes /* commentaires */ du code
précédent représentent une autre forme de commentaires.)
Extrait de /usr/include/bits/socket.h
/* Types of sockets.
enum __socket_type
{
SOCK_STREAM = 1,
*/
/* Sequenced, reliable, connection-based byte
streams. */
#define SOCK_STREAM SOCK_STREAM
SOCK_DGRAM = 2,
/* Connectionless, unreliable datagrams of fixed
maximum length. */
#define SOCK_DGRAM SOCK_DGRAM
...
Le dernier argument de la fonction socket() précise le protocole. Il est presque
toujours à 0. Les spécifications stipulent qu’une famille peut proposer plusieurs
protocoles, parmi lesquels nous pouvons choisir grâce à cet argument. En pratique,
la plupart des familles de protocoles n’en proposent qu’un seul. Autrement dit, cet
argument est généralement fixé à 0, c’est-à-dire au premier et seul protocole de la
famille. Ce sera le cas dans tous nos exemples de manipulation des sockets.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 208
07/06/12 18:42
0x400
Réseau
209
0x422 Adresses de socket
De nombreuses fonctions de sockets emploient une structure sockaddr pour passer
des informations d’adresses qui précisent un hôte. Cette structure est également
définie dans bits/socket.h.
Extrait de /usr/include/bits/socket.h
/* Get the definition of the macro to define the common sockaddr members. */
#include <bits/sockaddr.h>
/* Structure describing a generic socket address. */
struct sockaddr
{
__SOCKADDR_COMMON (sa_); /* Common data: address family and length. */
char sa_data[14]; /* Address data. */
};
La macro __SOCKADDR_COMMON est définie dans le fichier inclus bits/sockaddr.h.
Elle équivaut essentiellement à un entier court non signé. Cette valeur définit la
famille de l’adresse. Le reste de la structure concerne les données de l’adresse.
Puisque les sockets peuvent communiquer à l’aide de diverses familles de protocoles, chacune avec leur propre manière de définir les adresses des extrémités, la
définition d’une adresse doit également être variable, en fonction de sa famille. Les
familles d’adresses possibles sont données dans bits/socket.h. En général, elles
correspondent directement à des familles de protocoles.
Extrait de /usr/include/bits/socket.h
/* Address families. */
#define AF_UNSPEC PF_UNSPEC
#define AF_LOCAL PF_LOCAL
#define AF_UNIX
PF_UNIX
#define AF_FILE
PF_FILE
#define AF_INET
PF_INET
#define AF_AX25
PF_AX25
#define AF_IPX
PF_IPX
#define AF_APPLETALK PF_APPLETALK
#define AF_NETROM PF_NETROM
#define AF_BRIDGE PF_BRIDGE
#define AF_ATMPVC PF_ATMPVC
#define AF_X25
PF_X25
#define AF_INET6 PF_INET6
...
Puisqu’une adresse peut contenir différentes informations, en fonction de sa famille,
d’autres structures contiennent, dans la section donnée de l’adresse, les éléments
communs de la structure sockaddr ainsi que des informations propres à la famille
d’adresses. Ces structures ont une taille identique et nous pouvons forcer le type
de l’une dans le type d’une autre. Autrement dit, la fonction socket() accepte
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 209
07/06/12 18:42
210
Techniques de hacking

simplement un pointeur sur une structure sockaddr, qui peut en réalité pointer sur
une structure correspondant à une adresse IPv4, IPv6 ou X.25. Grâce à cette solution, les fonctions de sockets peuvent opérer sur divers protocoles.
Dans cet ouvrage, nous sommes concernés par Internet Protocol version 4, c’està-dire la famille de protocoles PF_INET, avec la famille d’adresses AF_INET. La
structure d’adresse pour AF_INET est définie dans le fichier netinet/in.h.
Extrait de /usr/include/netinet/in.h
/* Structure describing an Internet socket address. */
struct sockaddr_in
{
__SOCKADDR_COMMON (sin_);
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr;
/* Internet address. */
/* Pad to size of ’struct sockaddr’. */
unsigned char sin_zero[sizeof (struct sockaddr) __SOCKADDR_COMMON_SIZE sizeof (in_port_t) sizeof (struct in_addr)];
};
La macro __SOCKADDR_COMMON au début de la structure correspond simplement à
l’entier court non signé mentionné précédemment. Il définit la famille d’adresses.
Puisque l’adresse d’une extrémité de socket est constituée d’une adresse Internet et
d’un numéro de port, ces deux valeurs se retrouvent dans la structure. Le numéro
de port est un entier court sur 16 bits, tandis que la structure in_addr utilisée pour
l’adresse Internet contient un nombre sur 32 bits. Le reste de la structure correspond
à 8 octets de remplissage pour compléter la structure sockaddr. Cet espace n’est pas
utilisé, mais il doit exister pour permettre le forçage du type de la structure. Au final,
les structures d’adresse d’une socket ont l’aspect suivant.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 210
07/06/12 18:42
0x400
Réseau
211
0x423 Ordre des octets du réseau
Le numéro de port et l’adresse IP employés dans la structure d’adresse d’une socket
doivent respecter l’ordre des octets du réseau, c’est-à-dire big-endian.
Puisqu’il est contraire à l’organisation little-endian de l’architecture x86, les valeurs
réseau doivent être converties. Plusieurs fonctions sont réservées à ces conversions. Leurs prototypes sont définis dans les fichiers d’en-tête netinet/in.h et
arpa/inet.h. Voici un récapitulatif de ces fonctions :
AF_INET
htonl(long value)
ou host-to-network long
Convertit un entier sur 32 bits donné dans l’ordre des octets de l’hôte en entier
dans l’ordre des octets du réseau.
htons(short value)
ou host-to-network short
Convertit un entier sur 16 bits donné dans l’ordre des octets de l’hôte en entier
dans l’ordre des octets du réseau.
ntohl(long value)
ou network-to-host long
Convertit un entier sur 32 bits donné dans l’ordre des octets du réseau en entier
dans l’ordre des octets de l’hôte.
ntohs(long value)
ou network-to-host short
Convertit un entier sur 16 bits donné dans l’ordre des octets du réseau en entier
dans l’ordre des octets de l’hôte.
Pour assurer la compatibilité d’un programme avec toutes les architectures, ces
fonctions de conversion doivent être employées même si le processeur de l’hôte a
une architecture de type big-endian.
0x424 Convertir une adresse Internet
Lorsque nous rencontrons 12.110.110.204, nous reconnaissons une adresse Internet
(IP version 4). Cette notation familière (des nombres séparés par des points) sert
généralement à préciser des adresses Internet. Il existe des fonctions pour convertir
ce format en un entier sur 32 bits dans l’ordre des octets du réseau, et vice versa. Elles
sont définies dans le fichier arpa/inet.h et voici les deux fonctions les plus utiles :
inet_aton(char *ascii_addr, struct in_addr *network_addr)
ou ASCII to
network
Cette fonction convertit une chaîne ASCII qui contient une adresse IP donnée dans
le format nombres séparés par des points en une structure in_addr qui, comme
nous l’avons vu, contient uniquement un entier sur 32 bits, dans l’ordre des octets du
réseau, qui représente l’adresse IP.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 211
07/06/12 18:42
212

Techniques de hacking
inet_ntoa(struct in_addr *network_addr)
ou network to ASCII
Cette fonction réalise la conversion inverse. Elle prend en argument un pointeur sur
une structure in_addr qui contient une adresse IP et retourne un pointeur sur une
chaîne ASCII qui contient cette adresse dans le format nombres séparés par des
points. Puisque cette chaîne est placée dans un tampon mémoire alloué statiquement, nous pouvons y accéder jusqu’à l’appel suivant à inet_ntoa(), qui écrase alors
la conversion précédente.
0x425 Exemple de serveur simple
Pour mieux comprendre ces fonctions, utilisons-les dans un exemple. Le code
suivant attend des demandes de connexion TCP sur le port 7890. Lorsqu’un client se
connecte, il lui envoie le message Hello, world! et reçoit ensuite des données jusqu’à
la fermeture de la connexion. Pour cela, nous utilisons les fonctions et les structures
des sockets fournies par les fichiers d’en-tête mentionnés précédemment. Ils sont
donc inclus au début du programme. Nous avons ajouté une fonction d’affichage du
contenu de la mémoire à hacking.h.
Ajout à hacking.h
// Afficher les octets en mémoire au format hexadécimal et en caractères
// imprimables.
void dump(const unsigned char *data_buffer, const unsigned int length) {
unsigned char byte;
unsigned int i, j;
for(i=0; i < length; i++) {
byte = data_buffer[i];
printf(“%02x “, data_buffer[i]); // Afficher un octet en hexadécimal.
if(((i%16)==15) || (i==length-1)) {
for(j=0; j < 15-(i%16); j++)
printf(“
“);
printf(“| “);
for(j=(i-(i%16)); j <= i; j++) { // Afficher les octets
// imprimables de la ligne.
byte = data_buffer[j];
if((byte > 31) && (byte < 127)) // Caractère imprimable.
printf(“%c”, byte);
else
printf(“.”);
}
printf(“\n”); // Fin de l’affichage d’une ligne (16 octets).
} // Fin du if.
} // Fin du for.
}
Cette fonction est utilisée par le programme serveur pour afficher les données des
paquets. Cependant, puisqu’elle peut également servir dans d’autres circonstances,
nous l’avons placée dans hacking.h. La suite du serveur sera expliquée au fur et à
mesure de la présentation du code source.
© 2012 Pearson France – Techniques de hacking, 2e éd. – Jon Erickson
2536_Hacking.indb 212
07/06/12 18:42

Documents pareils