上周发一篇文章: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
#include
#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
#include
#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
#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 .
2011-09-15 10:37 ..
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 于深圳