原始套接字

概述

在WinSock的通信模型中,Socket可以分为两种类型:SOCK_STREAM和SOCK_DGRAM。前者又称为流式套接字,传输的是字节流,传输的数据没有边界,底层使用面向连接的TCP协议;后者是数据报套接字,传输的是数据报,底层使用的是面向非连接的UDP协议。这两种类型的WinSock处于应用层,只能使用预先定义好的协议及数据格式,虽然能够满足大部分网络应用程序的需求,但无法自定义传输协议格式。因此WinSock提供了原始套接字:SOCK_RAW,它让程序员可以自定义协议的首部,实现自己的传输协议。

ping

简介

最早的原始套接字是由Mike John Muuss的Ping源代码演变而来的,他也因此获得了USENIX协会1993年颁发的“终身成就奖”。
直接上代码

实例

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

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

//IP首部,20bytes
typedef struct
{
	unsigned char ip_verslen;//版本和首部长度Version,Headr length,4+4bit
	unsigned char ip_tos;//服务类型Type of Service,8bit
	unsigned short ip_totallen;//数据报的总长度Total length,16bit
	unsigned short ip_id;//标识符Unique identifier
	unsigned short ip_frag;//标志和片偏移Fragment offset field
	unsigned char ip_ttl;//生存时间TTL,8bit
	unsigned char ip_proto;//协议Protocol,8bit
	unsigned short ip_checksum;//校验和,16bit
	unsigned int ip_sour;//源IP地址Source IP address,32bit
	unsigned int ip_dest;//目的IP地址Target IP address,32bit
} IPv4_HDR, * PIPv4_HDR;

//ICMP首部,12bytes
typedef struct
{
	unsigned char icmp_type;//ICMP包类型,8bit
	unsigned char icmp_code;//代码,8bit
	unsigned short icmp_checksum;//校验和,16bit
	unsigned short icmp_id;//标识符,16bit
	unsigned short icmp_seq;//序列号,16bit	
	unsigned long timestamp;//时间戳,32bit
} ICMP_HDR, * PICMP_HDR;

PICMP_HDR picmp = NULL;
char Sendbuff[sizeof(ICMP_HDR) + 100] = { 0 };//要发送的ICMP包缓存,其中100为要发送的数据,这里均设置为0
char Recvbuff[0x1000] = { 0 };//接收ICMP返回信息缓存

//校验和计算
//通过对buf进行计算,得到对应的16位的结果,用于校验数据在传输过程中是否被篡改
unsigned short checksum(unsigned short* buf, int size)
{
	unsigned long checksum = 0;

	while (size > 1)
	{
		checksum += *buf++;
		size -= sizeof(unsigned short);
	}

	if (size)
	{
		checksum += *(unsigned char*)buf;
	}

	checksum = (checksum >> 16) + (checksum & 0xffff);
	checksum += (checksum >> 16);

	return (unsigned short)(~checksum);
}

