Windows平臺的原始套接字編程的知識點概要(備忘)

        其實從大學學習了C語言後,翻看整本教材只有C語言的語法,根本沒有網絡編程相關的任何內容,現在回想起來,都記不起自己何時在哪本書上學習了套接字編程,說起TCP、UDP,能知道他們的區別,相關的編程的“套路”,即分別在服務器端和客戶端應用程序的固定招數,函數也還是有那麼多,但是功能方面嘛,仔細想想,一般除了通信傳輸數據,文件等,就沒有其他目的了,即使有多播,組播,廣播等,目的也是爲了通信,而且都是在局域網內。以上提到的這些都太中規中矩了,總感覺少了些什麼,比如網絡性能監視類的程序,網絡探測、網絡攻擊等程序肯定光用這個套接字編程技術無法實現。

      寫這篇文章是爲了溫故知新,照着Windows網絡編程一書第7章內容而寫。所以全手打。

       -----------------------分----割----線-----------------------------------

       原始套接字是允許訪問底層傳輸協議的一種套接字類型,提供了普通套接字所不具備的功能,能夠對網絡數據包進行某種程度的控制操作。

      原始套接字提供普通套接字不具備的能力有:發送和接收內核不處理其協議字段的IPv4數據包。對於8位IPv4協議字段,大多數內核僅僅處理該字段值爲1(ICMP協議)、2(IGMP協議)、6(TCP協議)、17(UDP協議)的數據報,但是協議字段的值還有很多。例如,OSPF路由協議的值爲89,如果要處理OSPF數據報文,那麼程序必須使用原始套接字讀寫。

直接總結乾貨:

首先,在WinSock2中,很熟悉的兩個函數:socket()和WSASocket(),第二個參數用常量SOCK_RAW指明,第三個參數IPPROTO_RAW或者IPPROTO_IP

其次,可以不用在原始套接字上調用bind()函數,也可以不用調用connect()函數,

然後,接收數據時可以調用recvfrom()或者WSARecvFrom()函數。

當接收數據時,在接收到一個數據包後,IP協議棧會把滿足以下條件的IP數據包傳遞到原始套接字中:

1、既不是UDP的數據也不是TCP的數據包;2、部分ICMP分組;3、所有的IGMP分組;4、其他所有的IP數據包;5、重組後的分片數據。

傳遞到原始套接字中的數據包會根據接收條件決定是否拷貝,若滿足則拷貝到原始套接字的接收緩衝區中。

假如我們希望能夠接收到所有發給網卡的數據,甚至是流經網卡但並非給本機的數據,可以通過設置接收選項SIO_RCVALL就能實現,設置該套接字控制命令則需要通過函數WSAIoctl()。但是比如在設計一個ping程序時,發出請求後接收響應時,很可能有其他的ICMP消息產生干擾,此時就應該從數據來源IP地址,協議類型等方面來判斷接收到的數據,正確匹配。

當然了,發送數據時可以調用sendto()或WSASendTo()函數,廣播地址,多播地址同樣適用(廣播或者多播時需要通過setsocketopt()函數設置選項SO_BROADCAST!!)

connect()函數指明遠端地址。

如果要設置IP首部,IPv4時選項爲IP_HDRINCL,選項級別爲IPPROTO_IP。當然,此後才能調用bind()函數來指明本機IP地址。要知道,只有這個選項開啓了,才能在IP首部中修改源IP地址!可惜Windows對這些作了限制,如果源IP地址不正確,惡意代碼不能通過僞造的源IP地址進行拒絕服務攻擊,也不能發送IP欺騙數據包。如果原始套接字無法滿足需求,還有WInPcap編程直接操控數據幀。


有兩個編程示例:

第一個:使用原始套接字實現ping

    ping是很多操作系統上用來檢驗主機是否連通網絡,以及探測遠程主機是否存活的實用工具之一,它通過系那個遠程主機發送ICMP協議包並檢驗取回的遠程主機吸納供應包來實現上述功能。

概念:   位於網絡層的IP協議和ICMP協議屬於同等級的,

正因爲原始套接字才能構造、發送和接收ICMP協議的數據包。ping程序使用了ICMP協議的ECHO類型的請求報文,程序中需要發送ECHO請求和接收響應,並計算間隔時間,判斷網絡狀況。

ECHO請求由ICMP首部和ICMP數據組成,在前面加上IP首部構成IP數據包。即IP數據包=IP首部+ICMP首部+ICMP數據

所以,先定義ICMP首部的結構體,有類型,代碼,校驗和,ECHO請求的標識,序號5個成員共計8個字節

直接貼代碼:

頭文件:

#pragma pack(1)

#define ICMP_ECHOREPLY 0
#define ICMP_ECHOREQ 8

// IP首部結構體
typedef struct tagIPHDR
{
u_char  VIHL; // 版本號和首部長度
u_char TOS; // 服務類型
short TotLen; // 總的長度
short ID; // 標識
short FlagOff; // 分片標誌及偏移
u_char TTL; // TTL
u_char Protocol; // 協議
u_short Checksum; // 校驗和
struct in_addr iaSrc; // 源IP地址
struct in_addr iaDst; // 目的IP地址
}IPHDR, *PIPHDR;

