Copy Link
Add to Bookmark
Report
6x11 Linux Low Level Network Programming
...................
...::: phearless zine #6 :::...
..............>---[ Linux low level network programming ]---<...............
.........................>---[ by C0ldCrow ]---<............................
c0ldcrow.don@gmail.com
SADRZAJ
[> 1 <] UVODNA RIJEC
[> 2 <] TEORETSKI UVOD
[> 2.1 <] Slojevi protokola i enkapsulacija
[> 2.2 <] Velicina
[> 3 <] PROGRAMSKA SUCELJA ZA IZGRADNJU PAKETA
[> 4 <] LINUX KERNEL PF_PACKET SUCELJE
[> 4.1 <] Izgradnja zaglavlja
[> 4.2 <] Slanje paketa
[> 5 <] UPOTREBA BIBLIOTEKE - LIBPCAP
[> 5.1 <] Osnovni princip rada pcap programa
[> 5.2 <] Inicijalizacija pcap konteksta
[> 5.3 <] Callback funkcija i analiza paketa
[> 5.3.1 <] Pristup pojedinom zaglavlju
[> 5.3.2 <] Analiza DHCP zaglavlja i opcija
[> 6 <] KRATAK OSVRT NA LIBNET BIBLIOTEKU
[> 7 <] DALJNA LITERATURA
[> 8 <] ZAVRSNA RIJEC
////////////////////////////////////////////////////////////////////////////////
[> 1 <] UVODNA RIJEC
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Pred vama je tekst koji se bavi temom mreznog programiranja niske razine na
linuxu (eng. low-level network programming). Naglasak je stavljen na prakticne
primjere i koncept programiranja se objasnjava na njima, iako postoji i kratak
uvod posvecen osnovnim teoretskim konceptima.
Prakticni primjeri su ovdje dva programa koja sam napisao, a koja ne moraju
ostati samo primjeri vec mogu biti i od neke koristi. Objasnjena sto koji
program radi i nacin na koji to radi nalaze se u pripadajucim djelovima teksta.
Tekst ce imati smisla ukoliko ga pratite po kodu programa. Zato oni i jesu ovdje
Tekst sam napisao jer mislim da precesto u literaturi koja pokriva ovu
tematiku manjka prakticnih primjera. Sve ostaje nekako suho i nedoreceno.
Potrebna je i dokumentacija koja objasnjava svaku funkciju i svaki njezin
argument, ali iz toga ne vidimo cjelinu, nedostaje ona slika koja govori tisucu
rijeci. Nadam se da sam ponudio barem jedan dio te slike.
////////////////////////////////////////////////////////////////////////////////
[> 2 <] TEORETSKI UVOD
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Ne mogu odmah poceti govoriti o kodu i funkcijama, njihovim argumentima itd.
Mislim da prvo moram predstaviti osnovni nacin razmisljanja s kojim treba
pristupiti problemu. Za mene, dva osnovna pojma koja uvijek treba imati na umu
jesu "slojevitost protokola" i "velicina". Pokusati cu ih objasniti sto
jednostavnije i bez uporabe nekih slozenih termina, jer itako ih je tesko
prevesti, a samo bi smetali u izrazavanju biti problema.
---- [> 2.1 <] Slojevi protokola i enkapsulacija -------------------------------
Podaci koji se izmjenjuju izmedu dva racunala na mrezi nisu strukturirani s
brda s dola. Da bi uopce izmjenili informacije potrebni su neki standardi, tj.
jednaki pristup komunikaciji od svakog njenog sudionika. Tako nekako su
definirani protokoli, pa onda kazemo da protokoli omogucuju komunikaciju preko
mreze. Ali nije dovoljan samo jedan protokol. Uvijek vise njih sudjeluje u
mreznoj komunikaciji. Svaki od tih protokola ima svoje informacije koje prenosi
izmedu dva racunala, pa je time svaki od njih zasluzan za ispunjavanje jednog
djela zadataka koji se moraju izvrsiti za uspjesnu komunikaciju. Te informacije
spremljene su u ono sto nazivamo zaglavlja protokola, pa tako imamo TCP
zaglavlje, IP zaglavlje ... I svako od tih zagljavlja je tocno definirano i ima
svoje mjesto u procesu prijenosa informacija.
Ti protokoli su definirati po slojevima. Od visih do nizih slojeva. Visi
slojevi su opcenito logicki blizi krajnjem korisniku dok su nizi slojevi blizi
samom fizickom prijenosu informacija. Protokoli na visem nivou koriste one s
nizeg nivoa kako bi prenjeli informacije izmedu dva racunala. Kranje opcenito ti
slojevi bi isli nekako ovako:
+-+-+-+-+-+-+-+
| Application | --> HTTP, DNS, DHCP, SMTP ...
+-+-+-+-+-+-+-+
| Transport | --> TCP, UDP ...
+-+-+-+-+-+-+-+
| Network | --> IP (IPv4 ili IPv6), ICMP, ARP
+-+-+-+-+-+-+-+
| Link | --> Ethernet, PPP ...
+-+-+-+-+-+-+-+
Ovu skicu shvatite krajnje opcenito. Npr. ARP zapravo radi na nesto nizem
nivou od IP-a, ali opet nesto visem od Ethernet-a (jer treba prenjeti podatke na
lokalnoj mrezi), tako da bi on spadao negdje izmedju.
Ima i drugih slojeva. OSI model predvida 7 slojeva. No za nasu raspravu nije
trenutno vazno da li ima 7, 8, 9 ili koliko vec slojeva vec je vazno shvatiti da
protokol viseg nivoa koristi protokol nizega. To je enkapsulacija, a ona se
ocituje u zaglavljima protokola. Kada se posloze svi ti protokoli i sva
zaglavlja dodu na svoje mjesto dobivamo paket koji odlazi preko mreze na
odrediste.
Zaglavlja su ono s cime ce te vecinom baratati kada pisete neke programe
koji sami stvaraju pakete. Zaglavlja su ogledala svakoga protokola. Svaki
protokol ima svoje zaglavlje koje je strogo definirano u svojim opcijama i svoj
svojoj velicini.
Svasta sam sad tu napisao o tim slojevima, a vjerojatno se slabo daje ista
iz toga shvatiti, pa bi to bilo najbolje da objasnim na primjeru. Buduci da se
ovdje radi o dva programa koji su napisani za lokalnu mrezu, tocnije ethernet,
svi primjeri biti ce vezani za takvu mrezu.
Zamislimo da je do nas na lokalnoj mrezni dosao neko DHCP paket, nije sada
vazno da li je to DHCP REQUEST, DHCP DISCOVER ili sto vec. Evo kako bi to
u stvarnosti izgledalo. Sto je sve doslo do nas:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Frame | Ethernet | IP | UDP | DHCP | Frame |
| Header | Header | Header | Header | Header | Trailer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Kada ovako pogledamo, dosta toga mozemo reci. Ovaj dio "Frame Header" i
"Frame Trailer" mozemo slobodno zanemariti jer baratanje s tim odvija se u samoj
mreznoj kartici i za to je zasluzan njezin firmware. U biti do nas je doslo sve
od Ethernet zaglavlja do DHCP zaglavlja. Mozemo biti jos realniji pa reci da je
to sve neki niz bajtova. Prvih N bajtova tog niza cine Ethernet zaglavlje, nakon
tih N bajtova sljedi nekih M drugih bajtova koji cine IP zaglavlje. I tako do
kraja. Koliko tocno iznosi tih N bajtova te kako i kuda s njima, objasniti cu
kasnije.
Mislim da je zasda vazno shvatiti da se ta slojevitost protokola ogleda
upravo u njihovim zaglavljima. Vi kada pisete programe koji analiziraju pakete
koji su primljeni mozete svako zaglavlje posebno analizirati zajedno s svim
njegovim stavkama. Mozete pogledati samo odredenu stavku ili mozete preskociti
citavo jedno zaglavlje.
S programima koji sami pisu pakete i zaglavlja, sitacija je utoliko drukcija sto
je potrebno u potpunosti sastaviti ispravno zaglavlje (i to svako) prije slanja
paketa. Ali opet, sve se odvija po slojevima.
Za kraj ovog poglavlja jedna slika koja bi trebala sve razjasniti:
+-+-+-+-+-+-+-+-+-+-+-+-+
| FTP, HTTP, DHCP, DNS |
+-+-+-+-+-+-+-+-+-+-+-+-+
|----| |-----|
| |
| |
+-+-+-+-+-+-+-+-+-+
| TCP, UDP ... |
+-+-+-+-+-+-+-+-+-+
|-----|
|
|
+-+-+-+-+-+-+-+-+-+-+
| ARP, IP, RARP ... |
+-+-+-+-+-+-+-+-+-+-+
|----|----|
|
|
+-+-+-+-+-+-+
| Ethernet |
+-+-+-+-+-+-+
---- [> 2.2 <] Velicina -------------------------------------------------------
Druga vazna stvar koju je potrebno konstantno imati na umu je zapravo
velicina svakog pojedinog zaglavlja. Svako zaglavlje, bilo kojeg protokola, ima
tocno definirane stavke i svaka od tih stavki ima tocno definiranu duljinu u
bajtovima. Morate se pridrzavati tih duljina prilikom analiziranja paketa koje
ste pokupili s mreze ili prilikom kreiranja paketa za slanje. U protivnome, vasi
paketi ce imati manje smisla nego Severinina Stikla. Ako u definiciji protokola
pise da neki flag u zaglavlju mora imati dva bajta, onda on mora imati dva bajta
i tocka. Ako je vas program napisao paket u kojemu, prema njemu, to ima tri
bajta pojaviti ce se problemi za druge programe koji analiziraju pakete koje vi
saljete, najcesce se to odnosi na kernele operativnih sustava koji imaju
ugradenu podrsku za protokole.
Kako znati koje sve opcije (stavke) postoje u zaglavljima odredenih protokola i
kolike su one duljine? Prvi izvor su RFC dokumenti.
U RFC-ima je zapisano sve sto treba znati, ali su ruzni. Pa nije nekada na odmet
potraziti druge dokumente i tekstove na internetu koji objasnjavaju protokole i
imaju boje u sebi pa cak neki i animacije. Lakse se pamti.
Samo bih na jednu stvar htio skrenuti pozornost. Kada na internetu citate
razlicitu dokumentaciju o protokolima, najcesce ce te naici na ilustracije
zaglavlja protokola koje izgledaju ovako:
<-- 32 bita -->
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Source port | Dest port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Lenght | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Bla bla | Bla bla bla |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Nije zapravo zaglavlje niti jednog protokola, ali sam htio aludirati na to da se
cesto zaglavlja prikazuju u takvim tablicama. Osobno, te tablice nisu mi
prakticne. Puno jednostavnije i prakticnije je imati na umu ovu predodzbu:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Ethernet zaglavlje | IP zaglavlje | TCP zaglavlje | ...|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| n bitova | m bitova | (....) | x bitova |
+-+-+-+-+-+-+-+-+-+-+-+-+-+ +-+-+-+-+-+-+
| | |
Odredena Odredena Odredena
stavka stavka stavka
Lakse i prirodnije mi je shvatiti to linearno. Prvih N bajtova cini odredeno
zaglavlje, a unutar tih N bajtova ako uzmemo prvih npr. 5 bajtova, dobijemo
odredenu opciju, nakon tih 5, sljedecih 5 cini ovu itd. Potom dolazimo do novog
zaglavlja i tako do kraja paketa.
Zaglavlja svih protokola cemo u programu prikazivati pomocu struktura podataka.
Strukture predstavljaju najprirodniji zapis i organizaciju svih tih opcija u
svakom zaglavlju. Svaka stavka u pojedinom zaglavlju biti ce predstavljena
jednom stavkom u strukturi koja opisuje to zaglavlje. Tu ce nam biti vrlo vazna
tri tipa podataka koja nam stoje na raspolaganju. To su:
u_int8_t
u_int16_t
u_int32_t
Prvi tip podataka sirok je tocno 8 bitova tj. jedan bajt. Drugi tocno 16 bitova
tj. 2 bajta, a treci 32 bita tj. 4 bajta. Vjerojatno, kada ste se prvi puta
susreli s programskim jezikom C niste culi za ove tipove podataka. Oni su
definirani u <sys/types.h> i to ovako:
------------------------------- KOD <sys/types.h> ------------------------------
/* But these were defined by ISO C without the first `_'. */
typedef unsigned char u_int8_t;
typedef unsigned short int u_int16_t;
typedef unsigned int u_int32_t;
------------------------------- KOD <sys/types.h> ------------------------------
Dakle obicni "typedef" za "int" i "char". Mozete i vi u vasim programima
koristiti "unsigned char", "unsigned short int" ... Ali koristenje ovih tipova
podataka bi trebalo osigurati da su oni uvijek siroki 8, 16 i 32 bita cak i na
drugim arhitekturama (one koje nisu IA-32 koju ja imam).
A sto ako je neka stavka u paketima velika recimo 48 bita? S cime cemo to
predstaviti? Jednostavno, posluziti cemo se ovim 'trikom':
u_int8_t neka_stavka[6]
Sada je "neka_stavka" pokazivac na lokaciju u memoriji koja ima tocno 6 bajtova
alocirano. To cemo, kao sto ce te i vidjeti, koristiti prilikom baratanja mac
adresama.
////////////////////////////////////////////////////////////////////////////////
[> 3 <] PROGRAMSKA SUCELJA ZA IZGRADNJU PAKETA
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Kako napraviti svoj vlastiti paket i kako uopce prihvacati cijele pakete s
svim zaglavljima. Ako ste radili neku uobicajnu mreznu aplikaciju kao sto je
IRC ili SMTP klijent, onda ste upite posluzitelju slali u obliku obicnih
tekstualnih linija, a odgovore prihvacali u istom obliku. Niste uopce morali
razmisljati o zaglavljima protokola i njihovim stavkama. Razlog tome je sto su
svi ti protokoli implementirani u kodu kernela. I kernel se izmedu ostaloga brine
o pravilnoj izgradnji zaglavlja i samom slanju paketa na mrezu. Odmah se tu
i vidi direktna prednost implentacije podrske za protokole u kernelu. Sto bi
bilo kada bi svaka aplikacija morala sama voditi racuna o svakom pjedinom paketu
kojega salje?
No kernel nam omogucava da mi sami napravimo svoju implementaciju tih protokola
u korisnickoj aplikaciji. Ako od njega zatrazimo on ce nam jednostavno dati sve
bajtove koji su primljeni na nekoj mreznoj kartici (tj. sucelju), bez ikakvog
petljanja i mrdanja po njima. Na nasem programu onda ostaje da to sve analizira
i sredi. Isto vrijedi za slanje paketa. Kernel samo prenese kartici odredenu
kolicinu bajtova sto treba poslati na mrezu. Koji su to bajtovi? Da li oni imaju
smisla? - kernela bas nije briga. Na vasem programu ostaje da te bajtove poslozi
onako da oni cine jedan smisleni paket.
Slobodno mogu reci da postoje tri osnovna nacina koji omogucuju vasem programu
da sam barata cijelim paketima.
Prvi nacin je koristenje PF_PACKET sucelja linux kernela. To sucelje vam
omogucava slanje i primanje paketa na drugom OSI sloju, ono sto bi nazvali "data
link layer". Poziv sys call-u socket s pravim argumentima otvara nasem programu
takvo suecelje.
Drugi nacin je koristenje biblioteka koje imaju za zadatak olaksati samu uporabu
PF_PACKET sucelja i osigurati nezavisnost od platforme. Dvije najpoznatije
biblioteke su libpcap i libnet. Evo sto pise za jedno a sto za drugu:
libpcap -- Packet Capture library
libnet -- Packet Injection library
Iz samih naziva je jasno za sto je koja biblioteka dizajnirana. Potonja za
izgradnju i slanje paketa, a ova prva za prihvacanje paketa. Obje imaju brojne
funkcije koje skrivaju dobar dio izgradnje paketa na vasem OS-u, a vama daju da
odlucujete samo o onim stvarima koje su za vas bitne. Omogucuju vam da u jednom
potezu odredite mrezno sucelje na kojem zelite raditi ili da u jednom potezu
izgradite citavo zaglavlje jednog paketa.
Koja metoda je najbolja? Nebih volio o tome raspravljati. Po mojemu misljenju
niti jedna. Kljuc je u detaljima i onome sto vam trenutno treba.
////////////////////////////////////////////////////////////////////////////////
[> 4 <] LINUX KERNEL PF_PACKET SUCELJE
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Kao primjer izgradnje i slanje vlastitih paketa uporabom PF_PACKET sucelja,
napisati cemo jedan jednostavan program koji nam omogucuje da izgradimo arp
paket po zelji s odredisnim i izvornim adresama po nasoj volji, ukljucujuci i to
da li je "arp reply" ili "arp request" paket. Cijeli program dolazi u sklopu
ovog teksta. Zove se ados i napisan je prvenstveno kao vjezba. Prihvaca nekoliko
argumenata putem naredbenog sucelja. Mozete odrediti koju mac adresu zelite
koristiti kao izvornu a koju kao odredisnu u paketu. Isto vrijedi i za IP adrese.
Mozete takoder odrediti koliko paketa zelite poslati i s kojim vremenskim
razmacima izmedu svakoga paketa. Jos postoji opcija i za odredivanje tipa arp
paketa ("reply" ili "request") i sucelja s kojega saljete pakete.
Detaljnije o tome kako se koja opcija koristi i sto jos treba znati o programu
pise u komentarima u samome kodu programa. Program sigurno ima pogresaka, pa
ih slobodno mozete ispraviti :) I sami odlucite za sto ce te program koristiti.
Prvo cemo u memoriji izgraditi cijelokupni arp paket. Gdje su sve stavke
zaglavlja postavljene prema onome sto je korisnik uneo u program. Nakon toga taj
paket mozemo poslati na mrezu onoliko puta koliko zelimo, na sucelju na kojem
zelimo i s vremenskim razmakom s kojim zelimo.
---- [> 4.1 <] Izgradnja zaglavlja ---------------------------------------------
Kljuc nase izgradnje paketa je pravilna izgradnja zaglavlja svakoga protokola.
To ukljucuje ARP i ethernet zaglavlje. Za to nam treba neki nacin predstavljanja
tih zaglavlja u programu. Najprirodniji nacin za to je uporaba struktura.
Sutrukture predstavljaju idealnu razinu apstrakcije potrebnu za upravljanje
zaglavljima u nasem programu. Svaka varijabla unutar strukture odgovarati ce
jednoj stavci zaglavlja protokola. Definirati cemo jednu strukturu za svako
zaglavlje koje nam treba. Prvo na red dolazi ethernet zaglavlje:
------------------------------- KOD ados.c -------------------------------------
struct etherhdr {
u_int8_t ether_dhost[ETH_ALEN];
u_int8_t ether_shost[ETH_ALEN];
u_int16_t ether_type;
};
------------------------------- KOD ados.c -------------------------------------
Ethernet zaglavlje je zapravo vrlo jednostavno. Ima tri stavke. Prvo sadrzi
adresu primatelja paketa (to su 48 bitne MAC adrese), uz nju sadrzi i adresu
posaljitelja i jos jedno polje koje oznacava tip protokola na sljedecem nivou.
Obje adrese su istog formata. One zauzimaju 48-bita. Konstanta, ETH_ALEN koju
ovdje koristimo je definirana u <linux/if_ether.h> i iznosi 6. Predstavlja broj
okteta potrebnih za jednu mac adresu (citaj bajtova). Dakle imamo polje u kojem
je svaki clan velik 1 bajt a imamo 6 takvih clanova. Polje koje je tocno veliko
onoliko koliko nam treba za jednu mac adresu.
Zadnja stavka, kako pise u RFC-u, je duga 16 bita, pa tako mora biti i u nasem
programu. Zato koristimo prikladni tip podataka.
Uz ethernet zaglavlje jos nam je potrebno jedno. Zaglavlje arp-a. Ono je nesto
slozenije i mozemo ga predstaviti sljedecom strukturom:
------------------------------- KOD ados.c -------------------------------------
struct arp_header {
u_int16_t hwtype;
u_int16_t prtype;
u_int8_t hwadrlen;
u_int8_t pradrlen;
u_int16_t opcode;
u_int8_t src_hw[6];
u_int8_t src_ip[4];
u_int8_t dest_hw[6];
u_int8_t dest_ip[4];
};
------------------------------- KOD ados.c -------------------------------------
Sada ovdje postoji nekompaktibilnost sa definicijom ARP-a. Naime, definirano je
da sve poslje stavke "opcode" u zaglavlju bude varijabilne duljine, a nama je
ovdje to definirano fiksno. Cemu sad to?
Pa zato sto arp nije protokol koji sluzi iskljucivo za MAC i IP adrese nego
opcenito za prevodenje protokol adresa u adrese mreznog hardwera. A to ne mora
uvijek biti ethernet. Zato sluze prve cetiri stavke u zaglavlju ARP-a. Prva da
odredi tip hardware adrese, a druga da odredi tip protokol adrese. Sljedece
dvije stavke odreduju duljinu tih adresa, tako da znamo kako treba protumaciti
ono sto dolazi poslje stavke opcode i da bi znali kolika je njihova duljina.
Stavka "opcode" odreduje tip arp paketa. Opcode je kratica za "operation code".
Dvije vrijednosti nama od interesa su 1 i 2. Vrijednost 1 oznacuje da se radi o
"arp request" paketu, a vrijednost 2 oznacuje da se radi o "arp reply" paketu.
Na kraju smo dodali prostor za sve adrese. Buduci da nas program podrzava samo
ethernet mrezu stavljamo duljinu hardware adrese na 6 bajtova a protokol adrese
na 4 bajta = 32 bita (standardna IPv4 adresa).
Kada bi radili bas po pravilima onda bi nase arp zaglavlje zavrsilo tamo kod
stavke "opcode", a ostatak bi morali izgraditi ovisno o mrezi na kojoj program
radi ili ovisno o tome sto korisnik zeli.
Sada imamo dvije strukture koje opisuju dva zaglavlja, i to su jedina dva
zaglavlja koja nam trebaju. Ali jos nekako moramo napraviti paket. Tj. moramo
nekako predstaviti paket. Opet, pomoci ce nam jedna struktura definirana ovako
------------------------------- KOD ados.c -------------------------------------
struct complete_packet {
struct etherhdr eth_head;
struct arp_header arp_head;
} packet;
------------------------------- KOD ados.c -------------------------------------
Ta struktura sastoji se od dvije strukture jedna tipa "etherhdr", druga tipa
"arp_header". I odmah je definirana instanca te strukture imenom packet. Time
smo si znatno olaksali posao prilikom slanja paketa. Zbog ovoga:
Struktura "packet" velika je isto toliko koliko je velik i paket kojeg trebamo
poslati.
S time, sada, u jednom djelu memorije imamo cijeli paket u svoj svojoj duljini,
prilikom poziva funkcije "sendto" samo cemo joj dati pocetnu adresu tog djela
memorije i reci joj da od tog djela memorije zapise:
sizeof(struct complete_packet)
bajtova na mrezu. Drugim rjecima - cijeli paket.
Znaci sada imamo dio memorije koji predstavlja jedan cijeli paket. Sljedece sto
moramo napraviti je zapisivanje podataka u taj dio memorije i time formiranje
nasega paketa. Ukratko moramo svaku stavku svake strukture popuniti s pravim
informacijama. To bi trebao biti ovaj kod:
------------------------------- KOD ados.c -------------------------------------
packet.arp_head.hwtype=htons(ARPHRD_ETHER);
packet.arp_head.prtype=htons(ETH_P_IP);
packet.arp_head.hwadrlen=6;
packet.arp_head.pradrlen=4;
packet.arp_head.opcode=htons(arpPacketType);
packet.eth_head.ether_type=htons(ETH_P_ARP);
------------------------------- KOD ados.c -------------------------------------
Ovdje postavljamo neke stavke ARP i jednu stavku ethernet zaglavlja. Konstanta
ARPHDR_ETHER je definirana u <net/if_arp.h> i ona predstavlja ethernet kao tip
hardware-a. Vrijednost 1. Tu koristimo funkciju htons jer ta stavka mora biti
predstavljena u paketu koristeci "network byte order" gdje MSB ide prvi. A na
x86 arhitekturama LSB ide prvi. Funkcija htons obavlja tu konverziju. Kao
argument prihvaca broj dugi 16 bita i vraca isto toliko ali u drugom poretku
bajtova.
Isto vrijedi za ostale stavke arp zaglavlja. S tim da konstanta ETH_P_IP (mozete
i sami zakljuciti) predstavlja IP protokol. A varijabla "arpPacketType"
sadrzi 1 ili 2 ovisno o tome da li korisnik zeli poslati "arp reply" ili "arp
request". U "hwadrlen" i "pradrlen" mozemo samo spremiti duljine adresa jer one
u paketu zauzimaju samo jedan bajt pa nema smisla govoriti o redu bajtova ili
koristiti funkcije za koverziju.
Na kraju nam ostaje da jos spremimo adrese u nas paket. Buduci da se radi o
programu koji korisniku omogucuje unos svih adresa, korisnik ce to vjerojatno
unjeti u obliku znakovnih nizova. Pa cemo to morati prebaciti u binarni oblik.
Za to nam sluze dvije funkcije: "ether_aton" i "inet_aton". Obje vracaju adresu
u binarnom obliku i to u "network byte order". U programu privermeno spremimo
rezultat tih funkcija u strukture tipa "in_addr" i "ether_addr". I potom samo
iz njih prekopiramo u nase strukture za erhetnet i arp zaglavlje. Te strukture
definirane su u <net/ethernet.h> i <netinet/in.h> Sljedi kod:
------------------------------- KOD ados.c -------------------------------------
struct in_addr tmp_ip;
struct ether_addr *tmp_ether;
char *user_input;
/* Unesi izvorisnu ip adresu */
inet_aton(user_input &tmp_ip);
memcpy(packet.arp_head.src_ip, (u_int8_t *)&tmp_ip.s_addr, 4);
/* Isto primjenimo i za odredisnu ip adresu */
/* Unesi izvorisnu mac adresu */
tmp_ether=ether_aton(user_input);
memcpy(packet.arp_head.src_hw, tmp_ether->ether_addr_octet, 6);
memcpy(packet.etherhdr.ether_shost, tmp_ether->ether_addr_octet, 6);
/* Isto primjenimo i za odredisnu mac adresu */
------------------------------- KOD ados.c -------------------------------------
I to je to. Imamo u potpunosti izgraden paket, spreman za slanje. Tocnije
receno: negdje u memoriji imamo niz bajtova koji je dugacak:
sizeof(struct etherhdr) + sizeof(struct arp_header);
A, ako taj niz bajtova posaljemo na mrezu, to ce biti jedan sasvim obicni i
legitimni arp paket. Nista vise, nista manje. Pa probajmo ga sada poslati.
---- [> 4.1 <] Slanje paketa ----------------------------------------------------
Kako to poslati? Sta prvo treba napraviti? Odgovor je: isto ono sto i svaki
program ako zeli komunicirati preko mreze - otvoriti socket. Koristeci obicni i
dobro poznati sys call "socket". Samo ne mozemo otvoriti bas bilo kakav socket
nego onaj preko kojega cemo moci slati i primati citave pakete s zaglavljima
protokola. Drugim rjecima moramo kernelu reci da nista ne prcka po onome sto ide
na mreznu karticu. Moramo mu reci da smo mi u potpunosti izradili paket i da ga
samo treba poslati, nema potrebe za izgradnjom nikakvih dodatnih zaglavlja.
Evo kako cemo zamoliti kernel da nam to napravi:
------------------------------- KOD ados.c -------------------------------------
sockfd=socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP));
------------------------------- KOD ados.c -------------------------------------
Nista jednostavnije. Sve bajtove koje pisemo u sockfd kernel samo predaje
mreznoj ne dirajuci nista. Prvi argument socket funkciji je PF_PACKET. On nam
omogucava upravo to. Opcenito govoreci s prvim argumentom funkciji socket mi
odredujemo domenu mrezne komunikacije, tj. koje protokole treba koristiti za
komunikaciju preko mreze. PF_PACKET ide malo iznad toga, jer s njime mi kazemo
"samo cemo implenirati podrsku za protokole, od najviseg do najnizeg sloja". Da,
na primjer nismo na LAN mrezi nego da se spajamo na internet putem modema
koristeci dial-up, nebi smo morali praviti nikakva ethernet zaglavlja, ali bi
smo zato morali raditi PPP zaglavlja, a na visim slojevima opet bi morali
praviti i IP i TCP zaglavlja ako se zelimo spojiti na neko drugo racunalo.
Normalno da necemo pisati vlastitog irc bota koristeci PF_PACKET jer sto nas
briga tamo za PPP ili ethernet zaglavlje. A i bili bi gotovi valjda do sljedeceg
milenija, jer bi onda morali u potpunosti staviti podrsku za sve protokole. Jer
sto ako netko koristi dial-up, a netko se spaja na internet preko lokalne
ethernet mreze?
Drugi argument je tip socket-a Kod PF_PACKET-a imamo dva tipa SOCK_RAW i
SOCK_DGRAM. SOCK_RAW ce nam dati pakete koji imaju i ethernet zaglavlje i
omoguciti da saljemo pakete s vlastitim ethernet zaglavljem. Tocnije to se kaze:
SOCK_RAW nam daje pakete zajedno s "link level" zaglavljem --> 2 OSI sloj.
SOCK_DGRAM kaze kernelu da nam da sve osim tog "link level" zaglavlja. Tj. da
kernel sam sredi taj dio. Kada primamo pakete to znaci da ce kernel sam
analizirati ethernet zaglavlje i nama baciti paket bez toga, a kada saljemo
pakete kernel ce sam izgraditi ethernet zaglavlje, temeljem informacije koje mu
damo u strukturi "sockaddr_ll". Ona je deklarirana u <netpacket/packet.h>. Da
sada ne objasnjavam tu strukturu posebno mozete procitati "man 7 packet" tamo
sve pise. Dovoljno je reci da tu strukturu mozete shvatiti kao ekvivalent
strukturi "sockaddr" koju cesto koristimo prilikom programiranja standardnih
mreznih programa.
Posljedni argument nam odreduje koje sve protokole zelimo "primati" preko tog
socket-a. Nema bas previse utjecaja kod slanja paketa. Postavite na
htons(ETH_P_ALL) za sve protokole, a pogledajte u <linux/if_ether.h> za
dozvoljene vrijednosti.
I jos dvije stvari nam ostaju da bi poslali paket. Prvo tereba popuniti
sockaddr_ll strukturu (instancu moramo sami napraviti u programu) i drugo je
pozvati funkciju koja ce nam poslati to.
Ljepo pise da prilikom slanja paketa u sockaddr_ll strukturi dovoljno je imati
postavljeno sljedece vrijednosti:
sll_family --> Ljepo pise da je uvjek AF_INET (i tko smo mi da se bunimo)
sll_addr --> Predstavlja izvorisnu hardware adresu (mac adresa)
sll_halen --> Duljina ove gore adrese u bajtovima (za nas 6)
sll_ifindex --> Broj mreznog sucelja.
Tako sada pise u man stranici. Ali mi smo pametniji i necemo samo vjerovati sto
pise u nekom manualu tamo. U nasem slucaju za slanje paketa doboljno je
postaviti stavku ssl_ifindex ostalo ne moramo nista postavljati. Zasto sada to?
Pa zbog toga sto koristimo SOCK_RAW tip soketa. Kernel tu strukturu koristi samo
kada imamo SOCK_DGRAM tip socketa pa nekako mora znati kako izgraditi ethernet
zaglavlje. Zato je dovoljno imati samo postavljeno sucelje s kojega saljemo
pakete. Program ce raditi i ako ne postavite ostalo.
Ovaj zadnji argument moramo sami saznati. Mozemo se praviti pametni pa to ici
raditi uz pomoc ioctl-a. Ali mislim da nema potrebe za to. Dovoljno je koristiti
funkciju "if_nametoindex" deklariranu u <net/if.h>. Ona za argument prihvaca
niz znakova koji predstavljaju ime mreznog sucelja (eth0, eth1 ...) i vraca
index (broj) tog sucelja.
I na kraju ostaje jos ovo:
------------------------------- KOD ados.c ------------------------------------------
buff=&packet;
sendto(sockfd, buff, sizeof(struct complete_packet), 0, (struct sockaddr *)&sock, a);
------------------------------- KOD ados.c ------------------------------------------
buff je deklariran ovako: void *buff; Mogli smo i drugi argument funkciji dati
i na ovaj nacin: (void *)&packet, ali zbog cisto prakticnih razloga, jer itako
je linija koda dugacka, pa smo to rjesili red prije.
Mislim da je ovaj poziv funkcije relativno jasan. Prvi argument je socket file
descriptor na koji saljemo bajtove. Drugi argument je pokazivac na pocetak niza
bajtova koje treba posalti. Treci argument je koliko treba poslati bajtova (od
onoga na sto pokazuje buff). Cetvti argument su "flags" koji nam ne trebaju.
Peti argument je pokazivac na strukturu sockaddr, mi to nemamo nego imamo
sockaddr_ll pa cemo samo mu to podvaliti. I na kraju zadnji argument je velicina
strukture sockaddr_ll. Dakle:
a=sizeof(struct sokaddr_ll);
Opet zbog estetskih razlog ne stavljam u sam poziv funkcije.
I to je to. Nas paket je poslan. Mozete provjeriti rad programa uporabom
ethereala. Gledajte kakve pakete sallje.
Dakle, PF_PACKET sucelje nije komplicirano, ako znate sto treba. Sto je potpuno
razumljivo, jer onda nista nije komplicirano.
////////////////////////////////////////////////////////////////////////////////
[> 5 <] UPOTREBA BIBLIOTEKE - LIBPCAP
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Program ados koji sam/smo napisali u prvom djelu teksta u biti nije nista
posebno i vrlo ga je jednostavno napisati. Ne zahtjeva neke posebne vjestine.
Vise sluzi za primjer mreznog programiranja niske razine nego za neku prakticnu
uporabu. Ipak moze i za to posluziti. Normalno da ja ne odgovaram za vase
postupke i sve ono sto radite s tim programom. Ja ga nisam napisao za nikakve
ilegalne svrhe niti ga koristio za to.
Ovaj drugi program koji dolazi u skolopu teksta moze biti vec nesto korisniji.
Ali u svakom slucaju je zanimljiviji. Naime, on bi trebao poslati sms poruku
korisniku onda kada se na lokalnu mrezu prikljuci racunalo koje korisnik odredi
kao metu. Program je nastao iz osobnih potreba, koje cu ipak zadrzati za sebe.
E sada tu postoje neke kvake. Prvo program ce raditi samo na onim lokalnim
mrezama na kojima se koristi DHCP za konfiguraciju kljenata. On namime pasivno
nadgleda promet na mrezi. Ako primi neki DHCP request paket prvo pogleda da li
je on poslan od mac adrese koju je korisnik postavio kao metu. Ako je onda iz
tog paketa izvlaci IP adresu koju je to racunalo dobilo (ne mjesati DHCP INFORM
i DHCP REQUEST pakete) i to salje na sms korisniku.
Druga kvaka se tice ovog slanja sms-a. Zapravo, ne radi se o nikakvim sms-ovima,
nego je rjec vise o dobroj ideji. Naime u Hrvatskoj dva vodeca mobilna operatora
omogucavaju forwarding mail poruka na mobitel. Pa ako imate tu opciju ukljucenu
i mail adresu s koje to mozete raditi program vam samo salje obicni mail na tu
mail adresu, ostalo je do vaseg operatera. U biti, dobivate poruku na mobitel
cim vam stigne mail, a mail vam stigne cim zrtva posalje DHCP request, a zrtva
to zatrazi cim se prvi put spaja na mrezu. Simple as that.
Takoder neke postavke programa su "hard coded" u kod. Tako ce te morati barem
malo pogledati kod. A i mislim da onaj tko ne zeli pogledati kod nebi trebao
niti citati ovaj tekst.
Program ce raditi uz pomoc jednog filtera, koji bi trebao ograniciti kolicinu
paketa koje primamo i koje moramo analizirati. Filter je pcap filter string a
glasi ovako:
ether host <target_mac>
Time cemo osigurati da primamo samo pakete koji dolaze s "target_mac" adrese,
pretpostavljamo da je korisnik uneo zeljenu mac adresu prije. S tim filterom
cemo si uskratiti neke provjere tj. necemo morati provjeravati da li je paket
koji smo primili dosao s one mac adrese koju pratimo. Mogli smo sada tu i
prosiriti nas filter pa reci da jos zelimo samo i udp pakete. No mora i nama
nesto ostati da radimo u programu :)
---- [> 5.1 <] Osnovni princip rada pcap programa ------------------------------
Kako se programira koristeci biblioteku pcap? Pa u biti se svodi na ovo. Prvo
odredimo mrezno sucelje na kojem zelimo osluskivati za nase pakete. Potom
pozovemo jednu funkciju koja nam vraca jedan apstraktni tip podataka - pokazivac
na pcap_t. Taj pokazivac je nuzan za veliki broj pcap funkcija i on predstavlja
kontekst u kojem mi prihvacamo pakete. Mozete ga shvatiti da ima isto znacenje
kao i file descriptor koji vrati open() funkcija, a koristi se prilikom poziva
funkcija write(), read() ...
Kada smo to dobili, mozemo pozvati funkciju da podesimo filtere koji ce vaziti
za prihvacanje paketa i na kraju ulazimo u beskonacnu petlju. Program u toj
petlji ceka za paket, kada ga dobije (ako je prosao filter) pcap poziva jednu
nasu funkciju koju smo mi definirali koja ce potom analizirati paket. Inace, to
se zove callback funkcija. Kad ona zavrsi s radom, petlja se nastavlja.
Skica:
+-+-+-+-+-+-+-+-+-+-+
| Pripremi kontekst |
| za prihvat paketa |
+-+-+-+-+-+-+-+-+-+-+
|
| Ulazak u petlju
|
\|/
+-+-+-+-+-+-+-+-+-+-+
| Beskonacna petlja,| Paket analiziran
| cekaj pakete | <--------------|
+-+-+-+-+-+-+-+-+-+-+ |
| |
| Paket primljen +-+-+-+-+-+-+-+-+
|----------------->| Analiziraj ga |
+-+-+-+-+-+-+-+-+
Vrlo jednostavno i bez puno mudrovanja. Osnovni princip programiranja s pcap
bibliotekom.
---- [> 5.2 <] Inicijalizacija pcap konteksta ----------------------------------
Pocnimo s pronalaskom mreznog sucelja. Mozemo koristiti ovaj nacin:
------------------------------- KOD ninesix.c ----------------------------------
pcap_dev=pcap_lookupdev(pcap_errbuff);
------------------------------- KOD ninesix.c ----------------------------------
pcap_dev je deklariran kao pokazivac na znak. pcap_errbuff je polje znakova dugo
tocno PCAP_ERRBUF_SIZE, koje je pak konstanta definirana u <pcap.h>. Taj niz
znakova, kao argument prihvacaju gotovo sve pcap funkcije i ako nastupi greska
one vam u nj. ljepo daju string koji ju opisuje.
Uglavnom, nakon toga mozemo napraviti ovo:
------------------------------- KOD ninesix.c ----------------------------------
cap_fd=pcap_open_live(pcap_dev, 65535, 0, -1, pcap_errbuff);
------------------------------- KOD ninesix.c ----------------------------------
To je ta funkcija koja nam vraca, kako se na hrvatski kaze "packet capture
descripor". Inicijalizira kontekst u kojem prihvacamo pakete. Uz jedan kontekst
povezano je sucelje na kojem cekamo pakete, broj bajtova koje prihvacamo,
callback funkcija ...
Prvi argument je sucelje na kojem zelimo prihvacati pakete. NULL ce znaciti da
prihvacamo pakete na svim suceljima. Drugi argument je broj bajtova koje zelimo
maksimalno prihvatiti za jedan paket. Treci argument odreduje treba li postaviti
karticu u promisc mod. I cetvrti argument odreduje koliko milisekundi zelimo
cekati nakon sto dobijemo paket i prije no sto pozovemo nasu callback funkciju.
Vrijednost 0 odreduje da treba cekati beskonacno mnogo, a negativna vrijednost
odreduje da nista ne cekamo nego da cim primimo paket odmah pozovemo callback
funkciju ne cekajuci za dodatne pakete. Pomocu tog argumenta mozemo primiti i
vise paketa odjednom.
Sljedeci dio prorgama odnosi se na postavljanje filtera za prihvat paketa. Dvije
funkcije su u igri: pcap_compile() i pcap_setfilter(). Sve pise u uputsvima i
nije potrebno objasnjavati. Krecemo na onaj zanimljiviji dio. pcap_loop().
------------------------------- KOD ninesix.c ----------------------------------
pcap_loop(cap_fd, -1, analyze_packets, handler_args);
------------------------------- KOD ninesix.c ----------------------------------
Prvi argument je pokazivac na tip pcap_t (nas kontekst). Drugi argument je broj
paketa koje treba primiti prije no sto ce funkcija prestati s radom. Negativni
argument uzrokuje ulazak u beskonacnu petlju, tj. funkcija nikada nece prestati
s radom. Ili dok ne nastupi greska. Treci argument je pokazivac na callback
funkciju. Callback funkcija ukratko je funkcija koju ce pcap_loop pozivati
asinkrono, svaki puta kada treba pakete koji su prikupljeni analizirati. Ona
ne moze biti bilo kako deklarirana. To je funkcija tipa pcap_handler. Nuzna je
ova deklaracija:
------------------------------- KOD pcap.h ----------------------------------------
typedef void (*pcap_handler)(u_char *, const struct pcap_pkthdr *, const u_char *);
------------------------------- KOD pcap.h ----------------------------------------
Prvi argument je jednak kao i treci argument funkcije pcap_loop(). Osobno, za sada
nisam nasao nesto za sto bi to koristio. Trebalo bi sluziti ukoliko zelimo nasoj
callback funkciji poslati i neke svoje argumente, a ne samo ona dva.
Ne koristi se bas cesto.
Drugi argument je pokazivac ne jednu strukturu pcap biblioteke koja je
definirana u <pcap.h>, a ona sadrzi neke korisne informacije o pakeu kojega smo
primili na mreznom sucelju. Ovo:
------------------------------- KOD pcap.h -------------------------------------
struct timeval ts;
bpf_u_int32 caplen;
bpf_u_int32 len;
------------------------------- KOD pcap.h -------------------------------------
Prva stavka oznacava kada vrijeme kada je paket primljen, druga broj bajtova
sto nam stoji na raspolaganju, a treca broj bajtova paketa. To moze biti
razlicito od one prijasnje stavke. Drugim rijecima paket moze biti i dulji od
onoga sto smo prihvatili na mrezi. Ovisi kako ste podesili koliko najvise
bajtova zelite prihvatiti u funkciji pcap_open_live().
Treci argument je pokazivac na cijeli nas paket, ukljucujuci zaglavlja svih
protokola. Dakle cijeli paket. Tocnije receno taj treci argument pokazuje na
samo prvi bajt paketa, ostali bajtovi sljede u nizu, do kraja paketa.
---- [> 5.3 <] Callback funkcija i analiza paketa ------------------------------
I tu sada pocinje zabava. Treba napisati svoju callback funkciju. Prvo moramo
odrediti sto ce ona sve raditi. Nema bas puno posla sto se tice ovog programa:
1. Treba odrediti da li je paket koji smo primili dhcp paket.
2. Ako je dhcp paket, treba provjeriti da li je to DHCP request paket.
3. Ako je dhcp request paket trebalo bi nam ip adresa koju je dobilo to
racunalo. To znaci da cemo morati malo i analizirati dhcp paket.
4. Poslati mail u kojem cemo staviti i IP adresu racunala.
Primjetite da nigdje nisam napisao da treba provjeriti da li je paket koji smo
primili dosao s mac adrese koju smo si uzeli kao metu. Razlog tomu je taj sto
smo taj problem rjesili uz pomoc filtera za prihvacanje paketa. Kada smo stavili
da prihvacamo pakete samo s odredene mac adrese.
Krenimo redom. Tocka 1. Kako odrediti da li je paket koji smo primili DHCP
paket, a ne recimo neki ARP request? Za to moramo znati nesto o DHCP protokolu.
DHCP protokol za transport paketa koristi UDP protokol. Znaci ako imamo udp
zaglavlje u nasem paketu moglo bi biti rjec o DHCP paketu. UDP je sloj ispod
DHCP-a a sloj iznad IP-a (tj. mreznih protokola). UDP se oslanja na IP. Sto
znaci da ce nas paket takoder onda imati i IP zaglavlje. I na kraju da bi paketi
mogli putovati po lokalnoj mrezi treba nam ethernet, protokol na jos nizoj
razini.
Prema ovome nas DHCP paket bi ovako nekako trebao izgledati:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ethernet | IP | UDP | DHCP |
| zaglavlje | zaglavlje | zaglavlje | zaglavlje |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Opet tu do izrazaja dolazi slojevitost o kojoj sam pricao u uvodnom djelu. I
sada imamo nesto vise slojeva o kojima moramo brinuti nego u prethodnom primjeru
programa (ados). Moramo racunati na dva dodatna zaglavlja, tj, tri nova.
Ljepo to izgleda, ali postavlja se pitanje kako znati da nas paket bas ima IP
zaglavlje, tj. da je rijec o IP protokolu? Pretpostavimo da je uvijek rijec o
ethernet zaglavlju (jer ovo je program samo za taj tip LAN mreze). Ali kako cemo
mi znati da sljedece zaglavlje nije ono od ARP-a nego bas od IP-a? Nebi nikako
pametno bilo da idemo sljepo kretati se redom po bajtovima i na temelju njih
odrediti o kojem je zaglavlju rijec. Odgovor je jednostavan. Protokol nize
razine ce nam reci koji protokol vise razine sljedi. Divna stvar, ta
enkapsulacija :) Prisjetite se da ethernet zaglavlje ima jednu stavku koju smo u
proslom programu predstavili ovako u strukturi ethernet zaglavlja:
u_int16_t ether_type;
Rekli smo da ta stavka nosi informaciju o tome koji je protokol sljedece razine
koristen u paketu. Vrijednost 0x806 nam kaze da je rijec o ARP-u, a vrijednost
0x800 da je rijec o IP protokolu.
Analogno vrijedi i za IP zaglavlje. Ono je nesto vece i slozenije od ethernet
zaglavlja ali takoder ima jednu stavku koja nam govori o tome koji je sljedeci
protokol. Stavka se zove "protocol" (kako mastovito) i velika je 1 bajt.
Vrijednost koja nas zanima u toj stavci je 17. Ona oznacva da je sljedeci
protokol UDP.
Za UDP zaglavlje nema takve neke stavke koja bi nam otkrila o kojem je sljedecem
protokolu rijec. Ali zato postoje portovi. Svako UDP zaglavlje nosi u sebi dva
porta. Jedan je izvorisni a drugi odredisni port. Da li je rjec od DHCP paketu
mozemo jednostavno utvrditi provjeravajuci te portove. Ljepo pise u RFC-u za
DHCP da izvorisni port (host) koristi 68, a za odredisni (server) 67.
Da skratimo pricu:
Ako ether_type u ethernet zaglavlju jednako 0x800
Ako protocol u IP zaglavlju jedanko 17
Ako odredisni port udp zaglavlja jednako 67 a izvorisni 68
Rijec je o DHCP paketu.
I to je to. Sada samo to treba primjeniti u nasoj callback funkciji. No prvo
ostaje jos jedno pitanje za odgovoriti. Kako analizirati pakete? Kako doci do
svakog pogjedinog zaglavlja?
---- [> 5.3.1 <] Pristup pojedinom zaglavlju -----------------------------------
Idemo prvo vidjeti kako doci do svakog pojedinog zaglavlja. Rekli smo da nasa
funkcija dobiva pokazivac na prvi bajt cjelog paketa. A prvi bajt cijelog paketa
je prvi bajt erhernet zaglavlja. Recimo da smo taj pokazivac u funkciji nazvali
"packet_data". Sto bi bilo kada bi mi napravili ovo:
u_char *neki_pokazivac;
neki_pokazivac=packet_data + sizeof(struct ethernet_zaglavlje);
Pod pretpostavkom da je ethernet_zaglavlje struktura koja opisuje ethernet
zaglavlje, pokazivac neki_pokazivac bi pokazivao na neko mjestu u memoriji koje
je udaljeno od pocetka paketa za tocno onoliko bajtova koliko je veliko ethernet
zaglavlje. A to mjesto u memoriji je prvi bajt IP zaglavlja. I tako smo
jednostavnom operacijom zbrajanja skocili na drugo zaglavlje. Isto mozemo
napraviti ako zelimo doci na bilo koje zaglavlje.
udp_zaglavlje = packet_data + sizeof(struct ethernet_zaglavlje) + sizeof(struct ip_zaglavlje);
dhcp_zaglavlje = udp_zaglavlje + sizeof(struct udp_zaglavlje);
Vrlo jednostavno. Sada da vidimo kako cemo lagano moci gledati svaku stavko
svakoga pojedinog zaglavlja. Ovdje ce nam opet trebati strukture koje opisuju
svako zaglavlje. Vec znamo kako napisati strukturu za ethernet zaglavlje. Mozemo
i sami pisati svoje strukture ili mozemo koristiti one vec definirane u raznim
include datotekama.
<net/ethernet.h> --> Ethernet zaglavlje : struct ether_header;
<netinet/ip.h> --> IP zaglavlje : struct ip;
<netinet/udp.h> --> UDP zaglavlje : struct udphdr;
U svojem programu sam za DHCP zaglavlje sam napisao strukturu jer nisam nasao
nigdje u include datotekama. Niti znam da li ima. Tako je svejedno.
Da bi lagano pristupili elementima svakog pojedinog zaglavalja prvo treba
deklarirati pokazivace na te strukture.
struct ether_header *eth_head;
struct ip *ip_head;
struct udphdr *udp_head;
I sada jednostavno primjenimo malo carolije zvane type casting :) Za ethernet
zaglavlje:
eth_head =(struct ether_header *)packet_data;
I sada mozemo jednostavno dokuciti bilo koju stavku ethernet zaglavlja
dereferencijom pokazivaca. Da bi dokucili ether_type radimo ovo:
eth_head->ether_type;
Zasto to radi? Odgovor na to pitanje je stvar C jezika i prevoditelja. To bi
trebalo biti skraceno za ovo:
u_int16_t *ether_type;
ether_type=(u_int16_t *)(packet_data + 2*sizeof(u_int8_t));
*ether_type /* tu je ether_type */
Mozemo ovako reci: kada napisemo eth_head->ether_type. Prevoditelj zna da mi
referenciramo dva bajta u nizu koja se nalaze 2 bajta dalje od onoga na sto
pokazuje packet_data.
Isto vrijedi i za ostala zaglavlja. Samo onda moramo pripaziti na to da
pribrojimo jos i velicine onih zaglavlja prije. Evo za odredisni port udp
zaglavlja:
udp_head = (struct udphdr *) (packet_data + sizeof(struct ether_header) + sizeof(struct ip));
udp_head->dest;
Ja volim trpati dosta zagrada bilo gdje nesto racunam ;). A moze i duza verzija:
u_int16_t *udp_dest_port;
udp_dest_port=(u_int16_t *)((packet_data + sizeof(struct ether_header) + sizeof(struct ip)) + sizeof(u_int16_t));
*udp_dest_port /* Tu je odredisni port */
I na kraju evo koda koji bi trebao provjeriti da li smo primili DHCP paket.
------------------------------- KOD ninesix.c ----------------------------------
#define TO_IP_HEADER sizeof(struct ether_header)
#define TO_UDP_HEADER (TO_IP_HEADER + sizeof(struct ip))
#define TO_DHCP_HEADER (TO_UDP_HEADER + sizeof(struct udphdr))
/* JOS KODA */
void analyze_packets(u_char *handler_args, const struct pcap_pkthdr *packet_header, const u_char *packet_data)
{
/* ZAPAMTITI: ethernet -> IP protocol -> UDP -> DHCP */
struct ether_header *eth_head;
struct ip *ip_head; /* Strukture za zaglavlje svakog protokola, za DHCP */
struct udphdr *udp_head; /* nam ne treba jer ce mo njega u posebnoj funkciji srediti */
/* Pocnimo s ethernet zaglavljem */
eth_head=(struct ether_header *) packet_data;
if((ntohs(eth_head->ether_type)==ETHERTYPE_IP)) /* Je li IP sljedeci protokol? */
{
/* Da, IP je. Idemo po IP zaglavlje */
ip_head=(struct ip *)(packet_data + TO_IP_HEADER);
if(ip_head->ip_p==IPPROTO_UDP) /* Je li UDP sljedeci protokol */
{
/* Da, UDP je. Provjerimo UDP zaglavlje */
udp_head=(struct udphdr *)(packet_data + TO_UDP_HEADER);
if((ntohs(udp_head->source)==68) && (ntohs(udp_head->dest))==67) /* Po portovima gledamo da li je DHCP */
{
/* Imamo DHCP paket, U stvarnom kodu tu se obavlja jos neki logging,
no ubiti na ovom mjestu bi sada trebalo pozvati funkciju da analizira
DHCP paket i izvuce ono sto nam treba */
analyze_dhcp(packet_data);
}
}
}
}
------------------------------- KOD ninesix.c ---------------------------------
To bi trebalo biti to. Na pocetku odmah vidite uporabu nekih konstanti za lakse
snalazenje, da ne moram svaki puta kada skacem na novo zaglavlje pisati
kilometarske linije.
Takoder koristim konstante ETHERTYPE_IP i IPPROTO_UDP da bi odredio koji je
sljedeci protokol. Prva je definirana u <net/ethernet.h>, druga u
<netinet/in.h>. I jos bi trebalo pripaziti na to da smo mi paket dobili onakav
kakav je primljen, sto znaci da je sve u binarnom obliku (IP adrese, portovi) i
u obrnutom poretku bajtova, zato koristim funkciju ntohs da nam to pretvori u
"razumljivi" oblik.
---- [> 5.3.2 <] Analiza DHCP zaglavlja i opcija -------------------------------
Idemo sada analizirati DHCP zaglavlje. Analiza DHCP zaglavlja je malo skakljiva
jer je jedan dio zaglavlja je statican i jednak duljine za svaki DHCP paket, a
drugi dio zaglavlja je dinamican i njegova duljina varira. Taj dio se odnosi na
DHCP opcije, to je u biti konfiguracija koju server daje klijentu. Tih opcija
ima poprilicno mnogo. A mi bas moramo ici po tome petljati da bi dobili
informacije koje nas zanimaju. I za razliku od ARP zaglavlja ovo je uistinu
varijabilno, ne mozemo sami pretpostaviti koje opcije ce biti dane iako znamo na
kojoj smo mrezi.
Poceti cemo opet s strukturom s kojom cemo definirati zaglavlje DHCP paketa, ali
samo onaj staticki dio. Onaj dio koji znamo kako izgleda.
------------------------------- KOD ninesix.c ----------------------------------
struct dhcphdr {
u_int8_t opcode; /* opcode */
u_int8_t hwtype; /* hardware type */
u_int8_t hwaddrlen; /* hardware address lenght */
u_int8_t hopcount; /* hop count, used only by realy agents */
u_int32_t transid; /* transaction id */
u_int16_t numsec; /* number of seconds */
u_int16_t flags; /* flags */
u_int32_t ciaddr; /* client internet address */
u_int32_t yiaddr; /* your internet address */
u_int32_t siaddr; /* server internet address */
u_int32_t giaddr; /* gateway inetnet address */
u_int8_t chwaddr [16]; /* client hardwaare address */
u_int8_t shost [64]; /* server host name */
u_int8_t bfile [128]; /* boot file name */
};
------------------------------- KOD ninesix.c ----------------------------------
U usporedbi s udp zaglavljem ovo je ogromno. Pogotovo ovaj dio gdje imamo 16,
64 i 128 bajtova. A to je samo onaj statican dio. Bas zanimljivo, ovdje nam
nista nije od interesa. Osim mozda ako zelimo uzeti transaction id pa dodati u
program neke provjere da li se radi o istim transakcijama bla bla.
I tip DHCP poruke i adresa koju klijent uzima nalaze se opcijama, a one dolaze
poslje te strukture. Za tocno znacenje svake pojedine stavke treba pogledati u
RFC.
Pogledajmo kako izgled ovaj dio opcije:
|-> zavrsava staticki dio zaglavlja pocinju opcije
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| MC | MC | MC | MC | OC 1 | OL 1 | OPTION ... | OC 2 | OL 2 | OPTION ... | OC N | OL N | OPTION |
| byte 1 | byte 2 | byte 3 | byte 4 | | | num 1 | | | num 2 | | | num N |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
MC = Magic cookie
OC = Option code
OL = Option lenght
Prva cetiri bajta opcija cini tzv. "magicni kolacic". Ona su uvijek jednaka.
Redom: 99, 139, 83, 88. Zatim sljede opcije.
Svaka opcija sastoji se od tri djela.
- Koda
- Duljine
- Vrijednosti
Kod i duljina dolaze prije vrijednosti. Prvo dolazi kod. U njemu je smjesten
neki obicni broj koji odreduje sta ta opcija znaci, kako treba tumaciti
vrijednost. Nakon koda opcije dolazi jedan bajt u kojem je spremljena duljina
opcije, tj. duljina vrijednosti. Kod opcije je dug 1 bajt isto i duljina (pazi
odnosi se na velicinu mjesta u koju zapisujemo duljinu vrijednosti). A sama
vrijednost je dugacka onoliko koliko pise u OL (bajt prije pocetka vrijednosti).
Kad zavrsi vrijednost, dolazi novi kod opcije, koji definira drugu opciju itd.
Ako je kod opcije jednak 255 znaci da je to kraj opcija u dhcp paketu, poslje
nema vise nista. Gotovo je.
I sada bi trebalo pogledati kod koji ce proci te sve opcije i izvuci ono sto nam
treba.
------------------------------- KOD ninesix.c ----------------------------------
struct magic_cookie {
u_int8_t cook[4];
};
/* JOS KODA */
void analyze_dhcp(const u_char *packet_data)
{
struct dhcphdr *dhcp_head;
struct magic_cookie *mgcookie;
int is_dhcprequest=0;
u_int8_t *option_code, *option_lenght, *dhcp_msgtype;
u_int32_t *request_ip;
char *ip_addr;
/* Na pocetak DHCP zaglavlja */
dhcp_head=(struct dhcphdr *)(packet_data + TO_DHCP_HEADER);
fprintf(loggingfs, "\tDHCP Transaction ID 0x%x\n", ntohl(dhcp_head->transid));
/* Na pocetak DHCP opcija, nakon zaglavlja, magic cookie */
mgcookie=(struct magic_cookie *)(packet_data + TO_DHCP_HEADER + sizeof(struct dhcphdr));
------------------------------- KOD ninesix.c ----------------------------------
Pocetak funkcije za analizu dhcp paketa. Tamo kod poziva te funkcije dali smo
joj pokazivac na cijeli paket. To nije nuzno. Mozemo joj dati i pokazivac na
pocetak DHCP zaglavlja. Samo onda drukcije moramo napraviti type casting na
(struct dhcphdr *) u 10 liniji gornjeg koda. Princip je isti kao i kod zaglavlja
drugih protokola.
Ovdje zapisujemo i transaction ID paketa, koji je u binarnom obliku pa zato
ntohl(). I onda odemo na pocetak opcija, tamo gdje pocinje kolacic. Zatim imamo:
------------------------------- KOD ninesix.c ----------------------------------
option_code=(&mgcookie->cook[3]+sizeof(u_int8_t));
while(*option_code!=255)
{
option_lenght=(option_code+sizeof(u_int8_t));
if(*option_code==0x32)
request_ip=(u_int32_t *)(option_code+2*sizeof(u_int8_t));
if(*option_code==0x35)
{
dhcp_msgtype=(option_code+2*sizeof(u_int8_t));
if(*dhcp_msgtype==0x3)
is_dhcprequest=1;
}
option_code=(option_code+(((*option_lenght)+2)*sizeof(u_int8_t)));
}
------------------------------- KOD ninesix.c ----------------------------------
Ah, veselja li. Pokusajmo objasniti. Prvo varijablu option_code postavljamo da
pokazuje na prvu opciju. Ta prva opcija je jedan bajt dalje od zadnjeg bajta
kolacica. Zato zbrajamo adresu tog bajta uvecamo za jedan.
Zatim dolazimo do trece linije ovog koda i uvjeta u while() petlji. Uvijet kaze
da ce se petlja izvrsavati skroz dok ono na sto pokazuje option_code nije
jednako 255. A to ce biti jednako 255 onda kada smo dosli do kraja opcija.
Prva linija u petlji dohvaca duljinu trenutne opcije. Ta duljina je jedan bajt
udaljena od onoga sto pokazuje option_code. I to je sada to. Sada imamo kod
opcije u *option_code i duljinu opcije u *option_lenght.
Sada prvo provjerimo da li je kod opcije jednak 0x32. Ako je, to znaci da ta
opcija predstavlja IP adresu koju je klijent zatrazio. Pa pokazivac request_ip
samo preusmjerimo dva bajta dalje od lokacije na koju pokazuje option_code.
Sad se postavlja pitanje. Pa IP adresa je 4 bajta duga a mi pokazujemo samo na
njezin pocetak, prvi bajt. Kako cemo onda uzeti sva 4 bajta. Kljuc je u tipu
pokazivaca on je pokazivac na tip u_in32_t koji je dug 4 bajta time kompajler
zna da taj pokazivac pokazuje zapravo na 4 bajta u nizu, ne samo prvi.
Dalje provjeravamo da li je slucajno option_code jednak 0x35. Ako je to znaci da
ta opcija odreduje tip DHCP poruke (DHCP request, inform ...). Ako je to slucaj
onda pogledajmo vrijednost te opcije. Kao sto smo rekli vrijednost se nalazi 2
bajta vise od adrese na koju pokazuje option_code. I sad opet pitanje?
dhcp_msgtype je pokazivac na tip u_int8_t koji je velik samo jedan bajt. Kako mi
znamo da je ta vrijednost velika jedan bajt? Odgovor: Pise u RFC-u. Duljina
vrijednosti opcije s kodom 0x35 je jednaka 1 bajt. Znaci dhcp_msgtype
preusmjerimo na tu adresu i ocitamo sto je na njoj. Ako je to jednako 0x3 znaci
da je rijec o dhcp request paketu, pa si postavljamo kontrolnu varijablu na 1.
I na kraju petlje option_code preusmjeravamo da pokazuje na sljedeci kod opcije.
Preusmjeravamo tako trenutnu adresu na koju pokazuje option_code uvecamo za ono
sto pise na option_lenght plus 2 puta velicina u_int8_t. Npr. ako je trenutna
opcija duga 6 bajtova, a option code pokazjuje na adresu 1000.
option_code = 1000 + (6+2)*1
= 1008 --> adresa nove opcije, jer smo preskocili prva dva bajta ove
i cijelu njenu vrijednost. Preskocli smo OC, OL i OPTION
sto pise tamo u crtezu.
Petlja se ponavlja i sve ispocetka.
I nakon petlje provjerimo kontrolnu varijablu is_dhcprequest, ako je potavljena
na 1 znaci da je rijec o dhcp request paketu i onda saljemo mail.
------------------------------- KOD ninesix.c ---------------------------------
if(is_dhcprequest==1)
{
/* NAPOKON, POSALJI VISE TAJ MAIL */
}
------------------------------- KOD ninesix.c ---------------------------------
Na ip adresu nam pokazuje request_ip. Koristimo inet_ntoa() da je prevedemo u
ljepi string.
Evo, vise ni libpcap nije tesko koristiti :)
////////////////////////////////////////////////////////////////////////////////
[> 6 <] KRATAK OSVRT NA LIBNET
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Gore u tekstu vec sam spomenio jos jednu biblioteku: libnet. Ona prvenstveno
sluzi za izgradnju paketa i sadrzi funkcije koje olaksavaju taj posao. Libnet
uistinu pruza veliki nivo apstrakcije pri izgradnji paketa. Podrzava veliki broj
protokola, nema potrebe za upravljanje memorijom (malloc() i free()) sto se
paketa tice. Nije potrebno samostalno izracunavanje checksum polja u zaglavljima
paketa. Takoder je moguce prepustiti biblioteci da sama obavlja veliki dio vaseg
posla ili ne morate to prepustiti biblioteci.
Sama logika programiranja je u mnogocemu slicna programiranju s libpcap
bibliotekom. Da bi poslali paket morate napraviti ovo:
1. Inicijalizirati kontekst u kojem saljete pakete. To znaci odabir sucelja,
razine kontrole koju zelite imati ...
2. Pomocu odgovarajucih funkcija izgraditi zaglavlja paketa.
3. Poslati pakete na mrezu
4. Zatvoriti kontekst i osloboditi zauzete resurse.
Jednom kada izgradite zaglavlja paketa ona ostaju u vasem kontekstu pa ih mozete
slati i vise puta. Tako da nebi bio problem prepraviti onaj ados program da
koristi libnet. Evo primjera funkcije pomocu koje mozemo izgraditi arp zaglavle.
------------------------------ KOD primjer -------------------------------------
libnet_build_arp( (u_int16_t) ARPHRD_ETHER,
(u_int16_t) ETH_P_IP,
6,
4,
arpPacketType,
source_mac.ether_addr_octet,
(u_int8_t *) &source_ip.s_addr,
dest_mac.ether_addr_octet,
(u_int8_t *) &dest_ip.s_addr,
NULL,
0,
l,
0 ))
------------------------------ KOD primjer -------------------------------------
Znaci imamo jednu funkciju "libnet_build_arp()" kod koje gotovo svaki argument
predstavlja jednu stavku u zaglavlju. Ima i dodatnih argumenata. Npr. ovaj
predzadnji argument je pokazivac na "libnet_t" - tip podataka koji oznacava nas
kontekst.
Nakon toga mozemo izgraditi i ethernet zaglavlje pomocu jos jedne funkcije;
libnet_build_ethernet();
ili si jos mozemo olaksati posao i zatraziti od libnet-a da sama napravi
ethernet zaglavlje na ovaj nacin:
libnet_autobuild_ethernet();
A niti ne moramo izgraditi ethernet zaglavlje nego ce biblioteka sama to
napraviti za nas. Saljemo paket pozivom ove funkcije:
libnet_write(l);
Prihvaca samo jedan argument - pokazivac na nas kontekst.
////////////////////////////////////////////////////////////////////////////////
[> 7 <] DALJNA LITERATURA
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
[1.] Unix Network Programming, Vol. 1: The Sockets Networking API
ISBN: 0131411551
[2.] Building open source network security tools: components and techniques
ISBN: 0471205443
[3.] http://www.tcpdump.org/pcap.htm
[4.] http://www.cet.nau.edu/~mc8/Socket/Tutorials/section1.html
[5.] http://www.phrack.org/phrack/55/P55-06
[6.] man pcap
[7.] man packet
////////////////////////////////////////////////////////////////////////////////
[> 8 <] ZAVRSNA RIJEC
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
Gotovo. Nadam se da tekst valja i da ce nekome pomoci da nauci neke stvari,
mozda dobije inspiraciju za neki program ili nesto drugo. Isto se nadam i za
programe koji dolaze uz tekst. Jedino molim vas - programi su napisani kao
primjer uz tekst, a ja nemam vremena za dodavanje novih opcija i ispravak
gresaka.
Takoder bih zelio napomenuti da su programi napisani na engleskom jeziku. Meni
je jednostavnije na taj nacin pisati programe.
Zahvaljujem se svima koji su pomogli u objavljivanju teksta. Odmah pozdravljam
sve koje poznajem i s kojima sam bio/jesam u kontaktu ili ih znam samo iz
"videnja":
Shatterhand, h4z4rd, BoyScout, Exoduks, nimr0d, MilkFairy, ]seth[, Emp ...
Poseban pozdrav hess-u
EOF