原始套接字-raw socket
最近在研究高并发下扫描存活主机,基本想法是通过socket来模拟ICMP报文,然后就发现了socket的一片新天地----原始套接字(raw socket)。
raw socket,即原始套接字,可以接收本机网卡上的数据帧或者数据包,对于监听网络的流量和分析是很有作用的,一共可以有4种方式创建这种socket。
1.socket(PF_INET, SOCK_RAW, IPPROTO_TCP|IPPROTO_UDP|IPPROTO_ICMP)发送接收ip数据包
2.socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))发送接收以太网数据帧
3.socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))发送接收以太网数据帧(不包括以太网头部) [1]
4.socket(PF_INET, SOCK_PACKET, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))过时了,不要用啊
我们可看到通过原始套接字,我们可以拿到2层和3层的全部信息,也可以模拟二层和三层的各种协议,是不是很厉害了
所以如果要模拟ping,需要用第一种方法,
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)。
我们可以通过socket函数第三个参数,可以模拟创建各种三层的协议。后面一期将模拟TCP协议的时候,还会用到其他类型。
下面是封装之后的创建ICMP函数
/* create a socke to icmp */
int icmp_socket(char *ipv4)
{
int sockfd;
int size = ICMP_RECVBUF_SIZE;
if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
{
usleep(1000);
if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0)
{
ICMP_LOG_ERR("创建socket失败");
return -1;
}
}
//
//要过滤响应来源地址,需要这里绑定来源IP
{
struct sockaddr_in serv_addr;
unsigned long ul = 1;
ioctl(sockfd, FIONBIO, &ul); //设置为非阻塞模式
memset(&serv_addr, 0, sizeof(serv_addr)); //每个字节都用0填充
serv_addr.sin_family = AF_INET; //使用IPv4地址
serv_addr.sin_addr.s_addr = inet_addr(ipv4); //具体的IP地址
serv_addr.sin_port = htons(1234); //端口随意填写,
//将套接字和IP、端口绑定
if(connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
{
struct timeval tm;
fd_set set;
int error=-1, len;
len = sizeof(int);
tm.tv_sec = 1;
tm.tv_usec = 0;
FD_ZERO(&set);
FD_SET(sockfd, &set);
if( select(sockfd+1, NULL, &set, NULL, &tm) > 0)
{
getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, (socklen_t *)&len);
if(error == 0)
{
//ret = true;
}
else
{
ICMP_LOG_ERR("connect[%s] socket失败1",ipv4);
close(sockfd);
return -1;
}
}
else
{
ICMP_LOG_ERR("connect[%s] socket失败2",ipv4);
close(sockfd);
return -1;
}
}
else
{
//ret = true;
}
ul = 0;
ioctl(sockfd, FIONBIO, &ul); //设置为阻塞模式
}
//设置接收buf大小
if(setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size))!=0)
{
ICMP_LOG_ERR("setsockopt SO_RCVBUF error.\n\r");
close(sockfd);
return -1;
}
//设置超时时间,影响后面revform函数,让其不会一直阻塞
if(setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (char*)&icmp_wait_timeout, sizeof(struct timeval))!=0)
{
ICMP_LOG_ERR("setsockopt SO_RCVTIMEO error.\n\r");
close(sockfd);
return -1;
}
return sockfd;
}
这个函数中比较重要的点,在于要过滤响应来源地址,需要这里绑定来源IP,这句话的含义就是我们创建的Raw socket,是比较松散的过滤,如果不绑定来源IP,那么所有的ICMP报文我们都收到,如果网络环境比较干净,倒是无所谓,但是如果是复杂的环境,里面有其他ICMP报文的话,会干扰到我们分析响应结果。
下面这段话,是对于Raw Socket的一段非常重要的话
当内核有一个需要传递到原始套接字的IP数据报时,它将检查所有进程上的原始套接字,以寻找所有匹配的套接字。每个匹配懂得套接字将被递送以该iP数据报的一个副本。(事实证明,如果进程过多,匹配的套接字过多,内核会忙于数据报的软过滤和分发,而实际的套接字却空闲,导致性能下降)
内核对每个原始套接字均执行如下3个测试,只有这三个测试为真,内核才把接收到的数据报递送到这个套接字。
【1】如果创建这个套接字时制订了非0的协议参数(socket的第三个参数),那么接受到的数据报的协议字段必须匹配该值,否则数据报不递送到这个套接字
【2】如果这个原始套接字已由bind调用绑定了某个本地IP地址,那么接受到的数据报的目的IP地址必须匹配这个绑定的地址,否则该数据报不递送到这个套接字。
【3】如果这个原始套接字已由connect调用指定了某个外地IP地址,那么接受到的数据报的源IP地址必须匹配这个已连接地址,否则该数据报不递送到这个套接字。
bind和connect用的不恰当的话,直接影响你接收数据,处理不好,要么收的多,要么收不到。
下面是各个模块的功能函数。
计算校验码函数
unsigned short icmp_gen_chksum(unsigned short * data, int len)
{
int nleft = len;
int sum = 0;
unsigned short *w = data;
unsigned short answer = 0;
while (nleft > 1)
{
sum += *w++;
nleft -= 2;
}
if (nleft == 1)
{
* (unsigned char *) (&answer) = *(unsigned char *)w;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = ~sum;
return answer;
}
生成ICMP报文函数,这里注意如果前面没有绑定IP地址,那么也可以通过在此处配置icmp_id的方式来进行区分,这个ID是每一次操作会往返传递与两地之间,所以可以作为身份验证用。
int icmp_pkg_pack(void *buffer,const void *data, int data_size)
{
int packsize = 0;
struct icmp * icmp = malloc(sizeof(struct icmp));
icmp->icmp_type = ICMP_ECHO;
icmp->icmp_code = 0;
icmp->icmp_cksum = 0;
icmp->icmp_seq = htons(0);
icmp->icmp_id = 0xff00;
gettimeofday((struct timeval *) &icmp->icmp_data, NULL);
memcpy(buffer, icmp, sizeof(struct icmp));
packsize += sizeof(struct icmp);
if(data && data_size)
{
memcpy(buffer+packsize, data, data_size);
packsize += data_size;
}
return packsize;
}
发送ICMP报文函数,这个校验和的函数经过测试,没有任何问题
/* send icmp package */
int icmp_send_pkg(char* ipv4,int socket, const void *data, int size)
{
int packetsize;
unsigned short checksum = 0;
int n = 0;
struct sockaddr_in dst_addr;
char pkg_buffer[ICMP_BUF_SIZE];
packetsize = icmp_pkg_pack(pkg_buffer, data, size);
checksum = icmp_gen_chksum((unsigned short *)pkg_buffer, packetsize);
icmp_dst_addr(ipv4, &dst_addr);
memcpy(pkg_buffer + ICMP_PKG_CHKSUM_OFFSET, &checksum, ICMP_PKG_CHKSUM_SIZE);
if ((n = sendto(socket, pkg_buffer, packetsize, 0, (struct sockaddr *) &dst_addr, sizeof(struct sockaddr_in)))< 0)
{
ICMP_LOG_ERR("发送ICMP失败n = %d", n);
return 0;
}
return n;
}
接收ICMP报文函数,Revice函数在前面的socket定义的超时时间之后会自动返回,保证用户进程不会一直阻塞。
int icmp_recv_pkg(int socket, void *recvbuf, int size)
{
int n;
socklen_t fromlen;
struct sockaddr_in from_addr;
fromlen = sizeof(struct sockaddr_in);
if((n = recvfrom(socket, recvbuf, size, 0, (struct sockaddr *) &from_addr, &fromlen)) < 0)
{
ICMP_LOG_ERR("recvfrom error.n = %d\n\r", n);
return 0;
}
return n;
}
解析ICMP报文函数,这里主要是判断了有没有收到响应,还可以通过时间戳,计算经过了多少时间到达
(这里没写)
int icmp_pkg_unpack(char * buf, int len)
{
int iphdrlen;
struct ip * ip = NULL;
struct icmp * icmp = NULL;
ip = (struct ip *)buf;
iphdrlen = ip->ip_hl << 2;
icmp = (struct icmp *) (buf + iphdrlen);
len -= iphdrlen;
if (len < 8)
{
ICMP_LOG_ERR("ICMP packet\'s length is less than 8\n\r");
return - 1;
}
if (icmp->icmp_type != ICMP_ECHOREPLY)
{
return - 1;
}
return 0;
}
主函数测试ping函数和一些定义,还有一些核心头文件,基础头文件请自己添加
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#ifndef offsetof
#define offsetof(type, member) ((int) & ((type*)0) -> member )
#endif
#define ICMP_BUF_SIZE 256
#define ICMP_RECVBUF_SIZE (50 * 1024)
#define ICMP_PROTO_NAME "icmp"
#define ICMP_DATA "my ICMP Ping"
#define ICMP_PING_SUCC __ICMP_PING_SUCC
#define ICMP_PING_FAIL __ICMP_PING_FAIL
#define ICMP_LOG(fmt...) printf(fmt);
#define ICMP_LOG_ERR(fmt...) printf(fmt);
#define ICMP_PKG_CHKSUM_OFFSET offsetof(struct icmp, icmp_cksum)
#define ICMP_PKG_CHKSUM_SIZE 2
typedef enum
{
__ICMP_PING_FAIL = 0,
__ICMP_PING_SUCC,
}icmp_ping_rlst_t;
static struct timeval icmp_wait_timeout = {0,100000}; //sec
int icmp_ping_fun(char *ipv4)
{
int res=__ICMP_PING_FAIL;
char pkg_buffer[ICMP_BUF_SIZE];
int ping_socket = 0;
ping_socket =icmp_socket(ipv4);
if(ping_socket ==NULL)
{
return __ICMP_PING_FAIL;
}
else
{
int n=0;
n = icmp_send_pkg(ipv4,ping_socket, ICMP_DATA, sizeof(ICMP_DATA));
if(n < 8)
{
close(ping_socket );
return __ICMP_PING_FAIL;
}
else
{
n = icmp_recv_pkg(ping_socket, pkg_buffer, ICMP_BUF_SIZE);
if(n<8)
{
close(ping_socket );
return __ICMP_PING_FAIL;
}
else
{
if(icmp_pkg_unpack(pkg_buffer, n)==0)
{
close(ping_socket );
return __ICMP_PING_SUCC;
}
else
{
close(ping_socket );
return __ICMP_PING_FAIL;
}
}
}
}
}
这种写法,配合上多线程,同时ping65535个IP,3秒就能得到全部结果。
最后感谢这些人的奉献,参考了人家的代码,进行了一些改动。
C语言实现ICMP协议,并进行PING测试
raw_socket(原始套接字)以及普通socket使用终极总结
下一章会再次介绍一下利用原始套接字模拟tcp通讯,来进行远端端口扫描。
前一阵为了给孩子提供一个可以玩土的地方,和邻居合租的大棚,小孩子看到土的想法就是
结果发现还是我们这些农村出身的大人更喜欢这种种地的感觉。
从旁边大棚的废土里面捡到了几个草莓植株,居然开花了
希望大家以后能像这株草莓一样,被喜欢你的人发现。