现在的位置: 首页 > 综合 > 正文

关于TCP/IP数据包结构一文的进一步说明(常见协议数据报结构及TCP三次握手机制)

2013年12月07日 ⁄ 综合 ⁄ 共 10702字 ⁄ 字号 评论关闭

上周发一篇文章:http://blog.csdn.net/prsniper/article/details/6762145

大家反应比较活跃,看来对大家帮助不少,于是有此文。

 

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

先声明一下,文章被很多人转载,我欢迎大家转载,但是我发现已有一个人转载时,没有保留出处,而且连转载也没写明,竟然挂着“原创”的标题,实在是……

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

 

这篇文章可能表较长,所以这里写个目录,也可以说是摘要吧:

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运行),我用的是命令行:

:\>cd /D F:\

F:\>cd codes

F:\Codes>cd "Visual C++"

F:\Codes\Visual C++>cd PackAnalyser

F:\Codes\Visual C++\PackAnalyser>cd..

F:\Codes\Visual C++>cd tcpClient

F:\Codes\Visual C++\tcpClient>cd debug

F:\Codes\Visual C++\tcpClient\Debug>dir
 驱动器 F 中的卷是 FILES
 卷的序列号是 0005-DD75

 F:\Codes\Visual C++\tcpClient\Debug 的目录

2011-09-15  10:37    <DIR>          .
2011-09-15  10:37    <DIR>          ..
2011-09-15  13:45            12,268 tcp.obj
2011-09-15  13:45           155,714 tcpClient.exe
2011-09-15  13:45           176,624 tcpClient.ilk
2011-09-15  10:37         2,863,224 tcpClient.pch
2011-09-15  13:45           410,624 tcpClient.pdb
2011-09-15  13:49            41,984 vc60.idb
2011-09-15  13:45            69,632 vc60.pdb
               7 个文件      3,730,070 字节
               2 个目录 61,248,696,320 可用字节

F:\Codes\Visual C++\tcpClient\Debug>tcpclient.exe
what the fuck are you doing?

我放在这个目录,呵呵。大家编译时候不要忘记更改IP哦,不然又有人来这里发牢骚说源码不对了……

 

虎胆游侠

2011-09-15 15:25:00 于深圳

抱歉!评论已关闭.