欢迎光临
我们一直在努力

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}然后也是错的

解密方法分析:

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

  • 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+/UJutTOdUecfBj
    • crypto6_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);

    赞(0)
    未经允许不得转载:171主机测评 » ctf--密码学相关题目(Crypto)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址