libpcap使用
libpcap是一個網絡數據包捕獲函數庫,功能非常強大,Linux下著名的tcpdump就是以它爲基礎的。今天我們利用它來完成一個我們自己的網絡嗅探器(sniffer)
首先先介紹一下本次實驗的環境:
Ubuntu 11.04,IP:192.168.1.1,廣播地址:192.168.1.255,子網掩碼:255.255.255.0
可以使用下面的命令設置:
sudo ifconfig eth0 192.168.1.1 broadcast 192.168.1.255 netmask 255.255.255.0
1.安裝
在http://www.tcpdump.org/下載libpcap(tcpdump的源碼也可以從這個網站下載)
解壓
./configure
make
sudo make install
2.使用
安裝好libpcap後,我們要使用它啦,先寫一個簡單的程序,並介紹如何使用libpcap庫編譯它:
Makefile:
-
all: test.c
-
gcc -g -Wall -o test test.c -lpcap
-
-
clean:
-
rm -rf *.o test
其後的程序的Makefile均類似,故不再重複
test1.c
-
#include <pcap.h>
-
#include <stdio.h>
-
-
int main()
-
{
-
char errBuf[PCAP_ERRBUF_SIZE], * device;
-
-
device = pcap_lookupdev(errBuf);
-
-
if(device)
-
{
-
printf("success: device: %s\n", device);
-
}
-
else
-
{
-
printf("error: %s\n", errBuf);
-
}
-
-
return 0;
-
}
可以成功編譯,不過運行的時候卻提示找不到libpcap.so.1,因爲libpcap.so.1默認安裝到了/usr/local/lib下,我們做一個符號鏈接到/usr/lib/下即可:
運行test的時候輸出"no suitable device found",原因是我們沒有以root權限運行,root權限運行後就正常了:
下面開始正式講解如何使用libpcap:
首先要使用libpcap,我們必須包含pcap.h頭文件,可以在/usr/local/include/pcap/pcap.h找到,其中包含了每個類型定義的詳細說明。
1.獲取網絡接口
首先我們需要獲取監聽的網絡接口:
我們可以手動指定或讓libpcap自動選擇,先介紹如何讓libpcap自動選擇:
char * pcap_lookupdev(char * errbuf)
上面這個函數返回第一個合適的網絡接口的字符串指針,如果出錯,則errbuf存放出錯信息字符串,errbuf至少應該是PCAP_ERRBUF_SIZE個字節長度的。注意,很多libpcap函數都有這個參數。
pcap_lookupdev()一般可以在跨平臺的,且各個平臺上的網絡接口名稱都不相同的情況下使用。
如果我們手動指定要監聽的網絡接口,則這一步跳過,我們在第二步中將要監聽的網絡接口字符串硬編碼在pcap_open_live裏。
2.釋放網絡接口
在操作爲網絡接口後,我們應該要釋放它:
void pcap_close(pcap_t * p)
該函數用於關閉pcap_open_live()獲取的pcap_t的網絡接口對象並釋放相關資源。
3.打開網絡接口
獲取網絡接口後,我們需要打開它:
pcap_t * pcap_open_live(const char * device, int snaplen, int promisc, int to_ms, char * errbuf)
上面這個函數會返回指定接口的pcap_t類型指針,後面的所有操作都要使用這個指針。
第一個參數是第一步獲取的網絡接口字符串,可以直接使用硬編碼。
第二個參數是對於每個數據包,從開頭要抓多少個字節,我們可以設置這個值來只抓每個數據包的頭部,而不關心具體的內容。典型的以太網幀長度是1518字節,但其他的某些協議的數據包會更長一點,但任何一個協議的一個數據包長度都必然小於65535個字節。
第三個參數指定是否打開混雜模式(Promiscuous Mode),0表示非混雜模式,任何其他值表示混合模式。如果要打開混雜模式,那麼網卡必須也要打開混雜模式,可以使用如下的命令打開eth0混雜模式:
ifconfig eth0 promisc
第四個參數指定需要等待的毫秒數,超過這個數值後,第3步獲取數據包的這幾個函數就會立即返回。0表示一直等待直到有數據包到來。
第五個參數是存放出錯信息的數組。
4.獲取數據包
打開網絡接口後就已經開始監聽了,那如何知道收到了數據包呢?有下面3種方法:
a)
u_char * pcap_next(pcap_t * p, struct pcap_pkthdr * h)
如果返回值爲NULL,表示沒有抓到包
第一個參數是第2步返回的pcap_t類型的指針
第二個參數是保存收到的第一個數據包的pcap_pkthdr類型的指針
pcap_pkthdr類型的定義如下:
-
struct pcap_pkthdr
-
{
-
struct timeval ts;
-
bpf_u_int32 caplen;
-
bpf_u_int32 len;
-
};
注意這個函數只要收到一個數據包後就會立即返回
b)
int pcap_loop(pcap_t * p, int cnt, pcap_handler callback, u_char * user)
第一個參數是第2步返回的pcap_t類型的指針
第二個參數是需要抓的數據包的個數,一旦抓到了cnt個數據包,pcap_loop立即返回。負數的cnt表示pcap_loop永遠循環抓包,直到出現錯誤。
第三個參數是一個回調函數指針,它必須是如下的形式:
void callback(u_char * userarg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
第一個參數是pcap_loop的最後一個參數,當收到足夠數量的包後pcap_loop會調用callback回調函數,同時將pcap_loop()的user參數傳遞給它
第二個參數是收到的數據包的pcap_pkthdr類型的指針
第三個參數是收到的數據包數據
c)
int pcap_dispatch(pcap_t * p, int cnt, pcap_handler callback, u_char * user)
這個函數和pcap_loop()非常類似,只是在超過to_ms毫秒後就會返回(to_ms是pcap_open_live()的第4個參數)
例子:
test2:
-
#include <pcap.h>
-
#include <time.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
-
int main()
-
{
-
char errBuf[PCAP_ERRBUF_SIZE], * devStr;
-
-
-
devStr = pcap_lookupdev(errBuf);
-
-
if(devStr)
-
{
-
printf("success: device: %s\n", devStr);
-
}
-
else
-
{
-
printf("error: %s\n", errBuf);
-
exit(1);
-
}
-
-
-
pcap_t * device = pcap_open_live(devStr, 65535, 1, 0, errBuf);
-
-
if(!device)
-
{
-
printf("error: pcap_open_live(): %s\n", errBuf);
-
exit(1);
-
}
-
-
-
struct pcap_pkthdr packet;
-
const u_char * pktStr = pcap_next(device, &packet);
-
-
if(!pktStr)
-
{
-
printf("did not capture a packet!\n");
-
exit(1);
-
}
-
-
printf("Packet length: %d\n", packet.len);
-
printf("Number of bytes: %d\n", packet.caplen);
-
printf("Recieved time: %s\n", ctime((const time_t *)&packet.ts.tv_sec));
-
-
pcap_close(device);
-
-
return 0;
-
}
打開兩個終端,先ping 192.168.1.10,由於我們的ip是192.168.1.1,因此我們可以收到廣播的數據包,另一個終端運行test,就會抓到這個包。
下面的這個程序會把收到的數據包內容全部打印出來,運行方式和上一個程序一樣:
test3:
-
#include <pcap.h>
-
#include <time.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
-
void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
-
{
-
int * id = (int *)arg;
-
-
printf("id: %d\n", ++(*id));
-
printf("Packet length: %d\n", pkthdr->len);
-
printf("Number of bytes: %d\n", pkthdr->caplen);
-
printf("Recieved time: %s", ctime((const time_t *)&pkthdr->ts.tv_sec));
-
-
int i;
-
for(i=0; i<pkthdr->len; ++i)
-
{
-
printf(" %02x", packet[i]);
-
if( (i + 1) % 16 == 0 )
-
{
-
printf("\n");
-
}
-
}
-
-
printf("\n\n");
-
}
-
-
int main()
-
{
-
char errBuf[PCAP_ERRBUF_SIZE], * devStr;
-
-
-
devStr = pcap_lookupdev(errBuf);
-
-
if(devStr)
-
{
-
printf("success: device: %s\n", devStr);
-
}
-
else
-
{
-
printf("error: %s\n", errBuf);
-
exit(1);
-
}
-
-
-
pcap_t * device = pcap_open_live(devStr, 65535, 1, 0, errBuf);
-
-
if(!device)
-
{
-
printf("error: pcap_open_live(): %s\n", errBuf);
-
exit(1);
-
}
-
-
-
int id = 0;
-
pcap_loop(device, -1, getPacket, (u_char*)&id);
-
-
pcap_close(device);
-
-
return 0;
-
}
從上圖可以看出,如果我們沒有按Ctrl+c,test會一直抓到包,因爲我們將pcap_loop()設置爲永遠循環
由於ping屬於icmp協議,並且發出icmp協議數據包之前必須先通過arp協議獲取目的主機的mac地址,因此我們抓到的包是arp協議的,而arp協議的數據包長度正好是42字節(14字節的以太網幀頭+28字節的arp數據)。具體內容請參考相關網絡協議說明。
5.分析數據包
我們既然已經抓到數據包了,那麼我們要開始分析了,這部分留給讀者自己完成,具體內容可以參考相關的網絡協議說明。在本文的最後,我會示範性的寫一個分析arp協議的sniffer,僅供參考。要特別注意一點,網絡上的數據是網絡字節順序的,因此分析前需要轉換爲主機字節順序(ntohs()函數)。
6.過濾數據包
我們抓到的數據包往往很多,如何過濾掉我們不感興趣的數據包呢?
幾乎所有的操作系統(BSD, AIX, Mac OS, Linux等)都會在內核中提供過濾數據包的方法,主要都是基於BSD Packet Filter(BPF)結構的。libpcap利用BPF來過濾數據包。
過濾數據包需要完成3件事:
a) 構造一個過濾表達式
b) 編譯這個表達式
c) 應用這個過濾器
a)
BPF使用一種類似於彙編語言的語法書寫過濾表達式,不過libpcap和tcpdump都把它封裝成更高級且更容易的語法了,具體可以man tcpdump,以下是一些例子:
src host 192.168.1.177
只接收源ip地址是192.168.1.177的數據包
dst port 80
只接收tcp/udp的目的端口是80的數據包
not tcp
只接收不使用tcp協議的數據包
tcp[13] == 0x02 and (dst port 22 or dst port 23)
只接收SYN標誌位置位且目標端口是22或23的數據包(tcp首部開始的第13個字節)
icmp[icmptype] == icmp-echoreply or icmp[icmptype] == icmp-echo
只接收icmp的ping請求和ping響應的數據包
ehter dst 00:e0:09:c1:0e:82
只接收以太網mac地址是00:e0:09:c1:0e:82的數據包
ip[8] == 5
只接收ip的ttl=5的數據包(ip首部開始的第8個字節)
b)
構造完過濾表達式後,我們需要編譯它,使用如下函數:
int pcap_compile(pcap_t * p, struct bpf_program * fp, char * str, int optimize, bpf_u_int32 netmask)
fp:這是一個傳出參數,存放編譯後的bpf
str:過濾表達式
optimize:是否需要優化過濾表達式
metmask:簡單設置爲0即可
c)
最後我們需要應用這個過濾表達式:
int pcap_setfilter(pcap_t * p, struct bpf_program * fp)
第二個參數fp就是前一步pcap_compile()的第二個參數
應用完過濾表達式之後我們便可以使用pcap_loop()或pcap_next()等抓包函數來抓包了。
下面的程序演示瞭如何過濾數據包,我們只接收目的端口是80的數據包:
test4.c
-
#include <pcap.h>
-
#include <time.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
-
void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
-
{
-
int * id = (int *)arg;
-
-
printf("id: %d\n", ++(*id));
-
printf("Packet length: %d\n", pkthdr->len);
-
printf("Number of bytes: %d\n", pkthdr->caplen);
-
printf("Recieved time: %s", ctime((const time_t *)&pkthdr->ts.tv_sec));
-
-
int i;
-
for(i=0; i<pkthdr->len; ++i)
-
{
-
printf(" %02x", packet[i]);
-
if( (i + 1) % 16 == 0 )
-
{
-
printf("\n");
-
}
-
}
-
-
printf("\n\n");
-
}
-
-
int main()
-
{
-
char errBuf[PCAP_ERRBUF_SIZE], * devStr;
-
-
-
devStr = pcap_lookupdev(errBuf);
-
-
if(devStr)
-
{
-
printf("success: device: %s\n", devStr);
-
}
-
else
-
{
-
printf("error: %s\n", errBuf);
-
exit(1);
-
}
-
-
-
pcap_t * device = pcap_open_live(devStr, 65535, 1, 0, errBuf);
-
-
if(!device)
-
{
-
printf("error: pcap_open_live(): %s\n", errBuf);
-
exit(1);
-
}
-
-
-
struct bpf_program filter;
-
pcap_compile(device, &filter, "dst port 80", 1, 0);
-
pcap_setfilter(device, &filter);
-
-
-
int id = 0;
-
pcap_loop(device, -1, getPacket, (u_char*)&id);
-
-
pcap_close(device);
-
-
return 0;
-
}
在下面的這一個例子中,客戶機通過tcp的9732端口連接服務器,發送字符'A',之後服務器將'A'+1即'B'返回給客戶機,具體實現可以參考:http://blog.csdn.net/htttw/article/details/7519964
服務器的ip是192.168.56.101,客戶機的ip是192.168.56.1
服務器:
Makefile:
-
all: tcp_client.c tcp_server.c
-
gcc -g -Wall -o tcp_client tcp_client.c
-
gcc -g -Wall -o tcp_server tcp_server.c
-
-
clean:
-
rm -rf *.o tcp_client tcp_server
tcp_server:
-
#include <sys/types.h>
-
#include <sys/socket.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#include <unistd.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
-
#define PORT 9832
-
#define SERVER_IP "192.168.56.101"
-
-
int main()
-
{
-
-
int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
-
struct sockaddr_in server_addr;
-
server_addr.sin_family = AF_INET;
-
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
-
server_addr.sin_port = htons(PORT);
-
-
-
bind(server_sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
-
-
-
listen(server_sockfd, 5);
-
-
char ch;
-
int client_sockfd;
-
struct sockaddr_in client_addr;
-
socklen_t len = sizeof(client_addr);
-
while(1)
-
{
-
printf("server waiting:\n");
-
-
-
client_sockfd = accept(server_sockfd, (struct sockaddr *)&client_addr, &len);
-
-
-
read(client_sockfd, &ch, 1);
-
printf("get char from client: %c\n", ch);
-
++ch;
-
write(client_sockfd, &ch, 1);
-
-
-
close(client_sockfd);
-
}
-
-
return 0;
-
}
tcp_client:
-
#include <sys/types.h>
-
#include <sys/socket.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#include <unistd.h>
-
#include <stdlib.h>
-
#include <stdio.h>
-
-
#define PORT 9832
-
#define SERVER_IP "192.168.56.101"
-
-
int main()
-
{
-
-
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
-
-
struct sockaddr_in address;
-
address.sin_family = AF_INET;
-
address.sin_addr.s_addr = inet_addr(SERVER_IP);
-
address.sin_port = htons(PORT);
-
-
-
int result = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
-
if(result == -1)
-
{
-
perror("connect failed: ");
-
exit(1);
-
}
-
-
-
char ch = 'A';
-
write(sockfd, &ch, 1);
-
read(sockfd, &ch, 1);
-
printf("get char from server: %c\n", ch);
-
-
-
close(sockfd);
-
-
return 0;
-
}
運行方法如下,首先在服務器上運行tcp_server,然後運行我們的監聽器,然後在客戶機上運行tcp_client,注意,我們可以先清空arp緩存,這樣就可以看到整個通信過程(包括一開始的arp廣播)
在客戶機上運行下列命令來清空記錄服務器的arp緩存:
sudo arp -d 192.168.56.101
arp -a後發現已經刪除了記錄服務器的arp緩存
抓包的結果如下所示,由於包太多了,無法全部截圖,因此我把所有內容保存在下面的文本中了:
全部的包如下:
仔細研究即可發現服務器與客戶機是如何通過tcp通信的。
下面的這個程序可以獲取eth0的ip和子網掩碼等信息:
test5:
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <pcap.h>
-
#include <errno.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
-
int main()
-
{
-
-
char * dev;
-
char errbuf[PCAP_ERRBUF_SIZE];
-
dev = pcap_lookupdev(errbuf);
-
-
-
if(!dev)
-
{
-
printf("pcap_lookupdev() error: %s\n", errbuf);
-
exit(1);
-
}
-
-
-
printf("dev name: %s\n", dev);
-
-
-
bpf_u_int32 netp;
-
bpf_u_int32 maskp;
-
int ret;
-
ret = pcap_lookupnet(dev, &netp, &maskp, errbuf);
-
-
if(ret == -1)
-
{
-
printf("pcap_lookupnet() error: %s\n", errbuf);
-
exit(1);
-
}
-
-
-
char * net;
-
char * mask;
-
struct in_addr addr;
-
-
addr.s_addr = netp;
-
net = inet_ntoa(addr);
-
-
if(!net)
-
{
-
perror("inet_ntoa() ip error: ");
-
exit(1);
-
}
-
-
printf("ip: %s\n", net);
-
-
-
addr.s_addr = maskp;
-
mask = inet_ntoa(addr);
-
-
if(!mask)
-
{
-
perror("inet_ntoa() sub mask error: ");
-
exit(1);
-
}
-
-
printf("sub mask: %s\n", mask);
-
-
return 0;
-
}
結果如圖:
int pcap_lookupnet(const char * device, bpf_u_int32 * netp, bpf_u_int32 * maskp, char * errbuf)
可以獲取指定設備的ip地址,子網掩碼等信息
netp:傳出參數,指定網絡接口的ip地址
maskp:傳出參數,指定網絡接口的子網掩碼
pcap_lookupnet()失敗返回-1
我們使用inet_ntoa()將其轉換爲可讀的點分十進制形式的字符串
本文的絕大部分來源於libpcap的官方文檔:libpcapHakin9LuisMartinGarcia.pdf,可以在官網下載,文檔只有9頁,不過很詳細,還包括了數據鏈路層,網絡層,傳輸層,應用層等的分析。很好!
更多參考可以man pcap
最後爲了方便大家,本文的所有代碼和上述的pdf文檔都一併上傳上來了:
http://download.csdn.net/detail/htttw/4264686