首先明確本文的閱讀對象。顯然,你須要一些C語言的基本知識,除非只想瞭解pcap的基本原理。你不用是一個編程高手;對於想深刻 瞭解該領 域的編程者,我保證會盡可能詳細描述相關概念。另外,網絡方面的一些知識對閱讀本文是有幫助的。本文給出了一個網絡包嗅探器,全部的代碼已在默認內核 的FreeBSD 4.3上測試經過。編程
首先要了解的是pcap嗅探器的整體佈局。代碼流程以下:數組
實際上這是一個很簡單的過程。總共五步,其中一步是可選的(就是那個使你感到困惑的第三步)。下面咱們開始研究每一個步驟及如何實現它 們。網絡
這一步極其簡單。有兩種方法來設置咱們要嗅探的設備。session
第一種是簡單地讓用戶告訴咱們,考慮下面的程序:數據結構
用戶指定設備名做爲程序的第一個參數。如今,字符串"dev"保存了咱們要嗅探的,pcap能夠認識的接口名(固然,假設用戶給咱們的 是真實的接口)。app
另外一種方法也一樣簡單,看這個程序:tcp
在這裏,由pcap本身來設置設備。「等一下,Tim,」你會說:「errbuf字符串表明什麼?」。不少pcap命令容許咱們把這個 字符串做爲參 數。它的用途是:若是命令執行失敗,它會獲得出錯的細節信息。在這段代碼裏,若是pcap_lookupdev()失敗,errbuf會保存有一個錯誤信 息。很好,不是嗎?ide
創建嗅探會話的任務真的很簡單,用pcap_open_live()就能夠了。這個函數的原型(取自pcap man)以下:函數
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,
char *ebuf)
第一個參數是設備名,咱們在上一節已經介紹過了。snaplen是一個整型值,定義由pcap抓取的包的最大字節數。 promisc,當設置爲true時,使接口處於混雜模式(無論怎樣,即便設置爲false,在一些特定情形下接口可能仍是處於混雜模式)。to_ms是 讀超時(read time out),單位爲毫秒(0表示沒有超時;在一些平臺上,這意味着你可能會一直等待直到收到足夠數量的包,因此你應該使用一個非零值)。最後,ebuf用於 保存出錯信息(就象咱們前面的errbuf)。函數返回會話句柄。oop
爲了演示,考慮這個代碼段:
這個代碼打開"somedev"字符串指定的設備,告訴它每次讀BUFSIZ字節(定義於pcap.h中),置設備於混雜模式。嗅探直 到有錯誤發生,錯誤信息保存到errbuf中,用於後面的錯誤信息輸出。
關於混雜模式vs.非混雜模式:這是兩個很是不一樣的風格。非混雜模式嗅探只監聽與本地有直接關係的包。只有發往、源自或本地路由的包會 被嗅 探器捕獲。另外一方面,混雜模式監聽全部線上的通訊。在無交換環境中(non-switched environment),因此網絡通訊都會被監聽。它可讓咱們獲得更多的包,可是,這是能夠被檢測的:可 以經過測試強可靠性來發現網絡中是否有主機正在以混合模式監聽,另外混雜工做模式僅僅在非交換式的網絡中有效,並且在一個高負載的網絡環境中,混雜模式將 消耗大量的系統資源。
一般咱們只對特定網絡通訊感興趣。好比咱們只打算嗅探23端口(telnet)用於搜索密碼信息,或者劫持發往21端口的文件 (FTP), 也多是DNS通訊(port 53 UDP)。不管哪一種情形,咱們不多會盲目地嗅探全部的網絡通訊。考慮使用pcap_compile()和 pcap_setfilter()函數。
這個步驟也是至關的簡單。調用pcap_open_live()以後咱們已經有了一個可用的嗅探會話,能夠應用咱們的過濾器。爲何不 用if/else語句?兩個緣由:首先,pcap的過濾器有更好的效率,由於它直接做用於BPF(BSD Packet Filter)同時又減小了直接操做BPF所需的大量步驟。第二,這樣簡單得多:)
應用咱們的過濾器以前,咱們必須「編譯」它。過濾器表達式爲一個規則字符串(char數組)。在tcpdump的man文檔裏有其語法 的說明 書。閱讀語法說明的工做得你本身去作。儘管如此,咱們將會盡可能使用簡單的過濾器表達式,所以你也許足夠聰明從而能夠從個人例子中領悟出來。
經過pcap_compile()函數來「編譯」。它的原型爲:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize,
bpf_u_int32 netmask)
第一個參數是咱們的會話句柄(前文的例子裏是pcap_t *handle)。接下來的參數用於指向存放編譯後過濾器的空間。而後是過濾表達式。下一個optimize整數決定表達式是不是「優化的」(0爲 false、1爲true)。最後,咱們要指定網絡掩碼。函數失敗時返回-1;其它值表示成功。
表達式被編譯以後,就能夠應用它了。使用pcap_setfilter()函數,下面是pcap_setfilter()原型:
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
很直白,第一個參數是會話句柄,第二個是編譯後的表達式。
也許這個代碼示例能夠幫助你更好地理解:
這個程序在rl0設備上以混雜模式嗅探發往或源自23端口的全部通訊。
你可能注意到了,這個例子中有一個以前沒討論過的函數:pcap_lookupnet()。給一個設備名,獲得它的IP和網絡掩碼。爲 了應用過濾器,咱們就要知道網絡掩碼,這個函數就能夠派上用場了。
經試驗發現這個過濾器並不能在全部的操做系統上正常工做。在個人測試環境中,我發現OpenBSD 2.9支持這種過濾器,而FreeBSD 4.3卻不行。
到這裏咱們已經學習瞭如何定義設備、準備嗅探以及應用過濾器來過濾咱們不想嗅探的部分。如今是時候嗅探數據包了。
嗅探數據包有兩種主要方法。咱們能夠一次捕獲一個單獨的包,也能夠進入一個循環,等待N個包流入。咱們首先關注如何捕獲單個包,以後再 研究循環的方法。就單個包而言,咱們用pcap_next()。
pcap_next()的原型很簡單:
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
首個參數是會話句柄。第二個參數是一個指針,它指向的結構用於存放數據包的通常信息,如捕獲的時間,包長度,組成包的各部分長度。 pcap_next()返回的*u_char指向捕獲的包,稍後咱們將會討論讀取數據包自己的方法。
這個例子演示怎樣使用pcap_next()來嗅探數據包:
這個程序用pcap_lookupdev()取得設備並將其設置爲混雜模式,而後開始嗅探。它取得23端口(telnet)上的首個包 後輸出這個包 的大小(字節)。此外,這個程序有一個新的函數:pcap_close(),咱們等會兒再討論它(儘管函數名已經說明了一切)。
另外一種方法要複雜一些,不過更有用。一般不多有嗅探器直接調用pcap_next()函數(若是有的話),它們更經常使用的是 pcap_loop()或pcap_dispatch()。要學會這兩個函數,你必須先理解回調函數。
回調函數並非新鮮事物,它們在很多API中廣泛存在。回調的概念很簡單。假設個人程序要等待某個事件,爲簡單起見,就說是等待用戶輸 入 吧。用戶每按一次鍵,我想要經過函數來決定接下來作什麼。這個函數就能夠是回調函數,每次按下鍵盤,個人程序就會調用這個回調函數。回到pcap中,回調 函數被調用的時機由用戶按下一個鍵改成pcap嗅探到一個數據包。pcap_loop() 和 pcap_dispatch() 的用法很類似。每次嗅探到一個符合過濾要求(若是存在過濾器的話)的包後就會調用回調函數。
pcap_loop()的原型是:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
首個參數是會話句柄。接下來的cnt參數告訴pcap_loop()返回以前應該嗅探到多少個包(負數表示一直嗅探直到出錯爲止)。第 三個就是以前討論的回調函數啦。最後一個參數的做用是傳遞附加的自定義數據給回調函數,在 一些應用時有用,不少時候直接設爲NULL就行。後面咱們將會以例子的形式看到pcap用u_char指針傳遞一些頗有意思的信息。 pcap_dispatch()的用法幾乎同樣,惟一的區別是pcap_dispatch()只處理第一批從系統中收到的包,而pcap_loop()會 繼續處理接下來的包直到達到指定數量爲止。關於它們的細節差別,請參考pcap的man文檔。
拿出cap_loop()的例子以前,咱們得了解一下回調函數的原型:
void got_packet(u_char *args, const struct pcap_pkthdr *header,
const u_char *packet);
首先,它是一個無返回值的函數。這是能夠理解的,由於pcap_loop()不知道怎樣處理回調函數的返回值。
第一個參數就是咱們傳給pcap_loop()的最後一個數據。每次回調函數被調用時均可以取得這個數據。
第二個參數是pcap頭結構,它含有包什麼時候到達,多大等信息。pcap_pkthdr結構定義於pcap.h之中:
struct pcap_pkthdr {
struct timeval ts; /* time stamp */
bpf_u_int32 caplen; /* length of portion present */
bpf_u_int32 len; /* length this packet (off wire) */
};
結構成員名稱徹底能夠自解釋了。
最後的那個參數const u_char *packet是咱們最關心的,也是最容易引發pcap初學者混亂的。它是一個u_char指針,指向被pcap_loop()嗅探到的整個數據包的第一 個字節。
怎樣使用這個packet參數呢?數據包有不少屬性,只要思考一下就會知道,它不是一個真正的字符串,而是一系列的結構(例如, TCP/IP包應該有以太頭、IP頭、TCP頭,最後,還有包的載荷)。這個u_char指針指向的正是這些數據結構的序列化版本,因此在使用以前,要作 類 型轉換工做。
首先,咱們要定義這些結構,下面定義的是以太網TCP/IP數據包結構。
/* 以太網的地址佔6字節 */
#define ETHER_ADDR_LEN 6
/* 以太網頭 */
struct sniff_ethernet {
u_char ether_dhost[ETHER_ADDR_LEN]; /* 目的地址 */
u_char ether_shost[ETHER_ADDR_LEN]; /* 源地址 */
u_short ether_type; /* IP? ARP? RARP? 等 */
};
/* IP 頭 */
struct sniff_ip {
u_char ip_vhl; /* version << 4 | header length >> 2 */
u_char ip_tos; /* type of service */
u_short ip_len; /* total length */
u_short ip_id; /* identification */
u_short ip_off; /* fragment offset field */
#define IP_RF 0x8000 /* reserved fragment flag */
#define IP_DF 0x4000 /* dont fragment flag */
#define IP_MF 0x2000 /* more fragments flag */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
u_char ip_ttl; /* time to live */
u_char ip_p; /* protocol */
u_short ip_sum; /* checksum */
struct in_addr ip_src,ip_dst; /* source and dest address */
};
#define IP_HL(ip) (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip) (((ip)->ip_vhl) >> 4)
/* TCP 頭 */
struct sniff_tcp {
u_short th_sport; /* source port */
u_short th_dport; /* destination port */
tcp_seq th_seq; /* sequence number */
tcp_seq th_ack; /* acknowledgement number */
u_char th_offx2; /* data offset, rsvd */
#define TH_OFF(th) (((th)->th_offx2 & 0xf0) >> 4)
u_char th_flags;
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_ECE 0x40
#define TH_CWR 0x80
#define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
u_short th_win; /* window */
u_short th_sum; /* checksum */
u_short th_urp; /* urgent pointer */
};
注意:我發如今個人Slackware Linux 8(2.2.19內核)裏不能編譯這個結構。問題出如今include/features.h裏,除非在包含它以前定義_BSD_SOURCE,不然將以 POSIX接口實現。因此建議在包含全部頭文件以前先加入一行:
#define _BSD_SOURCE 1
這樣能確保使用BSD風格的API。固然,若是你不想用預約義,你能夠簡單地改一下結構,就象我在這裏作的那樣。
那麼,如何把咱們神祕的u_char指針應用到pcap工做中來呢?嗯~~這些結構定義了包中的頭部數據,那怎樣提取這些部分呢?準備 見證指針的典型應用之一吧。
咱們仍是假設處理以太網的TCP/IP包。一樣的方法可用於任何數據包,惟一的區別是你實際所使用的結構類型。讓咱們從用於解析數據包 的變量聲明及預處理定義開始:
如今開始神奇的類型轉換:
它怎樣工做?考慮一下數據包在內存中的佈局。u_char指針只是一個包含內存地址的變量,這就是指針的實質,指出內存所在位置。
爲了簡單起見,就說這個指針指向的地址爲X吧。若是咱們的這三個結構是線性存儲的,那麼第一個(sniff_ethernet)結構就 位於地址爲X的內存上,接下來咱們能夠很簡單地找到後面的結構:X地址加上14(或SIZE_ETHERNET)字節的以太網頭長度。
簡而言之,若是咱們有頭地址,那麼後面的結構地址就是當前頭地址加上頭長度。IP頭和以太網頭不同,這的長度是不固定的。它的長度由 它的成員指定,以字(4byte)爲單位,因此獲得字節長度還得剩上4。最小長度是20字節。
TCP頭也是變長的,一樣以4字節爲一個單位,最小長度也是20字節。
咱們來作個表格:
變量 | 位置 (bytes) |
sniff_ethernet | X |
sniff_ip | X + SIZE_ETHERNET |
sniff_tcp | X + SIZE_ETHERNET + {IP header length} |
payload | X + SIZE_ETHERNET + {IP header length} + {TCP header length} |
第一行的sniff_ethernet結構,正好在X處。sniff_ip,緊跟在sniff_ethernet以後,爲X加上以太網 頭所佔空間(14字節,或SIZE_ETHERNET)。sniff_tcp在sniff_ip後面,所以它的位置是X加上以太網頭和IP頭的大小。最 後,payload (它不是一個單一的結構,這與上層的協議有關)位於最後。
到這裏,咱們瞭解瞭如何調用回調函數,調用它以及找到嗅探到的包的屬性。你所期待的時刻到了:寫一個有用的嗅探器。因爲代碼長度的關 系,我不打算把它放到本文中。你能夠到這裏下載 sniffex.c並測試它。
如今你應該可以用pcap寫一個嗅探器了。你已經學習了打開pcap會話、關於它的屬性、嗅探數據包、應用過濾器和回調函數的基本概 念。是時候開始嗅探數據了。