文章总结: 文档解析了CTF中XOR加密的已知明文攻击,分析了已知前缀、零填充及密钥循环复用等缺陷。文章演示了利用已知明文恢复密钥并解密Flag的过程,阐述了XOR原理与攻击策略,并给出了使用标准算法、避免密钥重用及安全填充的建议。 综合评分: 100 文章分类: CTF,漏洞分析,代码审计
XOR加密的已知明文攻击深度解析
原创
破镜安全
破镜安全
2026年1月14日 08:00 四川
XOR加密的已知明文攻击深度解析
前言
在CTF密码学竞赛中,XOR加密是一个经典的考点。虽然XOR运算本身非常简单,但如果使用不当,会留下严重的安全漏洞。本文将通过一道实战题目,深入剖析XOR加密的工作原理、安全缺陷以及如何利用已知明文攻击来破解密文。
题目分析
题目文件
题目提供了一个Python加密脚本task.py:
from secret import flag
from os import urandom
assert flag.startswith(b'XMan{' ) and flag.endswith(b'}')
key=urandom(16)
def padding(m):
l = 16 - (len(m) % 16)
return m+(b'\x00'*l)
flag=padding(flag)+urandom(10)
enc=b''
for i in range(len(flag)):
enc+=(key[i%16]^flag[i]).to_bytes(1,'big')
print(enc.hex())
# d8db4398596f9123f9b70d6847ad6e14e1ef5ff6220cfe4f96c5520731c81c78a645cdd1aa9b95e54468
初步观察
从代码中我们可以提取以下关键信息:
- flag格式约束:flag必须以
XMan{开头,以}结尾 - 密钥生成:使用
urandom(16)生成16字节的随机密钥 - 填充机制:使用
\x00字节将flag填充到16字节的整数倍 - 随机数据:在填充后的flag末尾添加10字节随机数据
- 加密方式:使用16字节密钥循环与明文进行XOR运算
- 密文输出:十六进制格式的密文字符串
密文结构分析
给定的密文为:
d8db4398596f9123f9b70d6847ad6e14e1ef5ff6220cfe4f96c5520731c81c78a645cdd1aa9b95e54468
让我们先分析密文的长度和结构:
enc_hex = "d8db4398596f9123f9b70d6847ad6e14e1ef5ff6220cfe4f96c5520731c81c78a645cdd1aa9b95e54468"
enc = bytearray.fromhex(enc_hex)
print(f"密文总长度: {len(enc)} 字节")
# 输出: 密文总长度: 42 字节
42字节的密文可以分为三个部分:
- 第一部分(0-15字节):前16字节,对应第一个密钥循环周期
- 第二部分(16-31字节):中间16字节,对应第二个密钥循环周期
- 第三部分(32-41字节):最后10字节,这是添加的随机数据
通过分析可知,实际需要解密的内容是前32字节(两个完整的16字节块),最后10字节是随机数据,不影响flag的恢复。
XOR加密原理
XOR运算基础
XOR(异或)是一种位运算,符号为⊕。其真值表如下:
| A | B | A ⊕ B | | — | — | — | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 |
简单来说:相同为0,不同为1。
XOR运算的重要性质
XOR运算具有以下数学性质,这些性质是密码分析的关键:
- 可逆性:
A ⊕ B = C则A ⊕ C = B且B ⊕ C = A - 自反性:
A ⊕ A = 0 - 恒等性:
A ⊕ 0 = A - 交换律:
A ⊕ B = B ⊕ A - 结合律:
(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)
让我们用Python验证这些性质:
# 可逆性验证
A, B = 0x58, 0x80 # 'X' 和某个密钥字节
C = A ^ B # 加密
print(f"A ^ B = {C:#x}")
print(f"C ^ B = {C ^ B:#x} (恢复A)")
print(f"C ^ A = {C ^ A:#x} (恢复B)")
# 恒等性验证
print(f"A ^ 0 = {A ^ 0:#x} (等于A)")
本题的加密流程
让我们详细分析task.py中的加密流程:
步骤1:生成密钥
key = urandom(16) # 生成16字节随机密钥
步骤2:填充明文
def padding(m):
l = 16 - (len(m) % 16)
return m + (b'\x00' * l)
这个函数计算需要填充的字节数,使明文长度成为16的倍数。
例如,如果flag长度为19字节,那么:
l = 16 - (19 % 16) = 16 - 3 = 13
# 需要填充13个\x00字节,使总长度为32字节
步骤3:添加随机数据
flag = padding(flag) + urandom(10)
在填充后的flag末尾再添加10字节随机数据。
步骤4:XOR加密
enc = b''
for i in range(len(flag)):
enc += (key[i%16] ^ flag[i]).to_bytes(1, 'big')
这是核心加密逻辑。对于每个明文字节:
- 使用
i % 16来循环使用16字节密钥 - 将密钥字节与明文字节进行XOR运算
- 将结果转换为单字节并添加到密文中
加密过程示意图:
位置: 0 1 2 3 4 5 ... 15 16 17 18 ...
明文: 'X' 'M' 'a' 'n' '{' ... ... ... 'a' 'y' '}' ...
密钥: k0 k1 k2 k3 k4 k5 ... k15 k0 k1 k2 ...
密文: c0 c1 c2 c3 c4 c5 ... c15 c16 c17 c18 ...
可以看到,密钥每16字节循环一次。
安全漏洞分析
漏洞1:已知明文前缀
题目代码中有一个关键的断言:
assert flag.startswith(b'XMan{')
这意味着flag的前5个字节是已知的:XMan{。这是一个严重的安全问题。
根据XOR的可逆性,如果我们知道明文和密文,就能计算出密钥:
密钥[i] = 明文[i] ⊕ 密文[i]
因此,我们可以直接恢复密钥的前5个字节:
密钥[0] = 'X' ⊕ 密文[0]
密钥[1] = 'M' ⊕ 密文[1]
密钥[2] = 'a' ⊕ 密文[2]
密钥[3] = 'n' ⊕ 密文[3]
密钥[4] = '{' ⊕ 密文[4]
漏洞2:可预测的填充
填充函数使用固定的\x00字节进行填充:
return m + (b'\x00' * l)
这是另一个致命漏洞。根据XOR的恒等性:
密钥 ⊕ 0x00 = 密钥
这意味着,当明文是\x00时,密文直接等于密钥本身。如果我们能找到密文中哪些位置对应填充的\x00,就能直接读取密钥。
漏洞3:密钥循环使用
加密代码中使用i % 16来循环使用密钥:
enc += (key[i%16] ^ flag[i]).to_bytes(1, 'big')
这意味着:
- 位置0和位置16使用相同的密钥字节k0
- 位置1和位置17使用相同的密钥字节k1
- 以此类推
密钥循环使用扩大了攻击面。一旦我们恢复了密钥的某个字节,就能解密所有使用该密钥字节的位置。
攻击策略设计
综合以上三个漏洞,我们可以设计出完整的攻击方案。
关键观察
首先,我们需要理解明文的结构。密文长度为42字节,减去最后10字节随机数,实际加密的明文是32字节。
这32字节正好是两个16字节块,说明flag经过padding后的长度是32字节。
明文结构分析:
[0-15字节]:第一块,包含 XMan{ + flag的一部分
[16-31字节]:第二块,包含 flag的剩余部分 + } + 填充的\x00
[32-41字节]:随机数据(不需要解密)
攻击步骤
步骤1:恢复密钥的前5字节
利用已知明文XMan{,通过XOR运算恢复密钥的前5字节。
步骤2:恢复密钥的后11字节
关键推理:第二块(16-31字节)中,后半部分很可能是填充的\x00。由于密钥 ⊕ 0x00 = 密钥,我们可以从密文中直接读取密钥的后11字节。
具体来说,密钥的索引5-15对应密文的索引21-31(16+5到16+15)。
步骤3:使用完整密钥解密
有了完整的16字节密钥,我们就可以解密前32字节的密文,恢复出完整的flag。
完整解题过程
实战演练
让我们按照攻击策略,一步步实现解密过程。
第一步:读取并解析密文
enc = bytearray.fromhex("d8db4398596f9123f9b70d6847ad6e14e1ef5ff6220cfe4f96c5520731c81c78a645cdd1aa9b95e54468")
print(f"密文总长度: {len(enc)} 字节")
print(f"前16字节: {enc[0:16].hex()}")
print(f"中16字节: {enc[16:32].hex()}")
print(f"后10字节: {enc[32:42].hex()}")
输出结果:
密文总长度: 42 字节
前16字节: d8db4398596f9123f9b70d6847ad6e14
中16字节: e1ef5ff6220cfe4f96c5520731c81c78
后10字节: a645cdd1aa9b95e54468
第二步:恢复密钥的前5字节
known_prefix = b'XMan{'
key = [0] * 16
for i in range(len(known_prefix)):
key[i] = enc[i] ^ known_prefix[i]
print(f"密钥[{i}] = 0x{enc[i]:02x} ^ 0x{known_prefix[i]:02x} = 0x{key[i]:02x}")
输出结果:
密钥[0] = 0xd8 ^ 0x58 = 0x80
密钥[1] = 0xdb ^ 0x4d = 0x96
密钥[2] = 0x43 ^ 0x61 = 0x22
密钥[3] = 0x98 ^ 0x6e = 0xf6
密钥[4] = 0x59 ^ 0x7b = 0x22
第三步:恢复密钥的后11字节
for i in range(5, 16):
key[i] = enc[16 + i]
print(f"密钥[{i}] = 密文[{16+i}] = 0x{key[i]:02x}")
输出结果:
密钥[5] = 密文[21] = 0x0c
密钥[6] = 密文[22] = 0xfe
密钥[7] = 密文[23] = 0x4f
密钥[8] = 密文[24] = 0x96
密钥[9] = 密文[25] = 0xc5
密钥[10] = 密文[26] = 0x52
密钥[11] = 密文[27] = 0x07
密钥[12] = 密文[28] = 0x31
密钥[13] = 密文[29] = 0xc8
密钥[14] = 密文[30] = 0x1c
密钥[15] = 密文[31] = 0x78
第四步:查看完整密钥
print(f"完整密钥: {bytes(key).hex()}")
输出:
完整密钥: 809622f6220cfe4f96c5520731c81c78
第五步:解密前32字节
flag = b''
for i in range(32):
plaintext_byte = key[i % 16] ^ enc[i]
flag += plaintext_byte.to_bytes(1, 'big')
print(f"解密结果: {flag}")
输出:
解密结果: b'XMan{color_overlay}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
第六步:去除填充并输出flag
flag_clean = flag.rstrip(b'\x00')
print(f"Flag: {flag_clean.decode()}")
输出:
Flag: XMan{color_overlay}
成功获取flag!
简洁的解题脚本
基于以上分析,我们可以编写一个更简洁的解题脚本:
with open('task.py', 'r') as f:
for _ in range(17): f.readline()
enc = bytearray.fromhex(f.readline()[2:])
flag, flag0, n = b'', b'XMan{', 16
key = list(flag0 + enc[len(flag0) + n:n * 2])
for i in range(len(flag0)):
key[i] ^= enc[i]
for i in range(n * 2):
flag += (key[i % n] ^ enc[i]).to_bytes()
print(flag.rstrip(b'\0').decode())
代码解析
这个简洁脚本的核心思想:
构造密钥数组
key = list(flag0 + enc[len(flag0) + n:n * 2])
这行代码巧妙地构造了一个初始密钥数组:
- 前5字节:已知明文
XMan{ - 后11字节:密文的索引21-31(
enc[21:32])
修正密钥前5字节
for i in range(len(flag0)):
key[i] ^= enc[i]
将前5字节的明文与密文异或,得到真实的密钥。
技术总结
已知明文攻击(Known-Plaintext Attack)
已知明文攻击是密码分析中的经典方法。攻击者拥有一些明文-密文对,通过分析这些对应关系来推导密钥。
在XOR加密中,这种攻击特别有效,因为:
密钥 = 明文 ⊕ 密文
只要知道任意位置的明文和密文,就能直接计算出该位置的密钥。
填充预言(Padding Oracle)
本题虽然不是典型的Padding Oracle攻击,但利用了类似的思想:通过已知的填充模式获取额外信息。
当明文是\x00时,密文直接等于密钥。这使得我们可以从密文中直接读取密钥的部分字节。
密钥流重用问题
密钥循环使用是本题的核心漏洞。16字节密钥重复使用意味着:
- 位置0和位置16使用相同的密钥字节
- 一旦恢复某个密钥字节,就能解密所有使用该密钥字节的位置
安全建议
1. 使用标准加密算法
不要自己实现加密算法,应使用经过验证的标准算法:
- 对称加密:AES-256-GCM、ChaCha20-Poly1305
- 非对称加密:RSA-2048/4096、ECC
2. 避免密钥重用
如果必须使用XOR加密:
- 密钥长度应至少等于明文长度(一次一密)
- 每个密钥只使用一次
- 使用密码学安全的随机数生成器(CSPRNG)
3. 使用安全的填充方案
- 使用PKCS#7等标准填充方案
- 或使用不需要填充的加密模式(如CTR模式、GCM模式)
- 避免使用固定字节(如
\x00)进行填充
4. 添加消息认证
加密不等于认证,应使用认证加密(AEAD)方案:
- 使用HMAC对密文进行签名
- 或使用集成认证的加密模式(如AES-GCM)
- 防止密文被篡改或伪造
5. 避免可预测的明文格式
- 不要使用固定的flag格式
- 在明文前添加随机盐值
- 使用随机化的初始向量(IV)
总结
本题是一道经典的XOR加密已知明文攻击题目。通过深入分析,我们发现了三个关键漏洞:
- 已知明文前缀:flag格式
XMan{提供了5字节已知明文 - 可预测的填充:使用
\x00填充,使得密文直接暴露密钥 - 密钥循环使用:16字节密钥重复使用,扩大了攻击面
利用这些漏洞,我们成功恢复了完整的16字节密钥,并解密出flag:XMan{color_overlay}。
这道题目深刻展示了密码学中的一个重要原则:加密算法的安全性不仅取决于算法本身,更取决于实现方式和使用场景。
即使是简单的XOR加密,如果使用得当(如一次一密),也可以达到理论上不可破解的安全性。但如果使用不当,就会留下致命的安全漏洞。
对于CTF密码学题目的学习者来说,本题提供了一个很好的入门案例,展示了如何:
- 分析加密算法的实现细节
- 识别潜在的安全漏洞
- 利用已知信息进行密码分析
- 编写Python脚本实现攻击
希望本文能帮助读者更好地理解XOR加密的原理和安全性问题,在今后的CTF比赛和实际应用中能够识别和防范类似的安全风险。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全《XOR加密的已知明文攻击深度解析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论