libpcap講解與API接口函數講解

轉載有道:http://blog.chinaunix.net/uid-21556133-id-120228.html

libpcap(Packet Capture Library),即數據包捕獲函數庫,是Unix/Linux平臺下的網絡數據包捕獲函數庫。它是一個獨立於系統的用戶層包捕獲的API接口,爲底層網絡監測提供了一個可移植的框架。

一、libpcap工作原理
libpcap主要由兩部份組成:網絡分接頭(Network Tap)和數據過濾器(Packet Filter)。網絡分接頭從網絡設備驅動程序中收集數據拷貝,過濾器決定是否接收該數據包。Libpcap利用BSD Packet Filter(BPF)算法對網卡接收到的鏈路層數據包進行過濾。BPF算法的基本思想是在有BPF監聽的網絡中,網卡驅動將接收到的數據包複製一份交給BPF過濾器,過濾器根據用戶定義的規則決定是否接收此數據包以及需要拷貝該數據包的那些內容,然後將過濾後的數據給與過濾器相關聯的上層應用程序。
libpcap的包捕獲機制就是在數據鏈路層加一個旁路處理。當一個數據包到達網絡接口時,libpcap首先利用已經創建的Socket從鏈路層驅動程序中獲得該數據包的拷貝,再通過Tap函數將數據包發給BPF過濾器。BPF過濾器根據用戶已經定義好的過濾規則對數據包進行逐一匹配,匹配成功則放入內核緩衝區,並傳遞給用戶緩衝區,匹配失敗則直接丟棄。如果沒有設置過濾規則,所有數據包都將放入內核緩衝區,並傳遞給用戶層緩衝區。

二、libpcap的抓包框架
pcap_lookupdev()函數用於查找網絡設備,返回可被pcap_open_live()函數調用的網絡設備名指針。
pcap_open_live()函數用於打開網絡設備,並且返回用於捕獲網絡數據包的數據包捕獲描述字。對於此網絡設備的操作都要基於此網絡設備描述字。
pcap_lookupnet()函數獲得指定網絡設備的網絡號和掩碼。
pcap_compile()函數用於將用戶制定的過濾策略編譯到過濾程序中。
pcap_setfilter()函數用於設置過濾器。
pcap_loop()函數pcap_dispatch()函數用於捕獲數據包,捕獲後還可以進行處理,此外pcap_next()和pcap_next_ex()兩個函數也可以用來捕獲數據包。
pcap_close()函數用於關閉網絡設備,釋放資源。

其實pcap的應用程序格式很簡單,總的來說可以可以分爲以下5部分,相信看了以下的5部分,大概能對pcap的總體佈局有個大概的瞭解了吧:
1.我們從決定用哪一個接口進行嗅探開始。在Linux中,這可能是eth0,而在BSD系統中則可能是xl1等等。我們也可以用一個字符串來定義這個設備,或者採用pcap提供的接口名來工作。
2.初始化pcap。在這裏我們要告訴pcap對什麼設備進行嗅探。假如願意的話,我們還可以嗅探多個設備。怎樣區分它們呢?使用 文件句柄。就像打開一個文件進行讀寫一樣,必須命名我們的嗅探“會話”,以此使它們各自區別開來。
3.假如我們只想嗅探特定的傳輸(如TCP/IP包,發往端口23的包等等),我們必須創建一個規則集合,編譯並且使用它。這個過程分爲三個相互緊密關聯的階段。規則集合被置於一個字符串內,並且被轉換成能被pcap讀的格式(因此編譯它)。編譯實際上就是在我們的程序裏調用一個不被外部程序使用的函數。接下來我們要告訴 pcap使用它來過濾出我們想要的那一個會話。
4.最後,我們告訴pcap進入它的主體執行循環。在這個階段內pcap一直工作到它接收了所有我們想要的包爲止。每當它收到一個包就調用另一個已經定義好的函數,這個函數可以做我們想要的任何工作,它可以剖析所部獲的包並給用戶打印出結果,它可以將結果保存爲一個文件,或者什麼也不作。
5.在嗅探到所需的數據後,我們要關閉會話並結束。

三、實現libpcap的每一個步驟
(1)設置設備
這是很簡單的。有兩種方法設置想要嗅探的設備。
第一種,我們可以簡單的讓用戶告訴我們。考察下面的程序:
   #include
   #include
   int main(int argc, char *argv[])
   {
   char *dev = argv[1];
   printf("Device: %s", dev);
   return(0);
   }
用戶通過傳遞給程序的第一個參數來指定設備。字符串“dev”以pcap能“理解”的格式保存了我們要嗅探的接口的名字(當然,用戶必須給了我們一個真正存在的接口)。
另一種也是同樣的簡單。來看這段程序:
   #include
   #include
   int main()
   {
   char *dev, errbuf[PCAP_ERRBUF_SIZE];
   dev = pcap_lookupdev(errbuf);
   printf("Device: %s", dev);
   return(0);
   }
(2)打開設備進行嗅探
創建一個嗅探會話的任務真的非常簡單。爲此,我們使用pcap_open_live()函數。此函數的原型(根據pcap的手冊頁)如下:
   pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
