跳转到内容

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。

它会:

  1. 接收 Alice 和 Bob 真正的 Public Key;

  2. 不把真实 Public Key 原样转发给对方;

  3. 为 Alice 和 Bob 分别准备一套自己的密钥;

  4. Alice 请求 Bob Public Key 时,返回 Eve 的 Public Key;

  5. Bob 请求 Alice Public Key 时,同样返回 Eve 的 Public Key;

  6. 分别与 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 地址作用
AliceRocky Linux192.168.106.130以为自己正在和 Bob 建立共享秘密
Kali / EveKali Linux192.168.106.131替换双方 Public Key
BobRocky Linux192.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 正是把密钥交换、身份认证、参数协商以及后续安全状态建立结合起来的关键机制之一。