EP03:一份合法报文,为什么还能被重新利用?
一份曾经合法的数据包,如果被攻击者保存下来,过一段时间重新发送,接收方还能认出它是一份“旧数据”吗?
在前面的实验中,我们已经看到两个现象:
IP 通信能够正常建立,并不意味着通信过程天然安全;
数据包中的 Source IP,也不能单独证明真正的发送者是谁。
但攻击者并不一定需要修改数据,也不一定需要破解通信内容。
有时候,他只需要做一件更简单的事情:
把以前截获过的一份合法报文,再发送一次。
这就是:
Replay Attack,重放攻击。
对于普通 IP 通信来说,一份过去发送过的合法报文,如果再次进入网络,看起来仍然只是一个普通的数据包。
IP 协议本身并不会自动判断:
这个数据包以前是不是已经处理过?
本实验就来验证这一点。
一、实验目标
假设 Alice 向 Bob 合法发送一条业务指令:
MSGID=EP03-001;CMD=Open Valve
Bob 第一次正常收到并处理。
随后,这份数据包被保存下来。
过一段时间以后:
Alice
没有再次发送命令
但 Eve 将之前保存的数据包重新发送出去。
我们要观察:
Bob 是否会再次收到完全相同的业务数据?
实验过程可以概括成:
Alice
│
│ 合法发送一次
▼
Bob
│
│ 保存历史报文
▼
ep03-original.pcap
│
│ 交给 Eve
▼
Kali
│
│ Replay
▼
Bob
如果 Bob 再次收到:
MSGID=EP03-001;CMD=Open Valve
那么至少可以说明:
普通 IP/UDP 通信自身并没有维护一套用来识别历史报文的防重放安全状态。
二、实验环境
完全复用 EP02 的三台虚拟机。
| 角色 | 操作系统 | IP 地址 | 作用 |
|---|---|---|---|
| Alice | Rocky Linux | 192.168.106.130 | 合法发送一次业务报文 |
| Kali / Eve | Kali Linux | 192.168.106.131 | 重放历史报文 |
| Bob | Rocky Linux | 192.168.106.132 | 接收并观察数据 |
实验网络仍然为:
192.168.106.0/24
拓扑:
VMware 实验网络
192.168.106.0/24
Alice Kali / Eve Bob
.130 .131 .132
│ │ │
│ 合法发送 │ │
└────────────────────────────────>│
│ │
│ Replay │
└───────────────>│
本实验继续遵循整个 IPSec 实验体系的原则:
在能够清楚展示机制的前提下,尽量复用已有环境,不增加额外虚拟机和网络组件。
三、为什么使用 UDP
本实验使用 UDP,而不是 TCP。
原因很简单。
TCP 自己已经包含:
Sequence Number
ACK
连接状态
重传机制
会话生命周期
如果直接使用 TCP,很容易把:
TCP 的连接状态
和:
IPSec 的防重放机制
混在一起。
UDP 更简单:
Alice
│
│ UDP Payload
▼
Bob
我们只需要观察:
一份相同的 IP/UDP 数据报再次进入 Bob 后,应用程序会不会再次收到它。
四、实验边界:我们不研究 Eve 如何截获数据
这里需要先说明一个重要边界。
三台虚拟机连接在 VMware 虚拟交换网络中。
正常情况下:
Alice → Bob
的单播数据,并不意味着旁边的 Kali 一定可以直接被动抓到。
如果为了真正实现:
Eve 截获 Alice → Bob
再加入:
ARP Spoofing
MITM
混杂模式
实验重点就会从:
Replay
变成:
如何截获数据
因此 EP03 主动把两个问题拆开。
本实验不验证:
Eve 是怎样获得这份历史报文的。
只验证:
Eve 一旦已经拥有一份合法历史报文,能不能再次利用它。
具体做法是:
Alice 正常发送
↓
Bob 抓包保存
↓
PCAP 复制给 Kali
↓
模拟 Eve 已经获得历史报文
↓
Kali Replay
这样整个实验只保留一个变量:
历史报文被重新发送。
五、确认实验网络
首先确认三台机器仍然使用 EP02 的网络配置。
Alice:
ip route get 192.168.106.132
Kali:
ip route get 192.168.106.132
Bob:
ip route get 192.168.106.130
重点确认真正使用的实验接口。
例如 Kali:
192.168.106.132 dev eth1 src 192.168.106.131
说明:
实验接口 = eth1
Alice 测试 Bob:
ping -c 3 192.168.106.132
确认通信正常。
六、Bob 启动 UDP 接收程序
Bob 上运行:
python3 -u - <<'PY'
import socket
from datetime import datetime
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(
("192.168.106.132", 9999)
)
print("Bob listening on 192.168.106.132:9999")
while True:
data, addr = sock.recvfrom(65535)
print(
datetime.now().strftime("%H:%M:%S"),
"FROM",
f"{addr[0]}:{addr[1]}",
"DATA:",
data.decode(errors="replace")
)
PY
正常情况下:
Bob listening on 192.168.106.132:9999
Bob 开始等待 UDP 数据。
如果 firewalld 阻止该端口,可以临时放行:
sudo firewall-cmd --add-port=9999/udp
实验结束后可以删除:
sudo firewall-cmd --remove-port=9999/udp
无需永久保存。
七、Bob 准备保存第一份合法报文
另外打开一个 Bob 终端。
先确认实验接口:
ip route get 192.168.106.130
假设结果为:
dev ens37
开始抓取 Alice 发来的第一份 UDP 报文:
sudo tcpdump -i ens37 -nn -s0 -c 1 \
-w /tmp/ep03-original.pcap \
'udp and src host 192.168.106.130 and dst host 192.168.106.132 and dst port 9999'
其中:
-c 1
表示:
捕获第一份符合条件的报文后自动停止。
此时 tcpdump 等待 Alice 发送。
八、Alice 合法发送一次业务指令
Alice 执行:
python3 - <<'PY'
import socket
ALICE_IP = "192.168.106.130"
BOB_IP = "192.168.106.132"
SOURCE_PORT = 40000
DESTINATION_PORT = 9999
message = b"MSGID=EP03-001;CMD=Open Valve"
sock = socket.socket(
socket.AF_INET,
socket.SOCK_DGRAM
)
sock.bind(
(ALICE_IP, SOURCE_PORT)
)
sock.sendto(
message,
(BOB_IP, DESTINATION_PORT)
)
print("Alice sent:", message.decode())
sock.close()
PY
Alice 应该看到:
Alice sent: MSGID=EP03-001;CMD=Open Valve
这次发送固定使用:
Source IP = 192.168.106.130
Source Port = 40000
Destination IP = 192.168.106.132
Destination Port = 9999
这里有一个非常重要的实验条件:
Alice 只发送这一次。
后面不会再次运行这段程序。
九、Bob 第一次收到合法报文
查看 Bob 的 UDP 接收终端。
应该出现类似:
21:40:01 FROM 192.168.106.130:40000 DATA: MSGID=EP03-001;CMD=Open Valve
此时:
Alice 合法发送次数 = 1
Bob 接收次数 = 1
这是正常通信。
Alice
192.168.106.130
│
│ MSGID=EP03-001
│ CMD=Open Valve
▼
Bob
192.168.106.132
十、检查已经保存的历史报文
由于 tcpdump 使用:
-c 1
捕获完成后应该已经自动退出。
检查:
ls -lh /tmp/ep03-original.pcap
读取:
sudo tcpdump -nn -vvv -X \
-r /tmp/ep03-original.pcap
应该可以看到:
192.168.106.130.40000
>
192.168.106.132.9999
Payload 中还能看到:
MSGID=EP03-001;CMD=Open Valve
现在这份 PCAP 代表的是:
一份曾经真实由 Alice 发送,而且 Bob 已经接收过的合法历史报文。
这点非常重要。
我们并不是重新伪造一份看起来一样的数据。
而是直接保存原来的那一份。
十一、把历史报文交给 Eve
接下来模拟:
Eve 已经获得这份历史流量。
Bob:
sudo chmod 644 /tmp/ep03-original.pcap
Kali:
scp <Bob用户名>@192.168.106.132:/tmp/ep03-original.pcap .
确认:
ls -lh ep03-original.pcap
也可以在 Kali 读取:
tcpdump -nn -X -r ep03-original.pcap
应该再次看到:
192.168.106.130:40000
↓
192.168.106.132:9999
Payload:
MSGID=EP03-001;CMD=Open Valve
现在实验状态是:
Alice
已经完成一次合法发送
并且不再继续发送
Kali / Eve
已经获得:
ep03-original.pcap
Bob
已经处理过:
MSGID=EP03-001
接下来只改变一个条件:
Eve 将旧报文重新发出去。
十二、Kali 准备 Replay
确认 Scapy:
python3 -c "import scapy; print(scapy.__version__)"
如果没有:
sudo apt update
sudo apt install -y python3-scapy
Scapy 的作用不是重新构造业务内容。
而是:
读取 PCAP
↓
拿出原来的 IP 数据包
↓
重新发送
十三、Eve 重放历史报文
Kali 执行:
sudo python3 - <<'PY'
from scapy.all import rdpcap, IP, send
packets = rdpcap("ep03-original.pcap")
pkt = packets[0]
if IP not in pkt:
raise RuntimeError("No IPv4 packet found")
historical_packet = pkt[IP]
print("Eve is replaying:")
print(historical_packet.summary())
send(
historical_packet,
verbose=False
)
print("Replay finished")
PY
这里值得特别注意:
Kali 没有重新输入:
MSGID=EP03-001;CMD=Open Valve
Alice 也没有再次运行发送程序。
真正发生的是:
ep03-original.pcap
↓
读取历史报文
↓
提取原来的 IP Packet
↓
重新发送
也就是:
Capture
↓
Store
↓
Replay
这才是本实验真正要观察的动作。
十四、Bob 再次收到相同业务数据
回到 Bob 的 UDP 接收终端。
第一次已经存在:
21:40:01 FROM 192.168.106.130:40000 DATA: MSGID=EP03-001;CMD=Open Valve
Kali 执行 Replay 后,如果 Bob 又出现:
21:40:25 FROM 192.168.106.130:40000 DATA: MSGID=EP03-001;CMD=Open Valve
那么实验已经成功。
Bob 最终看到:
21:40:01
FROM 192.168.106.130:40000
DATA: MSGID=EP03-001;CMD=Open Valve
21:40:25
FROM 192.168.106.130:40000
DATA: MSGID=EP03-001;CMD=Open Valve
两份数据拥有完全相同的:
Source IP
192.168.106.130
相同的:
Source Port
40000
相同的:
MSGID
EP03-001
甚至相同的:
CMD
Open Valve
但它们是在两个不同时间进入 Bob 的。
更重要的是:
第二份业务数据根本不是 Alice 再次产生的。
它来自 Kali 对历史数据包的重放。
十五、把实验结果放在一起看
整个过程实际上只有三步。
第一步:
Alice
│
│ 合法报文
▼
Bob
MSGID=EP03-001
CMD=Open Valve
第二步:
合法报文
↓
保存为:
ep03-original.pcap
第三步:
Kali / Eve
│
│ Replay 历史报文
▼
Bob
MSGID=EP03-001
CMD=Open Valve
最终:
Alice 真正发送:
1 次
但:
Bob 收到相同业务数据:
2 次
这就是整个 EP03 最关键的实验结果。
十六、为什么 Bob 没有自动认出“这是一份旧报文”
我们故意在 Payload 中加入:
MSGID=EP03-001
从人的角度看:
第一次:
MSGID=EP03-001
第二次:
MSGID=EP03-001
显然值得怀疑。
但问题是:
IP 协议不知道 MSGID 是什么。
在 IP 看来:
MSGID=EP03-001
只是 Payload 中的一串字节。
普通 IP 协议不会自动建立:
已经收到 EP03-001
↓
保存状态
↓
再次收到 EP03-001
↓
识别为 Replay
↓
Drop
Bob 当前运行的简单 UDP 应用同样没有实现这样的业务状态。
所以第二份数据进入 Bob 后:
IP Packet
↓
UDP
↓
9999 端口
↓
应用程序
仍然会被正常交付。
十七、这不是在证明“UDP 不安全”
这里必须控制实验结论。
本实验不能简单总结成:
UDP 不支持防重放,所以 UDP 不安全。
更准确的说法应该是:
普通 IP/UDP 通信自身没有提供类似 IPSec 的通用防重放安全状态。
应用程序当然可以自己实现防重放。
例如设计:
Message ID
Timestamp
Nonce
Transaction ID
Session State
假设应用第一次处理:
MSGID=EP03-001
以后保存:
EP03-001
=
Already Processed
第二次再次出现:
EP03-001
↓
已经处理
↓
Reject
完全可以。
但这个机制属于:
应用层自己设计的防重放逻辑。
不是普通 IP 协议自动提供的。
十八、重放与“用户主动执行两次”不是一回事
还有一个容易混淆的问题。
假设合法用户真的执行了两次:
Open Valve
这是不是攻击?
不一定。
例如:
第一次业务请求
↓
合法
第二次用户主动操作
↓
也可能合法
这是业务系统需要判断的问题。
Replay 关注的是另一种情况:
合法用户只发送一次
↓
攻击者捕获历史数据
↓
攻击者重新利用
所以:
防重放不是简单地禁止两个相同字符串出现。
真正需要判断的是:
这份受保护的报文是不是以前已经被接收和处理过。
这也是后面 IPSec 防重放机制需要解决的问题。
十九、为什么安全通信需要专门的防重放状态
现在我们还没有使用 IPSec。
但通过 EP03 已经能够提前推导出一个需求:
即使以后做到:
攻击者看不懂数据
甚至:
攻击者不能修改数据
如果攻击者仍然可以:
捕获一份合法报文
↓
保存
↓
原样重新发送
那么安全问题依然存在。
这意味着真正的安全通信除了:
机密性
完整性
身份认证
还需要:
防重放
也就是说,接收方必须拥有某种状态,用来判断:
这个受保护的数据包
以前是否已经接收过?
至于 IPSec 具体怎么做到这一点,现在还不展开。
等进入 ESP 后,可以重新设计一个对应实验。
届时就可以形成非常直观的对照。
二十、普通通信与 IPSec 的对照
EP03 当前验证的是:
普通 IP / UDP
合法报文
↓
Capture
↓
Store
↓
Replay
↓
Bob 再次接收
后面进入 ESP 后,则可以验证:
IPSec ESP
合法 ESP 报文
↓
Capture
↓
Store
↓
Replay
↓
Anti-Replay Check
↓
识别历史报文
↓
Drop
这时读者再看到:
Sequence Number
Replay Window
就不会觉得它们只是突然出现的协议字段。
而会知道:
这些机制是为了处理 EP03 中已经真实观察到的问题。
实验总结
本实验没有破解通信,没有修改 Payload,也没有重新生成一条新的业务命令。
只做了三件事情:
Alice 合法发送一次
↓
保存历史报文
↓
Kali 重放历史报文
最终:
Alice 合法发送次数:
1
而:
Bob 接收到:
MSGID=EP03-001;CMD=Open Valve
2 次
第二次并不是 Alice 再次产生的业务请求。
而是历史报文被重新利用。
因此我们验证了:
一份报文曾经合法,并不意味着它在未来再次出现时仍然应该被信任。
普通 IP 协议负责:
把数据包送到目的地
但它本身并不负责:
判断这个数据包
是不是以前已经处理过的历史报文
因此:
能够正常接收数据
≠
能够识别 Replay
或者更容易记住:
普通 IP 只知道“这个数据包现在到了”,并不知道“这个数据包以前是不是来过”。
而这,正是安全通信还必须额外解决的另一个问题:
防重放。