其第一個參數是我們在上一節中指定的設備,snaplen是整形的,它定義了將被pcap捕捉的最大字節數。當promisc設爲true時將置指定接口爲混雜模式(然而,當它置爲false時接口仍處於混雜模式的非凡情況也是有可能的)。to_ms是讀取時的超時值,單位是毫秒(假如爲0則一直嗅探直到錯誤發生,爲-1則不確定)。最後,ebuf是一個我們可以存入任何錯誤信息的字符串(就像上面的errbuf)。此函數返回其會話句柄。
混雜模式與非混雜模式的區別:這兩種方式區別很大。一般來說,非混雜模式的嗅探器中,主機僅嗅探那些跟它直接有關的通信,如發向它的,從它發出的,或經它路由的等都會被嗅探器捕捉。而在混雜模式中則嗅探傳輸線路上的所有通信。在非交換式網絡中,這將是整個網絡的通信。這樣做最明顯的優點就是使更多的包被嗅探到,它們因你嗅探網絡的原因或者對你有幫助,或者沒有。但是,混雜模式是可被探測到的。一個主機可以通過高強度的測試判定另一臺主機是否正在進行混雜模式的嗅探。其次,它僅在非交換式的網絡環境中有效工作(如集線器,或者交換中的ARP層面)。再次,在高負荷的網絡中,主機的系統資源將消耗的非常嚴重。
(3)過濾通信
實現這一過程由pcap_compile()與pcap_setfilter()這兩個函數完成。
在使用我們自己的過濾器前必須編譯它。過濾表達式被保存在一個字符串中(字符數組)。其句法在tcpdump的手冊頁中被證實非常好。我建議你親自閱讀它。但是我們將使用簡單的測試表達式,這樣你可能很輕易理解我的例子。
我們調用pcap_compile()來編譯它,其原型是這樣定義的:
   int pcap_compile(pcap_t *p, strUCt bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
第一個參數是會話句柄。接下來的是我們存儲被編譯的過濾器版本的地址的引用。再接下來的則是表達式本身,存儲在規定的字符串格式裏。再下邊是一個定義表達式是否被優化的整形量(0爲false,1爲true,標準規定)。最後,我們必須指定應用此過濾器的網絡掩碼。函數返回-1爲失敗,其他的任何值都表明是成功的。
表達式被編譯之後就可以使用了。現在進入pcap_setfilter()。仿照我們介紹pcap的格式,先來看一看pcap_setfilter()的原型:
   int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
這非常直觀,第一個參數是會話句柄,第二個參數是被編譯表達式版本的引用(可推測出它與pcap_compile()的第二個參數相同)。
下面的代碼示例可能能使你更好的理解:
   #include
   pcap_t *handle; /* 會話的句柄 */
   char dev[] = "eth0"; /* 執行嗅探的設備 */
   char errbuf[PCAP_ERRBUF_SIZE]; /* 存儲錯誤 信息的字符串 */
   struct bpf_program filter; /*已經編譯好的過濾表達式*/
   char filter_app[] = "port 23"; /* 過濾表達式*/
   bpf_u_int32 mask; /* 執行嗅探的設備的網絡掩碼 */
   bpf_u_int32 net; /* 執行嗅探的設備的IP地址 */
   pcap_lookupnet(dev, &net, &mask, errbuf);
   handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
   pcap_compile(handle, &filter, filter_app, 0, net);
   pcap_setfilter(handle, &filter);
這個程序使嗅探器嗅探經由端口23的所有通信,使用混雜模式,設備是eth0。
(4)實際的嗅探
有兩種手段捕捉包。我們可以一次只捕捉一個包,也可以進入一個循環,等捕捉到多個包再進行處理。我們將先看看怎樣去捕捉單個包,然後再看看使用循環的方法。爲此,我們使用函數pcap_next()。
pcap_next()的原型及其簡單:
   u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
第一個參數是會話句柄,第二個參數是指向一個包括了當前數據包總體信息(被捕捉時的時間,包的長度,其被指定的部分長度)的結構體的指針(在這裏只有一個片斷,只作爲一個示例)。pcap_next()返回一個u_char指針給被這個結構體描述的包。我們將稍後討論這種實際讀取包本身的手段。
   這裏有一個演示怎樣使用pcap_next()來嗅探一個包的例子:
   #include
   #include
   int main()
   {
   pcap_t *handle; /* 會話句柄 */
   char *dev; /* 執行嗅探的設備 */
   char errbuf[PCAP_ERRBUF_SIZE]; /* 存儲錯誤信息的字符串 */
  
   struct bpf_program filter; /* 已經編譯好的過濾器 */
   char filter_app[] = "port 23"; /* 過濾表達式 */
   bpf_u_int32 mask; /* 所在網絡的掩碼 */
   bpf_u_int32 net; /* 主機的IP地址 */
   struct pcap_pkthdr header; /* 由pcap.h定義 */
   const u_char *packet; /* 實際的包 */
   /* Define the device */
   dev = pcap_lookupdev(errbuf);
   /* 探查設備屬性 */
   pcap_lookupnet(dev, &net, &mask, errbuf);
   /* 以混雜模式打開會話 */
   handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
   /* 編譯並應用過濾器 */
   pcap_compile(handle, &filter, filter_app, 0, net);
   pcap_setfilter(handle, &filter);
   /* 截獲一個包 */
   packet = pcap_next(handle, &header);
   /* 打印它的長度 */
   printf("Jacked a packet with length of [%d]
   ", header.len);
   /* 關閉會話 */
   pcap_close(handle);
   return(0);
   }
這個程序嗅探被pcap_lookupdev()返回的設備並將它置爲混雜模式。它發現第一個包經過端口23(telnet)並且告訴用戶此包的大小(以字 節爲單位)。這個程序又包含了一個新的調用pcap_close(),我們將在後面討論(儘管它的名字就足夠證實它自己的作用)。
實際上很少有嗅探程序會真正的使用pcap_next()。通常,它們使用pcap_loop()或者 pcap_dispatch()(它就是用了pcap_loop())。
pcap_loop()的原型如下:
   int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
第一個參數是會話句柄,接下來是一個整型,它告訴pcap_loop()在返回前應捕捉多少個數據包(若爲負值則表示應該一直工作直至錯誤發生)。第三個參數是回調函數的名稱(正像其標識符所指,無括號)。最後一個參數在有些應用裏有用,但更多時候則置爲NULL。假設我們有我們自己的想送往回調函數的參數,另外還有pcap_loop()發送的參數,這就需要用到它。很明顯,必須是一個u_char類型的指針以確保結果正確;正像我們稍後見到的,pcap使用了很有意思的方法以u_char指針的形勢傳遞信息。pcap_dispatch()的用法幾乎相同。唯一不同的是它們如何處理超時(還記得在調用pcap_open_live()時怎樣設置超時嗎?這就是它起作用的地方)。Pcap_loop()忽略超時而pcap_dispatch()則不。關於它們之間區別的更深入的討論請參見pcap的手冊頁。
回調函數的原型:
   void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
讓我們更細緻的考察它。首先,你會注重到該函數返回void類型,這是符合邏輯的,因爲pcap_loop()不知道如何去處理一個回調返回值。第一個參數相應於pcap_loop()的最後一個參數。每當回調函數被老婆 調用時,無論最後一個參數傳給pcap_loop()什麼值,這個值都會傳給我們回調函數的第一個參數。第二個參數是pcap頭文件定義的,它包括數據包被嗅探的時間、大小等信息。結構體pcap_pkhdr在pcap.h中定義如下:
   struct pcap_pkthdr {
   struct timeval ts; /* 時間戳 */
   bpf_u_int32 caplen; /* 已捕捉部分的長度 */
   bpf_u_int32 len; /* 該包的脫機長度 */
   };
這些量都相當明瞭。最後一個參數在它們中是最有意思的,也最讓pcap程序新手感到迷惑。這又是一個u_char指針,它包含了被pcap_loop()嗅探到的所有包。
但是你怎樣使用這個我們在原型裏稱爲packet的變量呢?一個數據包包含許多屬性,因此你可以想象它不只是一個字符串,而實質上是一個結構體的集合(比如,一個TCP/IP包會有一個以太網的頭部,一個IP頭部,一個TCP頭部,還有此包的有效載荷)。這個u_char就是這些結構體的串聯版本。爲了使用它,我們必須作一些有趣的匹配工作。
下面這些是一些數據包的結構體:
   /* 以太網幀頭部 */
   struct sniff_ethernet {
   u_char ether_dhost[ETHER_ADDR_LEN]; /* 目的主機的地址 */
   u_char ether_shost[ETHER_ADDR_LEN]; /* 源主機的地址 */
   u_short ether_type; /* IP? ARP? RARP? etc */
   };
   /* IP數據包的頭部 */
   struct sniff_ip {
   #if BYTE_ORDER == LITTLE_ENDIAN
   u_int ip_hl:4, /* 頭部長度 */
   ip_v:4; /* 版本號 */
   #if BYTE_ORDER == BIG_ENDIAN
   u_int ip_v:4, /* 版本號 */
   ip_hl:4; /* 頭部長度 */
   #endif
   #endif /* not _IP_VHL */
   u_char ip_tos; /* 服務的類型 */
   u_short ip_len; /* 總長度 */
   u_short ip_id; /*包標誌號 */
   u_short ip_off; /* 碎片偏移 */
   #define IP_RF 0x8000 /* 保留的碎片標誌 */
   #define IP_DF 0x4000 /* dont fragment flag */
   #define IP_MF 0x2000 /* 多碎片標誌*/
   #define IP_OFFMASK 0x1fff /*分段位 */
   u_char ip_ttl; /* 數據包的生存時間 */
   u_char ip_p; /* 所使用的協議 */
   u_short ip_sum; /* 校驗和 */
   struct in_addr ip_src,ip_dst; /* 源地址、目的地址*/
   };
   /* TCP 數據包的頭部 */
   struct sniff_tcp {
   u_short th_sport; /* 源端口 */
   u_short th_dport; /* 目的端口 */
   tcp_seq th_seq; /* 包序號 */
   tcp_seq th_ack; /* 確認序號 */
   #if BYTE_ORDER == LITTLE_ENDIAN
   u_int th_x2:4, /* 還沒有用到 */
   th_off:4; /* 數據偏移 */
   #endif
   #if BYTE_ORDER == BIG_ENDIAN
   u_int th_off:4, /* 數據偏移*/
   th_x2:4; /*還沒有用到 */
   #endif
   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_FINTH_SYNTH_RSTTH_ACKTH_URGTH_ECETH_CWR)
   u_short th_win; /* TCP滑動窗口 */
   u_short th_sum; /* 頭部校驗和 */
   u_short th_urp; /* 緊急服務位 */
   };
pcap嗅探數據包時正是使用的這些結構。接下來,它簡單的創建一個u_char字符串並且將這些結構體填入。那麼我們怎樣才能區分它們呢?預備好見證指針最實用的好處之一吧。
我們再一次假定要對以太網上的TCP/IP包進行處理。同樣的手段可以應用於任何數據包,唯一的區別是你實際所使用的結構體的類型。讓我們從聲明分解u_char包的變量開始:
   const struct sniff_ethernet *ethernet; /* 以太網幀頭部*/
   const struct sniff_ip *ip; /* IP包頭部 */
   const struct sniff_tcp *tcp; /* TCP包頭部 */
   const char *payload; /* 數據包的有效載荷*/
   /*爲了讓它的可讀性好,我們計算每個結構體中的變量大小*/
   int size_ethernet = sizeof(struct sniff_ethernet);
   int size_ip = sizeof(struct sniff_ip);
   int size_tcp = sizeof(struct sniff_tcp);
   現在我們開始讓人感到有些神祕的匹配:
   ethernet = (struct sniff_ethernet*)(packet);
   ip = (struct sniff_ip*)(packet + size_ethernet);
   tcp = (struct sniff_tcp*)(packet + size_ethernet + size_ip);
   payload = (u_char *)(packet + size_ethernet + size_ip + size_tcp);
  
此處如何工作?考慮u_char在內存中的層次。基本的,當pcap將這些結構體填入u_char的時候是將這些數據存入一個字符串中,那個字符串將被送入我們的回調函數中。反向轉換是這樣的,不考慮這些結構體制中的值,它們的大小將是一致的。例如在我的平臺上,一個sniff_ethernet結構體的大小是14字節。一個sniff_ip結構體是20字節,一個sniff_tcp結構體也是20字節。 u_char指針正是包含了內存地址的一個變量,這也是指針的實質,它指向內存的一個區域。簡單而言,我們說指針指向的地址爲x,假如三個結構體恰好線性排列,第一個(sniff_ethernet)被裝載到內存地址的x處則我們很輕易的發現其他結構體的地址,讓我們以表格顯示之:
   Variable Location (in bytes)
   sniff_ethernet X
   sniff_ip X + 14
   sniff_tcp X + 14 + 20
   payload X + 14 + 20 + 20
結構體sniff_ethernet正好在x處,緊接着它的sniff_ip則位於x加上它本身佔用的空間(此例爲14字節),依此類推可得全部地址。
注重:你沒有假定你的變量也是同樣大小是很重要的。你應該總是使用sizeof()來確保尺寸的正確。這是因爲這些結構體中的每個成員在不同平臺下可以有不同的尺寸。

下面是主要函數接口:
pcap_t *pcap_open_live(char *device, int snaplen,
   int promisc, int to_ms, char *ebuf)
   獲得用於捕獲網絡數據包的數據包捕獲描述字。device參數爲指定打開
   的網絡設備名。snaplen參數定義捕獲數據的最大字節數。promisc指定
   是否將網絡接口置於混雜模式。to_ms參數指定超時時間(毫秒)。
   ebuf參數則僅在pcap_open_live()函數出錯返回NULL時用於傳遞錯誤消
   息。
pcap_t *pcap_open_offline(char *fname, char *ebuf)
   打開以前保存捕獲數據包的文件,用於讀取。fname參數指定打開的文
   件名。該文件中的數據格式與tcpdump和tcpslice兼容。"-"爲標準輸
   入。ebuf參數則僅在pcap_open_offline()函數出錯返回NULL時用於傳
   遞錯誤消息。
pcap_dumper_t *pcap_dump_open(pcap_t *p, char *fname)
   打開用於保存捕獲數據包的文件,用於寫入。fname參數爲"-"時表示
   標準輸出。出錯時返回NULL。p參數爲調用pcap_open_offline()或
   pcap_open_live()函數後返回的pcap結構指針。fname參數指定打開
   的文件名。如果返回NULL,則可調用pcap_geterr()函數獲取錯誤消
   息。

char *pcap_lookupdev(char *errbuf)
   用於返回可被pcap_open_live()或pcap_lookupnet()函數調用的網絡
   設備名指針。如果函數出錯,則返回NULL,同時errbuf中存放相關的
   錯誤消息。
int pcap_lookupnet(char *device, bpf_u_int32 *netp,
   bpf_u_int32 *maskp, char *errbuf)
   獲得指定網絡設備的網絡號和掩碼。netp參數和maskp參數都是
   bpf_u_int32指針。如果函數出錯,則返回-1,同時errbuf中存放相
   關的錯誤消息。
int pcap_dispatch(pcap_t *p, int cnt,
   pcap_handler callback, u_char *user)
   捕獲並處理數據包。cnt參數指定函數返回前所處理數據包的最大值。
   cnt=-1表示在一個緩衝區中處理所有的數據包。cnt=0表示處理所有
   數據包,直到產生以下錯誤之一:讀取到EOF;超時讀取。callback
   參數指定一個帶有三個參數的回調函數,這三個參數爲:一個從
   pcap_dispatch()函數傳遞過來的u_char指針,一個pcap_pkthdr結構
   的指針,和一個數據包大小的u_char指針。如果成功則返回讀取到的
   字節數。讀取到EOF時則返回零值。出錯時則返回-1,此時可調用
   pcap_perror()或pcap_geterr()函數獲取錯誤消息。
int pcap_loop(pcap_t *p, int cnt,
   pcap_handler callback, u_char *user)
   功能基本與pcap_dispatch()函數相同,只不過此函數在cnt個數據包
   被處理或出現錯誤時才返回,但讀取超時不會返回。而如果爲
   pcap_open_live()函數指定了一個非零值的超時設置,然後調用
   pcap_dispatch()函數,則當超時發生時pcap_dispatch()函數會返回。
   cnt參數爲負值時pcap_loop()函數將始終循環運行,除非出現錯誤。
void pcap_dump(u_char *user, struct pcap_pkthdr *h,
   u_char *sp)
   向調用pcap_dump_open()函數打開的文件輸出一個數據包。該函數可
   作爲pcap_dispatch()函數的回調函數。
int pcap_compile(pcap_t *p, struct bpf_program *fp,
   char *str, int optimize, bpf_u_int32 netmask)
   將str參數指定的字符串編譯到過濾程序中。fp是一個bpf_program結
   構的指針,在pcap_compile()函數中被賦值。optimize參數控制結果
   代碼的優化。netmask參數指定本地網絡的網絡掩碼。
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
   指定一個過濾程序。fp參數是bpf_program結構指針,通常取自
   pcap_compile()函數調用。出錯時返回-1;成功時返回0。
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
   返回指向下一個數據包的u_char指針。
int pcap_datalink(pcap_t *p)
   返回數據鏈路層類型,例如DLT_EN10MB。
int pcap_snapshot(pcap_t *p)
   返回pcap_open_live被調用後的snapshot參數值。
int pcap_is_swapped(pcap_t *p)
   返回當前系統主機字節與被打開文件的字節順序是否不同。
int pcap_major_version(pcap_t *p)
   返回寫入被打開文件所使用的pcap函數的主版本號。
int pcap_minor_version(pcap_t *p)
   返回寫入被打開文件所使用的pcap函數的輔版本號。
int pcap_stats(pcap_t *p, struct pcap_stat *ps)
   向pcap_stat結構賦值。成功時返回0。這些數值包括了從開始
   捕獲數據以來至今共捕獲到的數據包統計。如果出錯或不支持
   數據包統計,則返回-1,且可調用pcap_perror()或
   pcap_geterr()函數來獲取錯誤消息。
FILE *pcap_file(pcap_t *p)
   返回被打開文件的文件名。
int pcap_fileno(pcap_t *p)
   返回被打開文件的文件描述字號碼。
void pcap_perror(pcap_t *p, char *prefix)
   在標準輸出設備上顯示最後一個pcap庫錯誤消息。以prefix參
   數指定的字符串爲消息頭。
char *pcap_geterr(pcap_t *p)
   返回最後一個pcap庫錯誤消息。
char *pcap_strerror(int error)
   如果strerror()函數不可用,則可調用pcap_strerror函數替代。
void pcap_close(pcap_t *p)
   關閉p參數相應的文件,並釋放資源。
libpcap(Packet Capture Library),即數據包捕獲函數庫,是Unix/Linux平臺下的網絡數據包捕獲函數庫。它是一個獨立於系統的用戶層包捕獲的API接口,爲底層網絡監測提供了一個可移植的框架。

一、libpcap工作原理
libpcap主要由兩部份組成:網絡分接頭(Network Tap)和數據過濾器(Packet Filter)。網絡分接頭從網絡設備驅動程序中收集數據拷貝,過濾器決定是否接收該數據包。Libpcap利用BSD Packet Filter(BPF)算法對網卡接收到的鏈路層數據包進行過濾。BPF算法的基本思想是在有BPF監聽的網絡中,網卡驅動將接收到的數據包複製一份交給BPF過濾器,過濾器根據用戶定義的規則決定是否接收此數據包以及需要拷貝該數據包的那些內容,然後將過濾後的數據給與過濾器相關聯的上層應用程序。
libpcap的包捕獲機制就是在數據鏈路層加一個旁路處理。當一個數據包到達網絡接口時,libpcap首先利用已經創建的Socket從鏈路層驅動程序中獲得該數據包的拷貝,再通過Tap函數將數據包發給BPF過濾器。BPF過濾器根據用戶已經定義好的過濾規則對數據包進行逐一匹配,匹配成功則放入內核緩衝區,並傳遞給用戶緩衝區,匹配失敗則直接丟棄。如果沒有設置過濾規則,所有數據包都將放入內核緩衝區,並傳遞給用戶層緩衝區。

二、libpcap的抓包框架
pcap_lookupdev()函數用於查找網絡設備,返回可被pcap_open_live()函數調用的網絡設備名指針。
pcap_open_live()函數用於打開網絡設備,並且返回用於捕獲網絡數據包的數據包捕獲描述字。對於此網絡設備的操作都要基於此網絡設備描述字。
pcap_lookupnet()函數獲得指定網絡設備的網絡號和掩碼。
pcap_compile()函數用於將用戶制定的過濾策略編譯到過濾程序中。
pcap_setfilter()函數用於設置過濾器。
pcap_loop()函數pcap_dispatch()函數用於捕獲數據包,捕獲後還可以進行處理,此外pcap_next()和pcap_next_ex()兩個函數也可以用來捕獲數據包。
pcap_close()函數用於關閉網絡設備,釋放資源。

其實pcap的應用程序格式很簡單,總的來說可以可以分爲以下5部分,相信看了以下的5部分,大概能對pcap的總體佈局有個大概的瞭解了吧:
1.我們從決定用哪一個接口進行嗅探開始。在Linux中,這可能是eth0,而在BSD系統中則可能是xl1等等。我們也可以用一個字符串來定義這個設備,或者採用pcap提供的接口名來工作。
2.初始化pcap。在這裏我們要告訴pcap對什麼設備進行嗅探。假如願意的話,我們還可以嗅探多個設備。怎樣區分它們呢?使用 文件句柄。就像打開一個文件進行讀寫一樣,必須命名我們的嗅探“會話”,以此使它們各自區別開來。
3.假如我們只想嗅探特定的傳輸(如TCP/IP包,發往端口23的包等等),我們必須創建一個規則集合,編譯並且使用它。這個過程分爲三個相互緊密關聯的階段。規則集合被置於一個字符串內,並且被轉換成能被pcap讀的格式(因此編譯它)。編譯實際上就是在我們的程序裏調用一個不被外部程序使用的函數。接下來我們要告訴 pcap使用它來過濾出我們想要的那一個會話。
4.最後,我們告訴pcap進入它的主體執行循環。在這個階段內pcap一直工作到它接收了所有我們想要的包爲止。每當它收到一個包就調用另一個已經定義好的函數,這個函數可以做我們想要的任何工作,它可以剖析所部獲的包並給用戶打印出結果,它可以將結果保存爲一個文件,或者什麼也不作。
5.在嗅探到所需的數據後,我們要關閉會話並結束。

三、實現libpcap的每一個步驟
(1)設置設備
這是很簡單的。有兩種方法設置想要嗅探的設備。
第一種,我們可以簡單的讓用戶告訴我們。考察下面的程序:
   #include
   #include
   int main(int argc, char *argv[])
   {
   char *dev = argv[1];
   printf("Device: %s", dev);
   return(0);
   }
用戶通過傳遞給程序的第一個參數來指定設備。字符串“dev”以pcap能“理解”的格式保存了我們要嗅探的接口的名字(當然,用戶必須給了我們一個真正存在的接口)。
另一種也是同樣的簡單。來看這段程序:
   #include
   #include
   int main()
   {
   char *dev, errbuf[PCAP_ERRBUF_SIZE];
   dev = pcap_lookupdev(errbuf);
   printf("Device: %s", dev);
   return(0);
   }
(2)打開設備進行嗅探
創建一個嗅探會話的任務真的非常簡單。爲此,我們使用pcap_open_live()函數。此函數的原型(根據pcap的手冊頁)如下:
   pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *ebuf)
