前言
有需要的朋友可以转去关于密码学的专栏学习更多密码学知识
除去密码题以外,本专栏也包含了11章的密码学知识详解,比较全面,较为系统的教学密码学
CTF密码学综合教学指南_其实防守也摸鱼的博客-CSDN博客

资源问题,由于我原先直接配套资源了,忘记要vip才能下载,我已经重新上传了免费资源,在审核,之后会显示在我主页,有需要的自取
Crypto1
这串字符 aW9kantmdWJzd3ItaGR2Yn0= 看起来是标准的 Base64 编码。我们来对它进行解码:
aW9kantmdWJzd3ItaGR2Yn0=
解码后的结果是:
iodj{tfubswr-hdvb}
💡 进一步分析:
这串解码出来的文本 iodj{tfubswr-hdvb} 看起来像是一个 CTF (夺旗赛)的 Flag,但经过了某种简单的替换加密(比如凯撒密码)。
如果我们观察它的结构 iodj{...},通常 Flag 的格式是 flag{...}。
- i 到 f 差了 3 位
- o 到 l 差了 3 位
- d 到 a 差了 3 位
- j 到 g 差了 3 位
所以这大概率是一个 凯撒密码(Caesar Cipher) ,偏移量为 3(或者说向前移 3 位)。我们对括号里的内容也做同样的偏移:
- t -> q
- f -> c
- u -> r
- b -> y
- s -> p
- w -> t
- r -> o
- h -> e
- d -> a
- v -> s
- b -> y
最终还原出的 Flag 应该是:
flag{qcryptop-easy}
(意思是 "flag{quick crypto easy}",很典型的 CTF 签到题风格 😄)
如果你只需要 Base64 解码的结果,那就是 iodj{tfubswr-hdvb};如果是为了做题,答案就是 flag{qcryptop-easy}!
Crypto2
【第一步】Base64 解码
原始密文 a2Nwa3t0eGRnLTIwMjYwNjAxfQ== 末尾有 ==,是标准 Base64 编码。解码得到:
kcpk{txdg-20260601}
【第二步】观察格式
解码结果符合 CTF flag 格式 xxx{xxxx-xxxxxxxx},但前缀是 kcpk 而不是 flag,说明 Base64 之后还有一层加密。
【第三步】已知明文攻击推导密钥
已知明文前缀应为 flag,对比密文前缀 kcpk,按 Vigenere 密钥公式 K = (C - P) mod 26:
| 位置 | 密文 C | 明文 P | 密钥 K |
|---|---|---|---|
| 0 | k(10) | f(5) | f(5) |
| 1 | c(2) | l(11) | r(17) |
| 2 | p(15) | a(0) | p(15) |
| 3 | k(10) | g(6) | e(4) |
前4位密钥为 frpe。
【第四步】分析密钥含义
对 frpe 做 ROT13 解密得到 secr,这是 secret 的前4个字母。因此推测完整密钥是 secret 的 ROT13,即 frperg。
【第五步】用密钥 frperg 做 Vigenere 解密
对 kcpk{txdg-20260601} 中所有字母逐位解密,密钥循环使用 frperg:
| 位置 | 字符 | 密钥 | 结果 |
|---|---|---|---|
| 0 | k | f | f |
| 1 | c | r | l |
| 2 | p | p | a |
| 3 | k | e | g |
| 5 | t | g | n |
| 6 | x | f | s |
| 7 | d | r | m |
| 8 | g | p | r |
非字母字符保持不变。
最终答案:
flag{nsmr-20260601}
注意:花括号内的
nsmr是通过密钥frperg严格解密出来的。如果题目有其他提示(比如 flag 内容应该是英文单词),可能需要调整密钥或解法。
Crypto3
最先是

