Pcap程式設計(轉)

post0發表於2007-08-11
Pcap程式設計(轉)[@more@]

By 阿美

Tim Carstens

此文的最近更新見於

好,讓我們從看看這篇文章寫給誰開始。顯而易見的,需要一些C語言基礎知識,除非你只想瞭解基本的理論。你不必是一個編碼專家,因為這個領域只有經驗豐富的程式設計師涉足,而我將盡可能詳細的描述這些概念。另外,考慮到這是有關一個包嗅探器的,所以對網路基礎知識的理解是有幫助的。所有在此出現的程式碼示例都已在FreeBSD 4.3平臺上測試透過。

開始:pcap應用程式的格式

我們所要理解的第一件事情是一個基於pcap的嗅探器程式的總體佈局。流程如下:

1.我們從決定用哪一個介面進行嗅探開始。在Linux中,這可能是eth0,而在BSD系統中則可能是xl1等等。我們也可以用一個字串來定義這個裝置,或者採用pcap提供的介面名來工作。

2.初始化pcap。在這裡我們要告訴pcap對什麼裝置進行嗅探。假如願意的話,我們還可以嗅探多個裝置。怎樣區分它們呢?使用 檔案控制程式碼。就像開啟一個檔案進行讀寫一樣,必須命名我們的嗅探“會話”,以此使它們各自區別開來。

3.如果我們只想嗅探特定的傳輸(如TCP/IP包,發往埠23的包等等),我們必須建立一個規則集合,編譯並且使用它。這個過程分為三個相互緊密關聯的階段。規則集合被置於一個字串內,並且被轉換成能被pcap讀的格式(因此編譯它)。編譯實際上就是在我們的程式裡呼叫一個不被外部程式使用的函式。接下來我們要告訴 pcap使用它來過濾出我們想要的那一個會話。

4.最後,我們告訴pcap進入它的主體執行迴圈。在這個階段內pcap一直工作到它接收了所有我們想要的包為止。每當它收到一個包就呼叫另一個已經定義好的函式,這個函式可以做我們想要的任何工作,它可以剖析所部獲的包並給使用者列印出結果,它可以將結果儲存為一個檔案,或者什麼也不作。

5.在嗅探到所需的資料後,我們要關閉會話並結束。

這是實際上一個很簡單的過程。一共五個步驟,其中一個(第3個)是可選的。我們為什麼不看一看是怎樣實現每一個步驟呢?

設定裝置

這是很簡單的。有兩種方法設定想要嗅探的裝置。

第一種,我們可以簡單的讓使用者告訴我們。考察下面的程式:

#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);

}

在這個例子裡,pcap就自己設定裝置。“但是,等一下,Tim”,你會說,“字串errbuf是做什麼的?”大多數的pcap命令允許我們向它們傳遞字串作為引數。這個字串的目的是什麼呢?如果命令失敗,它將傳給這個字串關於錯誤的描述。這樣,如果pcap_lookupdev()失敗,它將在 errbuf儲存錯誤資訊。很好,是不是?這就是我們怎樣去設定裝置。

開啟裝置進行嗅探

建立一個嗅探會話的任務真的非常簡單。為此,我們使用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)。此函式返回其會話控制程式碼。

舉個例子,考察以下程式碼片斷:

#include

...

pcap_t *handle;

handle = pcap_open_live(somedev, BUFSIZ, 1, 0, errbuf);

這個程式碼片斷開啟字串somedev的裝置,告訴它讀取被BUFSIZ指定的位元組數(BUFSIZ在pcap.h裡定義)。我們告訴它將裝置置為混雜模式,一直嗅探到錯誤發生,如果有了錯誤,把它存放在字串errbuf中。

混雜模式與非混雜模式的區別:這兩種方式區別很大。一般來說,非混雜模式的嗅探器中,主機僅嗅探那些跟它直接有關的通訊,如發向它的,從它發出的,或經它路由的等都會被嗅探器捕獲。而在混雜模式中則嗅探傳輸線路上的所有通訊。在非交換式網路中,這將是整個網路的通訊。這樣做最明顯的優點就是使更多的包被嗅探到,它們因你嗅探網路的原因或者對你有幫助,或者沒有。但是,混雜模式是可被探測到的。一個主機可以透過高強度的測試判定另一臺主機是否正在進行混雜模式的嗅探。其次,它僅在非交換式的網路環境中有效工作(如集線器,或者交換中的ARP層面)。再次,在高負荷的網路中,主機的系統資源將消耗的非常嚴重。

過濾通訊

通常,我們的嗅探器僅對某特定的通訊感興趣。例如,有時我們想嗅探到埠23(telnet)的包以獲得密碼;或者我們想截獲一個正透過埠21 (FTP)傳送的檔案;可能我們僅想要得到DNS的通訊(埠53,UDP)。無論哪種情況,我們都很少盲目的嗅探整個網路的通訊。下面討論pcap_compile()與pcap_setfilter()。

這個過程非常簡單。當我們已經呼叫了pcap_open_live()從而建立了一個嗅探會話之後就可以應用我們自己的過濾器了。為什麼要用我們自己的過濾器呢?有兩個原因。第一,pcap的過濾器太強大了,因為它直接使用 BPF過濾器,我們透過使用BPF驅動直接過濾跳過了很多的關節。第二,這樣做要容易的多。

在使用我們自己的過濾器前必須編譯它。過濾表示式被儲存在一個字串中(字元陣列)。其句法在tcpdump的手冊頁中被證明非常好。我建議你親自閱讀它。但是我們將使用簡單的測試表示式,這樣你可能很容易理解我的例子。