int main()
{
	WORD wVersionRequested = MAKEWORD(2, 2);//版本
	WSADATA wsaDATA;

	//打开网络库
	if (WSAStartup(wVersionRequested, &wsaDATA) != 0)
	{
		printf("打开网络库失败!\n");
		return -1;
	}

	SOCKET sock = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);//创建RAW socket

	hostent* phost = gethostbyname("www.baidu.com");//获取百度域名对应信息

	sockaddr_in addr;

	for (int i = 0; (phost->h_addr_list[i]); i++)//一个域名可能会对应多个IP,所有IP信息都打印出来
	{
		addr.sin_family = AF_INET;
		addr.sin_port = htons(0);//端口设置为ICMP用的0号号
		addr.sin_addr.S_un.S_addr = *(u_long*)phost->h_addr_list[i];//只取最后一个IP作为ping的目标

		printf("%s\n",inet_ntoa(addr.sin_addr));
	}

	//自定义ICMP包
	picmp = (PICMP_HDR)Sendbuff;
	picmp->icmp_type = 8;//请求ICMP echo
	picmp->icmp_code = 0;
	picmp->icmp_id = GetCurrentProcessId();//获取当前进程ID
	picmp->timestamp = GetTickCount();//获取当前经过时间(从开机计算,单位为毫秒)
	picmp->icmp_checksum = checksum((unsigned short *)Sendbuff, sizeof(ICMP_HDR) + 100);

	int re=0; 
	re = sendto(sock,Sendbuff, sizeof(ICMP_HDR) + 100,0,(SOCKADDR *)&addr,sizeof(addr));//发送ICMP请求
	if (re == SOCKET_ERROR)
	{
		printf("MYPING发送消息失败,错误码是: %d\n", WSAGetLastError());
	}

	struct sockaddr_in remote_addr;
	int remote_len = sizeof(remote_addr);
	re = recvfrom(sock, Recvbuff, 0x1000, 0, (struct sockaddr*)&remote_addr, &remote_len);
	
	PIPv4_HDR pipdata = (PIPv4_HDR)Recvbuff;//转IP数据包
	PICMP_HDR picmpdata = (PICMP_HDR)(Recvbuff+20);//将缓存偏移20(IP首部大小),转ICMP数据包

	//输出响应信息
	unsigned long trip_time;
	int buf_len;
	trip_time = GetTickCount() - picmpdata->timestamp;//计算相应时间
	buf_len = ntohs(pipdata->ip_totallen) - 20 - 12;//数据长度相当于IP包长度去掉IP首部和ICMP首部大小

	printf("%d bytes from %s:", buf_len, inet_ntoa(addr.sin_addr));
	printf(" icmp_seq = %d  time: %d ms\n", picmpdata->icmp_seq, trip_time);

	closesocket(sock);//关闭Socket句柄
	WSACleanup();//关闭网络库
	return 0;
}

注意事项

1.原始套接字发送数据的总长度不得超过65535(ip_totallen),这里数据的总长度与首部长度的关系如下图:
在这里插入图片描述
这里的ICMP由于加入了时间戳,因此比标准ICMP的首部要多4个字节,标准ICMP的首部大小为8字节。
关于发送数据总长度不能大于65535的原因是ping命令早期在发送大于65535的数据报时,需要分片才能发出去,目标机器在重组这些分片时会出现内存溢出、系统崩溃等现象,这种攻击也被称为死亡之Ping(Ping of Death),因此现在对ping命令发送数据包的大小做了限制。
除此之外,如果主机在短时间内收到大量的Ping数据包,会导致网络拥塞,系统变慢,形成拒绝服务攻击(DoS)。
2.由于Ping命令的ICMP请求会带来安全风险,因此一些服务器设置为不响应ICMP请求,或者直接使用防火墙对ICMP请求数据包进行了拦截,因此当ping一个服务器的返回结果是目标不可达时,并不意味着该服务器不在线或者输入的IP地址有误。
3.创建原始套接字要使用SOCK_RAW,原始套接字能自定义协议格式,因此也带来了很多安全隐患,如果当前Windows操作系统在默认环境下,无法使用recvfrom接收到ICMP数据包,则可能需要进行以下配置:
①需要使用管理员身份登录操作系统;
②关闭用户账户控制(UAC, User Account Control)
点击:开始菜单→运行→输入msconfig
在这里插入图片描述
单击【确定】后在系统配置窗口选择【工具】选项卡,并选择【更改UAC设置】,然后单击【启动】
在这里插入图片描述
在用户账户控制设置窗口中,拖动设置滑块到最下面,并单击【确认】即可。
在这里插入图片描述
③在Windows防火墙设置中增加入站规则,允许ICMPv4通过。

Tracert

简介