其第一個參數是我們在上一節中指定的設備,snaplen是整形的,它定義了將被pcap捕捉的最大字節數。當promisc設爲true時將置指定接口爲混雜模式(然而,當它置爲false時接口仍處於混雜模式的非凡情況也是有可能的)。to_ms是讀取時的超時值,單位是毫秒(假如爲0則一直嗅探直到錯誤發生,爲-1則不確定)。最後,ebuf是一個我們可以存入任何錯誤信息的字符串(就像上面的errbuf)。此函數返回其會話句柄。
混雜模式與非混雜模式的區別:這兩種方式區別很大。一般來說,非混雜模式的嗅探器中,主機僅嗅探那些跟它直接有關的通信,如發向它的,從它發出的,或經它路由的等都會被嗅探器捕捉。而在混雜模式中則嗅探傳輸線路上的所有通信。在非交換式網絡中,這將是整個網絡的通信。這樣做最明顯的優點就是使更多的包被嗅探到,它們因你嗅探網絡的原因或者對你有幫助,或者沒有。但是,混雜模式是可被探測到的。一個主機可以通過高強度的測試判定另一臺主機是否正在進行混雜模式的嗅探。其次,它僅在非交換式的網絡環境中有效工作(如集線器,或者交換中的ARP層面)。再次,在高負荷的網絡中,主機的系統資源將消耗的非常嚴重。
(3)過濾通信
實現這一過程由pcap_compile()與pcap_setfilter()這兩個函數完成。
在使用我們自己的過濾器前必須編譯它。過濾表達式被保存在一個字符串中(字符數組)。其句法在tcpdump的手冊頁中被證實非常好。我建議你親自閱讀它。但是我們將使用簡單的測試表達式,這樣你可能很輕易理解我的例子。
我們調用pcap_compile()來編譯它,其原型是這樣定義的:
   int pcap_compile(pcap_t *p, strUCt bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)
