文章总结: 本文通过CTF题目实战详解已知明文攻击。文章分析加密源码,识别流加密特征与可逆运算漏洞,推导密钥恢复公式并编写Python脚本成功还原密钥,最终解密获取Flag。案例揭示了自制加密算法的脆弱性,强调了严谨推导与验证的重要性。 综合评分: 92 文章分类: CTF,漏洞分析,逆向分析
CTF密码学深度解析:从零开始的已知明文攻击实战
原创
破镜安全 破镜安全
破镜安全
2026年1月22日 08:02 四川
CTF密码学深度解析:从零开始的已知明文攻击实战
前言
密码学是信息安全的基石,而破解密码则是理解密码学最直接的方式。本文将通过一道CTF密码学题目,带你深入了解”已知明文攻击”(Known-Plaintext Attack, KPA)的完整实施过程。无论你是密码学新手还是想深入了解攻击技术的安全研究人员,这篇文章都将为你提供详尽的技术分析和实践指导。
一、题目环境准备
1.1 题目文件清单
首先查看题目提供的所有文件:
$ ls -lh
total 20K
-rwxrwxrwx 1 root root 505 Oct 16 2013 encryptor.c
-rwxrwxrwx 1 root root 30 Oct 16 2013 msg001
-rwxrwxrwx 1 root root 30 Oct 16 2013 msg001.enc
-rwxrwxrwx 1 root root 415 Oct 16 2013 msg002.enc
让我们分析每个文件的作用:
encryptor.c (505字节)
- 这是一个C语言源代码文件
- 文件名暗示这是加密程序
- 大小适中,说明算法不会太复杂
msg001 (30字节)
- 一个30字节的小文件
- 可能是测试消息
msg001.enc (30字节)
- 与msg001大小完全相同
- .enc扩展名表示这是加密后的文件
- 大小相同说明这不是分组加密(分组加密通常会有padding)
msg002.enc (415字节)
- 较大的加密文件
- 按照CTF惯例,这里应该包含flag
1.2 初步判断
从文件大小的观察,我们可以得出几个重要结论:
- 这是流加密:msg001和msg001.enc大小完全相同(都是30字节),说明加密是逐字节进行的,不存在填充
- 有已知明文-密文对:msg001很可能是明文,msg001.enc是对应的密文
- 目标明确:需要解密msg002.enc获取flag
这为我们指明了方向:如果能从已知的明文-密文对中破解出密钥,就能解密目标文件。
二、源代码深度分析
2.1 完整代码展示
让我们查看encryptor.c的完整内容:
$ cat encryptor.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc != 3) {
printf("USAGE: %s INPUT OUTPUT\n", argv[0]);
return 0;
}
FILE* input = fopen(argv[1], "rb");
FILE* output = fopen(argv[2], "wb");
if (!input || !output) {
printf("Error\n");
return 0;
}
char k[] = "CENSORED";
char c, p, t = 0;
int i = 0;
while ((p = fgetc(input)) != EOF) {
c = (p + (k[i % strlen(k)] ^ t) + i*i) & 0xff;
t = p;
i++;
fputc(c, output);
}
return 0;
}
2.2 代码结构分析
让我们逐段剖析这个程序:
第1-3行:头文件引入
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
stdlib.h:标准库函数stdio.h:文件输入输出函数string.h:字符串操作函数(这里用到了strlen)
第5-9行:参数检查
if (argc != 3) {
printf("USAGE: %s INPUT OUTPUT\n", argv[0]);
return 0;
}
程序需要两个参数:输入文件和输出文件。
第10-15行:文件打开
FILE* input = fopen(argv[1], "rb");
FILE* output = fopen(argv[2], "wb");
if (!input || !output) {
printf("Error\n");
return 0;
}
以二进制模式打开文件,确保能正确处理所有字节值。
第16行:密钥定义(关键!)
char k[] = "CENSORED";
这是整个程序的核心秘密!密钥被”CENSORED”替代了,这正是我们需要破解的目标。
第17-18行:变量初始化
char c, p, t = 0;
int i = 0;
c:ciphertext,密文字节p:plaintext,明文字节t:临时变量,初始值为0i:位置计数器,初始值为0
第19-24行:加密核心循环
while ((p = fgetc(input)) != EOF) {
c = (p + (k[i % strlen(k)] ^ t) + i*i) & 0xff;
t = p;
i++;
fputc(c, output);
}
这是整个算法的精髓,让我们深入分析。
2.3 加密公式详解
核心加密公式:
c = (p + (k[i % strlen(k)] ^ t) + i*i) & 0xff;
这个公式看起来很复杂,让我们拆解成几个部分:
组成部分1:p
- 当前位置的明文字节
- 直接从输入文件读取
组成部分2:k[i % strlen(k)]
-
k[i % strlen(k)]表示密钥的循环使用 -
假设密钥长度为28,那么:
-
位置0使用k[0]
-
位置1使用k[1]
-
…
-
位置28使用k[0](28 % 28 = 0)
-
位置29使用k[1](29 % 28 = 1)
-
这是典型的流密码特征
组成部分3:t
- t是一个临时变量
- 观察第22行:
t = p - 这意味着t始终保存着前一个明文字节
- 第一个字节加密时,t=0(初始值)
组成部分4:k[i % strlen(k)] ^ t
^是异或运算符- 密钥字节与前一个明文字节进行异或
- 这形成了一个链式结构
组成部分5:i*i
- 位置的平方值
- 位置0:0*0 = 0
- 位置1:1*1 = 1
- 位置2:2*2 = 4
- 位置3:3*3 = 9
- 引入位置相关性
组成部分6:& 0xff
- 按位与运算,确保结果在0-255范围内
- 相当于对256取模
2.4 加密流程图解
让我用一个具体例子演示加密过程:
假设密钥是”KEY”(长度为3),明文是”ABC”:
位置 i=0:
- 读取明文:p = 'A' (0x41)
- 当前t值:t = 0(初始)
- 密钥字节:k[0%3] = 'K' (0x4B)
- 计算:c = (0x41 + (0x4B ^ 0) + 0) & 0xff
- 更新:t = 0x41
位置 i=1:
- 读取明文:p = 'B' (0x42)
- 当前t值:t = 0x41(前一个明文'A')
- 密钥字节:k[1%3] = 'E' (0x45)
- 计算:c = (0x42 + (0x45 ^ 0x41) + 1) & 0xff
- 更新:t = 0x42
位置 i=2:
- 读取明文:p = 'C' (0x43)
- 当前t值:t = 0x42(前一个明文'B')
- 密钥字节:k[2%3] = 'Y' (0x59)
- 计算:c = (0x43 + (0x59 ^ 0x42) + 4) & 0xff
- 更新:t = 0x43
2.5 算法特点总结
通过分析,我们发现这个算法有以下特点:
1. 流密码性质
- 每个字节独立加密
- 明文和密文长度相同
2. 密钥循环使用
- 密钥像流水一样循环使用
- 短密钥可以加密任意长度的明文
3. 链式依赖(CBC-like)
- 每个字节的加密依赖于前一个明文
- 类似分组密码的CBC模式
- 第一个字节特殊(t=0)
4. 位置相关
- 通过i*i引入位置因素
- 相同明文在不同位置产生不同密文
5. 运算类型
- 加法:
p + ... + i*i - 异或:
k[...] ^ t - 模运算:
& 0xff - 全部是可逆运算
最后一点非常关键!因为所有运算都是可逆的,这为我们的攻击提供了可能。
三、已知数据检查
3.1 查看明文内容
让我们看看msg001包含什么:
$ cat msg001
Hi! This is only test message
这是一句英文:”Hi! This is only test message”(嗨!这只是测试消息)
使用十六进制查看器查看精确内容:
$ xxd msg001
00000000: 4869 2120 5468 6973 2069 7320 6f6e 6c79 Hi! This is only
00000010: 2074 6573 7420 6d65 7373 6167 650a test message.
分析:
- 总共30个字节
- 前29个字节是可见字符
- 最后一个字节是
0x0a(换行符\n)
3.2 查看密文内容
现在看看对应的密文:
$ xxd msg001.enc
00000000: 9e97 4081 d0bc 93b2 98ff e7c3 4e31 695f [email protected]_
00000010: 35e1 e3dc 09ea a3a0 c3fa 0552 a653 5..........R.S
分析:
- 同样是30个字节
- 完全是二进制数据,无法直接阅读
- 每个字节都在0x00-0xFF范围内
3.3 明文-密文对照
让我们创建一个详细的对照表:
位置 明文(十六进制) 明文(字符) 密文(十六进制) 差值
---- ------------- --------- ------------- ----
0 48 H 9e +56
1 69 i 97 +2e
2 21 ! 40 +1f
3 20 (space) 81 +61
4 54 T d0 +7c
5 68 h bc +54
...
3.4 信息汇总
现在我们拥有的信息:
- 完整的加密算法(除了密钥)
- 30字节的已知明文
- 30字节的对应密文
- 需要解密的目标文件msg002.enc
这是教科书式的已知明文攻击场景!
四、密钥恢复原理推导
4.1 问题定义
已知:
- 加密公式:
c = (p + (k[i % len(k)] ^ t) + i*i) & 0xff - 明文字节p
- 密文字节c
- 位置i
- 前一个明文t(除了第一个字节t=0)
求:密钥k
4.2 数学推导过程
步骤1:从加密公式出发
c = (p + (k[i % len(k)] ^ t) + i*i) & 0xff
步骤2:移项,分离含有k的部分
由于& 0xff只是确保结果在0-255范围,在模256的意义下,我们可以写成:
c ≡ p + (k[i % len(k)] ^ t) + i*i (mod 256)
移项得:
c - p - i*i ≡ k[i % len(k)] ^ t (mod 256)
步骤3:利用异或的自反性
异或运算有一个重要性质:
如果 A ^ B = C,那么 A = C ^ B
证明:
C ^ B = (A ^ B) ^ B = A ^ (B ^ B) = A ^ 0 = A
应用这个性质:
k[i % len(k)] = (c - p - i*i) ^ t (mod 256)
步骤4:添加范围约束
由于我们处理的是字节,所有值都在0-255范围,所以:
k[i % len(k)] = ((c - p - i*i) ^ t) & 0xff
这就是我们的密钥恢复公式!
4.3 手工计算验证
让我们手工计算第一个字节的密钥,验证公式的正确性。
已知数据(位置i=0):
- 明文:p = 0x48(字符’H’)
- 密文:c = 0x9e
- 位置:i = 0
- 前一个明文:t = 0(初始值)
应用公式:
k[0] = ((c - p - i*i) ^ t) & 0xff
= ((0x9e - 0x48 - 0*0) ^ 0) & 0xff
= ((0x9e - 0x48) ^ 0) & 0xff
= (0x56 ^ 0) & 0xff
= 0x56 & 0xff
= 0x56
转换为字符:
0x56 对应ASCII字符 'V'
所以密钥的第一个字符是 ‘V’!
验证计算:
让我们反向验证,用k[0]=’V'(0x56)加密p=’H'(0x48),看是否得到c=0x9e:
c = (p + (k[0] ^ t) + i*i) & 0xff
= (0x48 + (0x56 ^ 0) + 0) & 0xff
= (0x48 + 0x56) & 0xff
= 0x9e & 0xff
= 0x9e ✓
完美匹配!我们的公式是正确的。
4.4 第二个字节验证
再验证第二个字节,确保理解正确:
已知数据(位置i=1):
- 明文:p = 0x69(字符’i’)
- 密文:c = 0x97
- 位置:i = 1
- 前一个明文:t = 0x48(第一个明文’H’)
应用公式:
k[1] = ((c - p - i*i) ^ t) & 0xff
= ((0x97 - 0x69 - 1*1) ^ 0x48) & 0xff
= ((0x97 - 0x69 - 1) ^ 0x48) & 0xff
= (0x2d ^ 0x48) & 0xff
= 0x65 & 0xff
= 0x65
转换为字符:
0x65 对应ASCII字符 'e'
密钥的第二个字符是 ‘e’!
目前我们有:k[0]=’V’, k[1]=’e’,密钥开头是”Ve…”
五、密钥恢复脚本实现
5.1 Python实现思路
基于上述数学推导,我们可以编写脚本自动恢复整个密钥。
核心思路:
- 读取明文和密文文件
- 对每个位置i,应用密钥恢复公式
- 注意正确更新t值(t总是前一个明文)
- 将恢复的密钥字节组合成字符串
5.2 完整脚本代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
密钥恢复脚本
通过已知明文攻击恢复加密密钥
"""
# 读取密文和明文文件
with open("msg001.enc", "rb") as f:
cipher = f.read()
with open("msg001", "rb") as f:
plain = f.read()
# 转换为字节列表(Python 3中已经是bytes类型)
cipher = list(cipher)
plain = list(plain)
# 初始化
key = "" # 存储恢复的密钥
t = 0 # t的初始值为0,对应加密算法中的初始状态
# 对每个字节应用密钥恢复公式
for i in range(len(plain)):
# 密钥恢复公式:k[i % len(k)] = ((c - p - i*i) ^ t) & 0xff
k_byte = ((cipher[i] - plain[i] - i*i) ^ t) & 0xff
# 将字节转换为字符并添加到密钥
key += chr(k_byte)
# 关键步骤:更新t为当前明文
# 这模拟了加密过程中的 t = p
t = plain[i]
# 输出恢复的密钥
print("Recovered Key:", key)
print("Key Length:", len(key))
5.3 代码详解
让我们详细解释几个关键点:
1. 为什么要更新t?
t = plain[i]
这一行至关重要!因为:
- 加密第i个字节时,t是第(i-1)个明文
- 加密第(i+1)个字节时,t是第i个明文
- 所以在恢复第i个密钥后,必须更新t为plain[i]
- 这样下一轮循环时,t的值才是正确的
2. 为什么要& 0xff?
k_byte = ((cipher[i] - plain[i] - i*i) ^ t) & 0xff
- Python中的整数可以任意大
- 减法可能产生负数
& 0xff确保结果在0-255范围内- 这对应C语言中的char类型自动模256
3. 密钥长度问题
由于密钥是循环使用的,我们恢复出的密钥长度等于明文长度(30字节)。但实际密钥可能更短,后面会重复。
5.4 执行结果
运行脚本:
$ python recover_key.py
Recovered Key: VeryLongKeyYouWillNeverGuessVe
Key Length: 30
分析结果:
- 恢复出30个字符
- 内容是:”VeryLongKeyYouWillNeverGuessVe”
- 注意到末尾的”Ve”和开头的”VeryLong…”重复了
这说明真实的密钥长度小于30!
5.5 确定真实密钥长度
观察恢复的密钥:
VeryLongKeyYouWillNeverGuessVe
^^
重复部分
末尾的”Ve”正好是开头”VeryLongKey…”的前两个字符,这表明:
- 真实密钥长度应该是28字符
- 第28、29位置(i=28, i=29)时,密钥循环到了k[0]和k[1]
因此,真实密钥是:
VeryLongKeyYouWillNeverGuess
共28个字符。
六、密钥验证
在继续之前,我们必须严格验证恢复的密钥是否正确。一个错误的密钥会导致后续所有工作白费。
6.1 验证方法设计
验证策略:使用恢复的密钥重新加密msg001,得到的结果应该与msg001.enc完全相同。
验证步骤:
- 使用恢复的密钥
- 实现加密算法
- 加密msg001
- 逐字节比较结果与msg001.enc
6.2 验证脚本实现
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
密钥验证脚本
使用恢复的密钥重新加密,验证正确性
"""
# 恢复的密钥(28字符)
key = "VeryLongKeyYouWillNeverGuess"
# 读取原始明文和密文
with open("msg001", "rb") as f:
plain = f.read()
with open("msg001.enc", "rb") as f:
original_cipher = f.read()
# 使用恢复的密钥重新加密
cipher = []
t = 0 # 初始化t为0
for i in range(len(plain)):
p = plain[i]
# 加密公式:c = (p + (k[i % len(k)] ^ t) + i*i) & 0xff
c = (p + (ord(key[i % len(key)]) ^ t) + i*i) & 0xff
cipher.append(c)
# 更新t为当前明文
t = p
# 转换为bytes
cipher_bytes = bytes(cipher)
# 逐字节对比
print("验证过程:")
print("原始密文长度:", len(original_cipher))
print("生成密文长度:", len(cipher_bytes))
if len(cipher_bytes) != len(original_cipher):
print("❌ 长度不匹配!")
else:
# 逐字节比较
mismatch = False
for i in range(len(cipher_bytes)):
if cipher_bytes[i] != original_cipher[i]:
print(f"❌ 位置{i}不匹配: 期望{original_cipher[i]:02x}, 实际{cipher_bytes[i]:02x}")
mismatch = True
if not mismatch:
print("✓ 密钥验证成功!")
print("恢复的密钥:", key)
print("密钥长度:", len(key))
6.3 验证结果
$ python verify_key.py
验证过程:
原始密文长度: 30
生成密文长度: 30
✓ 密钥验证成功!
恢复的密钥: VeryLongKeyYouWillNeverGuess
密钥长度: 28
完美!密文完全匹配,密钥100%正确。
6.4 使用C程序二次验证
为了更加确定,我们可以修改原始的C程序进行验证:
创建encryptor_with_key.c:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
if (argc != 3) {
printf("USAGE: %s INPUT OUTPUT\n", argv[0]);
return 0;
}
FILE* input = fopen(argv[1], "rb");
FILE* output = fopen(argv[2], "wb");
if (!input || !output) {
printf("Error\n");
return 0;
}
// 填入恢复的密钥
char k[] = "VeryLongKeyYouWillNeverGuess";
char c, p, t = 0;
int i = 0;
while ((p = fgetc(input)) != EOF) {
c = (p + (k[i % strlen(k)] ^ t) + i*i) & 0xff;
t = p;
i++;
fputc(c, output);
}
return 0;
}
编译并测试:
$ gcc encryptor_with_key.c -o encryptor_test
$ ./encryptor_test msg001 test.enc
$ diff msg001.enc test.enc
$ echo $?
0
diff命令返回0,说明两个文件完全相同!
使用十六进制对比:
$ xxd msg001.enc > original.hex
$ xxd test.enc > generated.hex
$ diff original.hex generated.hex
$ echo $?
0
两次验证都通过,密钥确认无误!
七、解密原理推导
现在我们有了正确的密钥,可以解密msg002.enc获取flag了。
7.1 解密公式推导
从加密公式:
c = (p + (k[i % len(k)] ^ t) + i*i) & 0xff
推导解密公式:
步骤1:移项
c - (k[i % len(k)] ^ t) - i*i = p (mod 256)
步骤2:添加范围约束
p = (c - (k[i % len(k)] ^ t) - i*i) & 0xff
这就是解密公式。
7.2 解密流程图解
让我们用具体例子说明解密过程:
初始状态:
- 密钥k = "VeryLongKeyYouWillNeverGuess"
- t = 0(初始值)
- i = 0
解密第0个字节:
- 读取密文:c = cipher[0]
- 使用密钥:k[0 % 28] = 'V' (0x56)
- 当前t值:t = 0
- 位置平方:i*i = 0
- 计算:p = (c - (0x56 ^ 0) - 0) & 0xff
- 更新:t = p(注意:更新为刚解密出的明文!)
- 输出:p
解密第1个字节:
- 读取密文:c = cipher[1]
- 使用密钥:k[1 % 28] = 'e' (0x65)
- 当前t值:t = plain[0](前一个解密出的明文)
- 位置平方:i*i = 1
- 计算:p = (c - (0x65 ^ t) - 1) & 0xff
- 更新:t = p
- 输出:p
依此类推...
7.3 关键注意事项
t的更新非常关键!
错误的做法:
# 错误!t不更新
for i in range(len(cipher)):
p = decrypt(cipher[i], key, t=0, i) # 总是用t=0
plain.append(p)
这样只有第一个字节能正确解密,后续全错!
正确的做法:
# 正确!t随解密进行更新
t = 0
for i in range(len(cipher)):
p = decrypt(cipher[i], key, t, i)
plain.append(p)
t = p # 更新t为刚解密的明文
八、解密脚本实现
8.1 完整解密脚本
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
解密脚本
使用恢复的密钥解密msg002.enc
"""
# 恢复的密钥
key = "VeryLongKeyYouWillNeverGuess"
# 读取目标密文
with open("msg002.enc", "rb") as f:
cipher = f.read()
print(f"密文长度: {len(cipher)} 字节")
# 解密过程
plain = []
t = 0 # 初始t=0
for i in range(len(cipher)):
c = cipher[i]
# 解密公式:p = (c - (k[i % len(k)] ^ t) - i*i) & 0xff
p = (c - (ord(key[i % len(key)]) ^ t) - i*i) & 0xff
plain.append(p)
# 关键:更新t为刚解密出的明文
t = p
# 转换为字符串
plaintext = bytes(plain).decode('utf-8', errors='ignore')
print("\n" + "="*60)
print("解密结果:")
print("="*60)
print(plaintext)
print("="*60)
# 提取flag
import re
flag_match = re.search(r'CTF\{[^}]+\}', plaintext)
if flag_match:
print(f"\nFlag: {flag_match.group(0)}")
# 保存解密结果
with open("msg002_decrypted.txt", "wb") as f:
f.write(bytes(plain))
print("\n解密内容已保存到 msg002_decrypted.txt")
8.2 执行解密
$ python decrypt.py
密文长度: 415 字节
============================================================
解密结果:
============================================================
The known-plaintext attack (KPA) is an attack model for
cryptanalysis where the attacker has samples of both the
plaintext (called a crib), and its encrypted version
(ciphertext). These can be used to reveal further secret
information such as secret keys and code books. The term
"crib" originated at Bletchley Park, the British World
War II decryption operation.
The flag is CTF{6d5eba48508efb13dc87220879306619}
============================================================
Flag: CTF{6d5eba48508efb13dc87220879306619}
解密内容已保存到 msg002_decrypted.txt
8.3 结果分析
解密出的内容是一段关于已知明文攻击的英文介绍,非常应景!
内容翻译:
已知明文攻击(KPA)是密码分析的一种攻击模型,攻击者拥有明文
(称为crib)及其加密版本(密文)的样本。这些可以用来揭示进一步
的秘密信息,如密钥和密码本。"crib"一词起源于布莱切利园,英国
二战解密行动。
flag是 CTF{6d5eba48508efb13dc87220879306619}
这段话特别提到了”crib”(已知明文片段),这正是二战时期破译Enigma密码机时使用的术语!
最终Flag:CTF{6d5eba48508efb13dc87220879306619}
九、完整攻击链回顾
让我们回顾整个攻击过程:
9.1 攻击流程图
1. 信息收集
├─ 分析题目文件
├─ 确认有已知明文-密文对
└─ 识别出流加密特征
2. 算法分析
├─ 阅读源代码
├─ 理解加密公式
├─ 识别变量含义
└─ 发现链式结构
3. 漏洞发现
├─ 识别运算全部可逆
├─ 确认存在数学弱点
└─ 判定可进行已知明文攻击
4. 数学推导
├─ 从加密公式出发
├─ 逐步移项变换
├─ 利用异或自反性
└─ 得出密钥恢复公式
5. 手工验证
├─ 计算第一个密钥字节
├─ 计算第二个密钥字节
└─ 验证公式正确性
6. 脚本实现
├─ 编写密钥恢复脚本
├─ 执行并获得密钥
└─ 分析密钥循环特征
7. 多重验证
├─ Python重新加密验证
├─ C程序编译验证
└─ 十六进制比对验证
8. 解密攻击
├─ 推导解密公式
├─ 编写解密脚本
├─ 解密目标文件
└─ 提取flag
9.2 关键成功因素
这次攻击成功的关键因素:
1. 正确识别攻击类型
- 快速识别出已知明文攻击场景
- 确认算法的可逆性
2. 严谨的数学推导
- 逐步推导密钥恢复公式
- 手工计算验证推导正确性
3. 注意实现细节
- 正确处理t的更新
- 理解密钥循环机制
- 确保所有字节操作在0-255范围
4. 充分的验证
- 多种方法交叉验证
- 确保每一步都正确再继续
十、算法安全性深度分析
10.1 致命弱点剖析
这个加密算法存在的根本问题:
弱点1:线性可逆性
c = (p + (k ^ t) + i*i) & 0xff
所有运算(加法、异或、模运算)都是可逆的,没有使用任何单向函数。
弱点2:密钥直接参与运算密钥字节直接与明文相关的量进行运算,没有经过复杂的变换。
弱点3:缺少扩散性每个密钥字节只影响特定位置的密文,没有充分的扩散。
弱点4:链式结构的伪装虽然使用了t=p的链式结构,但在已知明文的情况下,所有t值都是已知的,链式结构完全失效。
10.2 看似安全的设计
虽然算法有致命弱点,但它确实使用了一些看似增加安全性的技巧:
1. 链式依赖(t=p)
- 模仿CBC模式
- 让加密字节相互依赖
- 但对已知明文攻击无效
2. 位置相关性(i*i)
- 引入位置因素
- 相同明文不同位置密文不同
- 但仍是线性可逆的
3. 密钥循环
- 短密钥加密长明文
- 避免密钥过短的问题
- 但密钥泄露后全部内容都不安全
10.3 为什么链式结构没能阻止攻击?
有人可能会问:既然有链式结构(t依赖前一个明文),为什么还能破解?
关键在于:我们拥有完整的明文!
加密第0字节时:t = 0(已知)
加密第1字节时:t = plain[0](已知,因为我们有明文)
加密第2字节时:t = plain[1](已知)
加密第3字节时:t = plain[2](已知)
...
链式结构只能在不知道某个明文的情况下阻止解密后续内容。一旦拥有完整明文,所有t值都变成已知量,链式结构就形同虚设。
10.4 如果没有已知明文呢?
假设我们只有密文,没有明文,这个算法能抵抗攻击吗?
答案:仍然脆弱
可能的攻击方法:
1. 暴力破解密钥
- 如果密钥不够长(如本题28字节)
- 可以尝试暴力枚举
- 28字节密钥空间:256^28 ≈ 10^67(仍然很大,但不是不可能)
2. 字典攻击
- 如果密钥是可读的英文(如”VeryLongKeyYouWillNeverGuess”)
- 可以尝试常见单词组合
- 本题密钥是有意义的英文,字典攻击有效
3. 频率分析
- 如果明文是英文文本
- 可以利用字母频率特征
- 虽然有i*i和t的影响,但统计特征仍可能泄露信息
4. 已知明文片段攻击
- 如果能猜测出部分明文(如文件头、固定格式)
- 可以恢复部分密钥
- 进而尝试恢复完整密钥
十一、密码学知识扩展
11.1 已知明文攻击的历史
已知明文攻击不是理论概念,历史上有很多真实案例。
案例1:Enigma密码机(1939-1945)
二战期间,德国使用Enigma密码机加密军事通信。盟军密码学家(包括图灵)利用已知明文攻击破解了它。
关键突破点:
- 德军电报有固定格式
- 天气报告总以”WETTER”(德语”天气”)开头
- 军官签名是已知的
- 每日例行报告格式固定
这些”cribs”(已知明文片段)成为了突破口。通过:
- 寻找密文中可能对应”WETTER”的位置
- 利用Enigma的机械特性
- 使用”Bombe”机器进行快速验证
- 最终破译当日密钥设置
解密msg002.enc时看到的那段话,正是在向这段历史致敬!
案例2:WEP无线加密(2001)
WEP(有线等效加密)是早期Wi-Fi加密标准,在2001年被证明极度脆弱。
攻击原理:
- WEP使用RC4流密码
- 存在IV(初始化向量)重用问题
- 数据包中有已知的部分(如LLC头部)
- 收集足够多的数据包后,可以恢复密钥
攻击过程:
- 捕获大量加密数据包(几千到几万个)
- 利用数据包头部的已知明文
- 通过统计分析恢复密钥流
- 最终恢复WEP密钥
工具:Aircrack-ng套件可以在几分钟内破解WEP。
这导致WEP在2004年被正式弃用,由WPA取代。
案例3:PKZIP加密(1990s)
早期的ZIP文件加密使用简单的流密码,容易受到已知明文攻击。
攻击方法:
- ZIP文件有固定的文件头和结构
- 攻击者可以猜测某些文件内容(如README.txt)
- 创建相同文件的未加密版本
- 通过对比恢复密钥流
工具:pkcrack可以实现这种攻击。
现代ZIP加密(使用AES)已经修复了这个问题。
11.2 现代密码如何防御
现代加密算法(如AES、ChaCha20)能够抵抗已知明文攻击:
1. 使用强密钥调度算法
AES的密钥调度:
- 输入密钥经过复杂的扩展
- 生成多轮不同的子密钥
- 每轮使用不同的子密钥
- 即使知道某轮的输入输出,也难以推导密钥
原始密钥(128/192/256位)
↓
密钥扩展算法(使用S盒、移位、异或)
↓
轮密钥0, 轮密钥1, ..., 轮密钥10/12/14
2. 使用非线性S盒
S盒(Substitution Box)提供非线性变换:
// AES的S盒是预定义的256字节表
output = S_BOX[input];
即使知道input和output,也无法通过简单运算推导密钥。
3. 多轮迭代
AES-128进行10轮变换:
明文 → 轮1 → 轮2 → ... → 轮10 → 密文
每轮包括:
- SubBytes(S盒替换)
- ShiftRows(行移位)
- MixColumns(列混合)
- AddRoundKey(异或轮密钥)
4. 扩散和混淆
- 混淆(Confusion):密文与密钥的关系复杂
- 扩散(Diffusion):一个明文位影响多个密文位
AES的设计确保:
- 改变一个明文位,平均影响一半的密文位
- 改变一个密钥位,平均影响一半的密文位
11.3 Kerckhoffs原则
Auguste Kerckhoffs在1883年提出了密码学的基本原则:
“A cryptosystem should be secure even if everything about the system, except the key, is public knowledge.”
即使算法完全公开,只要密钥保密,系统就应该是安全的。
本题完美体现了这个原则:
- 加密算法完全公开(encryptor.c)
- 但由于算法设计不当
- 即使只公开算法,在已知明文时仍然不安全
现代密码学严格遵循这个原则:
- AES、RSA等算法完全公开
- 公开发表,接受全球密码学家审查
- 经过数十年的攻击尝试仍然安全
- 安全性完全依赖于密钥保密
11.4 流密码 vs 分组密码
本题使用的是流密码,让我们对比两种加密方式:
流密码(Stream Cipher)
- 逐字节(或逐位)加密
- 明文和密文长度相同
- 速度快,适合流式数据
- 例子:RC4、ChaCha20
- 本题的算法
分组密码(Block Cipher)
- 将明文分成固定大小的块
- 每块独立加密
- 通常需要填充
- 例子:AES、DES
- 更常用于文件加密
安全性对比:
- 流密码的安全性高度依赖于密钥流的质量
- 分组密码的安全性依赖于块变换的复杂度
- 现代的ChaCha20(流密码)和AES(分组密码)都很安全
- 关键是设计和实现是否正确
十二、实战技巧总结
12.1 CTF密码题解题思路
遇到密码学题目时的通用思路:
第一步:信息收集
- 列出所有提供的文件
- 检查文件大小和类型
- 查看是否有源代码
- 确认是否有已知明文
第二步:类型识别
- 流加密还是分组加密?(看文件大小)
- 对称加密还是非对称加密?
- 经典密码还是现代密码?
- 自制算法还是标准算法?
第三步:算法分析
- 如果有源代码,仔细阅读
- 理解每个变量的含义
- 画出数据流图
- 识别关键运算
第四步:弱点寻找
- 是否存在数学弱点?
- 是否有实现漏洞?
- 是否有信息泄露?
- 适用哪种攻击模型?
第五步:攻击实施
- 手工计算验证思路
- 编写自动化脚本
- 进行充分的验证
- 获取flag
12.2 Python在密码分析中的优势
本题使用Python的原因:
优势1:快速原型开发
# 几行代码就能验证想法
for i in range(len(plain)):
k = ((cipher[i] - plain[i] - i*i) ^ t) & 0xff
key += chr(k)
t = plain[i]
优势2:字节处理方便
# 读取二进制文件
with open("file.enc", "rb") as f:
data = f.read()
# 遍历每个字节
for byte in data:
process(byte)
优势3:丰富的库支持
import re # 正则表达式
import hashlib # 哈希函数
from Crypto.Cipher import AES # 加密库
优势4:交互式调试
# 可以随时打印中间结果
print(f"位置{i}: 明文={p:02x}, 密文={c:02x}, 密钥={k:02x}")
12.3 验证的重要性
在CTF中,验证非常重要!
为什么要多次验证?
- 一个错误的假设可能浪费数小时
- 密码学不允许”差不多”
- 必须100%正确才能拿到flag
如何进行验证?
- 手工计算验证公式
- 用小数据集测试
- 多种方法交叉验证
- 逐步检查中间结果
本题的验证策略:
- 手工计算前两个密钥字节
- Python脚本恢复完整密钥
- Python重新加密验证
- C程序编译验证
- 十六进制逐字节对比
多重验证确保了我们的密钥100%正确。
12.4 常见陷阱
在解这类题时,容易犯的错误:
陷阱1:忘记更新t
# 错误
for i in range(len(cipher)):
p = decrypt(c, k, t=0, i) # t总是0
# 正确
t = 0
for i in range(len(cipher)):
p = decrypt(c, k, t, i)
t = p # 更新t
陷阱2:Python整数溢出
# Python整数可以任意大
result = 0x100 - 0x50 # = 176,没问题
# 但C语言的char会溢出
# 需要手动& 0xff确保范围
result = (0x100 - 0x50) & 0xff
陷阱3:字符串编码问题
# Python 3中,文件读取返回bytes
data = open("file", "rb").read() # bytes类型
# 不能直接当字符串用
# chr()用于字节转字符
char = chr(data[0])
陷阱4:密钥长度判断错误
# 恢复的密钥可能包含循环部分
# "VeryLongKeyYouWillNeverGuessVe"
# 末尾的"Ve"是重复的
# 真实密钥是前28字符
十三、延伸思考
13.1 如果加密算法更复杂呢?
假设算法改为:
c = S_BOX[p ^ k[i % len(k)]] + i*i;
引入S盒后:
- 即使知道p和c,也无法直接求k
- 需要暴力枚举k[i]的256种可能
- 如果密钥够长,攻击难度大大增加
13.2 如果没有已知明文呢?
可能的攻击方法:
1. 猜测文件格式
- PDF文件:”%PDF-“
- PNG图片:”\x89PNG”
- JPEG图片:”\xFF\xD8\xFF”
- ZIP文件:”PK\x03\x04″
2. 利用语言特征
- 英文文本的字母频率
- 常见单词(the、and、of)
- 空格出现频率
3. 密钥暴力破解
- 如果密钥是有意义的单词
- 使用字典攻击
13.3 如何设计安全的加密?
原则1:不要自己发明加密算法
- 使用AES、ChaCha20等标准算法
- 使用成熟的密码库(OpenSSL、libsodium)
原则2:正确使用加密
- 选择合适的加密模式(如AES-GCM)
- 使用足够长的密钥(至少128位)
- 正确处理IV/Nonce
- 添加认证(MAC或AEAD)
原则3:密钥管理
- 安全生成随机密钥
- 安全存储密钥
- 定期轮换密钥
- 安全传输密钥
原则4:纵深防御
- 加密只是一层保护
- 还需要访问控制、审计等
十四、总结
14.1 解题关键回顾
这道题的成功要素:
- 识别攻击类型:快速判断出已知明文攻击场景
- 数学推导严谨:从加密公式正确推导出密钥恢复公式
- 实现细节正确:特别是t的更新逻辑
- 充分验证:多种方法确保密钥正确
- 完整执行:从密钥恢复到解密获取flag
14.2 学到的知识
通过这道题,我们学习了:
密码学理论:
- 已知明文攻击的原理
- 流密码的特征
- 链式结构(CBC-like)
- 密钥循环使用
- 可逆运算的风险
实战技能:
- 阅读和分析加密代码
- 数学推导和手工验证
- Python密码分析脚本编写
- 多重验证方法
- CTF解题思路
安全认知:
- 自制加密算法的危险
- Kerckhoffs原则的重要性
- 现代密码的设计原则
- 历史上的密码破译案例
14.3 最后的建议
对于学习者:
- 理论和实践结合学习密码学
- 亲手实现和破解加密算法
- 参加CTF比赛积累经验
- 阅读经典密码学著作
对于开发者:
- 永远不要自己发明加密算法
- 使用经过验证的标准算法
- 正确使用密码库
- 重视密钥管理
对于安全研究人员:
- 保持对新攻击技术的学习
- 理解攻击者的思维方式
- 在授权范围内进行测试
- 负责任地披露漏洞
14.4 参考资源
书籍推荐:
- 《应用密码学》(Bruce Schneier)
- 《密码编码学与网络安全》(William Stallings)
- 《The Code Book》(Simon Singh)- 密码学历史
在线资源:
- CryptoHack:互动式密码学学习平台
- Cryptopals:密码学挑战集合
- NIST密码标准文档
工具推荐:
- Python + pycryptodome:密码分析
- CyberChef:在线加密/解密工具
- Wireshark:网络流量分析
最终Flag:CTF{6d5eba48508efb13dc87220879306619}
通过这道题,我们完成了一次完整的密码攻击实践。从信息收集到算法分析,从数学推导到脚本实现,从严格验证到成功解密,每一步都体现了密码分析的严谨和技巧。
希望这篇详细的技术解析能够帮助你理解已知明文攻击的原理和实施方法,在密码学的学习道路上更进一步。记住:在实际应用中,永远使用经过验证的标准加密算法,切勿自行设计密码系统!
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:破镜安全 破镜安全 破镜安全《CTF密码学深度解析:从零开始的已知明文攻击实战》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论