用pcap編寫網絡嗅探器(Programming with pcap譯文)

首先明確本文的閱讀對象。顯然,你須要一些C語言的基本知識,除非只想瞭解pcap的基本原理。你不用是一個編程高手;對於想深刻 瞭解該領 域的編程者,我保證會盡可能詳細描述相關概念。另外,網絡方面的一些知識對閱讀本文是有幫助的。本文給出了一個網絡包嗅探器,全部的代碼已在默認內核 的FreeBSD 4.3上測試經過。編程

開始: pcap應用程序的格局

首先要了解的是pcap嗅探器的整體佈局。代碼流程以下:數組

  1. 咱們首先要作的是決定要嗅探的接口。在Linux裏它多是eth0,BSD裏多是xl1等等。咱們能夠用字符串定義它,也可 以詢問pcap獲得所要使用的接口的名稱。
  2. 初始化pcap。這裏咱們要告訴pcap對什麼設備進行嗅探。若是願意,咱們能夠嗅探多個設備。如何區分它們呢?答案是文件句柄 (File Handle)。和打開文件讀寫同樣,咱們必須爲咱們的嗅探「會話(session)」命名,以便區分其它的任務會話。
  3. 如 果咱們只想嗅探特定通訊(例如:僅TCP/IP包,僅流向23端口的包等等),咱們必須創建一個規則集,「編譯」之,而後應用它,這三個步驟關係密切。規 則集用一個字符串保存並轉換成pcap認識的格式(所以要編譯它),編譯工做實際上只是在程序中調用一個函數,不涉及外部程序。而後告訴pcap在咱們指 定的會話上應用這個規則。
  4. 最後,咱們讓pcap進入它的主循環。這時,pcap等待有數據包流入。每次獲得新數據包,它就會調用咱們指定的函數,咱們能夠 在這個函數裏作任何咱們想作的事:它能夠解析數據包並輸出給用戶,也能夠把數據保存成一個文件,或者什麼也不作。
  5. 在嗅探到咱們須要的東西之後,關閉會話,任務完成。

實際上這是一個很簡單的過程。總共五步,其中一步是可選的(就是那個使你感到困惑的第三步)。下面咱們開始研究每一個步驟及如何實現它 們。網絡

設置嗅探設備

這一步極其簡單。有兩種方法來設置咱們要嗅探的設備。session

第一種是簡單地讓用戶告訴咱們,考慮下面的程序:數據結構

  1. #include <stdio.h>
  2. #include <pcap.h>
  3.  
  4. int main(int argc, char *argv[])
  5. {
  6.     char *dev = argv[1];
  7.  
  8.     printf("Device: %s ", dev);
  9.     return(0);
  10. }

用戶指定設備名做爲程序的第一個參數。如今,字符串"dev"保存了咱們要嗅探的,pcap能夠認識的接口名(固然,假設用戶給咱們的 是真實的接口)。app

另外一種方法也一樣簡單,看這個程序:tcp

  1. #include <stdio.h>
  2. #include <pcap.h>
  3.  
  4. int main(int argc, char *argv[])
  5. {
  6.     char *dev, errbuf[PCAP_ERRBUF_SIZE];
  7.  
  8.     dev = pcap_lookupdev(errbuf);
  9.     if (dev == NULL) {
  10.         fprintf(stderr, "Couldn't find default device: %s ", errbuf);
  11.         return(2);
  12.     }
  13.     printf("Device: %s ", dev);
  14.     return(0);
  15. }

在這裏,由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

爲了演示,考慮這個代碼段:

  1. #include <pcap.h>
  2. ...
  3. pcap_t *handle;
  4.  
  5. handle = pcap_open_live(somedev, BUFSIZ, 1, 1000, errbuf);
  6. if (handle == NULL) {
  7.     fprintf(stderr, "Couldn't open device %s: %s ", somedev, errbuf);
  8.     return(2);
  9. }

這個代碼打開"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)

很直白,第一個參數是會話句柄,第二個是編譯後的表達式。