第一個參數是會話句柄。接下來的是我們存儲被編譯的過濾器版本的地址的引用。再接下來的則是表達式本身,存儲在規定的字符串格式裏。再下邊是一個定義表達式是否被優化的整形量(0爲false,1爲true,標準規定)。最後,我們必須指定應用此過濾器的網絡掩碼。函數返回-1爲失敗,其他的任何值都表明是成功的。
表達式被編譯之後就可以使用了。現在進入pcap_setfilter()。仿照我們介紹pcap的格式,先來看一看pcap_setfilter()的原型:
   int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
這非常直觀,第一個參數是會話句柄,第二個參數是被編譯表達式版本的引用(可推測出它與pcap_compile()的第二個參數相同)。
下面的代碼示例可能能使你更好的理解:
   #include
   pcap_t *handle; /* 會話的句柄 */
   char dev[] = "eth0"; /* 執行嗅探的設備 */
   char errbuf[PCAP_ERRBUF_SIZE]; /* 存儲錯誤 信息的字符串 */
   struct bpf_program filter; /*已經編譯好的過濾表達式*/
   char filter_app[] = "port 23"; /* 過濾表達式*/
   bpf_u_int32 mask; /* 執行嗅探的設備的網絡掩碼 */
   bpf_u_int32 net; /* 執行嗅探的設備的IP地址 */
   pcap_lookupnet(dev, &net, &mask, errbuf);
   handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
   pcap_compile(handle, &filter, filter_app, 0, net);
   pcap_setfilter(handle, &filter);