// ICMP首部結構體
typedef struct tagICMPHDR
{
u_char Type; // 類型(表示ECHO請求)
u_char Code; // 代碼
u_short Checksum; // 校驗和
u_short ID; // 標識
u_short Seq; // 序號
}ICMPHDR, *PICMPHDR;

#define REQ_DATASIZE 32 // Echo Request Data size

// ICMP ECHO請求結構體
typedef struct tagECHOREQUEST
{
ICMPHDR icmpHdr;              //ICMP協議首部
DWORD dwTime;               //獲取的當前系統時間戳
char cData[REQ_DATASIZE];  //ICMP數據
}ECHOREQUEST, *PECHOREQUEST;

// ICMP ECHO應答結構體
typedef struct tagECHOREPLY
{
IPHDR ipHdr;            //IP首部
ECHOREQUEST echoRequest;  //ICMP請求的結構體
char    cFiller[256];     //填充
}ECHOREPLY, *PECHOREPLY;

#pragma pack()

        

源文件:

#include <stdio.h>
#include <stdlib.h>
#include <winsock.h>


#include "ping.h"


void Ping(LPCSTR pstrHost);//調用了SendEchoRequest函數和RecvEchoReply函數。
void ReportError(LPCSTR pstrFrom);
int  WaitForEchoReply(SOCKET s);
u_short in_cksum(u_short *addr, int len);
int SendEchoRequest(SOCKET, LPSOCKADDR_IN);
DWORD RecvEchoReply(SOCKET, LPSOCKADDR_IN, u_char *);


void main(int argc, char **argv)
{
    WSADATA wsaData;
    WORD wVersionRequested = MAKEWORD(1,1);
    int nRet;


    if (argc != 2)
    {
fprintf(stderr,"\nUsage: ping hostname\n");
return;
    }


    nRet = WSAStartup(wVersionRequested, &wsaData);
    if (nRet)
    {
fprintf(stderr,"\nError initializing WinSock\n");
return;
    }


if (wsaData.wVersion != wVersionRequested)
{
fprintf(stderr,"\nWinSock version not supported\n");
return;
}


Ping(argv[1]);
    WSACleanup();
}


void Ping(LPCSTR pstrHost)
{
SOCKET  rawSocket;
LPHOSTENT lpHost;
struct    sockaddr_in saDest;
struct    sockaddr_in saSrc;
DWORD  dwTimeSent;
DWORD  dwElapsed;
u_char    cTTL;
int       nLoop;
int       nRet;


// 創建原始套接字,指定協議IPPROTO_ICMP
rawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (rawSocket == SOCKET_ERROR) 
{
ReportError("socket()");
return;
}

// 獲取IP地址
lpHost = gethostbyname(pstrHost);
if (lpHost == NULL)
{
fprintf(stderr,"\nHost not found: %s\n", pstrHost);
return;
}

saDest.sin_addr.s_addr = *((u_long FAR *) (lpHost->h_addr));
saDest.sin_family = AF_INET;
saDest.sin_port = 0;


printf("\nPinging %s [%s] with %d bytes of data:\n",
pstrHost,
inet_ntoa(saDest.sin_addr),
REQ_DATASIZE);


//連續ping4次
for (nLoop = 0; nLoop < 4; nLoop++)
{
SendEchoRequest(rawSocket, &saDest);//發送請求
nRet = WaitForEchoReply(rawSocket);//等待響應
if (nRet == SOCKET_ERROR)
{
ReportError("select()");
break;
}
if (!nRet)
{
printf("\nTimeOut");
break;
}


// 接收響應
dwTimeSent = RecvEchoReply(rawSocket, &saSrc, &cTTL);


// 計算間隔時間
dwElapsed = GetTickCount() - dwTimeSent;
printf("\nReply from: %s: bytes=%d time=%ldms TTL=%d", 
               inet_ntoa(saSrc.sin_addr), 
  REQ_DATASIZE,
               dwElapsed,
               cTTL);
}
printf("\n");
nRet = closesocket(rawSocket);
if (nRet == SOCKET_ERROR)
ReportError("closesocket()");
}


int SendEchoRequest(SOCKET s,LPSOCKADDR_IN lpstToAddr) 
{
static ECHOREQUEST echoReq;
static nId = 1;
static nSeq = 1;
int nRet;


// 結構體成員賦值,填充echo請求
echoReq.icmpHdr.Type = ICMP_ECHOREQ;
echoReq.icmpHdr.Code = 0;
echoReq.icmpHdr.Checksum = 0;
echoReq.icmpHdr.ID = nId++;
echoReq.icmpHdr.Seq = nSeq++;


//填寫數據
for (nRet = 0; nRet < REQ_DATASIZE; nRet++)
echoReq.cData[nRet] = ' '+nRet;


//獲取當前時間並記錄
echoReq.dwTime = GetTickCount();


//計算校驗和
echoReq.icmpHdr.Checksum = in_cksum((u_short *)&echoReq, sizeof(ECHOREQUEST));
   
nRet = sendto(s,
(LPSTR)&echoReq,
sizeof(ECHOREQUEST),
0,
(LPSOCKADDR)lpstToAddr, 
sizeof(SOCKADDR_IN));   


if (nRet == SOCKET_ERROR) 
ReportError("sendto()");
return (nRet);
}


