windows下手把手教你捕獲資料包
希望通過這一系列的文章,能使得關於資料包的知識得以普及,所以這系列的每一篇文章我都會有由淺入深的解釋、詳細的分析、以及編碼步驟,另外附上帶有詳細注釋的源碼
文章作者:nirvana
經常看到論壇有人問起關於資料包的截獲、分析等問題,幸好本人也對此略有所知,也寫過很多的sniffer,所以就想寫一系列的文章來詳細深入的探討關於資料包的知識。
我希望通過這一系列的文章,能使得關於資料包的知識得以普及,所以這系列的每一篇文章我都會有由淺入深的解釋、詳細的分析、以及編碼步驟,另外附上帶有詳細注釋的源碼(為了照顧大多數朋友,我提供的都是MFC的源碼)。
不過由於也是初學者,疏漏之處還望不吝指正。
本文凝聚著筆者心血,如要轉載,請指明原作者及出處,謝謝!^_^
OK,. Let’s go ! Have fun!! q^_^p
第二篇 手把手教你捕獲資料包
目錄:
一.捕獲資料包的實現原理
二.捕獲資料包的編程實現:
1. raw socket的實現方法
2. Winpcap的實現方法
a. 枚舉本機網卡的資訊
b. 開啟相應網卡並設定為混雜模式
c. 截獲資料包並儲存為檔案
作者:
CSDN VC/MFC Network編程版主 PiGGyXP
一.捕獲資料包的實現原理:--------------------------------------------------------------------
在通常情況下,Network通訊的通訊端程式只能響應與自己硬體地址相匹配的或是以廣播形式發出的資料幀,對於其他形式的資料幀比如已到達Network介面但卻不是發給此地址的資料幀,Network介面在驗證投遞地址並非自身地址之後將不引起響應,也就是說應用程式無法收取與自己無關的的資料包。
所以我們要想實現截獲流經Network裝置的所有資料包,就要採取一點特別的手段了:
將網卡設定為混雜模式。
這樣一來,該主機的網卡就可以捕獲到所有流經其網卡的資料包和幀。
但是要注意一點,這種截獲僅僅是資料包的一份拷貝,而不能對其進行截斷,要想截斷Network流量就要採用一些更底層的辦法了,不在本文的討論範圍之內。
二. 捕獲資料包的編程實現:
1.raw socket的實現方法--------------------------------------------------------------------
不同於我們常用的資料流通訊端和資料通訊端,在建立了原始通訊端後,需要用WSAIoctl()函數來設定一下,它的定義是這樣的
int WSAIoctl(
SOCKET s,
DWORD dwIoControlCode,
LPVOID lpvInBuffer,
DWORD cbInBuffer,
LPVOID lpvOutBuffer,
DWORD cbOutBuffer,
LPDWORD lpcbBytesReturned,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
雖然咋一看參數比較多,但是其實我們最關心的只是其中的第二項而已,我們需要做的就是把第二項設定為SIO_RCVALL,講了這麼多其實要做的就是這麼一行代碼,很簡單吧?^_^
當然我們還可以指定是否親自處理IP頭,但是這並不是必須的。
完整的代碼類似與如下這樣,加粗的代碼是與平常不同的需要注意的地方:
( 為了讓代碼一目瞭然,我把錯誤處理去掉了,下同)
#i nclude “WinSock2.h”
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1)
SOCKET SnifferSocket
WSADATA wsaData;
iFlag=WSAStartup(MAKEWORD(2,2),&wsaData); //開啟winsock.dll
SnifferSocket=WSASocket(AF_INET, //建立raw socket
SOCK_RAW,IPPROTO_IP,NULL,0,WSA_FLAG_OVERLAPPED);
char FAR name[128]; //擷取本機IP地址
gethostname(name, sizeof(name));
struct hostent FAR * pHostent;
pHostent = gethostbyname(name);
SOCKADDR_IN sa; //填充SOCKADDR_IN結構的內容
sa.sin_family = AF_INET;
sa.sin_port = htons(6000); // 連接埠號碼可以隨便改,當然與當然系統不能衝突
memcpy(&(sa.sin_addr),pHostent->h_addr,pHostent->h_length);
bind(SnifferSocket,(LPSOCKADDR)&sa,sizeof(sa)); //綁定
// 置ioctl來接收所有Network資料,關鍵步驟
DWORD dwBufferLen[10] ;
DWORD dwBufferInLen = 1 ;
DWORD dwBytesReturned = 0 ;
WSAIoctl(SnifferSocket, IO_RCVALL,&dwBufferInLen, izeof(dwBufferInLen),
&dwBufferLen, sizeof(dwBufferLen),&dwBytesReturned , NULL , NULL );
至此,實際就可以開始對Network資料包進行嗅探了,而對於資料包的接收還是和普通的socket一樣,通過recv()函數來完成,因為這裡涉及到不同的socket模型,接收方法差別很大,所以在此就不提供接收的代碼了。
2.winpcap的實現方法:-----------------------------------------------------------------------
winpcap驅動包,是我們玩轉資料包不可或缺的好東東,winpcap的主要功能在於獨立於主機協議(如TCP-IP)而發送和接收未經處理資料報,主要為我們提供了四大功能:
功能:
1> 捕獲未經處理資料報,包括在共用Network上各主機發送/接收的以及相互之間交換的資料報;
2> 在資料報發往應用程式之前,按照自訂的規則將某些特殊的資料報過濾掉;
3> 在Network上發送原始的資料報;
4> 收集Network通訊過程中的統計資訊
如果環境允許的話(比如你做的不是木馬程式),我還是推薦大家用winpcap來截獲資料包,因為它的功能更強大,工作效率更高,唯一的缺點就是在運行用winpcap開發的程式以前,都要在主機上先安裝winpcap的driver。
而且一會我們就會發現它比raw socket功能強大的多,而且工作得更為底層,最明顯的理由就是raw socket捕獲的資料包是沒有以太頭的,此乃後話。
至於怎麼來安裝使用,請參考本系列的系列一《手把手教你玩轉ARP包中的》,裡面有詳細的載入winpcap驅動的方法^_^
廢話不多說了,讓我們轉入正題, 具體用winpcap來截獲資料包需要做如下的一些工作:
A . 枚舉本機網卡的資訊(主要是獲得網卡的名稱)
其中要用到pcap_findalldevs函數,它是這樣定義的
/*************************************************
int pcap_findalldevs ( pcap_if_t ** alldevsp,
char * errbuf
)
功能:
枚舉系統所有Network裝置的資訊
參數: alldevsp: 是一個pcap_if_t結構體的指標,如果函數pcap_findalldevs函數執行成功,將獲得一個可用網卡的列表,而裡面儲存的就是第一個元素的指標。
Errbuf: 儲存錯誤資訊的字串
傳回值: int : 如果返回0 則執行成功,錯誤返回 -1。
*************************************************/
我們利用這個函數來獲得網卡名字的完整代碼如下:
pcap_if_t* alldevs;
pcap_if_t* d;
char errbuf[PCAP_ERRBUF_SIZE];
pcap_findalldevs(&alldevs,errbuf); // 獲得Network裝置指標
for(d=alldevs;d;d=d->next) // 枚舉網卡然後添加到ComboBox中
{
d->name; // d->name就是我們需要的網卡名字字串,按照你// 自己的需要儲存到你的相應變數中去
}
pcap_freealldevs(alldevs); // 釋放alldev資源
請期待下文。。。。。^_^
B. 開啟相應網卡並設定為混雜模式:
在此之前肯定要有一段讓使用者選擇網卡、並獲得使用者選擇的網卡的名字的代碼,既然上面已經可以獲得所有網卡的名字了,這段代碼就暫且略過了。
我們主要是要用到 pcap_open_live 函數,不過這個函數winpcap的開發小組已經建議用pcap_open 函數來代替,不過因為My Code裡面用的就是pcap_open_live,所以也不便於修改了,不過pcap_open_live使用起來也是沒有任何問題的,下面是pcap_open_live的函式宣告:
/*************************************************
pcap_t* pcap_open_live ( char * device,
int snaplen,
int promisc,
int to_ms,
char * ebuf
)
功能:
根據網卡名字開啟網卡,並設定為混雜模式,然後返回其控制代碼
參數:
Device : 就是前前面我們獲得的網卡的名字;
Snaplen : 我們從每個資料包裡取得資料的長度,比如設定為100,則每次我們只是獲得每個資料包 100 個長度的資料,沒有什麼特殊需求的話就把它設定為65535最大值就可以了;
Promisc:這個參數就是設定是否把網卡設定為“混雜模式”,設定為 1 即可;
to_ms : 逾時時間,毫秒,一般設定為 1000即可。
傳回值:
pcap_t : 類似於一個網卡“控制代碼”之類的,不過當然不是,這個參數是後面截獲資料要用到的。
******************************************************************************/
雖然看起來比較複雜,不過用起來還是非常簡單的,其實 1 行就OK了:
pcap_t* adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
// 開啟網卡,並且設定為混雜模式
// pCardName是前面傳來的網卡名字參數
adhandle = pcap_open_live(pCardName,65535,1,1000,errbuf);
C. 截獲資料包並儲存為檔案:------------------------------------------------------
當然,不把資料包儲存為檔案也可以,不過如果不儲存的話,只能在截獲到資料包的那一瞬間進行分析,轉眼就沒了^_^
所以,為了便於日後分析,所以高手以及我個人經常是把資料包儲存下來的慢慢分析的。
但是注意Network流量,在流量非常大的時候注意硬碟空間呵呵,常常幾秒中就有好幾兆是很正常的事情。
下面首先來詳細講解一下,這個步驟中需要用到的winpcap函數:
/**************************************************************
pcap_dumper_t* pcap_dump_open ( pcap_t * p,
const char * fname
)
功能:
建立或者開啟儲存資料包內容的檔案,並返回其控制代碼
參數:
pcap_t * p :前面開啟的網卡控制代碼;
const char * fname :要儲存的檔案名稱字
傳回值:
pcap_dumper_t* : 儲存檔案的描述控制代碼,具體細節我們不用關心
***************************************************************/
/***************************************************************
int pcap_next_ex ( pcap_t * p,
struct pcap_pkthdr ** pkt_header,
u_char ** pkt_data
)
功能:
從網卡或者資料包檔案中讀取資料內容
參數:
pcap_t * p: 網卡控制代碼
struct pcap_pkthdr ** pkt_header: 並非是資料包的指標,只是與資料包捕獲驅動有關的一個Header
u_char ** pkt_data:指向資料包內容的指標 ,包括了協議頭
傳回值:
1 : 如果成功讀取資料包
0 :pcap_open_live()設定的逾時時間之內沒有讀取到內容
-1: 出現錯誤
-2: 讀檔案時讀到了末尾
***************************************************************/
/***************************************************************
void pcap_dump ( u_char * user,
const struct pcap_pkthdr * h,
const u_char * sp
)
功能:
將資料包內容依次寫入pcap_dump_open()指定的檔案中
參數:
u_char * user : 網卡控制代碼
const struct pcap_pkthdr * h: 並非是資料包的指標,只是與資料包捕獲驅動有關的一個Header
const u_char * sp: 資料包內容指標
傳回值:
Void
****************************************************************/
下面給出一段完整的捕獲資料包的代碼,是線上程中寫的,為了程式清晰,我去掉了錯誤處理代碼以及線程退出的代碼,完整代碼可下載文後的樣本源碼,老規矩,重要的步驟用粗體字標出。
我們實際在捕獲資料包的時候也最好是把代碼放到另外的線程中。
/*********************************************************
* 進程:
* 這個是程式的核心部分,完成資料包的截獲
* 參數:
* pParam: 使用者選擇的用來捕獲資料的網卡的名字
*********************************************************/
UINT CaptureThread(LPVOID pParam)
{
const char* pCardName=(char*)pParam; // 轉換參數,獲得網卡名字
pcap_t* adhandle;
char errbuf[PCAP_ERRBUF_SIZE];
// 開啟網卡,並且設定為混雜模式
adhandle=pcap_open_live(pCardName,65535,1,1000,errbuf); {
pcap_dumper_t* dumpfile;
// 建立儲存截獲資料包的檔案
dumpfile=pcap_dump_open(adhandle, "Packet.dat");
int re;
pcap_pkthdr* header; // Header
u_char* pkt_data; // 資料包內容指標
// 從網卡或者檔案中不停讀取資料包資訊
while((re=pcap_next_ex(adhandle,&header,(const u_char**)&pkt_data))>=0)
{
// 將捕獲的資料包存入檔案
pcap_dump((unsigned char*)dumpfile,header,pkt_data);
}
return 0;
}
將個線程加入到程式裡面啟動以後。。。等等,如何來啟動這個線程就不用我說了吧,類似這樣的代碼就可以
::AfxBeginThread(CaptureThread,chNIC); // chNIC是網卡的名字,char* 類型
啟動線程一段時間以後(幾秒中就有效果了),可以看到資料包已經被成功的截獲下來,並儲存到程式目錄下的Packet.dat檔案中。
=====================================================
至此,資料包的截獲方法就講完了,大家看了這篇文章,其實你就一定也明白了,無論是raw socket的方法還是winpcap的方法,其實都很簡單的,真的沒有什麼東西,只是會讓不明白原理的人看起來很神秘而已,isn’t it?
呵呵,不過也不要高興的太早,這個儲存下來的資料包檔案,你可以試著用UltraEdit開啟這個檔案看看,是不是大部分都是亂碼?基本上沒有什麼可讀性,這是因為:
此時捕獲到的資料包並不僅僅是單純的資料資訊,而是包含有 IP頭、 TCP頭等資訊頭的最原始的資料資訊,這些資訊保留了它在Network傳輸時的原貌。通過對這些在低層傳輸的原始資訊的分析可以得到有關Network的一些資訊。由於這些資料經過了Network層和傳輸層的打包,因此需要根據其附加的幀頭對資料包進行分析。