這個程序使嗅探器嗅探經由端口23的所有通信,使用混雜模式,設備是eth0。
(4)實際的嗅探
有兩種手段捕捉包。我們可以一次只捕捉一個包,也可以進入一個循環,等捕捉到多個包再進行處理。我們將先看看怎樣去捕捉單個包,然後再看看使用循環的方法。爲此,我們使用函數pcap_next()。
pcap_next()的原型及其簡單:
   u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
第一個參數是會話句柄,第二個參數是指向一個包括了當前數據包總體信息(被捕捉時的時間,包的長度,其被指定的部分長度)的結構體的指針(在這裏只有一個片斷,只作爲一個示例)。pcap_next()返回一個u_char指針給被這個結構體描述的包。我們將稍後討論這種實際讀取包本身的手段。
   這裏有一個演示怎樣使用pcap_next()來嗅探一個包的例子:
   #include
   #include
   int main()
   {
   pcap_t *handle; /* 會話句柄 */
   char *dev; /* 執行嗅探的設備 */
   char errbuf[PCAP_ERRBUF_SIZE]; /* 存儲錯誤信息的字符串 */
  
   struct bpf_program filter; /* 已經編譯好的過濾器 */
   char filter_app[] = "port 23"; /* 過濾表達式 */
   bpf_u_int32 mask; /* 所在網絡的掩碼 */
   bpf_u_int32 net; /* 主機的IP地址 */
   struct pcap_pkthdr header; /* 由pcap.h定義 */
   const u_char *packet; /* 實際的包 */
   /* Define the device */
   dev = pcap_lookupdev(errbuf);
   /* 探查設備屬性 */
   pcap_lookupnet(dev, &net, &mask, errbuf);
   /* 以混雜模式打開會話 */
   handle = pcap_open_live(dev, BUFSIZ, 1, 0, errbuf);
   /* 編譯並應用過濾器 */
   pcap_compile(handle, &filter, filter_app, 0, net);
   pcap_setfilter(handle, &filter);
   /* 截獲一個包 */
   packet = pcap_next(handle, &header);
   /* 打印它的長度 */
   printf("Jacked a packet with length of [%d]
   ", header.len);
   /* 關閉會話 */
   pcap_close(handle);
   return(0);
   }
這個程序嗅探被pcap_lookupdev()返回的設備並將它置爲混雜模式。它發現第一個包經過端口23(telnet)並且告訴用戶此包的大小(以字 節爲單位)。這個程序又包含了一個新的調用pcap_close(),我們將在後面討論(儘管它的名字就足夠證實它自己的作用)。
實際上很少有嗅探程序會真正的使用pcap_next()。通常,它們使用pcap_loop()或者 pcap_dispatch()(它就是用了pcap_loop())。
pcap_loop()的原型如下:
   int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
第一個參數是會話句柄,接下來是一個整型,它告訴pcap_loop()在返回前應捕捉多少個數據包(若爲負值則表示應該一直工作直至錯誤發生)。第三個參數是回調函數的名稱(正像其標識符所指,無括號)。最後一個參數在有些應用裏有用,但更多時候則置爲NULL。假設我們有我們自己的想送往回調函數的參數,另外還有pcap_loop()發送的參數,這就需要用到它。很明顯,必須是一個u_char類型的指針以確保結果正確;正像我們稍後見到的,pcap使用了很有意思的方法以u_char指針的形勢傳遞信息。pcap_dispatch()的用法幾乎相同。唯一不同的是它們如何處理超時(還記得在調用pcap_open_live()時怎樣設置超時嗎?這就是它起作用的地方)。Pcap_loop()忽略超時而pcap_dispatch()則不。關於它們之間區別的更深入的討論請參見pcap的手冊頁。
回調函數的原型:
   void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
讓我們更細緻的考察它。首先,你會注重到該函數返回void類型,這是符合邏輯的,因爲pcap_loop()不知道如何去處理一個回調返回值。第一個參數相應於pcap_loop()的最後一個參數。每當回調函數被老婆 調用時,無論最後一個參數傳給pcap_loop()什麼值,這個值都會傳給我們回調函數的第一個參數。第二個參數是pcap頭文件定義的,它包括數據包被嗅探的時間、大小等信息。結構體pcap_pkhdr在pcap.h中定義如下:
   struct pcap_pkthdr {
   struct timeval ts; /* 時間戳 */
   bpf_u_int32 caplen; /* 已捕捉部分的長度 */
   bpf_u_int32 len; /* 該包的脫機長度 */
   };
這些量都相當明瞭。最後一個參數在它們中是最有意思的,也最讓pcap程序新手感到迷惑。這又是一個u_char指針,它包含了被pcap_loop()嗅探到的所有包。
但是你怎樣使用這個我們在原型裏稱爲packet的變量呢?一個數據包包含許多屬性,因此你可以想象它不只是一個字符串,而實質上是一個結構體的集合(比如,一個TCP/IP包會有一個以太網的頭部,一個IP頭部,一個TCP頭部,還有此包的有效載荷)。這個u_char就是這些結構體的串聯版本。爲了使用它,我們必須作一些有趣的匹配工作。
下面這些是一些數據包的結構體:
   /* 以太網幀頭部 */
   struct sniff_ethernet {
   u_char ether_dhost[ETHER_ADDR_LEN]; /* 目的主機的地址 */
   u_char ether_shost[ETHER_ADDR_LEN]; /* 源主機的地址 */
   u_short ether_type; /* IP? ARP? RARP? etc */
   };
   /* IP數據包的頭部 */
   struct sniff_ip {
   #if BYTE_ORDER == LITTLE_ENDIAN
   u_int ip_hl:4, /* 頭部長度 */
   ip_v:4; /* 版本號 */
   #if BYTE_ORDER == BIG_ENDIAN
   u_int ip_v:4, /* 版本號 */
   ip_hl:4; /* 頭部長度 */
   #endif
   #endif /* not _IP_VHL */
   u_char ip_tos; /* 服務的類型 */
   u_short ip_len; /* 總長度 */
   u_short ip_id; /*包標誌號 */
   u_short ip_off; /* 碎片偏移 */
   #define IP_RF 0x8000 /* 保留的碎片標誌 */
   #define IP_DF 0x4000 /* dont fragment flag */
   #define IP_MF 0x2000 /* 多碎片標誌*/
   #define IP_OFFMASK 0x1fff /*分段位 */
   u_char ip_ttl; /* 數據包的生存時間 */
   u_char ip_p; /* 所使用的協議 */
   u_short ip_sum; /* 校驗和 */
   struct in_addr ip_src,ip_dst; /* 源地址、目的地址*/
   };
   /* TCP 數據包的頭部 */
   struct sniff_tcp {
   u_short th_sport; /* 源端口 */
   u_short th_dport; /* 目的端口 */
   tcp_seq th_seq; /* 包序號 */
   tcp_seq th_ack; /* 確認序號 */
   #if BYTE_ORDER == LITTLE_ENDIAN
   u_int th_x2:4, /* 還沒有用到 */
   th_off:4; /* 數據偏移 */
   #endif
   #if BYTE_ORDER == BIG_ENDIAN
   u_int th_off:4, /* 數據偏移*/
   th_x2:4; /*還沒有用到 */
   #endif
   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_FINTH_SYNTH_RSTTH_ACKTH_URGTH_ECETH_CWR)
   u_short th_win; /* TCP滑動窗口 */
   u_short th_sum; /* 頭部校驗和 */
   u_short th_urp; /* 緊急服務位 */
   };
