libpcap入門教程
libpcap是一個開源的,用於捕捉網路包的庫。可以在大部分 *nix
系統下執行。另外, 還有一個windows版本的叫做winpcap。
包捕獲
包捕獲是收集網路上資料的過程。
首先看一下乙太網的包捕獲過程。當網絡卡收到一個乙太網資料幀的時候,網絡卡檢查目標的mac地址是否跟它的相等, 如果相等,它產生一箇中斷,網絡卡驅動處理這個中斷資訊。網絡卡驅動接入資料,並且把它複製到核心空間的一塊記憶體中。 然後它檢查 ethertype
欄位,來決定哪個協議棧處理。
當我們使用嗅探器的時候,上面的處理流程還是一樣的。但是,網絡卡驅動還把把複製一份傳送到包過濾器的核心模組。 然後包過濾器交給包捕獲的程式進行處理。如下圖
使用libpcap進行抓包
libpcap使用只需要遵循以下5步即可
1. 指定監聽的網口裝置
指定監聽的網口,可以明確進行指定,也可以通過pcap呼叫取得第一個可用的網路裝置。 明確指定網口,直接用網口的名字就行了。在linux下,可以通過 ifconfig
來檢視所有的網口。
pcap提供了以下的介面來獲取第一個可用的網口:
char *pcap_lookupdev(char *errbuf);
函式返回了第一個可用網口的名字。出錯時,錯誤資訊會通過errbuf返回。errbuf的空間必須提前分配好, 而且最小長度為 PCAP_ERRBUF_SIZE
(當前值為256)。
以下為一個例子:
#include<stdio.h> #include<pcap.h> int main(int argc, char *argv[]) { char *dev, errbuf[PCAP_ERRBUF_SIZE]; dev = pcap_lookupdev(errbuf); if (dev == NULL) { fprintf(stderr, "Couldn't find default device: %s\n", errbuf); return(2); } printf("Device: %s\n", dev); return(0); }
pcap提供了一個特定的網口,名字為 any
,指定所有可用的網口。
2. 開啟監聽的裝置
pcap提供開啟監聽裝置的介面也很簡單,如下:
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms, char *errbuf);
device引數是我們在第1步中指定的網口。
snaplen引數設定捕捉包的長度。在我們只希望檢視包頭的情況下,非常有用。預設的乙太網包長為1518位元組, 最大為65535。在 pcap.h
頭中定義了一個BUFSIZ。
promisc引數指定是否開啟混淆模式。關閉混淆模式,則只捕捉進入本機或者在本機路由轉發的包。 開啟混淆模式會捕捉網路上所有的包。另外,這個配置還受網絡卡的混淆模式影響。如果網絡卡設定了關閉混淆模式, 則這裡即使開啟也沒有用。
to_ms引數指定資料從核心態複製到使用者態等待的時間。由於從核心態切換到使用者態,需要比較大的效能消耗。 越低的值,效能消耗越大。如果是0,則會一直等待到有足夠的資料,才能複製到使用者態。tcpdump使用了1000。
errbuf引數跟第1步的errbuf一樣,用於出錯時,儲存錯誤資訊。
返回引數為後續需要使用到的session。
例子:
#include<pcap.h> int main { pcap_t *handle; handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf); return(2); } }
3. 設定過濾條件
設定過濾條件需要分三步:
- 找出當前網口的掩碼。
- 編譯過濾條件。
- 設定到上一步返回的session中。
找出當前網口的掩碼
pcap_lookupnet
原型如下:
int pcap_lookupnet(const char *device, bpf_u_int32 *net, bpf_u_int32 *mask, char *errbuf);
device引數為第1步中指定的網口。
net引數為返回網路碼。
mask引數返回掩碼。
errbuf為出錯時的錯誤資訊。
函數出錯時返回-1。
編譯過濾條件
編譯過濾條件的函式原型如下:
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask);
p引數是第2步的返回值。
fp引數是過濾條件編譯出來的結果。我們不太需要關心其具體的結構內容,只需要把它傳給下一步的呼叫即可。
str引數為我們寫的過濾條件表示式,比如 port 23
之類的。更詳細的過濾條件,可以參考pcap-filter手冊
optimize引數指出表示式是否需要優化。
netmask引數指定網路的掩碼。我們可以通過 pcap_lookupnet
來找出對應的掩碼。
錯誤時返回-1,成功時返回其他值。
設定過濾條件
設定過濾條件的原型如下:
int pcap_setfilter(pcap_t *p, struct bpf_program *fp);
p引數為第2步返回的值。
fp引數為編譯出來的過濾條件。
函式錯誤時返回-1,成功時返回其他值。
例子:
#include<pcap.h> int main() { pcap_t *handle;/* Session handle */ char dev[] = "rl0";/* Device to sniff on */ char errbuf[PCAP_ERRBUF_SIZE];/* Error string */ struct bpf_programfp;/* The compiled filter expression */ char filter_exp[] = "port 23";/* The filter expression */ bpf_u_int32 mask;/* The netmask of our sniffing device */ bpf_u_int32 net;/* The IP of our sniffing device */ if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) { fprintf(stderr, "Can't get netmask for device %s\n", dev); net = 0; mask = 0; } handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf); if (handle == NULL) { fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf); return(2); } if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) { fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } }
4. 捕捉包
通過以上的初始化工作後,就可以真正的開始捉包了。捉包有三種方式, pcap_next
, pcap_loop
, pcap_dispatch
。
pcap_next
pcap_next
一次只抓取一個包。 原型如下:
struct pcap_pkthdr{ struct timevalts; /* time stamp */ bpf_u_int32 caplen; /* length of portion present */ bpf_u_int32 len; /* length this packet (off wire) */ }; u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h);
p引數為第2步中的返回值。
h引數為傳出引數。
返回值為捕捉的包內容。
例子:
#include<pcap.h> #include<stdio.h> int main(int argc, char *argv[]) { pcap_t *handle;/* session handle */ char *dev;/* the device to sniff on */ char errbuf[pcap_errbuf_size];/* error string */ struct bpf_programfp;/* the compiled filter */ char filter_exp[] = "port 23";/* the filter expression */ bpf_u_int32 mask;/* our netmask */ bpf_u_int32 net;/* our ip */ struct pcap_pkthdrheader;/* the header that pcap gives us */ const u_char *packet;/* the actual packet */ /* define the device */ dev = pcap_lookupdev(errbuf); if (dev == null) { fprintf(stderr, "couldn't find default device: %s\n", errbuf); return(2); } /* find the properties for the device */ if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) { fprintf(stderr, "couldn't get netmask for device %s: %s\n", dev, errbuf); net = 0; mask = 0; } /* open the session in promiscuous mode */ handle = pcap_open_live(dev, bufsiz, 1, 1000, errbuf); if (handle == null) { fprintf(stderr, "couldn't open device %s: %s\n", dev, errbuf); return(2); } /* compile and apply the filter */ if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) { fprintf(stderr, "couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } if (pcap_setfilter(handle, &fp) == -1) { fprintf(stderr, "couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle)); return(2); } /* grab a packet */ packet = pcap_next(handle, &header); /* print its length */ printf("jacked a packet with length of [%d]\n", header.len); /* and close the session */ pcap_close(handle); return(0); }
pcap_loop
pcap_loop
是更加經常用到捉包函式。它使用一個回撥函式,當抓到包時,就呼叫加調函式進行處理。 然後繼續捉包,如果迴圈。它的處理流程如下圖:
pcap_loop
的函式原型如下:
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user);
p引數為第2步的返回值。
cnt引數表示要捕捉的包數,負數表示不限制。
callback引數為處理的回撥。回撥原型如下:
void got_packet(u_char *user,const struct pcap_pkthdr *header, const u_char *packet);
其中user引數跟 pcap_loop
中的user引數一致。header引數跟 pcap_next
的h引數一樣。packet引數為抓到的包。
user引數用於做透傳,為了把傳到回撥函式中使用,可以通過此方式值進行。
那麼我們主要的工作就在回撥函式上了,在回撥函式上,我們需要解析出鏈路層、網路層、傳輸層、應用層相關的資料。 最後,在應用層取出payload資料後,進行我們的業務協議解析。
pcap_dispatch
pcap_dispatch
與 pcap_loop
類似,它一直返回耗時。
5. 關閉pcap
關閉pcap原型如下:
void pcap_close(pcap_t *handler);
把第2步的返回值傳進行進行關閉即可。