PING (Packet Internet Grope),因特网包探索器,用于测试网络连接量的程序。ping命令的工作原理是:向网络上的另一个主机系统发送ICMP报文,如果指定系统得到了报文,它将把报文一模一样地传回给发送者,这有点象潜水艇声纳系统中使用的发声装置。
它是用来检查网络端对端是否通畅或者网络连接速度的命令。
ICMP(Internet Control Message,网际控制报文协议)是为网关和目标主机而提供的一种差错控制机制,使它们在遇到差错时能把错误报告给报文源发方。ICMP协议是IP层的一个协议,但是由于差错报告在发送给报文源发方时可能也要经过若干子网,因此牵涉到路由选择等问题,所以ICMP报文需通过IP协议来发送。ICMP数据报的数据发送前需要两级封装:首先添加ICMP报头形成ICMP报文,再添加IP报头形成IP数据报。如下所示
IP报头
ICMP报头
ICMP数据报
IP报头格式
由于IP层协议是一种点对点的协议,而非端对端的协议,它提供无连接的数据报服务,没有端口的概念,因此很少使用bind()和connect()函数,若有使用也只是用于设置IP地址。发送数据使用sendto()函数,接收数据使用recvfrom()函数。IP报头格式如下图:
其中ping程序只使用到以下数据:
1、IP报头长度ip_hl——以4字节为单位来记录IP报头的长度
2、生存时间TTL(Time To Live)――以秒为单位,指出IP数据报能在网络上停留的最长时间,其值由发送方设定,并在经过路由的每一个节点时减一,当该值为0时,数据报将被丢弃。
ICMP报头格式
ICMP报文分为两种,一是错误报告报文,二是查询报文。每个ICMP报头均包含类型、编码和校验和这三项内容,长度为8位,8位和16位,其余选项则随ICMP的功能不同而不同。
Ping命令只使用众多ICMP报文中的两种:"请求回送"(ICMP_ECHO)和"请求回应"(ICMP_ECHOREPLY)。
代码部分:
#pragma pack(4)
#include <stdlib.h>
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"Ws2_32.lib")
#define ICMP_ECHO 8 //发送的icmp_type ICMP回显请求报文
#define ICMP_ECHOREPLY 0 //接收的icmp_type ICMP回显应答报文
#define ICMP_MIN 8 // ICMP报文的最小长度是8字节(仅为首部,不包含icmp_data)
/* The IP header */
typedef struct ip
{
unsigned char ip_hl:4; // 4位首部长度 (length of the header)
unsigned char ip_v:4; // IP版本号
unsigned char ip_tos; // 8位服务类型TOS
unsigned short ip_len; // 16位总长度(字节)(total length of the packet)
unsigned short ip_id; // 16位标识
unsigned short ip_off; // 3位标志位
unsigned char ip_ttl; // 8位生存时间 TTL
unsigned char ip_p; // 8位协议
unsigned short ip_sum; // 16位IP首部校验和
unsigned long ip_src;// 32位源IP地址
unsigned long ip_dst; // 32位目的IP地址
}IpHeader;
//
// ICMP header
//
typedef struct icmp
{
unsigned char icmp_type;// 8位类型
unsigned char icmp_code;// 8位代码
unsigned short icmp_cksum;// 16位校验和
unsigned short icmp_id; //ID标识
unsigned short icmp_seq; // 报文序列号
unsigned long icmp_data; // 时间戳 不属于标准的ICMP头,只是用来记录时间
}IcmpHeader;
#define STATUS_FAILED 0xFFFF //状态失败
#define DEF_PACKET_SIZE 32 //发送数据包大小
#define DEF_PACKET_NUMBER 4 /* 发送数据报的个数 */
#define MAX_PACKET 1024 //最大包
void fill_icmp_data(char *, int);
USHORT checksum(USHORT *, int);
int decode_resp(char *,int ,struct sockaddr_in *);
void Usage(char *progname){
fprintf(stderr,"Usage:\n");
fprintf(stderr,"%s [number of packets] [data_size]\n",progname);
fprintf(stderr,"datasize can be up to 1Kb\n");
ExitProcess(STATUS_FAILED);
}
int main(int argc, char **argv){
WSADATA wsaData;
SOCKET sockRaw;
struct sockaddr_in dest,from;
struct hostent * hp;
int bread,datasize,times;
int fromlen = sizeof(from);
int statistic = 0; /* 用于统计结果 */
int timeout = 1000;
char *dest_ip;
char *icmp_data;
char *recvbuf;
unsigned int addr=0;
USHORT seq_no = 0;
if (WSAStartup(MAKEWORD(2,1),&wsaData) != 0){ //请求使用2.1版本的socket
fprintf(stderr,"WSAStartup failed: %d\n",GetLastError()); //请求失败,打印提示信息
ExitProcess(STATUS_FAILED); //结束进程
}
if (argc <2 ) {
Usage(argv[0]);
}
sockRaw = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP); 创建套接字
//
//为了使用发送接收超时设置(即设置SO_RCVTIMEO, SO_SNDTIMEO),必须将标志位设为WSA_FLAG_OVERLAPPED !
//
if (sockRaw == INVALID_SOCKET) {
//int res = WSAGetLastError();
fprintf(stderr,"WSASocket() failed: %d\n",WSAGetLastError());
ExitProcess(STATUS_FAILED);
}
bread = setsockopt(sockRaw,SOL_SOCKET,SO_RCVTIMEO,(char*)&timeout,
sizeof(timeout)); // 设置接收时限 1秒
if(bread == SOCKET_ERROR) {
fprintf(stderr,"failed to set recv timeout: %d\n",WSAGetLastError());
ExitProcess(STATUS_FAILED);
}
timeout = 1000; //超时间隔为1000ms
bread = setsockopt(sockRaw,SOL_SOCKET,SO_SNDTIMEO,(char*)&timeout,
sizeof(timeout)); // 设置发送时限 1秒
if(bread == SOCKET_ERROR) {
fprintf(stderr,"failed to set send timeout: %d\n",WSAGetLastError());
ExitProcess(STATUS_FAILED);
}
memset(&dest,0,sizeof(dest)); //用0来填充一块大小为sizeof(dest)的内存区域
hp = gethostbyname(argv[1]);
if (!hp){
addr = inet_addr(argv[1]); //inet_addr将IP地址从点数字符格式转换成网络字节格式整型。网络字节 7f 00 00 01 /主机字节 01 00 00 7f
}
if ((!hp) && (addr == INADDR_NONE) ) {
fprintf(stderr,"Unable to resolve %s\n",argv[1]);
ExitProcess(STATUS_FAILED);
}
if (hp != NULL)
memcpy(&(dest.sin_addr),hp->h_addr,hp->h_length);
else
dest.sin_addr.s_addr = addr;
if (hp)
dest.sin_family = hp->h_addrtype;
else
dest.sin_family = AF_INET; //AF_INET表示在Internet中通信
dest_ip = inet_ntoa(dest.sin_addr); //inet_ntoa网络地址转换转点分十进制的字符串指针
if(argc>2)
{
times=atoi(argv[2]);//atoi把字符串转换成整型数。 (第二个参数如果有则是发送包的次数)
if(times == 0)
times=DEF_PACKET_NUMBER;
}
else
times=DEF_PACKET_NUMBER; //发送包的个数 宏定义为4
if (argc >3)
{
datasize = atoi(argv[3]);
if (datasize == 0)
datasize = DEF_PACKET_SIZE;
if (datasize >1024) // 用户给出的数据包大小太大
{
fprintf(stderr,"WARNING : data_size is too large !\n");
datasize = DEF_PACKET_SIZE;
}
}
else
datasize = DEF_PACKET_SIZE; //发送数据包大小32字节
//
//创建ICMP报文
//
datasize += sizeof(IcmpHeader);//发送数据包大小 + Icmp头大小
icmp_data = (char*)malloc(MAX_PACKET); //分配发包内存
recvbuf = (char*)malloc(MAX_PACKET); //分配收包内存
if (!icmp_data) {
fprintf(stderr,"HeapAlloc failed %d\n",GetLastError()); //创建失败,打印提示信息
ExitProcess(STATUS_FAILED);
}
memset(icmp_data,0,MAX_PACKET); //把一个char a[20]清零, 就是 memset(a, 0, 20)
fill_icmp_data(icmp_data,datasize); 初始化ICMP首部
//
//显示提示信息
//
fprintf(stdout,"\nPinging %s %d bytes\n\n",dest_ip,datasize - sizeof(IcmpHeader));
//
// 开始发送/接受ICMP数据包
//
for(int i=0;i< times;i++){
int bwrote;
//初始化ICMP首部
((IcmpHeader*)icmp_data)->icmp_cksum = 0; //校验和置零
((IcmpHeader*)icmp_data)->icmp_data = GetTickCount();
((IcmpHeader*)icmp_data)->icmp_seq = seq_no++; //序列号++
((IcmpHeader*)icmp_data)->icmp_cksum = checksum((USHORT*)icmp_data,datasize); //计算校验和
bwrote = sendto(sockRaw,icmp_data,datasize,0,(struct sockaddr*)&dest,sizeof(dest)); //发送数据
if (bwrote == SOCKET_ERROR){ //发送失败
if (WSAGetLastError() == WSAETIMEDOUT) {//发送超时
printf("Request timed out.\n");
continue;
}
fprintf(stderr,"sendto failed: %d\n",WSAGetLastError());
ExitProcess(STATUS_FAILED);
}
if (bwrote < datasize ) {
fprintf(stdout,"Wrote %d bytes\n",bwrote);
}
bread = recvfrom(sockRaw,recvbuf,MAX_PACKET,0,(struct sockaddr*)&from,&fromlen);
if (bread == SOCKET_ERROR){
if (WSAGetLastError() == WSAETIMEDOUT) {
printf("Request timed out.\n");
continue;
}
fprintf(stderr,"recvfrom failed: %d\n",WSAGetLastError());
ExitProcess(STATUS_FAILED);
}
if(!decode_resp(recvbuf,bread,&from))
statistic++; // 成功接收的数目++
Sleep(1000);
}
//
//显示统计信息
//
fprintf(stdout,"\nPing statistics for %s \n",dest_ip);
fprintf(stdout," Packets: Sent = %d,Received = %d, Lost = %d (%2.0f%% loss)\n",times,
statistic,(times-statistic),(float)(times-statistic)/times*100);
WSACleanup();
return 0;
}
//
//收到的是IP包. 我们必须解码IP报头,找到ICMP数据
//
int decode_resp(char *buf, int bytes,struct sockaddr_in *from) {
IpHeader *iphdr;
IcmpHeader *icmphdr;
unsigned short iphdrlen;
iphdr = (IpHeader *)buf; //获取IP报文首地址
iphdrlen = (iphdr->ip_hl) * 4 ; //因为h_len是32位word,要转换成bytes必须*4
//word 字
//byte 字节
//bit 位
//1word=2byte
//1byte=8bit
if (bytes < iphdrlen + ICMP_MIN) { //回复报文长度小于IP首部长度与ICMP报文最小长度之和,此时ICMP报文长不含 icmp_data
printf("Too few bytes from %s\n",inet_ntoa(from->sin_addr));
}
icmphdr = (IcmpHeader*)(buf + iphdrlen); //越过ip报头,指向ICMP报头
//确保所接收的是我所发的ICMP的回应
if (icmphdr->icmp_type != ICMP_ECHOREPLY) { //回复报文类型不是请求回显
fprintf(stderr,"non-echo type %d recvd\n",icmphdr->icmp_type);
return 1;
}
if (icmphdr->icmp_id != (USHORT)GetCurrentProcessId()) { //回复报文进程号是否匹配
fprintf(stderr,"someone else's packet!\n");
return 1;
}
printf("%d bytes from %s:",bytes - iphdrlen - sizeof(IcmpHeader), inet_ntoa(from->sin_addr));
printf(" icmp_seq = %d. ",icmphdr->icmp_seq);
if(GetTickCount()-icmphdr->icmp_data < 1)
{
printf(" time: < 1 ms ");
}
else
{
printf(" time: %d ms ",GetTickCount()-icmphdr->icmp_data);
}
printf(" ttl: %d ",iphdr->ip_ttl);
printf("\n");
return 0;
}
//计算ICMP首部校验和
USHORT checksum(USHORT *buffer, int size) {
unsigned long cksum=0;
//把ICMP报头二进制数据以2字节(16bit)为单位累加起来
while(size >1) {
cksum+=*buffer++;
size -= 2;
}
//若ICMP报头为奇数个字节,会剩下最后一字节。把最后一个字节视为一个2字节数据的高字节,这个2字节数据的低字节为0,继续累加
if(size) {
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff); //高16bit和低16bit相加
cksum += (cksum >>16); //可能有进位情况,高16bit和低16bit再加1次
return (USHORT)(~cksum); //将该16bit的值取反,存入校验和字段
}
//
//设置ICMP报头
//
void fill_icmp_data(char * icmp_data, int datasize){
IcmpHeader *icmp_hdr;
char *datapart;
icmp_hdr = (IcmpHeader*)icmp_data;
icmp_hdr->icmp_type = ICMP_ECHO; // ICMP报文类型为请求回显
icmp_hdr->icmp_code = 0;
icmp_hdr->icmp_id = (USHORT)GetCurrentProcessId(); //获取当前的进程id
icmp_hdr->icmp_cksum = 0;
icmp_hdr->icmp_seq = 0;
datapart = icmp_data + sizeof(IcmpHeader); //跳过IcmpHeader
//
// Place some junk in the buffer.
//
memset(datapart,'E', datasize - sizeof(IcmpHeader)); //填充datapart中的所有字节为"E",长度为ICMP报文数据段长度
}
USHORT checksum(USHORT *buffer, int size)
校验和算法――这一算法称为网际校验和算法,把被校验的数据16位(16bit)进行累加,然后取反码,若数据字节长度为奇数,则数据尾部补一个字节的0以凑成偶数。此算法适用于IPv4、ICMPv4、IGMPV4、ICMPv6、UDP和TCP校验和,校验和字段为上述ICMP数据结构的icmp_cksum变量。
ICMP协议对于网络安全具有极其重要的意义。ICMP协议本身的特点决定了它非常容易被用于攻击网络上的路由器和主机。例如,可以利用操作系统规定的ICMP数据包最大尺寸不超过64KB这一规定,向主机发起“Ping of Death”(死亡之Ping)攻击。“Ping of Death” 攻击的原理是:如果ICMP数据包的尺寸超过64KB上限时,主机就会出现内存分配错误,导致TCP/IP堆栈崩溃,致使主机死机。(现在的操作系统已经取消了发送ICMP数据包的大小的限制,解决了这个漏洞)
此外,向目标主机长时间、连续、大量地发送ICMP数据包,也会最终使系统瘫痪。大量的ICMP数据包会形成“ICMP风暴”,使得目标主机耗费大量的CPU资源处理,疲于奔命。
int socket(int domain, int type, int protocol);
domain指定地址族,通常为AF_INET,表示互联网协议族(TCP/IP协议族)
type参数指定socket的类型,目前有如下几种:
SOCK_STREAM
SOCK_DGRAM
SOCK_RAW
SOCK_STREAM对应TCP协议,
SOCK_DGRAM对应UDP协议。
SOCK_RAW提供对internal network interfaces(内部网络接口)的访问,只有特权程序才能使用。对应IP协议、ICMP协议等等。
protocol指定用于这个socket的特定的协议(通常赋值0)。一般在特定的协议族特定的type下对应了单一的protocol。然而如多协议存在,则必须明确指定。/* Protocols */
#define IPPROTO_IP 0 /* dummy for IP */
#define IPPROTO_ICMP 1 /* control message protocol */
#define IPPROTO_IGMP 2 /* group control protocol */
#define IPPROTO_GGP 3 /* gateway^2 (deprecated) */
#define IPPROTO_ENCAP 4 /* IP in IP encapsulation */
#define IPPROTO_TCP 6 /* tcp */
#define IPPROTO_EGP 8 /* exterior gateway protocol */
#define IPPROTO_PUP 12 /* pup */
#define IPPROTO_UDP 17 /* user datagram protocol */
#define IPPROTO_IDP 22 /* xns idp */
#define IPPROTO_HELLO 63 /* "hello" routing protocol */
#define IPPROTO_ND 77 /* UNOFFICIAL net disk proto */
#define IPPROTO_EON 80 /* ISO clnp */
#define IPPROTO_RAW 255 /* raw IP packet */
#define IPPROTO_MAX 256