我們呼叫pcap_compile()來編譯它,其原型是這樣定義的:

int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)

第 一個引數是會話控制程式碼(pcap_t *handle在前一節的示例中)。接下來的是我們儲存被編譯的過濾器版本的地址的引用。再接下來的則是表示式本身,儲存在規定的字串格式裡。再下邊是一個定義表示式是否被最佳化的整形量(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[] = "rl0"; /* 執行嗅探的裝置 */

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的所有通訊,使用混雜模式,裝置是rl0。

你可能注意到前面的示例包含一個我們還沒提到的函式:pcap_lookupnet(),向這個函式提供裝置介面名,它將返回其IP和網路掩碼,這是很基本的,因為我們需要知道網路掩碼以便應用過濾器。此函式在此文最後的miscellaneous一節裡還有描述。

據我的經驗,這個過濾器在所有的作業系統下都不會工作。在我的測試環境裡,我發現OpenBSD 2.9預設核心支援這種過濾器,但FreeBSD 4.3預設核心則不支援。你的情況可能會有變化。

實際的嗅探

到此為止,我們已經學習瞭如何定義一個裝置,讓它準備嗅探,還有應用過濾器使我們嗅談到什麼或者不嗅探到什麼。現在到了真正去捕獲一些資料包的時候了。有兩種手段捕獲包。我們可以一次只捕獲一個包,也可以進入一個迴圈,等捕獲到多個包再進行處理。我們將先看看怎樣去捕獲單個包,然後再看看使用迴圈的方法。為此,我們使用函式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())。為了理解這兩個函式的用法,你必須理解回撥函式的思想。

回撥函式並不是什麼新東西,它在許多API裡面非常普遍。回撥函式的概念極其簡單。設想我有一個程式正等待某種排序的事件。為了達到這個例子的目的,讓我們假象我的程式想讓使用者在鍵盤上按下一個鍵,每當他們按下了一個鍵,我就想呼叫一個作相應處理的函式。我所用的函式就是一個回撥函式。使用者每按一個鍵一次,我的程式就呼叫回撥函式一次。回撥函式在應用在pcap裡,取代當使用者按下鍵時被呼叫的函式的是當pcap嗅探到一個資料包時所呼叫的函式。可以定義它們的回撥函式的兩個函式就是pcap_loop()和pcap_dispatch()。此二者在它們的回撥函式的使用上非常的相似。它們都是每當捕獲到一個符合我們過濾器的包時呼叫器回撥函式(當然是存在一個過濾器時,如果不存在則所有被嗅探到的包都被送到會調函式處理)。

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是怎樣做的例子之後就很容易去做了。若是還不行就參考你的本地的C引用文字,作為一個指標的解釋那就超出了本文的範圍。 Pcap_dispatch()的用法幾乎相同。唯一不同的是它們如何處理超時(還記得在呼叫pcap_open_live()時怎樣設定超時嗎?這就是它起作用的地方)。Pcap_loop()忽略超時而pcap_dispatch()則不。關於它們之間區別的更深入的討論請參見pcap的手冊頁。

在提供使用pcap_loop()的示例之前,我們必須檢查我們的回撥函式的格式。我們不能武斷的定義回撥函式的原型,否則pcap_loop()將會不知道如何去使用它。因此我們使用這樣的格式作為我們的回撥函式的原型:

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就是這些結構體的串聯版本。為了使用它,我們必須作一些有趣的匹配工作。

首先,在匹配它們之前必須定義這些實際的結構體。下面就是我用來描述一個透過乙太網的TCP/IP包的結構體的定義。我使用的所有這些定義都是直接從POSIX庫中提取的。通常,我只簡單的使用那些庫中的定義即可,但據我的經驗不同平臺的庫之間有輕微的差別,這使得它實現起來變得混亂。因此,為達到示例的目的,我就避免那些混亂而簡單的複製這些有關的結構體。所有這些都能在你的本地unix系統中的include/netinet中找到。下面就是這些結構體:

/* 乙太網幀頭部 */

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_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)

u_short th_win; /* TCP滑動視窗 */

u_short th_sum; /* 頭部校驗和 */

u_short th_urp; /* 緊急服務位 */

};

注:在Slackware Linux 8(核心版本2.2.19)上我發現使用以上結構體的程式碼將不能透過編譯。後來證明問題在於include/fearures.h,它只實現了一個 POSIX介面,除非定義BSD_SOURCE。如果它沒有被定義,我就只能使用一個不同的結構體去定義TCP頭部。使它們工作在FreeBSD或 OpenBSD系統上的更為通用的解決方法如下:

#define _BSD_SOURCE 1

事先要包含你自己的所有標頭檔案。這將確保正常使用BSD風格的API。如果不想這樣做,那你可以改變TCP頭結構(點此連結即可,內含註釋)。

那麼所有這些與pcap還有神秘的u_char是怎麼關聯的呢?看,幸運的是pcap嗅探資料包時正是使用的這些結構。接下來,它簡單的建立一個 u_char字串並且將這些結構體填入。那麼我們怎樣才能區分它們呢?準備好見證指標最實用的好處之一吧(在此,我可要刺激刺激那些堅持說指標無用的C 程式新手了)。

我們再一次假定要對乙太網上的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的包嗅探器了。你已經學習了基本的概念:開啟一個pcap會話,有關它的全體屬性,嗅探資料包,使用過濾器,使用回撥函式,等等。現在是進行資料包嗅探的時候了。

作者Blog:http://blog.csdn.net/plowboy/

相關文章Programming with pcap

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/8225414/viewspace-944770/,如需轉載,請註明出處,否則將追究法律責任。

相關文章