DWORD RecvEchoReply(SOCKET s, LPSOCKADDR_IN lpsaFrom, u_char *pTTL) 
{
ECHOREPLY echoReply;
int nRet;
int nAddrLen = sizeof(struct sockaddr_in);

nRet = recvfrom(s,
(LPSTR)&echoReply,
sizeof(ECHOREPLY),
0,
(LPSOCKADDR)lpsaFrom,
&nAddrLen);


if (nRet == SOCKET_ERROR) 
ReportError("recvfrom()");


*pTTL = echoReply.ipHdr.TTL;
return(echoReply.echoRequest.dwTime);  
}


void ReportError(LPCSTR pWhere)
{
fprintf(stderr,"\n%s error: %d\n",
WSAGetLastError());
}


int WaitForEchoReply(SOCKET s)
{
struct timeval Timeout;
fd_set readfds;


readfds.fd_count = 1;
readfds.fd_array[0] = s;
Timeout.tv_sec = 5;
    Timeout.tv_usec = 0;


return(select(1, &readfds, NULL, NULL, &Timeout));
}




//
// Mike Muuss' in_cksum() function
// and his comments from the original
// ping program
//
// * Author -
// * Mike Muuss
// * U. S. Army Ballistic Research Laboratory
// * December, 1983


/*
 * I N _ C K S U M
 *
 * Checksum routine for Internet Protocol family headers (C Version)
 *
 */
u_short in_cksum(u_short *addr, int len)
{
register int nleft = len;
register u_short *w = addr;
register u_short answer;
register int sum = 0;


/*
*  Our algorithm is simple, using a 32 bit accumulator (sum),
*  we add sequential 16 bit words to it, and at the end, fold
*  back all the carry bits from the top 16 bits into the lower
*  16 bits.
*/
while( nleft > 1 )  {
sum += *w++;
nleft -= 2;
}


/* mop up an odd byte, if necessary */
if( nleft == 1 ) {
u_short u = 0;


*(u_char *)(&u) = *(u_char *)w ;
sum += u;
}


/*
* add back carry outs from top 16 bits to low 16 bits
*/
sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
sum += (sum >> 16); /* add carry */
answer = ~sum; /* truncate to 16 bits */
return (answer);
}



第二個:使用原始套接字實現的數據包捕獲(極簡單的統計流量,嗅探器)

僅有一個函數:(僞代碼)

int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsaData;
    SOCKET SnifferSocket = INVALID_SOCKET;
    char recvbuf[DEFAULT_BUFLEN];
    int iResult;
    int recvbuflen = DEFAULT_BUFLEN;
struct hostent *local;
    char HostName[DEFAULT_NAMELEN];
struct in_addr addr;
struct sockaddr_in LocalAddr, RemoteAddr;
int addrlen = sizeof(struct sockaddr_in);
int in=0,i=0;
DWORD dwBufferLen[10];
    DWORD Optval= 1 ;
    DWORD dwBytesReturned = 0 ;


// 初始化套接字
    
//創建原始套接字
    SnifferSocket = socket ( AF_INET, SOCK_RAW, IPPROTO_IP);


//獲取本機名稱
iResult = gethostname( HostName, sizeof(HostName));


//獲取本機可用IP
    local = gethostbyname( HostName);
printf ("\n本機可用的IP地址爲:\n");


while (local->h_addr_list[i] != 0) {
        addr.s_addr = *(u_long *) local->h_addr_list[i++];
printf("\tIP Address #%d: %s\n", i, inet_ntoa(addr));
    }
   
printf ("\n請選擇捕獲數據待使用的接口號:");
scanf_s( "%d", &in);
    
memset( &LocalAddr, 0, sizeof(LocalAddr));
memcpy( &LocalAddr.sin_addr.S_un.S_addr, local->h_addr_list[in-1], sizeof(LocalAddr.sin_addr.S_un.S_addr));
LocalAddr.sin_family = AF_INET;
LocalAddr.sin_port=0;


//綁定本地地址
iResult = bind( SnifferSocket, (struct sockaddr *) &LocalAddr, sizeof(LocalAddr));

//設置套接字接收命令
iResult = WSAIoctl(SnifferSocket, SIO_RCVALL , &Optval, sizeof(Optval),  &dwBufferLen, sizeof(dwBufferLen), &dwBytesReturned , NULL , NULL );


//開始接收數據
    printf(" \n開始接收數據");
do
{
//接收數據
iResult = recvfrom( SnifferSocket, recvbuf, DEFAULT_BUFLEN, 0 ,(struct sockaddr *)&RemoteAddr,&addrlen);
if (iResult > 0) 
printf ("\n接收到來自%s的數據包,長度爲%d.",inet_ntoa(RemoteAddr.sin_addr),iResult );
else
printf("recvfrom failed with error: %ld\n", WSAGetLastError());
} while(iResult > 0);
return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章