Tracert命令可以让我们查询数据包从本地主机到远程主机经过了哪些路径,该命令是在Linux/UNIX下的版本是Traceroute,Traceroute是1998年由Van Jacobson编写的,他还设计了TCP/IP的流控制演算法,解决了TCP/IP的拥塞控制问题。
在这里插入图片描述
Tracert命令的原理是向目标主机发送具有不同大小的生存时间的数据包,根据ICMP的回复消息来确定本地主机到目标主机的路由路径。具体流程如下:
步骤①设置生存时间(TTL)为1的数据包,从本地主机向目标主机发出,若目标主机与本地主机在同一个局域网,无需路由转发,则直接返回:
在这里插入图片描述
若目标主机与本地主机不在同一个局域网,则数据包经过路由器向目标主机转发,生存时间每经过一个路由器值会减1,当值为0后,路由器会丢弃改数据包,返回当前路由器IP地址信息。
步骤②设置生存时间(TTL)为1的数据包,从本地主机向目标主机发出,数据包经过2个路由器向目标主机转发,且生存时间减2后为0,返回第2个路由信息。
步骤③将生存时间不断加1并从本地主机向目标主机发出,在生存时间为0后返回最后一个路由器信息,直到到达目标主机为止或生存时间达到上限30。
注意:
路由器设置了不响应ICMP请求,或者超过了数据包的过期时间则会显示请求超时。
本地主机每次会发送三个ICMP数据包,因此每条记录都会显示三个时间。
由于tracert的本质也是发送ICMP请求和接收ICMP回应,因此可以在ping程序的基础上进行改进,增加对生成时间的设置即可。

实例

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<WS2tcpip.h>
#include<winsock2.h>
#include<stdio.h>

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

//IP首部,20bytes
typedef struct
{
	unsigned char ip_verslen;//版本和首部长度Version,Headr length,4+4bit
	unsigned char ip_tos;//服务类型Type of Service,8bit
	unsigned short ip_totallen;//数据报的总长度Total length,16bit
	unsigned short ip_id;//标识符Unique identifier
	unsigned short ip_frag;//标志和片偏移Fragment offset field
	unsigned char ip_ttl;//生存时间TTL,8bit
	unsigned char ip_proto;//协议Protocol,8bit
	unsigned short ip_checksum;//校验和,16bit
	unsigned int ip_sour;//源IP地址Source IP address,32bit
	unsigned int ip_dest;//目的IP地址Target IP address,32bit
} IPv4_HDR, * PIPv4_HDR;

//ICMP首部,12bytes
typedef struct
{
	unsigned char icmp_type;//ICMP包类型,8bit
	unsigned char icmp_code;//代码,8bit
	unsigned short icmp_checksum;//校验和,16bit
	unsigned short icmp_id;//标识符,16bit
	unsigned short icmp_seq;//序列号,16bit	
	unsigned long timestamp;//时间戳,标准ICMP包没有这个成员,32bit
} ICMP_HDR, * PICMP_HDR;

PICMP_HDR picmp = NULL;
char Sendbuff[sizeof(ICMP_HDR) + 100] = { 0 };//要发送的ICMP包缓存,其中100为要发送的数据,这里均设置为0
char Recvbuff[0x1000] = { 0 };//接收ICMP返回信息缓存

//校验和计算
//通过对buf进行计算,得到对应的16位的结果,用于校验数据在传输过程中是否被篡改
unsigned short checksum(unsigned short* buf, int size)
{
	unsigned long checksum = 0;

	while (size > 1)
	{
		checksum += *buf++;
		size -= sizeof(unsigned short);
	}

	if (size)
	{
		checksum += *(unsigned char*)buf;
	}

	checksum = (checksum >> 16) + (checksum & 0xffff);
	checksum += (checksum >> 16);

	return (unsigned short)(~checksum);
}

//设置生存时间
int setttl(SOCKET s, int ttl)
{
	int	re = setsockopt(s, IPPROTO_IP, IP_TTL, (char*)&ttl, sizeof(ttl));	

	if (SOCKET_ERROR == re)
	{
		printf("设置TTL失败,错误码为:%d", WSAGetLastError());
	}
	return re;

}

