skaiuijing
引言 之前介绍了ARP协议及实现。
那么ARP协议能不能被用于网络攻击呢?当然可以。
现在我们将解析ARP数据包及实现数据包重定向或者是ARP欺骗。
ARP欺骗 ARP 欺骗(又称 ARP 中间人攻击)利用 ARP 协议的无状态特性,通过伪造 ARP 报文,诱导目标主机更新 ARP 缓存,将目标 IP 与攻击者的 MAC 地址错误绑定。目标主机(通常是网关)更新缓存后,流量不再通过受害主机,而是到达攻击者设备,进而被读取、篡改或丢弃。
原理 ARP 协议不验证响应的真实性 ,这就给攻击者留下了空间:
攻击者 C 向主机 A 发送伪造的 ARP 响应:
1.IP 地址 B 的 MAC 是 C 的 MAC。
2.A 更新本地 ARP 表,将 B 的 IP 映射到 C 的 MAC。
3.后续所有发往 B 的数据都会先到 C.
利用ARP欺骗原理,这样就实现了一个数据包的重定向。
对于攻击者C来说,它 可以选择:
1.转发给真正的 B(中间人攻击)
2.丢弃(拒绝服务)
3.修改数据后再转发(数据篡改)
网上大部分都是这么介绍ARP欺骗的,但是,我们可能会有一些问题。
问题 为什么流量会到达攻击者设备?既然APR是ip层获取mac的协议,为什么更改mac后不会转发给目标ip呢?
为什么目标主机不能更新为正确的ARP缓存呢?
数据包的处理 驱动在被注册到内核时,内核会把对应的驱动结构体注册到内核中的一个表,而路由表中,则对ip与网卡结构体进行了映射。
但是,这里的网卡结构只是出接口,而不是确保被送到受害者网卡B。
过程如下:
路由查找(IP 层)
协议栈先根据目的 IP 查路由表(FIB)。
路由表项里不仅有下一跳 IP,还有明确的 出接口(net_device) 。
这一步就决定了“用哪个网口”。
在Linux6.6中,这里的还包含网卡设备以及对应的驱动,也就是下面的net_device,里面有输入、输出等等方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct fib_nh_common { struct net_device *nhc_dev; netdevice_tracker nhc_dev_tracker; int nhc_oif; unsigned char nhc_scope; u8 nhc_family; u8 nhc_gw_family; unsigned char nhc_flags; struct lwtunnel_state *nhc_lwtstate; union { __be32 ipv4; struct in6_addr ipv6; } nhc_gw; int nhc_weight; atomic_t nhc_upper_bound; /* v4 specific, but allows fib6_nh with v4 routes */ struct rtable __rcu * __percpu *nhc_pcpu_rth_output; struct rtable __rcu *nhc_rth_input; struct fnhe_hash_bucket __rcu *nhc_exceptions; };
当数据发送出去时,内核会调用net_device对应的接口。
有交换机与无交换机 现在看来,网上的教程似乎都有点问题?
明明网卡B的驱动与ip在路由表中被规定的死死的,即使发生了ARP欺骗,数据包也会被送到网卡B手上啊?
就比如以下情况:
网关路由器A有两个网口B和C,网口B连接设备B,网口C连接攻击者C。
数据包到来时,虽然发生了ARP欺骗,但是根据路由表查找,最终还是会调用网口B的驱动啊,设备B不还是会接收到数据吗?
确实会接收到,但很可惜,其实没什么用。
笔者先不说为什么没用,因为这是无交换机场景。
先看看有交换机场景,
有交换机 二层转发设备 :交换机工作在数据链路层(OSI 第二层),主要根据 MAC 地址 来转发数据帧。
假设设备B和C不是直连网关路由器A,而是中间隔了一个交换机。
那么很显然了,交换机会把数据包发给攻击者C,攻击者C大获全胜!
那么为什么即使无交换机,直接调用驱动,也没什么用呢?
特殊情况 是不是只要数据包送到了网卡B的手上,攻击就失败了呢?其实不是的。
除了无交换机情况,有些时候,交换机会出现泛洪,数据包会被传输到所有网卡上,那么,这个时候,受害者网卡B会不会成功处理报文呢?
其实也不会的。
让我们看看Linux6.6源码就知道了。
Linux内核 其实原因很简单,网络是分层的,当数据包到达Linux网络时,链路层会根据mac进行判断。
数据包接收流程 1.数据包到达网卡(硬件),由驱动接收,进入内核。
2.进入协议栈的第一步是链路层(MAC层)处理,比如以太网驱动会先检查以太网帧头(MAC地址、类型等)。
3.如果MAC地址不匹配(既不是本机、广播、组播等),数据包会被丢弃。
4.只有通过MAC层检查的数据包,才会被上交到网络层(如IP协议)。
5.网络层(IP层)会进一步检查IP头部(如目的IP、校验和等),如果不符合要求(如目的IP不是本机、校验和错误等),也会丢弃。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 __be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev) { unsigned short _service_access_point; const unsigned short *sap; const struct ethhdr *eth; skb->dev = dev; skb_reset_mac_header(skb); eth = (struct ethhdr *)skb->data; skb_pull_inline(skb, ETH_HLEN); //如果MAC地址不匹配(既不是本机、广播、多播),数据包会被丢弃 if (unlikely(!ether_addr_equal_64bits(eth->h_dest, dev->dev_addr))) { if (unlikely(is_multicast_ether_addr_64bits(eth->h_dest))) { if (ether_addr_equal_64bits(eth->h_dest, dev->broadcast)) skb->pkt_type = PACKET_BROADCAST; else skb->pkt_type = PACKET_MULTICAST; } else { skb->pkt_type = PACKET_OTHERHOST; } } /* * Some variants of DSA tagging don't have an ethertype field * at all, so we check here whether one of those tagging * variants has been configured on the receiving interface, * and if so, set skb->protocol without looking at the packet. */ if (unlikely(netdev_uses_dsa(dev))) return htons(ETH_P_XDSA); if (likely(eth_proto_is_802_3(eth->h_proto))) return eth->h_proto; /* * This is a magic hack to spot IPX packets. Older Novell breaks * the protocol design and runs IPX over 802.3 without an 802.2 LLC * layer. We look for FFFF which isn't a used 802.2 SSAP/DSAP. This * won't work for fault tolerant netware but does for the rest. */ sap = skb_header_pointer(skb, 0, sizeof(*sap), &_service_access_point); if (sap && *sap == 0xFFFF) return htons(ETH_P_802_3); /* * Real 802.2 LLC */ return htons(ETH_P_802_2); }
所以说,当ARP欺骗发生后,即使数据包真的被受到了受害者B上,也没有任何影响。
真实的数据包流程如下:
1.C主机的ARP欺骗成功,A主机更新ARP缓存
2.A主机需要给B主机发消息,直接使用了缓存中错误的mac
3.假设出现了泛洪等情况,B主机收到了A主机发来的数据包,但是此时A主机的目标mac都是被欺骗的,因此B主机检查目标mac后发现,既不是本机,也不是广播多播这些,因此B主机选择丢弃该包
4.A主机的数据包到了邪恶的C主机这里。
可以看见,不管是正常情况,还是特殊情况,只要欺骗了对应的mac,即使主机B拿到了自己数据包,也不会进行处理的。
所以,通过交换机泛洪等手段,也是没法解决ARP欺骗问题的。
既然知道了arp欺骗的原理,现在让我们尝试使用eBPF技术解析ARP协议。
eBPF解析ARP协议 ARP的字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // ARP 协议头部结构,对应 RFC 826 中的 ARP 报文格式 struct arp_hdr { unsigned short ar_hrd; // 硬件类型(Hardware Type),如 Ethernet 为 1 unsigned short ar_pro; // 协议类型(Protocol Type),如 IPv4 为 0x0800 unsigned char ar_hln; // 硬件地址长度(Hardware Address Length),Ethernet 为 6 unsigned char ar_pln; // 协议地址长度(Protocol Address Length),IPv4 为 4 unsigned short ar_op; // 操作码(Opcode),1 表示请求,2 表示响应 } __attribute__((packed)); // 禁止结构体自动对齐,确保与网络字节流格式一致 // ARP 报文的以太网封装结构,包含请求方和目标方的地址信息 struct arp_ether { struct arp_hdr ea_hdr; // ARP 报文头部 unsigned char arp_sha[6]; // Sender Hardware Address:发送方 MAC 地址 unsigned int arp_spa; // Sender Protocol Address:发送方 IP 地址 unsigned char arp_tha[6]; // Target Hardware Address:目标方 MAC 地址(请求时为 0) unsigned int arp_tpa; // Target Protocol Address:目标方 IP 地址 } __attribute__((packed)); // 同样禁止结构体对齐,确保与实际报文格式匹配
我们选择使用XDP作为挂载点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 SEC("xdp") int capture_arp(struct xdp_md *ctx) { void *data = (void *)(long)ctx->data; void *data_end = (void *)(long)ctx->data_end; struct ethhdr *eth = data; if ((void *)(eth + 1) > data_end || eth->h_proto != bpf_htons(ETH_P_ARP)) return XDP_PASS; //跳过网卡头部 struct arphdr *arp = (void *)(eth + 1); if ((void *)(arp + 1) > data_end || arp->ar_hrd != bpf_htons(ARPHRD_ETHER) || arp->ar_pro != bpf_htons(ETH_P_IP)) return XDP_PASS; //跳过ARP头部 unsigned char *arp_data = (unsigned char *)(arp + 1); if (arp_data + 2 * ETH_ALEN + 2 * sizeof(__be32) > (unsigned char *)data_end) return XDP_PASS; //解析ip unsigned char *src_mac = arp_data; __be32 src_ip = *(__be32 *)(arp_data + ETH_ALEN); unsigned char *dst_mac = arp_data + ETH_ALEN + sizeof(__be32); __be32 dst_ip = *(__be32 *)(arp_data + ETH_ALEN + sizeof(__be32) + ETH_ALEN); __be16 opcode = arp->ar_op; return XDP_PASS; }
如果我们需要构造ARP欺骗,其实可以插入一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 提取目标 IP(被欺骗者)和源 IP(我们要伪装的 IP) __be32 target_ip = *(__be32 *)(arp_data + ETH_ALEN + sizeof(__be32) + ETH_ALEN); __be32 spoof_ip = bpf_htonl(SPOOFED_IP); // 伪装的 IP 地址(例如网关) // 判断是否需要欺骗(例如目标 IP 是我们想劫持的) if (target_ip == spoof_ip) { // 伪代码:构造 ARP 响应包 // struct ethhdr new_eth; // struct arphdr new_arp; // unsigned char payload[ARP_PAYLOAD_SIZE]; // new_eth.h_proto = bpf_htons(ETH_P_ARP); // new_eth.h_dest = 原始请求者的 MAC 地址 // new_eth.h_source = 欺骗者的 MAC 地址(我们) // new_arp.ar_op = bpf_htons(ARPOP_REPLY); // new_arp.ar_pro = bpf_htons(ETH_P_IP); // new_arp.ar_hrd = bpf_htons(ARPHRD_ETHER); // new_arp.ar_pln = 4; // new_arp.ar_hln = 6; // payload = [欺骗者 MAC][spoof_ip][目标 MAC][target_ip] // 发送伪造包(XDP 无法主动发送) // 需要配合 TC 或 AF_XDP 用户态程序完成发送,我们可以在传递事件到应用层,然后应用层触发事件发送ARP欺骗 }
结语 本节介绍了ARP协议及ARP欺骗,就先写到这里吧。
下一节要讲什么来着?
想起来了,本来应该先从Linux内存管理算法和ringbuf讲起的,继续回归到数据包与内存吧。