用C++實現網路程式設計---抓取網路資料包的實現方法
做過網管或協議分析的人一般都熟悉sniffer這個工具,它可以捕捉流經本地網絡卡的所有資料包。抓取網路資料包進行分析有很多用處,如分析網路是否有網路病毒等異常資料,通訊協議的分析(資料鏈路層協議、IP、UDP、TCP、甚至各種應用層協議),敏感資料的捕捉等。下面我們就來看看在windows下如何實現資料包的捕獲。
下面先對網路嗅探器的原理做簡單介紹。
嗅探器設計原理
嗅探器作為一種網路通訊程式,也是通過對網絡卡的程式設計來實現網路通訊的,對網絡卡的程式設計也是使用通常的套接字(socket)方式來進行。但是,通常的套接字程式只能響應與自己硬體地址相匹配的或是以廣播形式發出的資料幀,對於其他形式的資料幀比如已到達
具體到程式設計實現上,這種對網絡卡混雜模式的設定是通過原始套接字(raw socket)來實現的,這也有別於通常經常使用的資料流套接字和資料報套接字。在建立了原始套接字後,需要通過setsockopt()函式來設定IP頭操作選項,然後再通過bind()函式將原始套接字繫結到本地網絡卡。為了讓原始套接字能接受所有的資料,還需要通過ioctlsocket()來進行設定,而且還可以指定是否親自處理IP頭。至此,實際就可以開始對
資料包 | ||
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();