pcap嗅探數據包時正是使用的這些結構。接下來,它簡單的創建一個u_char字符串並且將這些結構體填入。那麼我們怎樣才能區分它們呢?預備好見證指針最實用的好處之一吧。
我們再一次假定要對以太網上的TCP/IP包進行處理。同樣的手段可以應用於任何數據包,唯一的區別是你實際所使用的結構體的類型。讓我們從聲明分解u_char包的變量開始:
   const struct sniff_ethernet *ethernet; /* 以太網幀頭部*/
   const struct sniff_ip *ip; /* IP包頭部 */
   const struct sniff_tcp *tcp; /* TCP包頭部 */
   const char *payload; /* 數據包的有效載荷*/
   /*爲了讓它的可讀性好,我們計算每個結構體中的變量大小*/
   int size_ethernet = sizeof(struct sniff_ethernet);
   int size_ip = sizeof(struct sniff_ip);
   int size_tcp = sizeof(struct sniff_tcp);
   現在我們開始讓人感到有些神祕的匹配:
   ethernet = (struct sniff_ethernet*)(packet);
   ip = (struct sniff_ip*)(packet + size_ethernet);
   tcp = (struct sniff_tcp*)(packet + size_ethernet + size_ip);
   payload = (u_char *)(packet + size_ethernet + size_ip + size_tcp);
  
