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

编码在现实网络中的应用、单字节 XOR 的加密与爆破、DH 密钥协商的最小实现。
一、现实背景:编码每天都在用
编码(Encoding)不依赖密钥,任何掌握规则的人都能还原;加密(Encryption)依赖密钥。日常网络中的编码:
| 编码 | 现实应用 | 例子 |
|---|---|---|
| Base64 | 邮件附件(MIME)、JWT 令牌、Data URI | ZmxhZw== → 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 流程
目的:理解双方如何在"只看得到公开数值"的情况下得到同一个秘密。
思路:秘密不在信道上传输,而是双方各自用"自己的私钥 + 对方的公钥"算出同一个数。窃听者缺少任一个私钥,就算不出来。
- 双方公开约定大素数
p与生成元g。 - Alice 选私钥
a,发送A = g^a mod p。 - Bob 选私钥
b,发送B = g^b mod p。 - 双方分别计算
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 深入实战》。