Première rédaction de cet article le 3 mars 2009
Dernière mise à jour le 5 mars 2009
Une fois capturés et stockés sur disque, au format pcap, les paquets IP doivent être disséqués (ou, pour parler moins brutalement, décodés) pour trouver les éléments intéressants. Quels sont les particularités de la dissection de paquets DNS ?
Supposons qu'on veuille placer ces éléments dans une base de données relationnelle, avec un schéma SQL qui ressemble à ceci :
CREATE TYPE protocols AS ENUM ('TCP', 'UDP'); CREATE TABLE DNS_Packets (id SERIAL UNIQUE NOT NULL, file INTEGER NOT NULL REFERENCES Pcap_files(id), rank INTEGER NOT NULL, -- Rank of the packet in the file date TIMESTAMP, -- Date of capture in UTC length INTEGER NOT NULL, -- Length on the cable, we may have stored -- less bytes added TIMESTAMP NOT NULL DEFAULT now(), src_address INET NOT NULL, dst_address INET NOT NULL, protocol protocols NOT NULL, src_port INTEGER NOT NULL, dst_port INTEGER NOT NULL, -- Field names and semantic are found in RFC 1034 and 1035. We do not -- try to be user-friendly query BOOLEAN NOT NULL, query_id INTEGER NOT NULL, opcode INTEGER NOT NULL, rcode INTEGER NOT NULL, aa BOOLEAN NOT NULL, tc BOOLEAN NOT NULL, rd BOOLEAN NOT NULL, ra BOOLEAN NOT NULL, qname TEXT NOT NULL, qtype INTEGER NOT NULL, -- With helper functions TODO to translate numeric values to well-known text like AAAA, MX, etc edns0_size INTEGER, -- NULL if no EDNS0 do_dnssec BOOLEAN, -- NULL if no EDNS0 ancount INTEGER NOT NULL, nscount INTEGER NOT NULL, arcount INTEGER NOT NULL );
et développons le code (ici, en C) qui va produire ces données.
Une partie du code n'est pas spécifique au DNS (ouverture du fichier, etc) et peut être trouvée dans « Lire des paquets capturés sur le réseau en C ». Je ne considère ici que la partie spécifique au DNS. On commence par créer des structures de données, en lisant de près les RFC 1034 et surtout RFC 1035 :
#define DNS_PORT 53 ... /* RFC 1035, section 4.1 : l'en-tête DNS a six champs de deux octets chacun */ #define SIZE_DNS 12 ... struct sniff_dns { /* RFC 1035, section 4.1 */ /* This is only the DNS header, the sections (Question, Answer, etc) follow */ uint16_t query_id; uint16_t codes; uint16_t qdcount, ancount, nscount, arcount; }; ... const struct sniff_dns *dns; const uint8_t *qsection;
Tout le contenu du paquet ne s'y trouve pas. En effet, le reste (la
section Question, la section
Answer, etc) a un format très variable, pour lequel
les struct
C ne conviennent pas.
On met alors en correspondance le contenu du paquet avec une structure DNS :
if (source_port == DNS_PORT || dest_port == DNS_PORT) { dns = (struct sniff_dns *) (packet + SIZE_ETHERNET + size_ip + SIZE_UDP);
Nous avons désormais beaucoup des informations souhaitées : le
Query ID, le nombre de réponses
(ancount
), le code de la réponse (dans le champ
codes
), etc.
Certaines de ces informations nécessitent encore un effort de
décodage. Ainsi, le fait que le paquet soit une question ou une
réponse est contenu dans le premier bit du champ
codes
. Créons une macro
pour simplifier son extraction :
#define DNS_QR(dns) ((ntohs(dns->codes) & 0x8000) >> 15)
(Il ne faut surtout pas oublier le ntohs puisque l'ordre
des octets
dans les paquets DNS n'est pas forcément celui de la machine.) La macro peut alors
être utilisée, par exemple dans une expression comme
(DNS_QR(dns) == 0 ? "Query" : "Response")
.
Méthode analogue pour accéder au code de retour d'une réponse, les quatre derniers bits :
#define DNS_RCODE(dns) (ntohs(dns->codes) & 0x000F) ... if (DNS_QR(dns) == 1) { returncode = DNS_RCODE(dns); }
Décodons désormais la section Question :
qsection = (uint8_t *) (packet + SIZE_ETHERNET + size_ip + SIZE_UDP + SIZE_DNS);
Elle comprend (RFC 1035, section 4.1.2), la question posée
(QNAME
), le type d'enregistrement DNS recherché
et leur classe. La question posée est un nom de domaine mais qui n'est pas un simple char
*
correspondant à la forme texte habituelle
(www.example.com
). Sur le câble, en effet, le nom de domaine est codée sous forme d'une suite de labels stockés par
(taille, valeur). Ainsi, www.example.com
est
représenté dans le paquet par ( (3, "www"), (7, "example"), (3,
"com"), (0, "") ), la dernière paire représentant la racine du DNS et
indiquant la fin du nom de domaine. Il va donc falloir parcourir la
section Question paire par
paire. sectionptr
va pointer en permanence vers
l'endroit de la section qu'on décode :
fqdn = malloc(MAX_NAME + 1); assert(fqdn != NULL); fqdn[0] = '\0'; end_of_name = false; for (sectionptr = qsection; !end_of_name;) { labelsize = (uint8_t) * sectionptr; if (labelsize == 0) { sectionptr++; end_of_name = true; } else { if (strlen(fqdn) == 0) { strncpy(fqdn, (char *) sectionptr + 1, labelsize); fqdn[labelsize] = '\0'; } else { fqdn = strncat(fqdn, ".", 1); fqdn = strncat(fqdn, (char *) sectionptr + 1, labelsize); } sectionptr = sectionptr + labelsize + 1; } } /* Maintenant, on peut utiliser la variable fqdn, qui contient le nom de domaine sous la forme traditionnelle www.example.com */
Ce code fonctionne mais est très dangereux à deux égards. Rappelez-vous d'abord que les paquets ont été formés par des gens que vous ne connaissez pas et qui ont pu, volontairement ou involontairement, créer des paquets invalides qui vont planter votre décodeur... ou, pire, lui faire exécuter du code arbitraire suite à un débordement de tampon. Il faut donc tester :
labelsize
) sont bien
inférieures à 63 (la taille maximale indiquée par le RFC). Au delà,
cela peut être de la compression (voir plus loin) ou bien une erreur
dans le paquet.Le code doit donc être modifié en :
if (labelsize > 63) { invalid = true; /* Depends on the section: in the Question section, you typically never have compression. */ goto nextpacket; }
Pour extraire le type d'enregistrement demandé (MX, NS, AAAA, etc) et la classe (presque toujours IN pour « INternet »), une fois arrivé à la fin du nom de domaine (là encore, le ntohs est indispensable) :
qtype = ntohs(*((uint16_t *) sectionptr)); sectionptr += 2; qclass = ntohs(*((uint16_t *) sectionptr)); sectionptr += 2;
Normalement, la même prudence doit s'appliquer ici : il faudrait toujours vérifier qu'on n'a pas dépassé les limites du paquet.
Autre piège que tous les programmeurs C expérimentés auront vu tout
de suite : les données ne sont plus
alignées. On essaie de lire deux octets alors
que sectionptr
peut être au milieu d'un mot de
deux octets. Selon le processeur, cela va se passer bien
(Pentium) ou bien générer un bus
error (UltraSparc). La solution que
j'adopte est d'utiliser une fonction qui refait des données alignées :
uint16_t unaligned_uint16(const uint8_t * p) { /* This assumes big-endian values in data stream (which is standard for TCP/IP, it is the "network byte order"). */ unsigned char* pByte = (unsigned char*) p; uint16_t val = (pByte[0] && 8) | pByte[1]; return val; }
Voyez la définition de cette fonction par Michael Burr, ainsi qu'une très intéressante discussion dans Safe, efficient way to access unaligned data in a network packet from C (voir aussi les autres articles de StackOverflow sur l'alignement). J'utilise cette fonction ainsi :
#ifdef PICKY_WITH_ALIGNMENT decoded.qtype = unaligned_uint16(sectionptr); #else decoded.qtype = ntohs(*((uint16_t *) sectionptr)); #endif
Et la macro PICKY_WITH_ALIGNMENT
est définie
selon pour les machines où c'est un problème (elle marche dans tous
les cas, mais diminue les performances.)
Je ne vais pas décoder la totalité du paquet. Mais, pour avoir les informations qui nous intéressent ici, comme la taille annoncée en EDNS0, il faut décoder la section Additional des requêtes pour voir si elles utilisent les pseudo-enregistrements OPT du RFC 6891 :
if (DNS_QR(dns) == 0) { /* Only for queries */ edns0 = false; edns_size = 0; if (dns->ancount == 0 && dns->nscount == 0) { /* Probably by far the most common case in queries... */ if (dns->arcount != 0) { /* There is an additional * section. Probably the OPT * of EDNS */ labelsize = (uint8_t) * sectionptr; if (labelsize == 0) { /* Yes, EDNS0 */ sectionptr++; add_type = ntohs(*((uint16_t *) sectionptr)); sectionptr += 2; if (add_type == OPT) { edns_size = ntohs(*((uint16_t *) sectionptr)); edns0 = true; } sectionptr += 2; } /* else ? */ } } }
Notez que les sections Answer et Authority des réponses ne sont pas décodées. C'est plus complexe en raison de la compression des noms qui est possible dans ces sections (RFC 1035, section 4.1.4). Des bibliothèques existent pour rendre cette tâche plus facile comme la libbind avec des fonctions comme dn_expand().
Le code complet (qui lit une trace au format Pcap, dissèque les
paquets DNS et stocke le résultat dans une base
PostgreSQL est disponible en dnstrace2postgresql.c
(le code SQL est en dnstrace2postgresql.sql
).
Version PDF de cette page (mais vous pouvez aussi imprimer depuis votre navigateur, il y a une feuille de style prévue pour cela)
Source XML de cette page (cette page est distribuée sous les termes de la licence GFDL)