此處如何工作?考慮u_char在內存中的層次。基本的,當pcap將這些結構體填入u_char的時候是將這些數據存入一個字符串中,那個字符串將被送入我們的回調函數中。反向轉換是這樣的,不考慮這些結構體制中的值,它們的大小將是一致的。例如在我的平臺上,一個sniff_ethernet結構體的大小是14字節。一個sniff_ip結構體是20字節,一個sniff_tcp結構體也是20字節。 u_char指針正是包含了內存地址的一個變量,這也是指針的實質,它指向內存的一個區域。簡單而言,我們說指針指向的地址爲x,假如三個結構體恰好線性排列,第一個(sniff_ethernet)被裝載到內存地址的x處則我們很輕易的發現其他結構體的地址,讓我們以表格顯示之:
   Variable Location (in bytes)
   sniff_ethernet X
   sniff_ip X + 14
   sniff_tcp X + 14 + 20
   payload X + 14 + 20 + 20
結構體sniff_ethernet正好在x處,緊接着它的sniff_ip則位於x加上它本身佔用的空間(此例爲14字節),依此類推可得全部地址。
注重:你沒有假定你的變量也是同樣大小是很重要的。你應該總是使用sizeof()來確保尺寸的正確。這是因爲這些結構體中的每個成員在不同平臺下可以有不同的尺寸。

