一般都熟悉sniffer這個工具,它可以捕捉流經本地網卡的所有數據包。抓取網絡數據包進行分析有很多用處,如分析網絡是否有網絡病毒等異常數據,通信協議的分析(數據鏈路層協議、IP、UDP、TCP、甚至各種應用層協議),敏感數據的捕捉等。下面我們就來看看在windows下如何實現數據包的捕獲。
下面先對網絡嗅探器的原理做簡單介紹。
嗅探器設計原理
嗅探器作為一種網絡通訊程序,也是通過對網卡的編程來實現網絡通訊的,對網卡的編程也是使用通常的套接字(socket)方式來進行。但是,通常的套接字程序只能響應與自己硬件地址相匹配的或是以廣播形式發出的數據幀,對於其他形式的數據幀比如已到達網絡接口但卻不是發給此地址的數據幀,網絡接口在驗證投遞地址並非自身地址之后將不引起響應,也就是說應用程序無法收取到達的數據包。而網絡嗅探器的目的恰恰在於從網卡接收所有經過它的數據包,這些數據包即可以是發給它的也可以是發往別處的。顯然,要達到此目的就不能再讓網卡按通常的正常模式工作,而必須將其設置為混雜模式。
具體到編程實現上,這種對網卡混雜模式的設置是通過原始套接字(raw socket)來實現的,這也有別於通常經常使用的數據流套接字和數據報套接字。在創建了原始套接字后,需要通過setsockopt()函數來設置IP頭操作選項,然后再通過bind()函數將原始套接字綁定到本地網卡。為了讓原始套接字能接受所有的數據,還需要通過ioctlsocket()來進行設置,而且還可以指定是否親自處理IP頭。至此,實際就可以開始對網絡數據包進行嗅探了,對數據包的獲取仍象流式套接字或數據報套接字那樣通過recv()函數來完成。但是與其他兩種套接字不同的是,原始套接字此時捕獲到的數據包並不僅僅是單純的數據信息,而是包含有 IP頭、 TCP頭等信息頭的最原始的數據信息,這些信息保留了它在網絡傳輸時的原貌。通過對這些在低層傳輸的原始信息的分析可以得到有關網絡的一些信息。由於這些數據經過了網絡層和傳輸層的打包,因此需要根據其附加的幀頭對數據包進行分析。下面先給出結構.數據包的總體結構:
| 數據包 | ||
| IP頭 | TCP頭(或其他信息頭) | 數據 |
數據在從應用層到達傳輸層時,將添加TCP數據段頭,或是UDP數據段頭。其中UDP數據段頭比較簡單,由一個8字節的頭和數據部分組成,具體格式如下:
| 16位 | 16位 |
| 源端口 | 目的端口 |
| UDP長度 | UDP校驗和 |
而TCP數據頭則比較復雜,以20個固定字節開始,在固定頭后面還可以有一些長度不固定的可選項,下面給出TCP數據段頭的格式組成:
| 16位 | 16位 | |||||||
| 源端口 | 目的端口 | |||||||
| 順序號 | ||||||||
| 確認號 | ||||||||
| TCP頭長 | (保留)7位 | URG | ACK | PSH | RST | SYN | FIN | 窗口大小 |
| 校驗和 | 緊急指針 | |||||||
| 可選項(0或更多的32位字) | ||||||||
| 數據(可選項) | ||||||||
對於此TCP數據段頭的分析在編程實現中可通過數據結構_TCP來定義:
typedef struct _TCP{ WORD SrcPort; // 源端口 WORD DstPort; // 目的端口 DWORD SeqNum; // 順序號 DWORD AckNum; // 確認號 BYTE DataOff; // TCP頭長 BYTE Flags; // 標志(URG、ACK等) WORD Window; // 窗口大小 WORD Chksum; // 校驗和 WORD UrgPtr; // 緊急指針 } TCP; typedef TCP *LPTCP; typedef TCP UNALIGNED * ULPTCP;
在網絡層,還要給TCP數據包添加一個IP數據段頭以組成IP數據報。IP數據頭以大端點機次序傳送,從左到右,版本字段的高位字節先傳輸(SPARC是大端點機;Pentium是小端點機)。如果是小端點機,就要在發送和接收時先行轉換然后才能進行傳輸。IP數據段頭格式如下:
| 16位 | 16位 | |||
| 版本 | IHL | 服務類型 | 總長 | |
| 標識 | 標志 | 分段偏移 | ||
| 生命期 | 協議 | 頭校驗和 | ||
| 源地址 | ||||
| 目的地址 | ||||
| 選項(0或更多) | ||||
同樣,在實際編程中也需要通過一個數據結構來表示此IP數據段頭,下面給出此數據結構的定義:
typedef struct _IP{ union{ BYTE Version; // 版本 BYTE HdrLen; // IHL }; BYTE ServiceType; // 服務類型 WORD TotalLen; // 總長 WORD ID; // 標識 union { WORD Flags; // 標志 WORD FragOff; // 分段偏移 }; BYTE TimeToLive; // 生命期 BYTE Protocol; // 協議 WORD HdrChksum; // 頭校驗和 DWORD SrcAddr; // 源地址 DWORD DstAddr; // 目的地址 BYTE Options; // 選項 } IP; typedef IP * LPIP; typedef IP UNALIGNED * ULPIP;
在明確了以上幾個數據段頭的組成結構后,就可以對捕獲到的數據包進行分析了。
嗅探器實現
嗅探器實質就是從網絡上獲取數據包的一種工具,它可以捕捉流經本地網卡的所有數據包。抓取網絡數據包進行分析有很多用處,如分析網絡是否有網絡病毒等異常數據,通信協議的分析(數據鏈路層協議、IP、UDP、TCP、甚至各種應用層協議),敏感數據的捕捉等。下面我們就來看看在windows下如何實現數據包的捕獲。
WINSOCK本身就提供了抓取流經網卡的所有數據包的函數,雖然只能在IP協議層上捕捉,但只要您的工作沒有涉及到數據鏈路層的話,這也就足夠用了。抓取數據包的編程方法基本和編寫其它網絡應用程序一樣,只需多一個步驟,即將SOCKET設置為接收所有數據的模式,這是用WSAIoctl來實現的。
編程實現主要有以下幾個步驟:
1. 初始化WINSOCK庫;
2. 創建SOCKET句柄;
3. 綁定SOCKET句柄到一個本地地址;
4. 設置該SOCKET為接收所有數據的模式;
5. 接收數據包;
6. 關閉SOCKET句柄,清理WINSOCK庫;
(1)初始化winsock庫
Winsock是Windows下的網絡編程接口,它是由Unix下的BSD Socket發展而來,是一個與網絡協議無關的編程接口。Winsock在常見的Windows平台上有兩個主要的版本,即Winsock1和Winsock2。編寫與Winsock1兼容的程序你需要引用頭文件WINSOCK.H,如果編寫使用Winsock2的程序,則需要引用WINSOCK2.H。此外還有一個MSWSOCK.H頭文件,它是專門用來支持在Windows平台上高性能網絡程序擴展功能的。使用WINSOCK.H頭文件時,同時需要庫文件WSOCK32.LIB,使用WINSOCK2.H時,則需要WS2_32.LIB,如果使用MSWSOCK.H中的擴展API,則需要MSWSOCK.LIB。正確引用了頭文件,並鏈接了對應的庫文件,你就構建起編寫WINSOCK網絡程序的環境了。
每個Winsock程序必須使用WSAStartup載入合適的Winsock動態鏈接庫,如果載入失敗,WSAStartup將返回SOCKET_ERROR,這個錯誤就是WSANOTINITIALISED,WSAStartup的定義如下:
int WSAStartup( WORD wVersionRequested, LPWSADATA lpWSAData );
wVersionRequested指定了你想載入的Winsock版本,其高字節指定了次版本號,而低字節指定了主版本號。你可以使用宏MAKEWORD(x, y)來指定版本號,這里x代表主版本,而y代表次版本。lpWSAData是一個指向WSAData結構的指針,WSAStartup會向該結構中填充其載入的Winsock動態鏈接庫的信息。
當你使用完Winsock接口后,要調用下面的函數對其占用的資源進行釋放:
int WSACleanup(void);
如果調用該函數失敗也沒有什么問題,因為操作系統為自動將其釋放,對應於每一個WSAStartup調用都應該有一個WSACleanup調用.
錯誤處理
Winsock函數調用失敗大多會返回 SOCKET_ERROR(實際上就是-1),你可以調用WSAGetLastError得到錯誤的詳細信息:
int WSAGetLastError (void);
對該函數的調用將返回一個錯誤碼,其碼值在WINSOCK.H或WINSOCK2.H(根據其版本)中已經定義,這些預定義值都以WSAE開頭.同時你還可以使用WSASetLastError來自定義錯誤碼值.
下面是我的winsock初始化例子:
WORD wVersion; WSADATA wsadata; int err; wVersion = MAKEWORD(2,2); // WSAStartup() initiates the winsock,if successful,the function returns zero err = ::WSAStartup(wVersion,&wsadata); if(err!=0) { printf("Couldn't initiate the winsock!\n"); }
【注意】使用WORD 、WSADATA 和WSAStartup等時必須包含其頭文件,加入語句:
#include "winsock2.h" #include "windows.h"
我在初始化winsock庫時遇到了一個問題,調用WSAStartUp(),編譯后出現unresolved external symbol_WSAStartup@8,開始覺得很奇怪,因為頭文件之類的都包含了怎么會出錯呢?后來上網查了下才知道,需要包含一個動態鏈接庫WS2_32.LIB,方法有兩種:
第一種:
在菜單 project ->settings -> link -> object/library modules 下面輸入ws2_32.lib 然后確定即可
第二種:
在頭文件中加入語句#pragma comment( lib, "ws2_32.lib" ) 來顯式加載。 即:
#include <winsock2.h>
#pragma comment(lib, "WS2_32")
(2)創建socket句柄
winsock庫初始化成功后就可以創建socket句柄了,使用函數socket即可。socket函數原型為:
int socket(int domain, int type, int protocol);
domain指明所使用的協議族,通常為AF_INET,表示互聯網協議族(TCP/IP協議族);type參數指定socket的類型:SOCK_STREAM 或SOCK_DGRAM,Socket接口還定義了原始Socket(SOCK_RAW),允許程序使用低層協議;protocol通常賦值"0"。Socket()調用返回一個整型socket描述符,你可以在后面的調用使用它。
創建了socket句柄后,要將該句柄與本地IP綁定后才能使用。在進行綁定之前,要先獲得本地機器的相關信息,包括主機名,主機IP地址等。最后進行綁定。我的代碼如下:
SOCKET ServerSock=socket(AF_INET,SOCK_RAW,IPPROTO_IP); char mname[128]; struct hostent* pHostent; sockaddr_in myaddr; //Get the hostname of the local machine if( -1 == gethostname(mname, sizeof(mname))) { closesocket(ServerSock); printf("%d",WSAGetLastError()); exit(-1); } else { //Get the IP adress according the hostname and save it in pHostent pHostent=gethostbyname((char*)mname); //填充sockaddr_in結構 myaddr.sin_addr = *(in_addr *)pHostent->h_addr_list[0]; myaddr.sin_family = AF_INET; myaddr.sin_port = htons(8888);//對於IP層可隨意填 //bind函數創建的套接字句柄綁定到本地地址 if(SOCKET_ERROR==bind(ServerSock,(struct sockaddr *)&myaddr,sizeof(myaddr))) { closesocket(ServerSock); cout<<WSAGetLastError<<endl; exit(-1); } }
接下來的工作就是把該socket設為接收所有數據的模式。
//設置該SOCKET為接收所有流經綁定的IP的網卡的所有數據,包括接收和發送的數據包 u_long sioarg = 1; DWORD dwValue=0; if( SOCKET_ERROR == WSAIoctl( ServerSock, SIO_RCVALL , &sioarg,sizeof(sioarg),NULL,0,&dwValue,NULL,NULL ) ) { closesocket(ServerSock); cout << WSAGetLastError(); exit(-1); }
接收網絡數據的工作了。
【注意】這里一定要保證gethostname、gethostbyname、bind、ioctlsocket等函數都能夠被正確執行,我在開始時就因為幾個參數設置不對而導致bind和ioctlsocket執行錯誤,費了半天周折才搞定。
以下是我的完整代碼:
WORD wVersion; WSADATA wsadata; int err; wVersion = MAKEWORD(2,2); // WSAStartup() initiates the winsock,if successful,the function returns zero err = ::WSAStartup(wVersion,&wsadata); if(err!=0) { printf("Couldn't initiate the winsock!\n"); } else { // create a socket SOCKET ServerSock=socket(AF_INET,SOCK_RAW,IPPROTO_IP); char mname[128]; struct hostent* pHostent; sockaddr_in myaddr; //Get the hostname of the local machine if( -1 == gethostname(mname, sizeof(mname))) { closesocket(ServerSock); printf("%d",WSAGetLastError()); exit(-1); } else { //Get the IP adress according the hostname and save it in pHostent pHostent=gethostbyname((char*)mname); //填充sockaddr_in結構 myaddr.sin_addr = *(in_addr *)pHostent->h_addr_list[0]; myaddr.sin_family = AF_INET; myaddr.sin_port = htons(8888);//對於IP層可隨意填 //bind函數創建的套接字句柄綁定到本地地址 if(SOCKET_ERROR==bind(ServerSock,(struct sockaddr *)&myaddr,sizeof(myaddr))) { closesocket(ServerSock); cout<<WSAGetLastError<<endl; printf("..............................Error……"); getchar(); exit(-1); } //設置該SOCKET為接收所有流經綁定的IP的網卡的所有數據,包括接收和發送的數據包 u_long sioarg = 1; DWORD dwValue=0; if( SOCKET_ERROR == WSAIoctl( ServerSock, SIO_RCVALL , &sioarg,sizeof(sioarg),NULL,0,&dwValue,NULL,NULL ) ) { closesocket(ServerSock); cout << WSAGetLastError(); exit(-1); } //獲取分析數據報文 char buf[65535]; int len = 0; listen(ServerSock,5); do { len = recv( ServerSock, buf, sizeof(buf),0); if( len > 0 ) { //報文處理 } }while( len > 0 ); } } ::WSACleanup();
