一、实现ARP欺骗的原理:
根据ARP协议的工作原理,我们知道ARP大多时候都会发起广播请求,而处于同一局域网内的所有主机都可以收到某主机发出的ARP广播请求,利用这个工作原理我们可以接收到网络上与自己无关的ARP请求包,然后回复一个带有假的MAC地址的reply包以达到ARP欺骗的目的。
二、具体思路:
三、实现过程:
1、使用SOCK_RAW即原始套接字进行ARP数据包的抓取,然后使用recvfrom()函数获取数据包中的数据;并将请求包中的源MAC、源IP、目的IP等信息提取出来:
2、自定义一个以太网ARP协议数据帧结构体然后将相关数据填充进去,其中需要使用socket和struct ifreq结构体获取本地主机的IP。而数据填充时的源MAC则是自定义的假的MAC地址,所以不需要使用该结构体来获取本地的MAC了。在欺骗应答包中的目的MAC以及目的IP则是从广播ARP请求中提取出来的;
3、发送欺骗应答包。这里主要是使用原始套接字在二层网络进行相关的操作,所以需要用到的是struct sockaddr_ll结构体而不是我们常用的struct sockaddr_in结构体 (这里就有关于二层网络套接字操作与三层网络套接字的一些区别,在后面会进行详细讨论。)
第一次测试:程序运行并抓包分析过程如图1下:
图1
测试的时候,我在过滤条件中添加了对我自己的PC进行过滤,就是我用PC来ping虚拟机,虚拟机就能接收到PC发来的ARP广播请求,接着虚拟机自动会给PC回复一个正常的应答包,同时也会将我自定义的应答包发给PC,此时,在PC的以太网端口进行抓包就能看到一个广播的ARP包下面会有两个应答包,如下图2所示:
图2
PC的IP为192.168.2.191,虚拟机的IP为:192.168.2.224;
欺骗包中假的MAC地址为:66:66:66:66:66:66
这三个ARP数据包的具体内容分别如下:图3为PC的广播包,图4为虚拟机正常的应答包,图5为自定义的欺骗应答包;
图3
在ARP请求广播包中,源MAC为:74:4d:35:c1:8b:bb;
源IP:192.168.2.191
目的IP:192.168.2.224
图4
在虚拟机原本的应答包中,源MAC为:00:50:56:39:87:5d,这是虚拟机的MAC地址。
图5
这是自定义的欺骗ARP应答包,MAC地址为:66:66:66:66:66:66;
另外,在数据包中还能看到数据字段的信息:Cheat ARP package! 以及同一IP重复了两个MAC地址的警示信息。
第二次测试:使用另一部PC进行测试
上述测试的虚拟机是安装在测试所用的pc上的。然后再使用连接在同一局域网的其他PC进行测试,虚拟机上运行的程序能够正常给同一局域网的不同PC发送欺骗ARP应答包,所以基本实现了ARP欺骗的目的。
如下图6所示,另一部PC的IP为:192.168.2.130 MAC:fc:aa:14:35:c1:7e
图6
在PC上ping虚拟机,pc上同样抓到以下图7所示的三个包:
图7
图8
图9
上面的图8和图9分别是来自pc的ARP请求包和来自虚拟机的正常的应答包。下面图10是pc收到的来自虚拟机的ARP欺骗应答包:
图10
第三次测试:监听本地PC:192.168.2.191,只要是它发出的ARP广播,虚拟机运行的程序都会回复一个欺骗ARP应答包给它。
上述两次测试都是将虚拟机作为ARP请求包的目的进行测试,而第三次测试虚拟机则是完全充当旁观者,在本地PC与外界通信发出相关ARP广播时,给本地PC回复一个假的应答包,已达到欺骗的目的。图11是相关的抓包显示:
图11
图12
图13
图13是本地PC192.168.2.191 ping 192.168.2.17时发起的ARP广播请求,图14则是虚拟机程序回复的欺骗ARP应答包,而图15是主机192.168.2.17自己回复的应答包:
图14
图15
经过上述三次测试以及代码优化,基本能够实现ARP欺骗回复了,如果取消掉部分过滤条件,则可以对同一局域网内所有的ARP请求包进行欺骗回复干扰。
四、相关知识点讨论:
1、SOCK_RAW(原始套接字):原始套接字又分为链路层原始套接字和网络层套接字。链路层原始套接字调用socket()函数创建。第一个参数指定协议族类型为PF_PACKET,第二个参数type可以设置为SOCK_RAW或SOCK_DGRAM,第三个参数是协议类型(该参数只对报文接收有意义)。参数type设置为SOCK_RAW时,套接字接收和发送的数据都是从MAC首部开始的。在发送时需要由调用者从MAC首部开始构造和封装报文数据。
原始套接字(SOCK_RAW)与标准套接字(SOCK_STREAM、SOCK_DGRAM)的区别在于原始套接字直接置“根”于操作系统网络核心(Network Core),而 SOCK_STREAM、SOCK_DGRAM 则“悬浮”于 TCP 和 UDP 协议的外围;
流式套接字只能收发 TCP 协议的数据,数据报套接字只能收发 UDP 协议的数据,原始套接字可以收发内核没有处理的数据包。
在进行ARP欺骗的过程中所使用到的接受信息和发送信息的套接字如下:
接收信息:sock_raw_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP)
发送信息:send_sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)
这里除了使用原始套接字外,还使用了PF_PACKET协议族,用于在链路层收发原始(raw )分组。所以,地址也不再是 sockaddr_in 而是采用 sockaddr_ll 地址。
2、sockaddr_ll 地址:表示设备无关的物理层地址结构。
struct sockaddr_ll {
unsigned short sll_family; //这里使用PF_PACKET
unsigned short sll_protocol; //物理层协议
int sll_ifindex; //接口号
unsigned short sll_hatype; //报头类型
unsigned char sll_pkttype; //分组类型
unsigned char sll_halen; //地址长度
unsigned char sll_addr[8]; //物理层地址,即目的MAC地址
};
一开始以为使用socket发送数据需要绑定相关的端口和网址,所以就不断的常识将获取到的相关地址写入struct sockaddr_ll中,到后面才发现SOCK_RAW模式下可以不需要绑定MAC地址,并且不需要其进行IP地址的相关操作,因为网卡驱动程序接收到报文后会对自己组织的整个以太网数据帧进行处理,将它准确发送到目的地(此处省略一万字,具体的工作流程还在学习中)。所以我们只要确保自定义的数据帧准确无误就可以了。
因为对于二层报文发送,没有根据目的地址进行选路的依据,所以发送者必须指定要使用的出接口,sockaddr_ll.sll_ifindex就是指本地的网卡index.
所以我们就要用到结构体struct ifreq和ioctl()函数去获取本地的网卡index。
这里就有另外一个结构体struct ifreq出现了。
3、struct ifreq:这个结构定义在/usr/include/net/if.h,用来配置和获取ip地址,掩码,MTU等接口信息的。
struct ifreq
{
# define IFHWADDRLEN 6
# define IFNAMSIZ IF_NAMESIZE
union
{
char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "en0". */
} ifr_ifrn;
union
{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
__caddr_t ifru_data;
} ifr_ifru;
};
这里就需要调用ioctl()函数,相关的request经过查询资料后知道是:SIOCGIFINDEX。
另外,详细实现过程见代码,可执行文件需要在管理员权限下运行。
因为PF_PACKET协议族的套接字必须用管理员身份创建。
PF_PACKET协议族允许应用程序接收传递给自己主机的数据包,但不能接收到发送给其他主机的包
对于在二层网络上操作的原始套接字,其工作流程跟一般的标准套接字是有区别的:
五、原始套接字报文收发流程
图1 原始套接字接收流程
如上图1所示为链路层和网络层原始套接字的收发总体流程。网卡驱动收到报文后在软中断上下文中由netif_receive_skb()处理,匹配是否有注册的链路层原始套接字,若匹配上就通过skb_clone()来克隆报文,并将报文交给相应的原始套接字。对于IP报文,在协议栈的ip_local_deliver_finish()函数中会匹配是否有注册的网络层原始套接字,若匹配上就通过skb_clone()克隆报文并交给相应的原始套接字来处理。
注意:这里只是将报文克隆一份交给原始套接字,而该报文还是会继续走后续的协议栈处理流程。
图2 原始套接字发送流程
如图2所示,链路层原始套接字的发送,直接由套接字层调用packet_sendmsg()函数,最终再调用网卡驱动的发送函数。网络层原始套接字的发送实现要相对复杂一些,由套接字层调用inet_sendmsg()->raw_sendmsg(),再经过路由和邻居子系统的处理后,最终调用网卡驱动的发送函数。若注册了ETH_P_ALL类型套接字,还需要将外发报文再收回去。