ctf--密码学相关题目(Crypto)

前言

有需要的朋友可以转去关于密码学的专栏学习更多密码学知识

除去密码题以外,本专栏也包含了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}然后也是错的

解密方法分析:

这份文件采用了多种古典密码算法的组合

  1. Base32编码 - 第一层编码
  2. 维吉尼亚密码 - 第二层加密,采用分段密钥的方式
    • 前4个字符使用密钥 khsy
    • 中间6个字符使用密钥 drz
    • 数字部分保持不变

Crypto4

文件识别

上传的文件名是 crypto4,没有扩展名。直接读取后,文件开头是:

复制代码
1f 8b 08 ...

其中 1f 8bgzip 文件的标准魔数,所以第一步判断它是一个 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

其中 ustartar 包的标识,所以这个文件实际结构是:

复制代码
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

如果 pq 非常接近,那么可以用 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

是否为完全平方数。

如果 pq 很接近,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

pq 是方程:

复制代码
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+/UJutTOdUecfBj
  • crypto6_hint.txt:加密方式说明

第 3 步:阅读 hint,梳理加密链路

提示文件给出加密流程(解密时需要逆向):

  1. flag 与 Python random(用 4 位 PIN 作种子)生成的 keystream 进行 XOR
  2. 结果用 PKCS7 填充到 16 字节,再用 AES-128-CBC 加密
  3. IV(16字节) + 密文 拼接后做 Base64 编码
  4. 密钥派生:aes_key = SHA256("crypto6-{PIN}".encode() + salt_bytes)[:16]
  5. salt = 53796d6d65747269634b657953616c74(hex,对应 SymmetricKeyS
  6. PIN 为 0000--9999

第 4 步:解 Base64 + 拆 IV/密文

Base64 解码得到 48 字节:前 16 字节是 IV,后 32 字节是 AES 密文。

第 5 步:暴力枚举 PIN(0000--9999)

对每个 PIN:

  1. SHA256("crypto6-{PIN}" + salt)[:16] 派生 AES key
  2. AES-128-CBC 解密,能成功 unpad 的才是候选(淘汰大部分错误 PIN)
  3. 把解密结果与 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.pngfflag_e.png 是已知明文/密文对,可以反推出密钥,再解 flag_e.png

题目文件

你给的文件里主要有:

  • encode.py:加密脚本
  • fflag.png:已知原图
  • fflag_e.pngfflag.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

因此满足共模攻击条件。

数学原理

因为 e1e2 互素,所以根据扩展欧几里得算法,一定存在整数 st,使得:

复制代码
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

处理负指数

扩展欧几里得算法求出来的 st 可能是负数。

在模运算中,负指数不能直接使用普通幂运算,需要转换成模逆元。

如果:

复制代码
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) 与对应的两段密文 c1c2

复制代码
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)

题目分析

  1. 已知线索 :哈希值的前10位是 c2979c7124
  2. 密码规则 :密码长度为 4 位,字符集为数字和字母(即 0-9, a-z, A-Z)。
  3. 目标:找到满足条件的完整密码哈希值,该哈希值即为最终的 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);