也許這個代碼示例能夠幫助你更好地理解:

  1. #include <pcap.h>
  2. ...
  3. pcap_t *handle; /* 會話句 柄 */
  4. char dev[] = "rl0"/* 被嗅探的 設備 */
  5. char errbuf[PCAP_ERRBUF_SIZE]; /* 錯誤信息 */
  6. struct bpf_program fp; /* 編譯後的過濾表達式 */
  7. char filter_exp[] = "port 23"/* 過 濾表達式 */
  8. bpf_u_int32 mask; /* 嗅探設備的網絡掩碼 */
  9. bpf_u_int32 net; /* 嗅探設備 的IP */
  10.  
  11. if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
  12.     fprintf(stderr, "Can't get netmask for device %s ", dev);
  13.     net = 0;
  14.     mask = 0;
  15. }
  16. handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
  17. if (handle == NULL) {
  18.     fprintf(stderr, "Couldn't open device %s: %s "
  19.             somedev, errbuf);
  20.     return(2);
  21. }
  22. if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
  23.     fprintf(stderr, "Couldn't parse filter %s: %s "
  24.             filter_exp, pcap_geterr(handle));
  25.     return(2);
  26. }
  27. if (pcap_setfilter(handle, &fp) == -1) {
  28.     fprintf(stderr, "Couldn't install filter %s: %s ",
  29.             filter_exp, pcap_geterr(handle));
  30.     return(2);
  31. }

這個程序在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()來嗅探數據包:

  1. #include <pcap.h>
  2. #include <stdio.h>
  3.  
  4. int main(int argc, char *argv[])
  5. {
  6.     pcap_t *handle; /* 會話句柄 */
  7.     char *dev; /* 嗅探的設備 */
  8.     char errbuf[PCAP_ERRBUF_SIZE]; /* 錯誤信息 */
  9.     struct bpf_program fp; /* 編譯的過濾器 */
  10.     char filter_exp[] = "port 23"/* 過 濾表達式 */
  11.     bpf_u_int32 mask; /* 網 絡掩碼 */
  12.     bpf_u_int32 net; /* IP */
  13.     struct pcap_pkthdr header; /* pcap頭 */
  14.     const u_char *packet; /* 數據包 */
  15.  
  16.     /* 定義設備 */
  17.     dev = pcap_lookupdev(errbuf);
  18.     if (dev == NULL) {
  19.         fprintf(stderr, "Couldn't find default device: %s ", errbuf);
  20.         return(2);
  21.     }
  22.     /* 取得設備屬性 */
  23.     if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
  24.         fprintf(stderr, "Couldn't get netmask for device %s: %s "
  25.                 dev, errbuf);
  26.         net = 0;
  27.         mask = 0;
  28.     }
  29.     /* 以混雜模式打開會話 */
  30.     handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
  31.     if (handle == NULL) {
  32.         fprintf(stderr, "Couldn't open device %s: %s ",
  33.                 somedev, errbuf);
  34.         return(2);
  35.     }
  36.     /* 編譯並應用過濾器 */
  37.     if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
  38.         fprintf(stderr, "Couldn't parse filter %s: %s "
  39.                 filter_exp, pcap_geterr(handle));
  40.         return(2);
  41.     }
  42.     if (pcap_setfilter(handle, &fp) == -1) {
  43.         fprintf(stderr, "Couldn't install filter %s: %s ",
  44.                 filter_exp, pcap_geterr(handle));
  45.         return(2);
  46.     }
  47.     /* 抓取一個數據包 */
  48.     packet = pcap_next(handle, &header);
  49.     /* 輸出數據包長度 */
  50.     printf("Jacked a packet with length of [%d] ",
  51.             header.len);
  52.     /* 關閉會話 */
  53.     pcap_close(handle);
  54.     return(0);
  55. }

這個程序用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包。一樣的方法可用於任何數據包,惟一的區別是你實際所使用的結構類型。讓咱們從用於解析數據包 的變量聲明及預處理定義開始:

  1. /* 以太網頭老是14字節 */
  2. #define SIZE_ETHERNET 14
  3.  
  4. const struct sniff_ethernet *ethernet; /* The ethernet header */
  5. const struct sniff_ip *ip; /* The IP header */
  6. const struct sniff_tcp *tcp; /* The TCP header */
  7. const char *payload; /* Packet payload */
  8.  
  9. u_int size_ip;
  10. u_int size_tcp;

如今開始神奇的類型轉換:

  1. ethernet = (struct sniff_ethernet*)(packet);
  2. ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
  3. size_ip = IP_HL(ip)*4;
  4. if (size_ip < 20) {
  5.     printf(" * Invalid IP header length: %u bytes ", size_ip);
  6.     return;
  7. }
  8. tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
  9. size_tcp = TH_OFF(tcp)*4;
  10. if (size_tcp < 20) {
  11.     printf(" * Invalid TCP header length: %u bytes ", size_tcp);
  12.     return;
  13. }
  14. payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);

它怎樣工做?考慮一下數據包在內存中的佈局。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並測試它。

Wrapping Up

如今你應該可以用pcap寫一個嗅探器了。你已經學習了打開pcap會話、關於它的屬性、嗅探數據包、應用過濾器和回調函數的基本概 念。是時候開始嗅探數據了。

相關文章
相關標籤/搜索