拿到这道题目时,首先看到题目名称是"x_xor_md5",这个名称已经给出了非常明确的提示:题目涉及XOR异或运算和MD5哈希算法。
题目提供了一个文件:6c0c57fa88eb44f3b179a6e9798fc7b6
从文件名来看,这是一个32位的十六进制字符串,很可能也是一个MD5值。这可能是题目设计者的小心思,文件名本身就可能包含关键信息。
作为密码分析的第一步,我使用xxd命令查看文件的十六进制内容:
xxd -g 1 6c0c57fa88eb44f3b179a6e9798fc7b6
输出结果:
00000000: 69 35 41 01 1c 9e 75 78 5d 48 fb f0 84 cd 66 79 i5A...ux]H....fy
00000010: 55 30 49 4c 56 d2 73 70 12 45 a8 ba 85 c0 3e 53 U0ILV.sp.E....>S
00000020: 73 1b 78 2a 4b e9 77 26 5e 73 bf aa 85 9c 15 6f s.x*K.w&^s.....o
00000030: 54 2c 73 1b 58 8a 66 48 5b 19 84 b0 80 ca 33 73 T,s.X.fH[.....3s
00000040: 5c 52 0c 4c 10 9e 32 37 12 0c fb ba cb 8f 6a 53 \R.L..27......jS
00000050: 01 78 0c 4c 10 9e 32 37 12 0c fb ba cb 8f 6a 53 .x.L..27......jS
00000060: 01 78 0c 4c 10 9e 32 37 12 0c fb ba cb 8f 6a 53 .x.L..27......jS
00000070: 01 78 0c 4c 10 9e 32 37 12 0c 89 d5 a2 fc .x.L..27......
观察这个十六进制dump,我立即注意到一个非常重要的现象:
0x50、0x60、0x70行的前14个字节完全相同!
0x50: 01 78 0c 4c 10 9e 32 37 12 0c fb ba cb 8f 6a 53
0x60: 01 78 0c 4c 10 9e 32 37 12 0c fb ba cb 8f 6a 53
0x70: 01 78 0c 4c 10 9e 32 37 12 0c (后面不完整)
这绝不是巧合。在密码学中,密文出现重复模式通常意味着什么?
XOR(异或)是一种位运算,它具有以下数学性质:
交换律:A ⊕ B = B ⊕ A
结合律:(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)
自反性:A ⊕ A = 0
恒等性:A ⊕ 0 = A
可逆性:A ⊕ B ⊕ B = A
第5条性质使得XOR成为一种简单的加密方法:
加密:密文 = 明文 ⊕ 密钥
解密:明文 = 密文 ⊕ 密钥
当密钥长度小于明文长度时,通常会循环使用密钥。例如,密钥长度为16字节,那么:
明文第0字节与密钥第0字节异或
明文第1字节与密钥第1字节异或
...
明文第16字节与密钥第0字节异或(循环)
明文第17字节与密钥第1字节异或
...
我观察到的重复模式说明:这些位置的明文内容相同,使用相同的密钥部分加密后,产生了相同的密文。
如果明文的0x50-0x5F、0x60-0x6F位置都是相同的内容(比如空格、NULL字符或其他填充字符),那么使用16字节的循环密钥加密后,必然产生相同的密文。
基于以上分析,我提出假设:
假设1:使用了16字节的循环密钥进行XOR加密
假设2:0x50-0x6F位置的明文是重复的字符(如0x00或0x20)
如果明文是0x00(NULL字符),那么:
密文 = 0x00 ⊕ 密钥 = 密钥
这意味着重复的密文序列本身可能就是密钥!
让我提取这个序列作为初始密钥:
key = bytes([0x01, 0x78, 0x0c, 0x4c, 0x10, 0x9e, 0x32, 0x37,
0x12, 0x0c, 0xfb, 0xba, 0xcb, 0x8f, 0x6a, 0x53])
使用提取的密钥对整个文件进行XOR解密:
with open('6c0c57fa88eb44f3b179a6e9798fc7b6', 'rb') as f:
data = f.read()
key = bytes([0x01, 0x78, 0x0c, 0x4c, 0x10, 0x9e, 0x32, 0x37,
0x12, 0x0c, 0xfb, 0xba, 0xcb, 0x8f, 0x6a, 0x53])
result = bytearray()
for i, byte in enumerate(data):
result.append(byte ^ key[i % len(key)])
解密结果:
00000000: 68 4d 4d 4d 0c 00 47 4f 4f 44 00 4a 4f 42 0c 2a hMMM..GOOD.JOB.*
00000010: 54 48 45 00 46 4c 41 47 00 49 53 00 4e 4f 54 00 THE.FLAG.IS.NOT.
00000020: 72 63 74 66 5b 77 45 11 4c 7f 44 10 4e 13 7f 3c rctf[wE.L.D.N..<
00000030: 55 54 7f 57 48 14 54 7f 49 15 7f 0a 4b 45 59 20 UT.WH.T.I...KEY
看到这个结果,我心里一半欣喜一半疑惑:
好消息:
出现了可读的英文单词:GOOD、JOB、THE、FLAG、IS、NOT、KEY
这些单词说明我的思路是对的,确实是XOR加密
问题:
开头是"hMMM"而不是正常的"Hmmm"
第0x20行是"rctf[wE"而不是常见CTF的FLAG格式"RCTF{We"
大小写完全反了!
让我仔细分析这个大小写问题。在ASCII编码中:
大写字母 'H' = 0x48 = 0100 1000
小写字母 'h' = 0x68 = 0110 1000
差值 = 0x20 = 0010 0000
观察可知:大小写字母在ASCII码中只有第6位(从低位数起第5位)不同。这个差值正好是0x20。
对比我的解密结果:
得到'h'(0x68),期望'H'(0x48),差值0x20
得到'r'(0x72),期望'R'(0x52),差值0x20
得到'c'(0x63),期望'C'(0x43),差值0x20
所有字母的大小写都是反的!
这意味着什么?我的密钥每个字节都需要再异或0x20。
可能的原因:
题目故意设置的障碍,增加难度
提取密钥时的假设有偏差(明文可能是0x20而不是0x00)
让我验证:如果明文是0x20(空格),那么:
密钥实际值 = 密文 ⊕ 0x20
这正好解释了为什么需要对密钥每个字节异或0x20。
对密钥的每个字节异或0x20:
key_original = bytes([0x01, 0x78, 0x0c, 0x4c, 0x10, 0x9e, 0x32, 0x37,
0x12, 0x0c, 0xfb, 0xba, 0xcb, 0x8f, 0x6a, 0x53])
key_fixed = bytes([b ^ 0x20 for b in key_original])
# 结果: 21 58 2c 6c 30 be 12 17 32 2c db 9a eb af 4a 73
使用新密钥重新解密:
00000000: 48 6d 6d 6d 2c 20 67 6f 6f 64 20 6a 6f 62 2c 0a Hmmm, good job,.
00000010: 74 68 65 20 66 6c 61 67 20 69 73 20 6e 6f 74 20 the flag is not
00000020: 52 43 54 46 7b 57 65 31 6c 5f 64 30 6e 33 5f 1c RCTF{We1l_d0n3_.
00000030: 75 74 5f 77 68 34 74 5f 69 35 5f 2a 6b 65 79 00 ut_wh4t_i5_*key.
00000040: 7d 0a 20 20 20 20 20 20 20 20 20 20 20 20 20 20 }.
太好了!现在可以清楚地读出:
Hmmm, good job,
the flag is not
RCTF{We1l_d0n3_ut_wh4t_i5_*key*}
但仔细观察,还有问题:
观察每行的第16个字节(最后一列):
0x0F位置:0x0a(换行符,合理)
0x1F位置:0x20(空格,合理)
0x2F位置:0x1c(非可见字符,不对)
0x3F位置:0x00(NULL,不对)
从FLAG的上下文看,0x2F位置应该是'6',0x3F位置应该是'*'。
让我分析:FLAG应该是"RCTF{We1l_d0n3_6ut_wh4t_i5_key}",但现在0x2F显示的是不可见字符。
密钥是16字节循环使用的,所以:
0x2F位置使用密钥的第15个字节(索引15)
0x3F位置也使用密钥的第15个字节
让我利用已知明文来计算正确的密钥字节。
XOR加密有一个致命弱点:如果知道明文和密文,可以直接计算出密钥:
密钥 = 明文 ⊕ 密文
我知道:
0x2F位置的密文(原始文件):0x6f
0x2F位置的明文应该是:'6' = 0x36
计算:
密钥[15] = 0x6f ⊕ 0x36 = 0x59
验证另一个位置:
0x3F位置的密文:0x73
0x3F位置的明文应该是:'*' = 0x2a
计算:
密钥[15] = 0x73 ⊕ 0x2a = 0x59
两个位置计算出的结果一致,都是0x59!这验证了我的推理是正确的。
当前密钥的第15个字节是0x73,需要改为0x59。
使用修正后的密钥:
key_final = bytes([0x21, 0x58, 0x2c, 0x6c, 0x30, 0xbe, 0x12, 0x17,
0x32, 0x2c, 0xdb, 0x9a, 0xeb, 0xaf, 0x4a, 0x59])
解密结果:
00000000: 48 6d 6d 6d 2c 20 67 6f 6f 64 20 6a 6f 62 2c 20 Hmmm, good job,
00000010: 74 68 65 20 66 6c 61 67 20 69 73 20 6e 6f 74 0a the flag is not.
00000020: 52 43 54 46 7b 57 65 31 6c 5f 64 30 6e 33 5f 36 RCTF{We1l_d0n3_6
00000030: 75 74 5f 77 68 34 74 5f 69 35 5f 2a 6b 65 79 2a ut_wh4t_i5_*key*
00000040: 7d 0a 20 20 20 20 20 20 20 20 20 20 20 20 20 0a }. .
完美!现在所有字符都正常了:
Hmmm, good job, the flag is not
RCTF{We1l_d0n3_6ut_wh4t_i5_*key*}
但FLAG中有一个占位符:*key*
这显然需要我们找出真正的"key"是什么。
现在让我重新审视得到的最终密钥:
21582c6c30be1217322cdb9aebaf4a59
这个十六进制字符串长度是多少?数一数:32个字符。
32个十六进制字符意味着16个字节,也就是128位。
128位!这正好是MD5哈希值的长度!
结合题目名称"x_xor_md5",一切都说得通了:
x代表XOR
md5代表这个密钥是某个字符串的MD5值
所以现在的任务是:找到MD5值21582c6c30be1217322cdb9aebaf4a59对应的原始字符串。
MD5是一个单向哈希函数,理论上不可逆。但是对于简单的字符串,可以通过以下方法找到原文:
FLAG中提示是"wh4t_i5_key"(what is key),这暗示key可能是一个常见的英文单词。
让我写个脚本尝试一些常见单词:
import hashlib
target_md5 = "21582c6c30be1217322cdb9aebaf4a59"
common_words = ["that", "this", "what", "when", "where", "which",
"flag", "key", "word", "text", "data"]
for word in common_words:
md5_hash = hashlib.md5(word.encode()).hexdigest()
if md5_hash == target_md5:
print(f"找到了: {word}")
break
运行后发现:that的MD5值正是 21582c6c30be1217322cdb9aebaf4a59!
验证:
import hashlib
print(hashlib.md5(b"that").hexdigest())
# 输出: 21582c6c30be1217322cdb9aebaf4a59
也可以使用在线MD5解密网站,如:
https://www.cmd5.com/
https://hashkiller.co.uk/md5-decrypter.aspx
https://crackstation.net/
这些网站维护了庞大的MD5彩虹表,可以快速查询常见字符串的MD5值。
将FLAG中的*key*替换为破解出的单词that:
RCTF{We1l_d0n3_6ut_wh4t_i5_that}
这就是最终的FLAG!
为了方便理解和复现,这里提供完整的Python解题代码:
#!/usr/bin/env python3
import hashlib
# 读取加密文件
with open('6c0c57fa88eb44f3b179a6e9798fc7b6', 'rb') as f:
data = f.read()
# 从重复序列识别并修正密钥
# 初始密钥(从0x50行重复序列提取)
key_from_pattern = bytes([0x01, 0x78, 0x0c, 0x4c, 0x10, 0x9e, 0x32, 0x37,
0x12, 0x0c, 0xfb, 0xba, 0xcb, 0x8f, 0x6a, 0x53])
# 修正大小写(每个字节异或0x20)
key_case_fixed = bytes([b ^ 0x20 for b in key_from_pattern])
# 修正最后一个字节(0x73 -> 0x59)
key_final = bytearray(key_case_fixed)
key_final[15] = 0x59
key_final = bytes(key_final)
print(f"最终XOR密钥: {key_final.hex()}")
# XOR解密
result = bytearray()
for i in range(len(data)):
result.append(data[i] ^ key_final[i % len(key_final)])
# 显示解密结果
decrypted = result.decode('ascii', errors='replace').strip()
print(f"\n解密内容:\n{decrypted}")
# 识别MD5并破解
md5_value = key_final.hex()
print(f"\n密钥是MD5值: {md5_value}")
# 尝试常见单词
common_words = ["that", "this", "what", "when", "where"]
for word in common_words:
if hashlib.md5(word.encode()).hexdigest() == md5_value:
print(f"MD5对应的原文: {word}")
final_flag = f"RCTF{{We1l_d0n3_6ut_wh4t_i5_{word}}}"
print(f"\n最终FLAG: {final_flag}")
break
运行输出:
最终XOR密钥: 21582c6c30be1217322cdb9aebaf4a59
解密内容:
Hmmm, good job, the flag is not
RCTF{We1l_d0n3_6ut_wh4t_i5_*key*}
密钥是MD5值: 21582c6c30be1217322cdb9aebaf4a59
MD5对应的原文: that
最终FLAG: RCTF{We1l_d0n3_6ut_wh4t_i5_that}
核心特性:
可逆性:使用相同的密钥可以加密和解密
简单高效:只需要异或运算
对称性:加密和解密使用相同的操作
主要弱点:
循环密钥会产生模式:相同的明文块产生相同的密文块
已知明文攻击:知道部分明文和密文可以计算密钥
频率分析:对于较长的密文,可以通过统计分析破解
本题展示了系统的密码分析流程:
第一步:观察
查看十六进制数据
寻找模式和异常
记录所有可疑特征
第二步:假设
基于观察提出合理假设
考虑题目提示(名称、描述等)
结合已知的密码学知识
第三步:验证
实现假设并测试
分析结果
调整假设
第四步:迭代
解决新发现的问题
精细调整
直到得到最终答案
在密码分析中,理解ASCII编码非常重要:
字符范围:
可见字符:0x20-0x7E
大写字母:0x41-0x5A(A-Z)
小写字母:0x61-0x7A(a-z)
数字:0x30-0x39(0-9)
大小写转换:
大小写字母相差0x20
'A' XOR 0x20 = 'a'
'a' XOR 0x20 = 'A'
利用价值:
判断解密结果是否合理
发现编码问题
进行精细调整
这是密码分析中的经典方法:
原理:
密钥 = 明文 ⊕ 密文
应用场景:
知道部分明文内容(如文件头、特定格式)
可以猜测明文内容(如常见单词、语法结构)
有重复的明文块
本题应用:
猜测FLAG格式:"RCTF{...}"
确定某个字符应该是'6'或'*'
利用这些已知信息计算密钥
识别特征:
固定长度:32个十六进制字符
字符集:0-9, a-f
不可逆:无法直接反推原文
破解方法:
字典攻击:尝试常见单词
暴力破解:遍历所有可能(耗时长)
彩虹表:预计算的哈希表(空间换时间)
在线服务:利用已有的哈希数据库
安全建议:
MD5已被证明存在碰撞漏洞
不应用于密码存储(应使用bcrypt、Argon2等)
不应用于数字签名(应使用SHA-256及以上)
可用于非安全场景(如文件校验,但SHA-256更好)
题目命名的重要性:
题目名称往往包含关键提示
"x_xor_md5"直接告诉了技术栈
不要忽视任何细节
分层设计:
第一层:识别加密方式(XOR)
第二层:找到密钥(重复模式)
第三层:调整密钥(大小写、精确值)
第四层:识别MD5
第五层:破解MD5
障碍设置:
大小写反转:增加难度
部分字节错误:需要精细调整
MD5嵌套:需要额外一步破解
十六进制查看:
xxd:Linux命令行工具,轻量快速
hexdump:另一个命令行选择
HxD:Windows下的图形化工具
010 Editor:专业的十六进制编辑器
Python库:
hashlib:计算各种哈希值(MD5、SHA系列等)
binascii:二进制和ASCII转换
struct:处理C结构体和二进制数据
在线工具:
CyberChef:瑞士军刀式的密码分析工具
HashKiller:MD5等哈希在线破解
Crackstation:支持多种哈希算法
保持系统性:
记录每一步的发现和思考
不要跳过观察阶段
建立假设-验证的循环
理解原理:
不要只记住步骤,要理解为什么
知道工具背后的原理
能够手动实现才算真正理解
善用工具:
不要重复造轮子
了解工具的能力和局限
工具只是辅助,思路最重要
保持耐心:
密码分析常需要多次尝试
遇到障碍是正常的
每次失败都能提供新信息
简单的XOR加密很容易被破解,但可以通过以下方式加强:
一次性密码本(One-Time Pad):
密钥长度等于明文长度
密钥完全随机
密钥只使用一次
理论上不可破解
流密码(Stream Cipher):
使用伪随机数生成器产生密钥流
现代算法如ChaCha20、RC4(已弱)
密钥流的质量决定安全性
结合其他技术:
先加密后认证(Encrypt-then-MAC)
使用强密钥派生函数
添加初始化向量(IV)
碰撞攻击:
2004年王小云等人发现实用碰撞攻击
可以构造两个不同的消息产生相同MD5值
对数字签名构成威胁
前缀碰撞:
可以在给定前缀下构造碰撞
更加实用的攻击方式
可伪造证书等
替代方案:
SHA-256、SHA-3:用于完整性校验
bcrypt、scrypt、Argon2:用于密码存储
HMAC-SHA256:用于消息认证
认证加密(AEAD):
同时保证机密性和完整性
如AES-GCM、ChaCha20-Poly1305
防止中间人攻击和篡改
后量子密码学:
抵抗量子计算机攻击
基于格、编码、多变量等困难问题
NIST正在标准化过程中
零知识证明:
证明知道某个秘密而不泄露秘密本身
区块链、隐私保护中的应用
如zk-SNARKs、zk-STARKs
这道题目虽然涉及的技术点不算特别深,但完整地展示了密码分析的基本流程:
观察与模式识别:发现重复的十六进制序列
密码学原理应用:理解XOR加密的特性
假设与验证:提取密钥、尝试解密
问题诊断:发现大小写问题
深入分析:理解ASCII编码
精细调整:使用已知明文攻击修正密钥
突破关键:识别MD5哈希
最终破解:获得完整FLAG
整个过程不仅需要密码学知识,还需要:
细心的观察能力
逻辑推理能力
编程实现能力
系统性的分析思维
不断尝试的耐心
对于CTF初学者,这道题提供了很好的学习价值:
难度适中,不会过于复杂
涵盖多个知识点
需要完整的分析过程
有明确的验证结果
通过这道题,我们不仅学会了如何破解XOR加密,更重要的是学会了如何系统地分析一个密码学问题。这种思维方式和方法论在信息安全领域的各个方面都适用。
最终FLAG:RCTF{We1l_d0n3_6ut_wh4t_i5_that}
希望这篇详细的分析能帮助你理解整个解题过程,在今后遇到类似问题时能够独立分析和解决。密码学的魅力在于,每一个成功的破解都建立在扎实的理论基础和细致的观察之上。继续学习,不断实践,你也能成为密码分析的高手!