然后提交发现错了
再问AI就是说前面给的 flag{nujgoi-0606261818} 是误判:它只是 Hill 暴力枚举里"能凑出 flag 前缀"的候选,但 nujgoi 没有语义,平台判错也合理。
由于原先提交其他都是猜测花括号里的前六个字母会不会是crypto这样的
flag{crypto-XXXXXXXXX}这种格式,就猜测了一下flag{crypto-0606261818}
解密过程总结:
第一层:Base32 解码
Plain Text
输入: OBZXGZL3NFVHOZTLMUWTANRQGYZDMMJYGE4H2===
输出: psse{ijwfke-0606261818}
这个没错估计
第二层:分段维吉尼亚密码解密
- 第一部分 psse 使用密钥 khsy 解密 → flag
- 第二部分 ijwfke 使用密钥 drz 解密 → fsxctf
解出
flag{fsxctf-0606261818}然后也是错的
解密方法分析:
这份文件采用了多种古典密码算法的组合:
- Base32编码 - 第一层编码
- 维吉尼亚密码 - 第二层加密,采用分段密钥的方式
- 前4个字符使用密钥 khsy
- 中间6个字符使用密钥 drz
- 数字部分保持不变
Crypto4
文件识别
上传的文件名是 crypto4,没有扩展名。直接读取后,文件开头是:
1f 8b 08 ...
其中 1f 8b 是 gzip 文件的标准魔数,所以第一步判断它是一个 gzip 压缩文件。
使用 Python 解压:
import gzip
from pathlib import Path
p = Path("crypto4")
data = gzip.decompress(p.read_bytes())
print(len(data))
print(data[:100])
解压后发现内容里出现了:
crypto4_key1.pem
crypto4_key2.pem
crypto4_ciphertext.txt
ustar
其中 ustar 是 tar 包的标识,所以这个文件实际结构是:
crypto4
└── gzip
└── tar
├── crypto4_key1.pem
├── crypto4_key2.pem
└── crypto4_ciphertext.txt
解包内容
继续用 Python 读取 tar 包:
import gzip
import tarfile
import io
from pathlib import Path
p = Path("crypto4")
data = gzip.decompress(p.read_bytes())
tf = tarfile.open(fileobj=io.BytesIO(data))
for m in tf.getmembers():
content = tf.extractfile(m).read()
print("-----", m.name, "-----")
print(content.decode())
得到三个文件:
crypto4_key1.pem
crypto4_key2.pem
crypto4_ciphertext.txt
其中 crypto4_ciphertext.txt 内容为:
c1 = 3354246622808497330149126245003577859891574982636581634192727601714675024977381407280027224565451159379278508414240817015858101837242677812127234810981
c2 = 5115765747974166108965699592958570313222959607511161841544516184137445502772662097626485065903071299352237876686313647549519071032788508081992097512529587052579086108840853748146607700578887123941261991820810912871398126565745374487381778957072734221212547483638063514496723988036523341070440656719975595949
两个 .pem 文件都是 RSA 公钥。
分析公钥
重点看 crypto4_key1.pem:
-----BEGIN PUBLIC KEY-----
MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQCNVzniS18fPeyefpYEOxpdH3sC
dvvJ6BUbrbsQOnhWarc/JHGLP0XSS3xCvWLMrMaSYgDSoPNx/vrm//dGIvXafGnM
ZYuUBOTY37dEEpxcGeqMAliZsdJiTNn9T9ffeZsTjbRScjqkTQQVbM9UyLfDi9C8
KpU3hL0Ti4Tc5rtjgwIBAw==
-----END PUBLIC KEY-----
解析这个公钥后,可以得到 RSA 参数:
e = 3
也就是公钥指数非常小。
RSA 加密公式是:
c = m^e mod n
这里:
e = 3
所以:
c = m^3 mod n
如果明文 m 比较短,并且没有使用 padding,那么可能会出现:
m^3 < n
这种情况下模运算不会真正发生,也就是:
c = m^3
于是解密就不需要私钥,只要对密文 c 开整数三次方即可:
m = ∛c
这是一种典型的 RSA 小指数攻击,常见于 e = 3 且没有填充的情况。
对 c1 开三次方
对 c1 尝试整数三次方根:
c1 = 3354246622808497330149126245003577859891574982636581634192727601714675024977381407280027224565451159379278508414240817015858101837242677812127234810981
lo, hi = 0, 1
while hi ** 3 <= c1:
hi *= 2
while lo + 1 < hi:
mid = (lo + hi) // 2
if mid ** 3 <= c1:
lo = mid
else:
hi = mid
print(lo ** 3 == c1)
print(lo)
print(lo.to_bytes((lo.bit_length() + 7) // 8, "big"))
输出结果:
True
149691910197857642980538259626730817958315354568061
b'flag{rsarsarsa001515}'
其中:
lo ** 3 == c1
结果为 True,说明 c1 正好是某个整数的三次方。
将这个整数转成字节后得到:
flag{rsarsarsa001515}
为什么不用 c2
c2 很大,看起来不满足直接开三次方的条件,或者对应的是另一个 RSA 公钥加密结果。
本题中 c1 已经可以直接通过小指数攻击恢复出完整 flag,因此不需要继续处理 c2。
完整解题脚本
import gzip
import tarfile
import io
from pathlib import Path
p = Path("crypto4")
# 1. gzip 解压
data = gzip.decompress(p.read_bytes())
# 2. 读取 tar 包
tf = tarfile.open(fileobj=io.BytesIO(data))
files = {}
for m in tf.getmembers():
files[m.name] = tf.extractfile(m).read()
print(files.keys())
# 3. 取出密文
ciphertext = files["crypto4_ciphertext.txt"].decode()
print(ciphertext)
c1 = 3354246622808497330149126245003577859891574982636581634192727601714675024977381407280027224565451159379278508414240817015858101837242677812127234810981
# 4. 整数三次方根
lo, hi = 0, 1
while hi ** 3 <= c1:
hi *= 2
while lo + 1 < hi:
mid = (lo + hi) // 2
if mid ** 3 <= c1:
lo = mid
else:
hi = mid
# 5. 验证并转字节
assert lo ** 3 == c1
flag = lo.to_bytes((lo.bit_length() + 7) // 8, "big")
print(flag.decode())
最终答案:
flag{rsarsarsa001515}
Crypto5
文件识别
题目给的文件名是 crypto5,直接打开会看到大量乱码,但文件头是:
1F 8B 08
这是典型的 gzip 文件头。
解压后发现它其实是一个 tar.gz 归档,里面有三个文件:
crypto5_key1.pem
crypto5_key2.pem
crypto5_ciphertext.txt
其中:
crypto5_key1.pem # 第一组 RSA 公钥
crypto5_key2.pem # 第二组 RSA 公钥
crypto5_ciphertext.txt # 两段密文
密文内容是:
c1 = 23867958079544472581633928506111792891783130652779359989541178620210226545296996090272860004137111158823174742513575054971786472358794929432517266388141169595809805084915033971750274417222585056224131509724604421288504881787429567207574101535404678561791286058318665873528271429888496911253566171729944663916
# encrypted with n1 (key1)
c2 = 46482278369812079316254029460917906019191286058067415080810714478286725479841781582226340693837593472045825173150808324209354687097881481571483789943876815281341467653655869024778272063298881280321912192594867027906143824471474090446978150218670159918851800361120937678195275722667414725609948404934777813896
# encrypted with n2 (key2)
也就是说:
c1 用 key1 的 n1 加密
c2 用 key2 的 n2 加密
最终 flag 应该是两段明文拼接起来。
提取 RSA 参数
两个 .pem 文件都是 RSA 公钥,需要从中提取:
n: RSA 模数
e: RSA 公钥指数
提取后得到:
key1:
e1 = 65537
n1 位数约为 1023 bits
key2:
e2 很大,接近 n2
n2 位数约为 1023 bits
这里已经可以看出两个突破口:
key1 的 e 正常,但 n 可能存在因子弱点
key2 的 e 异常大,可能是小 d 造成的 Wiener 攻击场景
第一段:攻击 key1
观察思路
RSA 中:
n = p * q
如果 p 和 q 非常接近,那么可以用 Fermat 分解。
Fermat 分解基于:
n = p * q
令:
a = (p + q) / 2
b = (q - p) / 2
那么:
n = a^2 - b^2 = (a - b)(a + b)
所以只要从:
a = ceil(sqrt(n))
开始枚举,检查:
b^2 = a^2 - n
是否为完全平方数。
如果 p 和 q 很接近,a 很快就能命中。
实际结果
对 n1 做 Fermat 分解,第一次就成功,也就是:
迭代次数 = 0
说明:
p 和 q 极其接近
分解得到:
p1 = 6805866451734290271837213651940970271217416382343806044233277708418719025348826608681502713158162092319669032114226004615012807512855113844269894861580349
q1 = 6805866451734290271837213651940970271217416382343806044233277708418719025348826608681502713158162092319669032114226004615012807512855113844269894861671541
验证:
p1 * q1 == n1
成立。
求私钥 d1
RSA 私钥指数:
phi1 = (p1 - 1) * (q1 - 1)
d1 = inverse(e1, phi1)
也就是:
d1 = pow(e1, -1, phi1)
解密 c1
RSA 解密公式:
m1 = pow(c1, d1, n1)
转成字节:
m1_bytes = m1.to_bytes((m1.bit_length() + 7) // 8, "big")
得到:
m1 = flag{hard
第一段明文是:
flag{hard
第二段:攻击 key2
观察思路
第二个公钥的 e2 非常大。
正常 RSA 里常用:
e = 65537
但这里 e2 是一个接近 n2 的大数。这种情况很可能是因为私钥指数 d 很小。
RSA 中有关系:
e * d ≡ 1 mod phi(n)
也就是说存在整数 k,使得:
e*d - k*phi(n) = 1
变形:
e*d / phi(n) ≈ k
进一步可得:
e / n ≈ k / d
如果 d 足够小,就可以通过 e/n 的连分数展开找到 k/d。
这就是 Wiener 攻击。
Wiener 攻击适用条件大致是:
d < n^0.25 / 3
Wiener 攻击流程
对:
e2 / n2
做连分数展开,得到若干 convergents,也就是近似分数:
k / d
对于每个候选 (k, d),检查:
phi = (e*d - 1) // k
然后利用:
phi(n) = (p - 1)(q - 1)
= n - p - q + 1
可得:
p + q = n - phi + 1
令:
s = p + q = n - phi + 1
则 p、q 是方程:
x^2 - s*x + n = 0
的两个根。
判别式:
Δ = s^2 - 4n
如果 Δ 是完全平方数,则:
p = (s + sqrt(Δ)) // 2
q = (s - sqrt(Δ)) // 2
再验证:
p * q == n
如果成立,就说明找到了正确的 d。
实际结果
对 key2 进行 Wiener 攻击成功,得到:
d2 位数 = 240 bits
这说明 d2 确实非常小,因此符合 Wiener 攻击条件。
恢复出的因子为:
p2 = 10567511415864993824956602926728313020581878146888908033462612886440699703390528757924216656536876472873574389540884717819821630954926757101686288166077341
q2 = 7534471815182910416011609703307515430220725438139394686883162761388426514334213456134425595252883278498330345704934339676006813740663997943650869751682369
验证:
p2 * q2 == n2
成立。
解密 c2
使用恢复出的 d2 解密:
m2 = pow(c2, d2, n2)
转字节:
m2_bytes = m2.to_bytes((m2.bit_length() + 7) // 8, "big")
得到:
m2 = rsa-2606a}
第二段明文是:
rsa-2606a}
拼接 flag
两段明文分别是:
m1 = flag{hard
m2 = rsa-2606a}
拼接:
flag{hardrsa-2606a}
所以完整 flag 为:
flag{hardrsa-2606a}
关键代码
import math
import base64
def parse_pem_pubkey(path):
data = open(path).read()
b64 = ''.join(line for line in data.splitlines() if not line.startswith('-----'))
der = base64.b64decode(b64)
def parse(data, idx):
tag = data[idx]
idx += 1
length = data[idx]
idx += 1
if length & 0x80:
n = length & 0x7f
length = int.from_bytes(data[idx:idx+n], 'big')
idx += n
return tag, length, idx
_, _, idx = parse(der, 0)
_, length, idx2 = parse(der, idx)
idx = idx2 + length
_, _, idx = parse(der, idx)
idx += 1
_, _, idx = parse(der, idx)
_, length, idx = parse(der, idx)
n = int.from_bytes(der[idx:idx+length], 'big')
idx += length
_, length, idx = parse(der, idx)
e = int.from_bytes(der[idx:idx+length], 'big')
return n, e
Fermat 分解:
def fermat(n):
a = math.isqrt(n)
if a * a < n:
a += 1
while True:
b2 = a * a - n
b = math.isqrt(b2)
if b * b == b2:
p = a - b
q = a + b
return p, q
a += 1
Wiener 攻击:
def continued_fraction(num, den):
result = []
while den:
q = num // den
result.append(q)
num, den = den, num - q * den
return result
def convergents(cf):
result = []
for i in range(len(cf)):
if i == 0:
result.append((cf[0], 1))
elif i == 1:
result.append((cf[0] * cf[1] + 1, cf[1]))
else:
numerator = cf[i] * result[-1][0] + result[-2][0]
denominator = cf[i] * result[-1][1] + result[-2][1]
result.append((numerator, denominator))
return result
def wiener(e, n):
cf = continued_fraction(e, n)
for k, d in convergents(cf):
if k == 0:
continue
if (e * d - 1) % k != 0:
continue
phi = (e * d - 1) // k
s = n - phi + 1
delta = s * s - 4 * n
if delta < 0:
continue
sqrt_delta = math.isqrt(delta)
if sqrt_delta * sqrt_delta != delta:
continue
if (s + sqrt_delta) % 2 != 0:
continue
p = (s + sqrt_delta) // 2
q = (s - sqrt_delta) // 2
if p * q == n:
return d, p, q
return None
解密:
p1, q1 = fermat(n1)
phi1 = (p1 - 1) * (q1 - 1)
d1 = pow(e1, -1, phi1)
m1 = pow(c1, d1, n1)
d2, p2, q2 = wiener(e2, n2)
m2 = pow(c2, d2, n2)
b1 = m1.to_bytes((m1.bit_length() + 7) // 8, "big")
b2 = m2.to_bytes((m2.bit_length() + 7) // 8, "big")
flag = (b1 + b2).decode()
print(flag)
输出:
flag{hardrsa-2606a}
Crypto6
第 1 步:识别文件类型
读取上传文件的前几个字节,发现以 1F 8B 08 开头,这是 gzip 压缩格式的魔数,说明文件是一个 gzip 压缩包。
第 2 步:解压 gzip + tar
使用 gzip.decompress 解压后,内容以 crypto6_output.txt 开头并出现 ustar 标识,表明这是一个 tar 归档。提取后得到两个文件:
crypto6_output.txt:密文ZGfmn7Fon7DYM+6NOXVnx+Jwd7Fozfm26ElJrEwYPqfVOtaZv+/UJutTOdUecfBjcrypto6_hint.txt:加密方式说明
第 3 步:阅读 hint,梳理加密链路
提示文件给出加密流程(解密时需要逆向):
- flag 与 Python
random(用 4 位 PIN 作种子)生成的 keystream 进行 XOR - 结果用 PKCS7 填充到 16 字节,再用
AES-128-CBC加密 IV(16字节) + 密文拼接后做Base64编码- 密钥派生:
aes_key = SHA256("crypto6-{PIN}".encode() + salt_bytes)[:16] salt = 53796d6d65747269634b657953616c74(hex,对应SymmetricKeyS)- PIN 为 0000--9999
第 4 步:解 Base64 + 拆 IV/密文
Base64 解码得到 48 字节:前 16 字节是 IV,后 32 字节是 AES 密文。
第 5 步:暴力枚举 PIN(0000--9999)
对每个 PIN:
- 用
SHA256("crypto6-{PIN}" + salt)[:16]派生 AES key - 用
AES-128-CBC解密,能成功unpad的才是候选(淘汰大部分错误 PIN) - 把解密结果与
random.Random(seed)生成的 keystream 做 XOR
由于 hint 没明确 seed 类型与生成方法,我同时测试了 3 种 seed(int(PIN)、字符串 PIN、字符串 crypto6-{PIN})和 5 种 keystream 生成方式(randint / getrandbits / randrange / randbytes / random()*256)。
第 6 步:用「可打印字符比例」打分筛选
对每个候选 flag 计算可打印 ASCII 占比,命中明显高分的组合:
- PIN =
2606 - seed 为整数
2606 - keystream 生成方式:
random.Random(2606).randbytes(n) - 可打印率 100%
最终明文
flag{crypto6-26200606a}
Crypto7
感觉应该是缺了信息了,也是解不出来的
题目提示是:AES
PKCS7
经过多种常见密钥/IV/模式(ECB、CBC,0填充IV,MD5/SHA派生,常用CTF口令、密钥列表等)的尝试,仍未能成功解密。
已确认的信息
- 密文(Base64):neIUMtajFEWLAwyyCFbvKcle/D5pf50JfClZDFAZM8c=
- 解码后字节长度为 32(恰好两个 AES 分组),符合 AES + PKCS7 的特征。
- 题目仅给出"AES / PKCS7"提示,但 未给出密钥(Key)和 IV。
关键阻塞点
AES 是对称加密,不知道密钥就无法逆向解密------这不是单纯的"编码",必须有 key(以及 CBC 模式下的 IV)。我已批量尝试了几百个常见候选(默认弱密钥、crypto7/AES/PKCS7/kcpk/txdg-20260601、口令的 MD5/SHA1/SHA256 派生、零 IV 等),均未产生可解析的明文。
请补充以下任一信息
- 密钥 Key(字符串或 hex),可能是 16/24/32 字节
- 加密模式(ECB / CBC / GCM 等)
- 若是 CBC,请提供 IV
easyCostas
题目问题,没给nc,没有真实的服务器连接信息,就解不出来
这个题的关键不是"解码文件",而是先构造一个 16×16 的 Costas 阵。通过后服务会给出 RSA 参数,再利用 (p+1)(q+1) 和 n 还原 phi 解密。
可以直接输入这 16 个点:
(1,1)
(2,3)
(3,9)
(4,10)
(5,13)
(6,5)
(7,15)
(8,11)
(9,16)
(10,14)
(11,8)
(12,7)
(13,4)
(14,12)
(15,2)
(16,6)
这是 Welch 构造,取素数 P = 17,原根 g = 3:
points = [(i, pow(3, i - 1, 17)) for i in range(1, 17)]
通过校验后,服务端会输出三行:
Plain Text
A = (p+1)*(q+1)
n = p*q
c = flag^e mod n
其中:
A = (p + 1) * (q + 1)
= pq + p + q + 1
= n + p + q + 1
所以:
p + q = A - n - 1
phi = (p - 1) * (q - 1)
= n - (p + q) + 1
= 2*n - A + 2
解密脚本:
from Crypto.Util.number import long_to_bytes, inverse
e = 65537
A = int(input("A = "))
n = int(input("n = "))
c = int(input("c = "))
phi = 2 * n - A + 2
d = inverse(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m))
这是因为:
- A = (p+1)(q+1) = n + p + q + 1
- 所以 p + q = A - n - 1
- phi = (p-1)(q-1) = n - (p+q) + 1 = 2n - A + 2
测试结果显示公式 phi = 2*n - A + 2 可以正确解密。
示例:如果有真实的服务器连接信息,大概就是:
Costas 阵点:
(1,1) (2,3) (3,9) (4,10) (5,13) (6,5) (7,15) (8,11) (9,16) (10,14) (11,8) (12,7) (13,4) (14,12) (15,2) (16,6)
服务端返回的 RSA 参数:
- A = (p+1)*(q+1) =
- 104524895049395201967674443577141039392828270838995668608741621218637881913841378898593311689018065020967458457816125545680370526650471200255075661908388096006125007106235960112617387467305561354516587204128765384775813264793342846961739912053385814784770187628266887902918935823353964271442769125348777709760
- n = p*q =
- 104524895049395201967674443577141039392828270838995668608741621218637881913841378898593311689018065020967458457816125545680370526650471200255075661908388075095705175812929329383200042221769259506291428622664863600910187097927175464886285251912534815784395052068668658759836823569405832656019818838142821182967
- c = flag^e mod n =
- 74548228395014992797117121266683925790896540015983710626175102422077719428730946114002414869343701018344463252227324873058705619245577065432361866672991956806843776291537593368285015438464752477332129667395167032317629428873721520609114751117449113232046198993734115044670014112318428456858379316876267183564
就能解出flag
Just Once_atta
这是典型的"一次一密被重复使用"漏洞:两个文件用了同一个循环异或密钥加密,其中 fflag.png 和 fflag_e.png 是已知明文/密文对,可以反推出密钥,再解 flag_e.png。
题目文件
你给的文件里主要有:
encode.py:加密脚本fflag.png:已知原图fflag_e.png:fflag.png的加密结果flag_e.png:真正要解的密文图片
encode.py 内容核心如下:
from itertools import *
from key import key
ki = cycle(key)
fr1 = open("flag.png", "rb")
fr2 = open("fflag.png", "rb")
fw1 = open("flag_e.png", "wb")
fw2 = open("fflag_e.png", "wb")
for now in fr1:
for nowByte in now:
newByte = nowByte ^ ord(next(ki))
fw1.write(bytes([newByte]))
for now in fr2:
for nowByte in now:
newByte = nowByte ^ ord(next(ki))
fw2.write(bytes([newByte]))
加密原理
每个字节都做了异或:
密文字节 = 明文字节 ^ 密钥字节
所以反过来:
明文字节 = 密文字节 ^ 密钥字节
并且如果同时知道明文和密文:
密钥字节 = 明文字节 ^ 密文字节
这就是本题突破点。
漏洞点
脚本里用了:
ki = cycle(key)
这表示密钥会循环使用。
更关键的是,ki 只创建了一次
ki = cycle(key)
然后先加密 flag.png,再继续加密 fflag.png。
也就是说,两个文件共用了同一个循环密钥流。
题目标题:
just once 有些东西只能用一次
暗示的就是一次一密不能重复使用。这里密钥流被重复使用了,所以可以用已知明文攻击。
第一步:用已知明密文恢复密钥流
已知:
fflag.png
fflag_e.png
所以可以逐字节异或:
ks = fflag.png ^ fflag_e.png
代码:
from pathlib import Path
p = Path("附件目录")
plain = (p / "fflag.png").read_bytes()
cipher = (p / "fflag_e.png").read_bytes()
keystream = bytes(a ^ b for a, b in zip(plain, cipher))
print(keystream[:32])
print(keystream[:32].hex())
得到前 32 字节密钥流:
416c6974615f69735f736f5f63777465416c6974615f69735f736f5f63777465
转成 ASCII:
Alita_is_so_cwteAlita_is_so_cwte
看起来周期是 16 字节:
Alita_is_so_cwte
但这里有一个小坑。
第二步:确定密钥长度
检查密钥流的最短重复周期:
for L in range(1, 1000):
ok = all(keystream[i] == keystream[i - L] for i in range(L, len(keystream)))
if ok:
print(L)
break
输出最短周期:
16
所以密钥长度是 16 字节。
第三步:处理密钥偏移
加密顺序是:
flag.png -> flag_e.png
fflag.png -> fflag_e.png
如果 flag.png 的长度不是 16 的倍数,那么 fflag.png 开始加密时密钥会有偏移。
但 flag_e.png 的长度是:
7664 字节
而:
7664 % 16 = 0
所以 fflag.png 开始加密时,密钥刚好重新回到开头。
因此从 fflag.png ^ fflag_e.png 恢复出来的密钥可以直接用于解密 flag_e.png,不需要额外偏移。
第四步:修正密钥
一开始根据已知明密文得到的密钥是:
Alita_is_so_cwte
但用它解 flag_e.png 后,PNG 文件头出现异常:
IJDR
正常 PNG 文件头中应该有:
IHDR
PNG 文件标准头为:
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52
也就是:
.PNG........IHDR
用 Alita_is_so_cwte 解出来的是:
.PNG........IJDR
其中 J 应该是 H。
这说明密钥中对应位置有 1 字节不对。
根据 PNG 头反推正确密钥:
cipher = flag_e.read_bytes()
png_header = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
real_key_prefix = bytes(cipher[i] ^ png_header[i] for i in range(16))
print(real_key_prefix)
得到:
Alita_is_so_cute
所以正确密钥是:
Alita_is_so_cute
cwte 应该是已知明文文件中某个字节被干扰导致的误差,结合 PNG 文件头可以修正为 cute。
第五步:解密 flag_e.png
最终解密脚本:
from pathlib import Path
cipher_path = Path("flag_e.png")
out_path = Path("decoded_flag.png")
key = b"Alita_is_so_cute"
cipher = cipher_path.read_bytes()
plain = bytes(
cipher[i] ^ key[i % len(key)]
for i in range(len(cipher))
)
out_path.write_bytes(plain)
运行后得到正常图片 decoded_flag.png。
图片内容显示:
flag{657be30363225dfa595d3c8e59577181}
最终答案
flag{657be30363225dfa595d3c8e59577181}
rsaattack_1911275dde531ba71cf
e1 = 17
e2 = 65537
两个密文分别保存在:
flag.enc1
flag.enc2
攻击条件
RSA 共模攻击需要满足:
c1 = m^e1 mod N
c2 = m^e2 mod N
gcd(e1, e2) = 1
这里:
e1 = 17
e2 = 65537
计算可得:
gcd(17, 65537) = 1
因此满足共模攻击条件。
数学原理
因为 e1 和 e2 互素,所以根据扩展欧几里得算法,一定存在整数 s 和 t,使得:
s * e1 + t * e2 = 1
也就是:
17s + 65537t = 1
两边作为指数:
m^(17s + 65537t) = m^1 = m
又因为:
c1 = m^17 mod N
c2 = m^65537 mod N
所以:
c1^s * c2^t
= (m^17)^s * (m^65537)^t
= m^(17s) * m^(65537t)
= m^(17s + 65537t)
= m mod N
因此可以直接恢复:
m = c1^s * c2^t mod N
处理负指数
扩展欧几里得算法求出来的 s 或 t 可能是负数。
在模运算中,负指数不能直接使用普通幂运算,需要转换成模逆元。
如果:
s < 0
那么:
c1^s mod N = inverse(c1, N)^(-s) mod N
如果:
t < 0
那么:
c2^t mod N = inverse(c2, N)^(-t) mod N
所以实际计算逻辑是:
if s < 0:
c1 = inverse(c1, N)
s = -s
if t < 0:
c2 = inverse(c2, N)
t = -t
然后再计算
m = pow(c1, s, N) * pow(c2, t, N) % N
解题脚本
完整 Python3 解题脚本如下
N = 119948438574946396961241802562855881094434889390583047090848937974817859617078945408041482897601510287785159609736391149578405254362690072493864468045703358185051265421707586687446960709325978018375102945209782341228149058599671266964703755620116064591962525601416845289122515579227855172912274929724239354077
e1 = 17
e2 = 65537
c1 = 55167794593756053273856793408318453272073830102737097218968137273520532833073128447879637837769356057556897700925935115372647692172407464363817360303614014307378923796364920927493405095583428095459378756321614378700023486082556366356221691763707023605536508524714070043501953158830796134970478896337965683264
c2 = 10090004404549507308479531798426364252971608196992243500095345207574466449752556983009583962312305262255238400318102859347054243896319273674547641580106245611724390156050522716206815991021043711461586874690431002470362806203534186638630357105365389418433293629767742846606876808312082345913017513221755880253
def egcd(a, b):
if b == 0:
return a, 1, 0
g, x1, y1 = egcd(b, a % b)
x = y1
y = x1 - (a // b) * y1
return g, x, y
def inverse(a, n):
g, x, y = egcd(a, n)
if g != 1:
raise ValueError("不存在逆元")
return x % n
g, s, t = egcd(e1, e2)
print("gcd =", g)
print("s =", s)
print("t =", t)
if g != 1:
raise ValueError("e1 和 e2 不互素,不能直接使用共模攻击")
if s < 0:
c1 = inverse(c1, N)
s = -s
if t < 0:
c2 = inverse(c2, N)
t = -t
m = pow(c1, s, N) * pow(c2, t, N) % N
hex_m = hex(m)[2:]
if len(hex_m) % 2 != 0:
hex_m = "0" + hex_m
plaintext = bytes.fromhex(hex_m)
print(plaintext)
运行结果
运行后得到:
gcd = 1
恢复出的明文为:
b'flag{ef1a47b67b41da4ac7584085e612c71d}AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
其中真正的 flag 是前面的部分:
flag{ef1a47b67b41da4ac7584085e612c71d}
后面的 A 是填充字符。
为什么会有很多 A
源代码里有这一行:
data = flag.strip("\n").ljust(128, "A")
这表示程序先去掉 flag 末尾的换行符,然后把内容填充到 128 字节。
如果原始 flag 不足 128 字节,就用字符 "A" 补齐。
所以恢复出来的明文形如:
flag{...}AAAAAAAAAAAA...
真正的 flag 只需要取:
flag{ef1a47b67b41da4ac7584085e612c71d}
最终答案
flag{ef1a47b67b41da4ac7584085e612c71d}
SameMod_5afea374dbb92
题目分析
附件给出两组公钥 (N, e1)、(N, e2) 与对应的两段密文 c1、c2:
N = 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249
e1 = 773, e2 = 839
c1 = 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349
c2 = 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535
特征:模数 N 相同、明文 m 相同、公钥指数不同 ------这是 RSA 的共模攻击(Common Modulus Attack) 经典场景。
攻击原理
由加密定义:
c1 ≡ m^e1 (mod N)
c2 ≡ m^e2 (mod N)
若 gcd(e1, e2) = 1,由扩展欧几里得算法可得整数 s, t 满足:
s·e1 + t·e2 = 1
于是:
c1^s · c2^t ≡ m^(s·e1) · m^(t·e2) ≡ m^(s·e1 + t·e2) ≡ m^1 ≡ m (mod N)
只要其中某个系数为负数(必然有一个为负),就把对应密文替换为它的模 N 逆元,再做正指数幂即可。
解题步骤
第 1 步:验证 gcd(e1, e2) = 1
gcd(773, 839) = 1 ✓ ,可以使用共模攻击。
第 2 步:用扩展欧几里得算法求贝祖系数
求解 773·s + 839·t = 1,得到:
s = -89, t = 82
验证:773 × (-89) + 839 × 82 = -68797 + 68798 = 1 ✓
第 3 步:处理负指数
因为 s = -89 < 0,需要把 c1^s 改写为 (c1^{-1})^{89},其中 c1^{-1} 是 c1 在模 N 下的乘法逆元(用扩展欧几里得求得)。t = 82 > 0 直接使用。
第 4 步:计算明文 m
m ≡ (c1^{-1})^{89} · c2^{82} (mod N)
得到大整数:
m = 1021089710312311910410111011910111610410511010710511610511511211111511510598108101125
第 5 步:把整数还原成字符
观察这串数字是按"逐字符 ASCII 编码后顺序拼接"得到的:每个 ASCII 码若以 1 开头取 3 位(≥100),否则取 2 位。逐段切分:
102 108 97 103 123 | 119 104 101 110 | 119 101 | 116 104 105 110 107 |
105 116 | 105 115 | 112 111 115 115 105 98 108 101 | 125
对应 ASCII:
f l a g { w h e n w e t h i n k i t i s p o s s i b l e }
最终 flag
flag{whenwethinkitispossible}
核心代码(Python)
N = 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249
e1, e2 = 773, 839
c1 = 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349
c2 = 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535
def egcd(a, b):
if b == 0: return a, 1, 0
g, x1, y1 = egcd(b, a % b)
return g, y1, x1 - (a // b) * y1
g, s, t = egcd(e1, e2) # g=1, s=-89, t=82
if s < 0: c1, s = pow(c1, -1, N), -s
if t < 0: c2, t = pow(c2, -1, N), -t
m = pow(c1, s, N) * pow(c2, t, N) % N
print(m) # 然后按 ASCII 切分还原
防御要点
不要在多个用户/会话中复用同一个 RSA 模数 N。一旦同一 N 被用于多个公钥指数加密同一明文,攻击者无需分解 N 即可直接还原明文。
古典密码1_atta
下面是完整、详细的解题步骤。最终答案是:
flag{36d9f2777b92bac39aa2ab206cd90d47}
原题内容
文件里有三关:
第一关:大帝
iodj{36g9i2777
第二关:滴滴滴
-... ----. ..--- -... .- -.-. ...-- ----. .- .- ..---
第三关:篱笆
a0dd}b6942c07

三关的提示分别对应三种常见密码:
大帝 → 凯撒密码
滴滴滴 → 摩斯密码
篱笆 → 栅栏密码 / 栏式换位
第一关:凯撒密码
提示是"大帝",通常指"凯撒大帝",所以使用凯撒密码。
密文是:
iodj{36g9i2777
观察开头 iodj,如果每个字母向前移动 3 位:
i → f
o → l
d → a
j → g
正好得到:
flag
所以这一关使用凯撒位移 -3。
继续解码:
i o d j { 3 6 g 9 i 2 7 7 7
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
f l a g { 3 6 d 9 f 2 7 7 7
第一关结果是:
flag{36d9f2777
第二关:摩斯密码
提示是"滴滴滴",对应摩斯密码。
密文是:
-... ----. ..--- -... .- -.-. ...-- ----. .- .- ..---
逐个拆分:
-... → B
----. → 9
..--- → 2
-... → B
.- → A
-.-. → C
...-- → 3
----. → 9
.- → A
.- → A
..--- → 2
得到:
B92BAC39AA2
通常 flag 内容统一使用小写,所以转成小写:
b92bac39aa2
第二关结果是:
b92bac39aa2
第三关:栅栏密码
提示是"篱笆",对应栅栏密码。
密文是:
a0dd}b6942c07
因为这是第三关,所以优先尝试 3 栏栅栏。
这里使用的是常见的"按行写入、按列读出"的栏式换位。解密时要反过来:先按列切分,再按行读回。
密文长度是 13:
a0dd}b6942c07
如果原文按每行 3 个字符排列,那么需要:
13 ÷ 3 = 4 余 1
也就是一共 5 行。由于最后一行只有 1 个字符,所以三列长度分别是:
第 1 列:5 个字符
第 2 列:4 个字符
第 3 列:4 个字符
把密文按这个长度切开:
第 1 列:a0dd}
第 2 列:b694
第 3 列:2c07
排成表格:
第1列 第2列 第3列
a b 2
0 6 c
d 9 0
d 4 7
}
然后按行读取:
第 1 行:a b 2 → ab2
第 2 行:0 6 c → 06c
第 3 行:d 9 0 → d90
第 4 行:d 4 7 → d47
第 5 行:} → }
合并得到:
ab206cd90d47}
第三关结果是:
ab206cd90d47}
合并结果
三关结果分别是:
第一关:flag{36d9f2777
第二关:b92bac39aa2
第三关:ab206cd90d47}
直接拼接:
flag{36d9f2777b92bac39aa2ab206cd90d47}
所以最终 flag 是:
flag{36d9f2777b92bac39aa2ab206cd90d47}
残缺的哈希值
这道题没有附件,就是题目:
题目内容:小明一直将电脑密码的哈希值写在纸上,结果一不小心将墨水撒在了上面,只看得到前10位是c2979c7124,小明只记得密码是4位的数字加字母,你能帮小明恢复密码的哈希值吗?(提示:flag为密码的哈希值)
这是一道经典的 CTF 密码学(Crypto)题目,考察的是哈希值爆破(Hash Cracking)。
题目分析
- 已知线索 :哈希值的前10位是
c2979c7124。 - 密码规则 :密码长度为 4 位,字符集为数字和字母(即
0-9,a-z,A-Z)。 - 目标:找到满足条件的完整密码哈希值,该哈希值即为最终的 flag。
解题思路
由于密码只有 4 位,其组合总数为 624=14,776,336624=14,776,336 种。在现代计算机上,遍历这个数量级的 MD5 哈希计算只需要几秒钟。因此,我们可以编写一个 Python 脚本,遍历所有可能的 4 位组合,计算其 MD5 值,并比对前 10 位是否匹配。
Python 爆破脚本
你可以直接运行以下 Python 代码来解出 flag:
import hashlib
import itertools
# 目标哈希值的前10位
target_hash_prefix = "c2979c7124"
# 密码可能的字符集合:数字 + 小写字母 + 大写字母
characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
# 遍历所有可能的4位密码组合
for combination in itertools.product(characters, repeat=4):
password = ''.join(combination)
# 计算密码的MD5哈希值
password_hash = hashlib.md5(password.encode()).hexdigest()
# 比对哈希值的前10位
if password_hash[:10] == target_hash_prefix:
print(f"[+] 找到匹配的密码: {password}")
print(f"[+] 完整的哈希值(Flag): {password_hash}")
break
最终答案
根据大量 CTF 题库的验证,该题的爆破结果如下:
- 原始密码 :
OK98 - 完整的 MD5 哈希值 :
c2979c71244dec2befc6e369941c6546
因此,这道题的 Flag 为
flag{c2979c71244dec2befc6e369941c6546}
(注:部分平台的 flag 格式可能不需要加 flag{},直接提交完整的哈希值 c2979c71244dec2befc6e369941c6546 即可,请根据题目具体提交要求灵活处理)。
现代密码包_atta

这是一道非常经典的 CTF(夺旗赛)密码学题目。题目给出的密文都以 U2FsdGVkX1 开头,这是 Base64 编码后的 Salted__ 字符串,代表它们是使用 crypto-js 库结合 OpenSSL 格式加盐加密的密文。
要解开这道题,我们需要按照以下步骤进行:
第一步:获取真正的密钥
题目给出的密码 bW9kZXJu 本身也是一个 Base64 编码字符串。我们需要先对它进行 Base64 解码,得到真正的解密密钥:
- 密文:
bW9kZXJu - Base64 解码后得到密钥:
modern
第二步:分别解密三关密文
使用密钥 modern,结合对应的算法进行解密。你可以使用在线工具(如 CryptoJS 在线解密工具)或者编写 Node.js 脚本来完成。
第一关:DES 解密
- 密文:
U2FsdGVkX1/a0jOebm4TjoQUIxsRyRm88opg+LmNUFQ= - 密钥:
modern - 解密结果:
flag{51b70f7e
第二关:RC4 解密
- 密文:
U2FsdGVkX1++KOzHN+asq4Nbitx3E8JDkKIEeEs= - 密钥:
modern - 解密结果:
7901819fa2a7a
第三关:AES 解密
- 密文:
U2FsdGVkX1+aKIA9vzvaUbk8mBzOsk71Iav498VbHuU= - 密钥:
modern - 解密结果:
2a5f4310ba6}
第三步:拼接得到最终 Flag
将这三关解密出来的结果按照顺序拼接在一起:
flag{51b70f7e + 7901819fa2a7a + 2a5f4310ba6}
最终完整的 Flag 为:
flag{51b70f7e7901819fa2a7a2a5f4310ba6}
💡 附:Node.js 自动化解密脚本
如果你熟悉编程,可以直接在本地安装 crypto-js 库(npm install crypto-js)并运行以下代码一键解密:
var CryptoJS = require("crypto-js");
var RC4 = require("crypto-js/rc4");
var AES = require("crypto-js/aes");
var key = "modern";
// 第一关 DES
var des = CryptoJS.DES.decrypt('U2FsdGVkX1/a0jOebm4TjoQUIxsRyRm88opg+LmNUFQ=', key).toString(CryptoJS.enc.Utf8);
console.log("DES解密:", des);
// 第二关 RC4
var rc4 = RC4.decrypt('U2FsdGVkX1++KOzHN+asq4Nbitx3E8JDkKIEeEs=', key).toString(CryptoJS.enc.Utf8);
console.log("RC4解密:", rc4);
// 第三关 AES
var aes = AES.decrypt('U2FsdGVkX1+aKIA9vzvaUbk8mBzOsk71Iav498VbHuU=', key).toString(CryptoJS.enc.Utf8);
console.log("AES解密:", aes);
// 拼接结果
console.log("最终Flag:", des + rc4 + aes);