下面是主要函數接口:
pcap_t *pcap_open_live(char *device, int snaplen,
   int promisc, int to_ms, char *ebuf)
   獲得用於捕獲網絡數據包的數據包捕獲描述字。device參數爲指定打開
   的網絡設備名。snaplen參數定義捕獲數據的最大字節數。promisc指定
   是否將網絡接口置於混雜模式。to_ms參數指定超時時間(毫秒)。
   ebuf參數則僅在pcap_open_live()函數出錯返回NULL時用於傳遞錯誤消
   息。
pcap_t *pcap_open_offline(char *fname, char *ebuf)
   打開以前保存捕獲數據包的文件,用於讀取。fname參數指定打開的文
   件名。該文件中的數據格式與tcpdump和tcpslice兼容。"-"爲標準輸
   入。ebuf參數則僅在pcap_open_offline()函數出錯返回NULL時用於傳
   遞錯誤消息。
pcap_dumper_t *pcap_dump_open(pcap_t *p, char *fname)
   打開用於保存捕獲數據包的文件,用於寫入。fname參數爲"-"時表示
   標準輸出。出錯時返回NULL。p參數爲調用pcap_open_offline()或
   pcap_open_live()函數後返回的pcap結構指針。fname參數指定打開
   的文件名。如果返回NULL,則可調用pcap_geterr()函數獲取錯誤消
   息。

char *pcap_lookupdev(char *errbuf)
   用於返回可被pcap_open_live()或pcap_lookupnet()函數調用的網絡
   設備名指針。如果函數出錯,則返回NULL,同時errbuf中存放相關的
   錯誤消息。
int pcap_lookupnet(char *device, bpf_u_int32 *netp,
   bpf_u_int32 *maskp, char *errbuf)
   獲得指定網絡設備的網絡號和掩碼。netp參數和maskp參數都是
   bpf_u_int32指針。如果函數出錯,則返回-1,同時errbuf中存放相
   關的錯誤消息。
int pcap_dispatch(pcap_t *p, int cnt,
   pcap_handler callback, u_char *user)
   捕獲並處理數據包。cnt參數指定函數返回前所處理數據包的最大值。
   cnt=-1表示在一個緩衝區中處理所有的數據包。cnt=0表示處理所有
   數據包,直到產生以下錯誤之一:讀取到EOF;超時讀取。callback
   參數指定一個帶有三個參數的回調函數,這三個參數爲:一個從
   pcap_dispatch()函數傳遞過來的u_char指針,一個pcap_pkthdr結構
   的指針,和一個數據包大小的u_char指針。如果成功則返回讀取到的
   字節數。讀取到EOF時則返回零值。出錯時則返回-1,此時可調用
   pcap_perror()或pcap_geterr()函數獲取錯誤消息。
int pcap_loop(pcap_t *p, int cnt,
   pcap_handler callback, u_char *user)
   功能基本與pcap_dispatch()函數相同,只不過此函數在cnt個數據包
   被處理或出現錯誤時才返回,但讀取超時不會返回。而如果爲
   pcap_open_live()函數指定了一個非零值的超時設置,然後調用
   pcap_dispatch()函數,則當超時發生時pcap_dispatch()函數會返回。
   cnt參數爲負值時pcap_loop()函數將始終循環運行,除非出現錯誤。
void pcap_dump(u_char *user, struct pcap_pkthdr *h,
   u_char *sp)
   向調用pcap_dump_open()函數打開的文件輸出一個數據包。該函數可
   作爲pcap_dispatch()函數的回調函數。
int pcap_compile(pcap_t *p, struct bpf_program *fp,
   char *str, int optimize, bpf_u_int32 netmask)
   將str參數指定的字符串編譯到過濾程序中。fp是一個bpf_program結
   構的指針,在pcap_compile()函數中被賦值。optimize參數控制結果
   代碼的優化。netmask參數指定本地網絡的網絡掩碼。
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
   指定一個過濾程序。fp參數是bpf_program結構指針,通常取自
   pcap_compile()函數調用。出錯時返回-1;成功時返回0。
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
   返回指向下一個數據包的u_char指針。
int pcap_datalink(pcap_t *p)
   返回數據鏈路層類型,例如DLT_EN10MB。
int pcap_snapshot(pcap_t *p)
   返回pcap_open_live被調用後的snapshot參數值。
int pcap_is_swapped(pcap_t *p)
   返回當前系統主機字節與被打開文件的字節順序是否不同。
int pcap_major_version(pcap_t *p)
   返回寫入被打開文件所使用的pcap函數的主版本號。
int pcap_minor_version(pcap_t *p)
   返回寫入被打開文件所使用的pcap函數的輔版本號。
int pcap_stats(pcap_t *p, struct pcap_stat *ps)
   向pcap_stat結構賦值。成功時返回0。這些數值包括了從開始
   捕獲數據以來至今共捕獲到的數據包統計。如果出錯或不支持
   數據包統計,則返回-1,且可調用pcap_perror()或
   pcap_geterr()函數來獲取錯誤消息。
FILE *pcap_file(pcap_t *p)
   返回被打開文件的文件名。
int pcap_fileno(pcap_t *p)
   返回被打開文件的文件描述字號碼。
void pcap_perror(pcap_t *p, char *prefix)
   在標準輸出設備上顯示最後一個pcap庫錯誤消息。以prefix參
   數指定的字符串爲消息頭。
char *pcap_geterr(pcap_t *p)
   返回最後一個pcap庫錯誤消息。
char *pcap_strerror(int error)
   如果strerror()函數不可用,則可調用pcap_strerror函數替代。
void pcap_close(pcap_t *p)
   關閉p參數相應的文件,並釋放資源。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章