int main()
{
	WORD wVersionRequested = MAKEWORD(2, 2);//版本
	WSADATA wsaDATA;

	//打开网络库
	if (WSAStartup(wVersionRequested, &wsaDATA) != 0)
	{
		printf("打开网络库失败!\n");
		return -1;
	}

	SOCKET sockRaw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);//创建RAW socket

	hostent* phost = gethostbyname("www.baidu.com");//获取百度域名对应信息

	sockaddr_in addr;

	for (int i = 0; (phost->h_addr_list[i]); i++)//一个域名可能会对应多个IP,所有IP信息都打印出来
	{
		addr.sin_family = AF_INET;
		addr.sin_port = htons(0);//端口设置为ICMP用的0号号
		addr.sin_addr.S_un.S_addr = *(u_long*)phost->h_addr_list[i];//只取最后一个IP作为ping的目标

		//printf("%s\n", inet_ntoa(addr.sin_addr));
	}
	printf("Tracert to %s\n", inet_ntoa(addr.sin_addr));

	//自定义ICMP包
	picmp = (PICMP_HDR)Sendbuff;
	picmp->icmp_type = 8;//请求ICMP echo
	picmp->icmp_code = 0;
	picmp->icmp_id = GetCurrentProcessId();//获取当前进程ID
	picmp->timestamp = GetTickCount64();//获取当前经过时间(从开机计算,单位为毫秒)
	picmp->icmp_checksum = checksum((unsigned short*)Sendbuff, sizeof(ICMP_HDR) + 100);

	int re = 0;
	//设置接收的最长等待时间
	unsigned int  timeout = 2000;
	re = setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO,(char*)&timeout, sizeof(int));
	if (SOCKET_ERROR == re)
	{
		printf("设置超时时间失败,错误码为: %d\n", WSAGetLastError());
		return -1;
	}

	int ttlcount = 1;//生存时间
	while (TRUE)
	{
		setttl(sockRaw, ttlcount);

		re = sendto(sockRaw, Sendbuff, sizeof(ICMP_HDR) + 100, 0, (SOCKADDR*)&addr, sizeof(addr));//发送ICMP请求
		if (re == SOCKET_ERROR)
		{
			printf("MyTracert发送消息失败,错误码是: %d\n", WSAGetLastError());
		}

		sockaddr_in remote_addr;
		int remote_len = sizeof(remote_addr);
		
		re = recvfrom(sockRaw, Recvbuff, 0x1000, 0, (struct sockaddr*)&remote_addr, &remote_len);//接收返回的ICMP响应

		if (SOCKET_ERROR == re)
		{
			if (WSAGetLastError() == WSAETIMEDOUT)//打印超时信息
			{
				printf("TTL = %d 响应超时\n", ttlcount);
				ttlcount++;
				continue;
			}
			printf("接收数据出错,错误码为: %d\n", WSAGetLastError());
			ttlcount++;
			continue;
		}
		printf("TTL = %d from %s\n", ttlcount, inet_ntoa(remote_addr.sin_addr));//打印获取到的路由器IP地址
		ttlcount++;

		if ((remote_addr.sin_addr.S_un.S_addr == addr.sin_addr.S_un.S_addr)|(ttlcount == 30))//到达目标或者超过30跳则退出循环
			break;
	}

	closesocket(sockRaw);//关闭Socket句柄
	WSACleanup();//关闭网络库
	return 0;
}

运行结果与Windows自带Tracert命令对比:
在这里插入图片描述

注意事项

1.若在运行时所有接收ICMP响应的操作都超时,则应关闭Windows的公共网络的防火墙。在这里插入图片描述
2.每次运行Tracert的结果可能会不一样,由于网络路径上的拥塞情况,数据在传输过程中可能会选择不同的路由进行通信。

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

oldmao_2000

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值