關於TCP/IP數據包結構一文的進一步說明(常見協議數據報結構及TCP三次握手機制)


來源:http://blog.csdn.net/prsniper/article/details/6781270

大家反應比較活躍,看來對大家幫助不少,於是有此文。

 

================================================================

先聲明一下,文章被很多人轉載,我歡迎大家轉載,但是我發現已有一個人轉載時,沒有保留出處,而且連轉載也沒寫明,竟然掛着“原創”的標題,實在是……

================================================================

 

這篇文章可能表較長,所以這裏寫個目錄,也可以說是摘要吧:

1.TCP三次握手機制

2.數據包攔截的C++源碼(VS6/VC++)

3.TCP通信C源碼,包括服務端和客戶端

4.常見協議數據包頭部的C語言定義(包括在抓包源碼中)

5.WinHex分析數據報數據

 

//請在轉載的時候,保留出處,至少聲明是轉載,尊重他人即尊重自己。限於水平,同時精力有限,不足指出望大家批評指正。

用到一張圖片,以便理解:(大家可以閱讀http://blog.csdn.net/prsniper/article/details/6762145 便於理解)

1.TCP三次握手機制

  第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。

  第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;

  第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。

  完成三次握手,客戶端與服務器開始傳送數據,簡單來說呢,就是:

    A告訴B:“我要去你家做客。”,B收到,說:“歡迎”,殺雞宰羊,開門等候,A得到確認,沐浴更衣,刷牙洗臉,帶上筷子,告訴B:“我將要到達。”,雙方會面…關於三次握手得到的數據分析,參見下面WinHex部分。

 

2.數據包攔截的C++源碼(VS6/VC++)

    VC++源碼,可能比較長,大家可以滾動鼠標往下看

////==================================================
/*  Analyser.cpp - TCP/IP Pack Analysing Module
 *  Coded by http://blog.csdn.net/prsniper
 *  Please retain this information when copying
*///==================================================

#include "ANALYSER.H"

int main(int argc, char* argv[])
{	//初始化SOCKET
	WSADATA wsaData;
	int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
	if(ret != NO_ERROR)
	{	printf("Error at WSAStartup();\n");
		return 0;
	}
	//創建套接字,soket_raw
	SOCKET lpRawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
	if (lpRawSocket == INVALID_SOCKET) 
	{	printf("Error at socket(): %ld\n", WSAGetLastError());
		WSACleanup();
		return 0;
	}
	//獲取本機IP地址
	char strHost[200];
	if(gethostname((char*)strHost, sizeof(strHost) - 1) == SOCKET_ERROR)
	{	strcpy(strHost, "localhost");	//主機名獲取失敗,使用localhost
		printf("using \"localhost\" as host name!\n");
	}
	hostent *lpHost = gethostbyname((char*)strHost);
	if(lpHost == NULL) 
	{	printf("gethostbyname(); failed!\n");
		lpHost = gethostbyaddr("127.0.0.1", 4, PF_INET);
		if(lpHost == NULL)
		{	printf("get host failed!\n");
			WSACleanup();
			return 0;
		}
	}
	//struct in_addr addr;
	//if(lpHost->h_addrtype == AF_INET)
	//{	while(lpHost->h_addr_list[ret] != 0)
	//	{	addr.s_addr = *(u_long*)lpHost->h_addr_list[ret++];
	//		printf("\tIPv4 Address #%d: %s\n", ret, inet_ntoa(addr));
	//	}
	//}
	//綁定端口,把hostname與sa綁定,sa與lpRawSocket綁定
	SOCKADDR_IN sa;
	memcpy(&sa.sin_addr.S_un.S_addr, lpHost->h_addr_list[0], lpHost->h_length);
	sa.sin_family = AF_INET;
	sa.sin_port = htons(8972);
	if(bind(lpRawSocket, (PSOCKADDR)&sa, sizeof(sa)) == SOCKET_ERROR)
	{	printf("bind(); failed!\n");
		WSACleanup();
		return 0;
	}

	//設置RAW socket
	DWORD dwBuffer[10];
	DWORD dwCount = 0;
	DWORD dwSize = 1;
	/*
	WSAIoctl()函數參數
	s:一個套接口的句柄。
	dwIoControlCode:將進行的操作的控制代碼。
	lpvInBuffer:輸入緩衝區的地址。
	cbInBuffer:輸入緩衝區的大小。
	lpvOutBuffer:輸出緩衝區的地址。
	cbOutBuffer:輸出緩衝區的大小。
	lpcbBytesReturned:輸出實際字節數的地址。
	lpOverlapped:WSAOVERLAPPED結構的地址。
	lpCompletionRoutine:一個指向操作結束後調用的例程指針。
	*/
	ret = WSAIoctl(lpRawSocket, SIO_RCVALL, &dwSize, sizeof(dwSize), &dwBuffer, sizeof(dwBuffer), &dwCount, NULL, NULL);
	if(ret == SOCKET_ERROR)
	{	printf("WSAIoctl(); failed!\n");
		WSACleanup();
		return 0;
	}
	//偵聽IP報文
	char Buffer[8192] = {0};	//Windows 一般一個包裹8K=8192字節(Byte)
	BYTE *lpBuffer = (BYTE *)&Buffer;
	FILE *lpFile = fopen("TCPIP.LOG","wb");	//存在會覆蓋
	int i = 1;
	while (1)
	{	//memset(Buffer, 0, sizeof(Buffer));
		ret = recv(lpRawSocket, Buffer, sizeof(Buffer), 0);
		if(ret > 0)
		{	Buffer[ret] = NULL;	//結束字符串
			//收到數據包,解碼(這個就算了)
			//ret = *(int*)Buffer[12];
			printf("from: %d.%d.%d.%d To %d.%d.%d.%d\n", Buffer[12], Buffer[13], Buffer[14], Buffer[15], Buffer[16], Buffer[17], Buffer[18], Buffer[19]);
			printf("\nBegin Pack ID: %d ==================", i);
		 	if(lpFile != NULL)
			{	fseek(lpFile, 0, SEEK_END);
				fwrite(lpBuffer, ret, 1, lpFile);
				fclose(lpFile);
				lpFile = fopen("TCPIP.LOG","ab");	//二進制追加模式
			}
			printf("\nEnd Pack ID: %d ==================\n", i);
			i++;
		}
	}
	return 1;
}

inline void fnAnalyse(BYTE *pData)
{	static ICMPHEADER hICMPData;
	static IPHEADER hIPData;
	static TCPHEADER hTCP;
	static UDPHEADER hUDP;
	static BYTE *lpStr;
	//copy pack data
	memcpy(&hIPData, pData, sizeof(IPHEADER));
	//轉換爲網絡字節序,即大端模式(big-endian)
	//hIPData.iSrcAddr = htons(hIPData.iSrcAddr);	//32位要另外寫htons函數,或分成兩個short
	//hIPData.iDstAddr = htons(hIPData.iDstAddr);	//
	hIPData.sLength = htons(hIPData.sLength);
	hIPData.sRepare = htons(hIPData.sRepare);
	//顯示信息
	lpStr = pData + sizeof(IPHEADER);
	printf("IPv%x	Head Length:%d Bytes.	Serve Type:%d\n",
		hIPData.cLenVer >> 4, (hIPData.cLenVer & 0x0F) * 4, hIPData.cServer);
	//不寫了...
}
    編譯運行,會將得到的數據包,保存在當前文件夾的TCPIP.LOG中,二進制模式

3.TCP通信C源碼,包括服務端和客戶端

    這是服務端,也就是監聽端的C語言源代碼

#include <winsock2.h>
#include <stdio.h>
 
#pragma comment(lib,"ws2_32.lib")

#define MAX_RECV_LENGTH 1024	//1K Bytes
#define MAX_SEND_LENGTH 1024
 
int main(int argc, char* argv[])
{   SOCKET sckServer;
    SOCKADDR_IN lpServer;
    SOCKADDR_IN lpClient;
    WORD wVersionRequested;
    char lpSend[MAX_SEND_LENGTH];
    char lpGet[MAX_RECV_LENGTH];
    WSADATA wsaData;
    int ret;

    wVersionRequested = MAKEWORD(1, 1);
    ret = WSAStartup( wVersionRequested, &wsaData );
    if (ret != 0) return 0;
    if((LOBYTE( wsaData.wVersion ) != 1)||(HIBYTE( wsaData.wVersion ) != 1))
    {   WSACleanup();
        return 0; 
    }
	sckServer= socket(AF_INET, SOCK_STREAM, 0);		//監聽的套接字
    lpServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    lpServer.sin_family = AF_INET;
    lpServer.sin_port = htons(1987);	//using port:1987
    if( SOCKET_ERROR == bind(sckServer, (SOCKADDR*)&lpServer, sizeof(SOCKADDR)) )
    {   printf("bind error\n");
		WSACleanup();
        return 0;
    }
    if( SOCKET_ERROR == listen(sckServer,5) )
    {   printf("listen error");
		WSACleanup();
        return 0;
    }
	//變量重用是大俠一大風格,不過請勿模仿,最簡單的建議是儘量不對全局變量重用
    ret = sizeof(SOCKADDR);
    while(1)
    {   SOCKET sckReq = accept(sckServer, (SOCKADDR*)&lpClient, &ret);
        if(sckReq == INVALID_SOCKET)
        {   printf("accept error\n");
            return 0;
        }
		//測試TCP三次握手,註釋下面收發部分
		sprintf(lpSend,"%s","what the fuck are you doing?\n");
        if( SOCKET_ERROR == send(sckReq,lpSend,strlen(lpSend)+1,0) )
        {   printf("send err\n");
            return 0;
        }
        recv(sckReq,lpGet,MAX_RECV_LENGTH,0);
        printf("%s\n",lpGet);
		
        closesocket(sckReq);
    }
	return 1;
}


    以下是客戶端,連接請求端的C語言源碼:

#include <winsock2.h>
#include <stdio.h>

#pragma comment(lib,"ws2_32.lib")

#define MAX_RECV_LENGTH 1024	//1K
#define SEND_MAX_LENGTH 1024
 
int main(int argc, char* argv[])
{   SOCKET sckClient;
	SOCKADDR_IN lpAddr;
	char lpGet[MAX_RECV_LENGTH];
	char lpSend[SEND_MAX_LENGTH];
    WORD wVersionRequested;
    WSADATA wsaData;
    int ret;
    
    wVersionRequested = MAKEWORD(1, 1);		//爲了低版本的Windows
    ret = WSAStartup(wVersionRequested, &wsaData);
    if(ret != 0) return 0;
    if(LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1) 
    {   WSACleanup();
        return 0; 
    }
    sckClient = socket(AF_INET,SOCK_STREAM,0);
    lpAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.2");	//connect to host
    lpAddr.sin_family = AF_INET;
    lpAddr.sin_port = htons(1987);		//server port:1987
    if( SOCKET_ERROR == connect(sckClient,(SOCKADDR*)&lpAddr,sizeof(SOCKADDR)) )
    {   printf("connect error\n");
		WSACleanup();
        return 0;
    }
	//測試TCP三次握手,註釋下面收發部分
    if(recv(sckClient, lpGet, MAX_RECV_LENGTH, 0) == SOCKET_ERROR)
    {   printf("recive error\n");
		WSACleanup();
        return 0;
    }else
    {   printf("%s\n", lpGet);
    }
    strcpy(lpSend, "fucking this fucking code is what the fuck i\'m doing!\n"); 
    send(sckClient, lpSend, sizeof(lpSend) + 1, 0); 
	
    closesocket(sckClient);
    WSACleanup(); 
	return 1;
}

4.常見協議數據包頭部的C語言定義(包括在抓包源碼中)

    這裏列出TCP,UDP,ICMP,IGMP的數據包頭部C語言結構定義,ICMP常見應用是ping命令,IGMP是個危險的協議。也許有人會說,那HTTP協議呢,FTP協議呢,TELNET協議呢?這些都是基於TCP/IP的,也可能使用UDP,但是他們的命令都是ASCII明文,比如HTTP協議的頭部包含在TCP包的數據部分

// ==========================================================
// ANALYSER.H - Struct, Global Variable, Function Definitions
// Coded by http://blog.csdn.net/prsniper
// ==========================================================

#if !defined(__RANGER_ANA_H_)
#define __RANGER_ANA_H_

#if _MSC_VER >= 1000	//Visual Studio 6.0
#pragma once
#endif // _MSC_VER >= 1000

#include "Winsock2.h"
#include <stdio.h>

#define SIO_RCVALL _WSAIOW(IOC_VENDOR, 1)
#pragma comment(lib,"ws2_32.lib")

typedef struct _IPHEADER 
{	BYTE cLenVer;		//4位首部長度+4位IP版本號
	BYTE cServer;		//8位服務類型
	USHORT sLength;		//16位總長度(字節)
	USHORT sRepare;		//16位標識          
	USHORT sFlagOffset;	//3位標誌位+13段偏移量  
	BYTE cTimeToLife;	//8位生存時間 TTL
	BYTE cProtocol;		//8位協議 (1=ICMP,2=IGMP,6=TCP,17=UDP等)
	USHORT sChecksum;	//16位IP首部校驗和
	ULONG iSrcAddr;		//32位源始IP地址
	ULONG iDstAddr;		//32位目的IP地址
}IPHEADER;

typedef struct _TCPHEADER
{	USHORT sSrcPort;		//16位源端口  
	USHORT sDstPort;		//16位目的端口  
	ULONG iDataNum;			//32位數據序號
	ULONG iCheckNum;		//32位確認序號
	BYTE cHeadLen;			//4位首部長度+6位保留字(其中四位) 
	BYTE cUAPRSF;			//6位標誌位  
	USHORT sWindow;			//16位窗口大小  
	USHORT sPackChecksum;	//16位校驗和  
	USHORT sEmergency;		//16位緊急數據偏移量(緊急指針)
}TCPHEADER;  

typedef struct _UDPHEADER
{	USHORT sSrcPort;	//16位原始端口
	USHORT sDstPort;	//16位目的端口
	USHORT sPackLen;	//16位數據包長度
	USHORT sChecksum;	//16位校驗和,只提示,不強制重發
}UDPHEADER;

typedef struct _ICMPHEADER  
{	BYTE cType;				//8位類型  
	BYTE cCode;				//8位代碼  
	USHORT sChecksum;		//16位校驗和  
	USHORT sRecCode;		//16位識別號(一般爲進程號)  
	USHORT sPackNum;		//16位報文序列號  
	ULONG iTimeValue;		//32位時間戳,GetTickCount();
}ICMPHEADER;

//IGMP[Internet Group Management Protocol]是IP主機用作向相鄰多目路由器報告多目組成員。
typedef struct _IGMPHEADER	//畸形IGMP包會試TCPIP堆棧崩潰,這裏不解析了
{	UCHAR cVerType;		//4位版本 4位類型
	UCHAR cUnKnow;		//未使用
	USHORT sChecksum;	//16位校驗和 
	ULONG iGroupAddr;	//32位組地址 
}IGMPHEADER;


#endif //!defined(__RANGER_ANA_H_)

源碼到此結束,因爲是控制檯窗口,數據也保存到文件,運行效果就不截圖了

5.WinHex分析數據報數據

    用WinHex打開保存的文件,如下圖


TCP/IP數據包結構參見這篇文章:http://blog.csdn.net/prsniper/article/details/6762145,下面的大端模式即高位在低地址(在前面),舉個例子IP爲127.0.0.1在x86中是這樣存儲:0x0100007F,而大端模式是:0x7F000001。

   首先,這是IP數據包,第一個字節是4位版本+首部長度,如圖前四位是4,後四位是5,則這個包裹是IPv4,頭長5*32bit=20Byte。第二個字節是服務類型,其值是0x00,第三第四字節是包裹總長,值爲大端模式0x0034=52,可以據此計算包裹爲紅色部分。第五第六字節爲重組標識,值爲大端模式0x4986=18822。第七第八字節爲3位標識和13位段偏移量,這兩個字節(0x4000)的二進制值爲01000000,可以算出該數據報不允許分段,段偏移量爲0。第9字節爲生存時間TTL=0x40=64,第十字節爲協議代碼,值爲0x06=6(TCP)第11,12字節爲頭部校驗和,值爲0x6F77。下面爲雙方IP地址,0xC0A80164是大端模式爲192.168.1.100,0xC0A80002一樣,爲192.168.0.2。也就是從192.168.1.100發向192.168.0.2(我給路由分了幾個網段,呵呵)。因爲頭部長度已經標誌爲5,即沒有選項,那麼IP包頭部到此結束。

    然後,TCP頭部部分,前面兩個短整型是始末端口,爲大端模式0x06F9=1785,請求連接的話,本地端口是隨機的;目的端口0x07C3=1987,這個就是服務端的監聽端口。下來是數據序號0x73F5BBBE=1945484222,因爲是手提,一直不關機,合上待機就拿到辦公室,所以已經累計發了很多數據了;確認序號0x00000000,說明兩者還沒有開始傳輸數據。然後看偏移,值爲80,前四位是8=1000,也就是數據距離包頭8*32=32Byte(32正好是這個字節到IP包頭的偏移?!),後四位保留爲0。下一個字節爲0x02=00000010,前兩位保留爲0,後六位分別對應:UAPRSF,得出SYN=1,這是一個請求或者接受請求報文!窗口字段0x4000=16384,這個不管它。包校驗和0x6DF1,緊急指針0x0000,因爲URG位已經爲0,即使緊急指針不爲0也無效。

    後面的數據究竟是什麼意義,我還不太清楚,不過對TCP三次握手的說明,應該沒有影響了。

 

    下面看第二個包包,類似的我就不說了,它是從192.168.0.2發向192.168.1.100的,數據序號0x73F5BBBF,正好是請求包的數據序號+1;確認序號0xD4CF3750;標誌0x10=00010000,所以ACK=1,說明確認號有效,這是服務端給請求方發的“同意連接”確認包。

 

    再下面第三個包是綠色部分,一樣道理分析之,我這裏就不寫這麼多了。時間有限,偷懶一下。另一方面來說,我認爲我表達的不是很通俗。這裏有價值之處在於C/C++的源代碼,大家對fnAnalyse()函數稍作修改就可以做全自動的協議分析,這纔是本文的核心,也是它的價值所在。

 

    可能是使用了代碼的原因,不同形式的語言或者說表達方式很難融合到一起,熟悉C/C++的朋友(Java跟C++很像,簡直就是C++生的)就好理解多了。

    好啦,就這麼多吧,下面發下使用編譯出來的控制檯的方法(可以直接雙擊運行,或者在VS6 IDE運行),我用的是命令行:

  1. :\>cd /D F:\  
  2.   
  3. F:\>cd codes  
  4.   
  5. F:\Codes>cd "Visual C++"  
  6.   
  7. F:\Codes\Visual C++>cd PackAnalyser  
  8.   
  9. F:\Codes\Visual C++\PackAnalyser>cd..  
  10.   
  11. F:\Codes\Visual C++>cd tcpClient  
  12.   
  13. F:\Codes\Visual C++\tcpClient>cd debug  
  14.   
  15. F:\Codes\Visual C++\tcpClient\Debug>dir  
  16.  驅動器 F 中的卷是 FILES  
  17.  卷的序列號是 0005-DD75  
  18.   
  19.  F:\Codes\Visual C++\tcpClient\Debug 的目錄  
  20.   
  21. 2011-09-15  10:37    <DIR>          .  
  22. 2011-09-15  10:37    <DIR>          ..  
  23. 2011-09-15  13:45            12,268 tcp.obj  
  24. 2011-09-15  13:45           155,714 tcpClient.exe  
  25. 2011-09-15  13:45           176,624 tcpClient.ilk  
  26. 2011-09-15  10:37         2,863,224 tcpClient.pch  
  27. 2011-09-15  13:45           410,624 tcpClient.pdb  
  28. 2011-09-15  13:49            41,984 vc60.idb  
  29. 2011-09-15  13:45            69,632 vc60.pdb  
  30.                7 個文件      3,730,070 字節  
  31.                2 個目錄 61,248,696,320 可用字節  
  32.   
  33. F:\Codes\Visual C++\tcpClient\Debug>tcpclient.exe  
  34. what the fuck are you doing?  


我放在這個目錄,呵呵。大家編譯時候不要忘記更改IP哦,不然又有人來這裏發牢騷說源碼不對了……




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