CTF 密码学入门:编码、XOR 与 DH 密钥协商

CTF 密码学入门

编码在现实网络中的应用、单字节 XOR 的加密与爆破、DH 密钥协商的最小实现。

14th Aug 2026

5 min read

一、现实背景:编码每天都在用

编码(Encoding)不依赖密钥,任何掌握规则的人都能还原;加密(Encryption)依赖密钥。日常网络中的编码:

编码现实应用例子
Base64邮件附件(MIME)、JWT 令牌、Data URIZmxhZw== → flag
Hex颜色值 #FF0000、MAC 地址、哈希摘要666c6167 → flag
URL 编码浏览器地址栏中的中文与特殊字符%66%6c%61%67 → flag

先运行最小的解码例子:

import base64
# base64 模块:标准库,提供 base64 编码解码函数。

print(base64.b64encode(b"flag"))
# b64encode(data):把字节串编码成 base64,参数是待编码的 bytes,
# 返回值是编码结果 bytes:b'ZmxhZw=='。

print(base64.b64decode("ZmxhZw=="))
# b64decode(s):把 base64 字符串解码回原始字节,参数可以是 str 或 bytes,
# 返回值 b'flag'。这一步用于解开题目给的 base64 密文。

print(bytes.fromhex("666c6167"))
# bytes.fromhex(hex_str):把十六进制字符串逐对转成字节,
# 参数 "666c6167" 对应 b'flag'。用于解开题目给的 hex 密文。

print(b"flag".hex())
# bytes.hex():反过来,把字节转成十六进制字符串 "666c6167"。

实际输出:

b'ZmxhZw=='
b'flag'
b'flag'
666c6167

URL 编码:

from urllib.parse import quote, unquote

print(quote("你好"))                    # %E4%BD%A0%E5%A5%BD
print(unquote("%66%6c%61%67"))          # flag

实际输出:

%E4%BD%A0%E5%A5%BD
flag

二、环境准备

pip3 install pycryptodome sympy gmpy2

