EP05:有了共同秘密,对面就一定可信吗?
在 EP04 中,我们已经验证了一件非常重要的事情:
Alice 和 Bob 不需要直接发送最终 Shared Secret,也能够通过交换公开信息,在本地分别计算出相同的共享秘密。
这个结果解决了一个问题:
如何在不可信网络中建立共同秘密?
但如果继续追问,就会发现其中还隐藏着一个没有解决的问题。
Alice 收到了一个 Public Key。
Bob 也收到了一个 Public Key。
可是:
Alice 怎么知道这个 Public Key 一定属于 Bob?
Bob 又怎么知道:
自己收到的 Public Key 一定属于 Alice?
如果攻击者位于通信双方之间,并且能够替换这些公开信息,那么密钥交换过程是否仍然可信?
这就是本次实验要验证的问题。
一、实验目标
本实验继续沿用 EP04 的 X25519 密钥交换过程。
区别只有一个:
在 EP04 中,Kali 只是一个老老实实转发 Public Key 的中转节点。
本实验中,Kali 将从旁观者变成主动攻击者 Eve。
它会:
接收 Alice 和 Bob 真正的 Public Key;
不把真实 Public Key 原样转发给对方;
为 Alice 和 Bob 分别准备一套自己的密钥;
Alice 请求 Bob Public Key 时,返回 Eve 的 Public Key;
Bob 请求 Alice Public Key 时,同样返回 Eve 的 Public Key;
分别与 Alice 和 Bob 建立两个不同的 Shared Secret。
最终我们希望验证:
Alice Shared Secret
=
Eve 与 Alice 的 Shared Secret
以及:
Bob Shared Secret
=
Eve 与 Bob 的 Shared Secret
但:
Alice Shared Secret
≠
Bob Shared Secret
这说明:
密钥交换能够建立共同秘密,但它本身并不能证明通信对端的真实身份。
二、实验环境
本实验继续复用前面实验中的三台虚拟机。
| 角色 | 操作系统 | IP 地址 | 作用 |
|---|---|---|---|
| Alice | Rocky Linux | 192.168.106.130 | 以为自己正在和 Bob 建立共享秘密 |
| Kali / Eve | Kali Linux | 192.168.106.131 | 替换双方 Public Key |
| Bob | Rocky Linux | 192.168.106.132 | 以为自己正在和 Alice 建立共享秘密 |
继续使用:
OpenSSL
Python 3
curl
tcpdump
不增加新的虚拟机,也不引入 IKE 或 IPSec 配置。
三、实验过程
整个攻击过程可以简化为:
Alice Kali / Eve Bob
│ │ │
│──── Alice Public ────────>│ │
│ │<──────── Bob Public ─────│
│ │ │
│ │ 不转发真实 Public Key │
│ │ │
│<──── Eve Public A ────────│ │
│ │──── Eve Public B ───────>│
│ │ │
│ Alice Private │ Bob Private │
│ + │ + │
│ Eve Public A │ Eve Public B│
│ ↓ │ ↓ │
│ Secret AE │ Secret BE │
│ │ │
从 Alice 的角度来看:
我拿到了 Bob Public Key
从 Bob 的角度来看:
我拿到了 Alice Public Key
但实际上:
Alice 拿到的是 Eve Public A
Bob 拿到的是 Eve Public B
四、重新生成 Alice 和 Bob 的密钥
为了避免 EP04 和 EP05 的文件混在一起,本实验重新建立目录并生成新的密钥。
Alice
创建目录:
mkdir -p ~/ep05
cd ~/ep05
生成 Alice Private Key:
openssl genpkey \
-algorithm X25519 \
-out alice_private.pem
导出 Alice Public Key:
openssl pkey \
-in alice_private.pem \
-pubout \
-out alice_public.pem
检查:
ls -l
应当看到:
alice_private.pem
alice_public.pem
Bob
Bob 上执行:
mkdir -p ~/ep05
cd ~/ep05
生成 Bob Private Key:
openssl genpkey \
-algorithm X25519 \
-out bob_private.pem
导出 Bob Public Key:
openssl pkey \
-in bob_private.pem \
-pubout \
-out bob_public.pem
检查:
ls -l
应当看到:
bob_private.pem
bob_public.pem
至此:
Alice:
Alice Private
Alice Public
Bob:
Bob Private
Bob Public
两边的 Private Key 仍然只保存在本地。
五、Eve 生成两套密钥
EP04 中 Kali 不需要自己的密钥。
但本实验不同。
Eve 想分别欺骗 Alice 和 Bob,因此我们为两个方向各准备一套密钥。
在 Kali:
mkdir -p ~/ep05
cd ~/ep05
第一套:用于欺骗 Alice
生成:
openssl genpkey \
-algorithm X25519 \
-out eve_alice_private.pem
导出公钥:
openssl pkey \
-in eve_alice_private.pem \
-pubout \
-out eve_alice_public.pem
第二套:用于欺骗 Bob
生成:
openssl genpkey \
-algorithm X25519 \
-out eve_bob_private.pem
导出:
openssl pkey \
-in eve_bob_private.pem \
-pubout \
-out eve_bob_public.pem
检查:
ls -l
此时 Kali 应当拥有:
eve_alice_private.pem
eve_alice_public.pem
eve_bob_private.pem
eve_bob_public.pem
这就是本实验建议保留的第一张截图。
它说明:
Eve 已经为 Alice 和 Bob 两个方向分别准备了自己的密钥材料。
六、把 EP04 的 Relay 改成恶意 Relay
EP04 中 Relay 的行为非常简单:
收到什么
就保存什么
对方请求什么
就原样返回什么
EP05 中,我们故意修改它的行为。
Alice 和 Bob 上传自己的 Public Key 时:
正常保存
但双方下载对端 Public Key 时:
不返回真实 Public Key
而是返回 Eve Public Key
在 Kali:
cd ~/ep05
nano malicious_relay.py
写入:
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
BASE = Path("captured")
BASE.mkdir(exist_ok=True)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
data = self.rfile.read(length)
if self.path == "/alice_public.pem":
path = BASE / "alice_public.pem"
elif self.path == "/bob_public.pem":
path = BASE / "bob_public.pem"
else:
self.send_error(404)
return
path.write_bytes(data)
print(f"[+] Captured real key: {path}")
self.send_response(201)
self.end_headers()
self.wfile.write(b"stored\n")
def do_GET(self):
# Alice thinks she is downloading Bob's public key
if self.path == "/bob_public.pem":
data = Path("eve_alice_public.pem").read_bytes()
print(
"[!] Alice requested Bob Public Key "
"-> sending Eve Public A"
)
# Bob thinks he is downloading Alice's public key
elif self.path == "/alice_public.pem":
data = Path("eve_bob_public.pem").read_bytes()
print(
"[!] Bob requested Alice Public Key "
"-> sending Eve Public B"
)
else:
self.send_error(404)
return
self.send_response(200)
self.send_header(
"Content-Type",
"application/x-pem-file"
)
self.send_header(
"Content-Length",
str(len(data))
)
self.end_headers()
self.wfile.write(data)
server = HTTPServer(("0.0.0.0", 8000), Handler)
print("[+] EP05 malicious relay listening on 0.0.0.0:8000")
server.serve_forever()
启动:
python3 malicious_relay.py
正常情况下看到:
[+] EP05 malicious relay listening on 0.0.0.0:8000
从这一刻开始:
Kali 已经不再是一个透明中转节点。
七、Alice 和 Bob 上传真实 Public Key
Alice:
cd ~/ep05
上传:
curl \
--data-binary @alice_public.pem \
http://192.168.106.131:8000/alice_public.pem
返回:
stored
Kali:
[+] Captured real key: captured/alice_public.pem
Bob:
cd ~/ep05
上传:
curl \
--data-binary @bob_public.pem \
http://192.168.106.131:8000/bob_public.pem
Kali:
[+] Captured real key: captured/bob_public.pem
到这里 Eve 已经得到了:
Alice Public
Bob Public
但攻击真正关键的部分还没有发生。
八、Alice 请求 Bob Public Key
Alice 仍然执行与 EP04 几乎一样的操作:
curl \
http://192.168.106.131:8000/bob_public.pem \
-o bob_public.pem
从 Alice 本机看:
bob_public.pem
文件已经正常下载。
所以 Alice 很容易认为:
我已经获得了 Bob 的 Public Key。
但是观察 Kali 的恶意 Relay:
[!] Alice requested Bob Public Key -> sending Eve Public A
真正发生的是:
Alice 请求:
Bob Public
↓
Kali 截获
↓
返回:
Eve Public A
所以 Alice 本地这个叫作:
bob_public.pem
的文件,实际上装的是:
Eve Public A
九、Bob 请求 Alice Public Key
Bob:
curl \
http://192.168.106.131:8000/alice_public.pem \
-o alice_public.pem
Bob 同样认为:
alice_public.pem
属于 Alice。
但 Kali 日志显示:
[!] Bob requested Alice Public Key -> sending Eve Public B
实际上:
Bob 请求:
Alice Public
↓
Kali 截获
↓
返回:
Eve Public B
因此 Bob 本地所谓:
alice_public.pem
实际内容是:
Eve Public B
十、关键攻击证据
此时观察 Kali 的 Relay 日志,可以同时看到:
[!] Alice requested Bob Public Key -> sending Eve Public A
[!] Bob requested Alice Public Key -> sending Eve Public B
这就是本实验建议保留的第二张截图。
两张截图已经能够把 Eve 的攻击准备和 Public Key 替换过程完整展示出来:
第一张:
Eve 生成两套密钥
第二张:
Eve 实际替换双方 Public Key
后面的 Shared Secret 验证不再要求单独截图。
十一、现在三台机器分别拥有什么
在继续计算之前,可以先把当前状态整理一下。
Alice
Alice 认为自己拥有:
Alice Private
Alice Public
Bob Public
但实际上是:
Alice Private
Alice Public
Eve Public A
Bob
Bob 认为自己拥有:
Bob Private
Bob Public
Alice Public
实际上是:
Bob Private
Bob Public
Eve Public B
Eve
Eve 则拥有:
Alice Public
Bob Public
Eve Private A
Eve Public A
Eve Private B
Eve Public B
此时的密钥关系已经彻底改变。
十二、Alice 计算自己的 Shared Secret
Alice 不知道自己的 Public Key 已经被替换。
因此仍然按照正常过程计算:
openssl pkeyutl \
-derive \
-inkey alice_private.pem \
-peerkey bob_public.pem \
-out alice_shared.bin
计算 Hash:
sha256sum alice_shared.bin
Alice 会认为:
alice_shared.bin
是:
Alice 与 Bob 的 Shared Secret
但实际上参与计算的是:
Alice Private
+
Eve Public A
因此得到的是:
Secret AE
十三、Bob 计算自己的 Shared Secret
Bob 同样不知道发生了攻击。
执行:
openssl pkeyutl \
-derive \
-inkey bob_private.pem \
-peerkey alice_public.pem \
-out bob_shared.bin
查看:
sha256sum bob_shared.bin
参与 Bob 计算的实际上是:
Bob Private
+
Eve Public B
因此结果是:
Secret BE
如果比较 Alice 和 Bob 的 Hash,会发现:
Alice Shared Secret
≠
Bob Shared Secret
这和 EP04 的结果已经完全不同。
十四、Eve 计算与 Alice 相同的 Shared Secret
Kali 在 Alice 上传 Public Key 时已经将真正的 Alice Public 保存到:
captured/alice_public.pem
因此执行:
openssl pkeyutl \
-derive \
-inkey eve_alice_private.pem \
-peerkey captured/alice_public.pem \
-out eve_alice_shared.bin
然后:
sha256sum eve_alice_shared.bin
如果将这个结果与 Alice 的:
sha256sum alice_shared.bin
进行比较,会发现:
Alice Shared Secret
=
Eve-A Shared Secret
这说明 Alice 真正建立秘密的对象不是 Bob。
而是 Eve。
十五、Eve 再计算与 Bob 相同的 Shared Secret
Kali 同样保存了 Bob 真正的 Public Key:
captured/bob_public.pem
执行:
openssl pkeyutl \
-derive \
-inkey eve_bob_private.pem \
-peerkey captured/bob_public.pem \
-out eve_bob_shared.bin
计算:
sha256sum eve_bob_shared.bin
与 Bob 的:
sha256sum bob_shared.bin
比较。
应当得到:
Bob Shared Secret
=
Eve-B Shared Secret
因此完整关系变成:
Alice
│
│ Secret AE
│
Eve
│
│ Secret EB
│
Bob
而不是:
Alice
│
│ Secret AB
│
Bob
十六、实验结果
EP04 中:
Alice Shared
=
Bob Shared
但是 EP05 中,由于 Eve 替换了 Public Key:
Alice Shared
=
Eve-A Shared
同时:
Bob Shared
=
Eve-B Shared
但:
Alice Shared
≠
Bob Shared
也就是说:
Alice 和 Bob 都成功完成了 Shared Secret 计算,但他们并没有与彼此建立同一个 Shared Secret。
从 Alice 的视角来看:
密钥交换成功
从 Bob 的视角来看:
密钥交换也成功
真正的问题是:
他们没有能力证明收到的 Public Key 究竟属于谁。
十七、为什么密钥交换无法解决身份问题
X25519 负责完成的是数学计算。
对于 Alice 来说,只要拥有:
Alice Private
+
某个 Public Key
就可以计算 Shared Secret。
但算法不会自动判断:
这个 Public Key
究竟属于 Bob
还是属于 Eve
因此:
能够成功计算 Shared Secret,只能说明对方拥有与那个 Public Key 对应的 Private Key。
它不能自动证明:
这个人就是我原本希望通信的 Bob。
这就是密钥交换与身份认证之间最重要的区别。
密钥交换解决的是:
如何建立共同秘密。
身份认证解决的是:
和我建立共同秘密的人到底是谁。
二者相关,但不是同一个问题。
十八、为什么这是中间人攻击
本实验中 Eve 最终掌握了两套不同的共享秘密:
Alice ←→ Eve
Secret AE
以及:
Eve ←→ Bob
Secret EB
所以真正形成的是:
Alice
↓
Eve
↓
Bob
Alice 并没有和 Bob 直接形成密码学上的共享秘密。
如果继续在这些 Shared Secret 上派生业务数据加密密钥,那么 Eve 就可以分别参与两侧的加密通信。
因此这种攻击被称为:
Man-in-the-Middle,MITM,中间人攻击。
本实验没有继续实现业务数据解密和重新加密。
因为做到这里,我们已经能够证明本次实验真正关心的问题:
未经过身份认证的密钥交换,可能被中间人劫持。
继续增加 AES、业务数据转发等操作,只会让实验偏离核心目标。
十九、EP04 与 EP05 放在一起看
这两个实验实际上构成了一组非常完整的对照实验。
EP04
条件:
Kali 原样转发双方 Public Key
结果:
Alice Shared
=
Bob Shared
得到结论:
双方不需要直接发送最终 Shared Secret,也能够建立共同秘密。
EP05
只改变一个条件:
Kali 替换双方 Public Key
结果变成:
Alice Shared = Eve-A Shared
Bob Shared = Eve-B Shared
得到新的结论:
建立 Shared Secret 并不等于确认了通信对端的身份。
因此整个逻辑链变成:
需要保护通信
↓
需要加密
↓
需要共同秘密
↓
不能直接发送最终秘密
↓
需要密钥交换
↓
EP04:
双方可以建立 Shared Secret
↓
但是 Public Key 可以被替换
↓
EP05:
中间人也能分别建立 Shared Secret
↓
密钥交换不能证明身份
↓
需要身份认证
二十、实验结论
本实验中,Alice 和 Bob 都没有发现明显的“密钥交换失败”。
双方都正常:
生成了自己的 Private Key;
获得了一个所谓的对端 Public Key;
完成了 Shared Secret 派生。
但由于 Eve 替换了双方 Public Key,真正形成的是:
Alice ←→ Eve ←→ Bob
而不是:
Alice ←→ Bob
这说明:
成功建立共同秘密,并不能自动证明通信对端就是预期的对象。
密钥交换解决的是:
如何建立共同秘密。
但它还缺少一个关键能力:
如何确认参与密钥交换的对端身份。
所以,在真正的安全通信系统中,密钥交换不能孤立存在。
还必须加入身份认证机制,例如:
预共享秘密;
数字证书;
其他能够证明通信身份的可信凭据。
到这里,我们又自然逼出了安全通信体系中的下一个组成部分:
身份认证。
而在 IPSec 中,IKE 正是把密钥交换、身份认证、参数协商以及后续安全状态建立结合起来的关键机制之一。