浏览器打开 CyberChef(https://gchq.github.io/CyberChef)。

三、核心知识 1:XOR 与单字节爆破

异或的性质:A ^ B = C,且 C ^ B = A。同一个密钥异或两次就还原,所以加密和解密是同一个运算。

3.1 加密

目的:先用已知明文和密钥生成密文,验证"异或加密"的写法,同时为后面爆破提供测试对象。

plain = b"flag{xor_is_easy}"
key = 0x42   # 密钥是单个字节(0-255 的整数),这里选 0x42

cipher = bytes([b ^ key for b in plain])
# 列表推导式:对 plain 的每个字节 b,做 b ^ key(按位异或),
# bytes(...) 把结果整数列表转回字节串。这就是单字节 XOR 加密。
print(cipher.hex())
# .hex():把密文字节串转成十六进制字符串,方便在题目里粘贴/对比。

实际输出:

242e2325393a2d301d2b311d2723313b3f

3.2 爆破(密钥只有 256 种可能)

目的:不知道密钥时,把 256 种可能全部试一遍。

思路:如果密钥猜对了,解密结果会是一段正常文本;CTF 里 flag 固定以 flag{ 开头,这就是天然的判据。用 b"flag{" in plain 过滤即可。

cipher = bytes.fromhex("242e2325393a2d301d2b311d2723313b3f")
# bytes.fromhex(...):把题目给的十六进制密文还原成字节串。

for k in range(256):
    # 枚举 0-255 共 256 个可能的单字节密钥。
    plain = bytes([b ^ k for b in cipher])
    # 对每个密钥尝试解密:每个密文字节与 k 异或。
    # 异或的自反性保证:加密用 key,解密用同一个 key。
    if b"flag{" in plain:
        # 如果解密结果包含 flag 前缀,说明密钥猜对了。
        print("key =", k)           # 输出密钥数值
        print(plain.decode())       # 输出解密出的明文
        break
        # 找到即停止,避免继续打印无意义结果。

实际输出:

key = 66
flag{xor_is_easy}

判断依据:明文包含 flag{ 前缀。CTF 里 flag 都有固定格式,这就是天然的校验。

3.3 多字节 XOR 简介

密钥是多个字节循环使用时,先猜密钥长度,再对每个位置分别做单字节分析。工具 xortool:

pip install xortool
xortool -x -c 20 cipher.bin

四、核心知识 2:凯撒密码

每个字母平移固定位数:

def caesar(s, shift):
    out = ""
    for c in s:
        if c.isalpha():
            base = ord('a') if c.islower() else ord('A')
            out += chr((ord(c) - base + shift) % 26 + base)
        else:
            out += c
    return out

print(caesar("flag{caesar_is_fun}", 3))    # 加密
print(caesar("iodj{fdhvdu_lv_ixq}", -3))   # 解密

实际输出:

iodj{fdhvdu_lv_ixq}
flag{caesar_is_fun}

五、核心知识 3:DH 密钥协商

5.1 要解决的问题

对称加密要求双方共享同一密钥,但互联网上的双方从未见过面,信道还可能被窃听。DH(Diffie-Hellman)解决"在公开信道协商共享密钥"。

5.2 流程

目的:理解双方如何在"只看得到公开数值"的情况下得到同一个秘密。

思路:秘密不在信道上传输,而是双方各自用"自己的私钥 + 对方的公钥"算出同一个数。窃听者缺少任一个私钥,就算不出来。

  1. 双方公开约定大素数 p 与生成元 g。
  2. Alice 选私钥 a,发送 A = g^a mod p。
  3. Bob 选私钥 b,发送 B = g^b mod p。
  4. 双方分别计算 B^a mod p 与 A^b mod p,得到相同值 g^(ab) mod p。

5.3 最小实现

p, g = 0xffffffffffffffc5, 2
# p:双方公开约定的素数(演示用 60 位,实际协议要求 2048 位以上)。
# g:生成元,双方公开约定。
a, b = 1234567, 7654321
# a 是 Alice 的私钥,b 是 Bob 的私钥——这两个值绝不能公开。

A = pow(g, a, p)
# pow(g, a, p) = g^a mod p,即快速幂取模。
# A 是 Alice 的"公钥",可以公开,Alice 把它发给 Bob。
B = pow(g, b, p)
# 同理,B 是 Bob 的公钥,Bob 发给 Alice。

s_alice = pow(B, a, p)
# Alice 用 Bob 的公钥 B 和自己的私钥 a 计算 (g^b)^a = g^(ab)。
s_bob = pow(A, b, p)
# Bob 用 Alice 的公钥 A 和自己的私钥 b 计算 (g^a)^b = g^(ab)。
# 两个结果相等,这就是共享密钥。

print("A =", A)
print("B =", B)
print("Alice 计算:", s_alice)
print("Bob 计算:", s_bob)
print("一致:", s_alice == s_bob)
# 输出一致: True,说明双方在公开信道中成功协商出同一密钥。

实际输出:

A = 6842530808116395773
B = 3514199851527377648
Alice 计算: 17750252846772688935
Bob 计算: 17750252846772688935
一致: True

窃听者只能看到 p, g, A, B,要算出 a 必须求解离散对数,参数足够大时不可行。HTTPS 的 TLS 协议正是用 DH 协商密钥、用 RSA/ECDSA 证书认证身份。

六、必学工具

工具 1:CyberChef

浏览器工具,三个常用操作:

操作用法
From Base64左侧搜 Base64 拖入,粘贴密文,自动解码
From Hex拖入 From Hex 解码十六进制
Magic直接粘贴未知文本,自动猜测编码方式

工具 2:Python 标准库

import base64, binascii

print(base64.b64decode("ZmxhZw=="))   # b'flag'
print(bytes.fromhex("666c6167"))      # b'flag'
print(ord('A'))                       # 65,ASCII 编码

工具 3:pycryptodome

CTF 密码学最常用的库,三个函数:

from Crypto.Util.number import long_to_bytes, bytes_to_long, inverse

print(inverse(3, 7))                    # 5:3 在模 7 下的逆元
print(long_to_bytes(0x666c6167))        # b'flag'
print(bytes_to_long(b"flag"))           # 1718378855

实际输出:

5
b'flag'
1718378855

七、小 CTF 实战:单字节 XOR

题目:给出密文 hex,提示"密钥是一个字节,明文是 flag"。

242e2325393a2d301d2b311d2723313b3f

解题脚本:

思路:密文是字节串,密钥只可能 0-255;逐个试,用 flag{ 前缀过滤正确结果。

from binascii import unhexlify
# unhexlify 与 bytes.fromhex 等价:把十六进制字符串转成字节串。

cipher = unhexlify("242e2325393a2d301d2b311d2723313b3f")

for k in range(256):
    plain = bytes([b ^ k for b in cipher])
    # 每个密文字节与候选密钥 k 异或,得到候选明文。
    if b"flag{" in plain:
        print("key =", k)          # 预期 66
        print(plain.decode())      # 预期 flag{xor_is_easy}
        break

实际输出:

key = 66
flag{xor_is_easy}

八、小结

密码学入门三点:编码识别(Base64/Hex/URL)、XOR 与单字节爆破、DH 的数学原理。这三样足以解决入门级密码题;RSA 与分组密码的误用攻击见《RSA 深入实战》。