这些题目还是比较简单的,所以放一起了。

zeroG CTF 2026

Twin Orbit

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from Crypto.Util.number import bytes_to_long

def encrypt_message(flag: bytes, n: int):
m = bytes_to_long(flag)

e1 = 65537
e2 = 17

c1 = pow(m, e1, n)
c2 = pow(m, e2, n)

return e1, e2, c1, c2

思路

已知$$c_1=m^{e_1} \pmod n ,c_2=m^{e_2} \pmod n$$

用扩展欧几里得求出: se1+te2=gcd(e1,e2)=1

则$$m=c_1^s*c_2^t \pmod n$$

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Util.number import *
import gmpy2
n = 78429219359517922271023478963814594552681246043944770910304760471867765174623304038843626799213010074714647155283331308571847776870166597053823412781788611608177305819593874012686298378748721435009046767613360191457980203020570462985478543330425482286818857391023923223033155751757576833456411434713984471383
e1 = 65537
e2 = 17
c1 = 71282312105868131740394478794008286284074152062907735987516077413351604126882776234623911447307962528308126218712123568701353026231889282844009867916343556840839139885445525543186695511199429927944296268193188530317628821728534582820389657490317666947095834711636160892093284048993666399747630728635978820198
c2 = 70751964066395185933408819650408191047287659276501425712138199434404000627978244880544478152411510337684996008892030606559772725285766060014956720285732207231186134371296910276169402781919701351782279058927891124229021162906608266092058475936622126108377526917909078829421351716554783863514592590874754685769

g,s,t=gmpy2.gcdext(e1,e2)
print(s,t)
#s=-8
A=pow(c1,s,n)
B=pow(c2,t,n)
m=pow(A*B,1,n)
flag=long_to_bytes(m)
print(flag)

Lunar LCG

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

class LunarLCG:
def __init__(self, m, a, c, state):
self.m = m
self.a = a
self.c = c
self.state = state

def next_state(self):
self.state = (self.a * self.state + self.c) % self.m
return self.state

def next_byte(self):
"""
The relay station uses the lowest 8 bits of each new state
as one byte of keystream.
"""
return self.next_state() & 0xff

def xor_encrypt(data: bytes, prng: LunarLCG) -> bytes:
out = bytearray()

for b in data:
k = prng.next_byte()
out.append(b ^ k)

return bytes(out)

思路

对于LunarLCG函数:state = (a * state + c) % m然后只取低8位作为 k 和明文逐字节异或得到密文。

题目给了6个s,用3个恢复a和c:

$$x_1-x_2=a(x_0-x_1) \pmod m \Rightarrow a=(x_1-x_2)(x_0-x_1)^{-1} \pmod m$$

然后异或回去就行了,但如果从leak[0]开始的话是乱码,枚举一下即可

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
from Crypto.Util.number import *
import gmpy2

m = 170141183460469231731687303715884105727
leak_states = [
48077378362307815584689819960136019875,
100310108693164117002347749113390493183,
145646689101109657050476193569066602802,
63949818470656288394594660187785964270,
46314465195318558087862397882705709486,
103138436636073932218183299598776830813,
]
ciphertext ="39fe07de62fdc9bf74bbbcbd7e202386ca9e40451b46c74968e30fff138a95"

x0,x1,x2=leak_states[0],leak_states[1],leak_states[2]
A=pow(x0-x1,-1,m)
a=pow((x1-x2)*A,1,m)
c=pow(x1-a*x0,1,m)
ct=bytes.fromhex(ciphertext)

class LunarLCG:
def __init__(self, m, a, c, state):
self.m = m
self.a = a
self.c = c
self.state = state

def next_state(self):
self.state = (self.a * self.state + self.c) % self.m
return self.state

def next_byte(self):
return self.next_state() & 0xff

def xor_encrypt(data: bytes, prng: LunarLCG) -> bytes:
out = bytearray()

for b in data:
k = prng.next_byte()
out.append(b ^ k)

return bytes(out)

for i in range(6):
prng = LunarLCG(m, a, c, leak_states[i])
flag = xor_encrypt(ct, prng)
if b"flag{" in flag:
print(flag)
#b'flag{ZeroG_lcg_stream_recovery}'

Phobos Padding

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from Crypto.Util.number import bytes_to_long

def encrypt(flag: bytes, public_keys):
"""
public_keys:
[
(n1, e),
(n2, e),
(n3, e),
]

Warning:
This demo intentionally uses raw RSA without padding.
"""
m = bytes_to_long(flag)

result = []

for n, e in public_keys:
c = pow(m, e, n)
result.append((n, e, c))

return result

思路

已知$$m^3 \equiv c_i \pmod {n_i} i=1,2,3$$,则用中国剩余定理可以找到:$$M \equiv m^3 \pmod {n_1n_2n_3} $$

若m^3<n1n2n3,则直接开三次根

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from Crypto.Util.number import *
import gmpy2
import libnum
e = 3

n1 = 9203118261705868019110006623273896134322296004495934622126321588206198211590594608536574205500841860912183113474492528101942483463604127057100041845594123
c1 = 225326225723570437926892098700724301640108952320044616725184090895511961737080288471190011942447422341235122945729017303171992927231675218640713872178033

n2 = 8218974785294030613346971087108222043759818458429043768635262660088269400867661193359046399568686339887944628791712180696779799918022646158973494803220299
c2 = 3407676048044393024576659577470571794093695115844258472643168272782162860244002027327745232045383478691907846926814490953793141526176684717238078901972654

n3 = 8640442409248695297781745462901828098989267118787634310572918885729221856234292677073935037333836295724444289085611427540896246989248186559475612627680863
c3 = 6492260343134932927953198433174002823828534869771319070490239692685600132982822403083735209163800494671140850876058194194328293660168048521787716473266503

remainders=[c1,c2,c3]
modulis=[n1,n2,n3]
M=libnum.solve_crt(remainders,modulis)
m,t=gmpy2.iroot(M,3)
flag=long_to_bytes(int(m))
print(flag)
#b'flag{ZeroG_hastad_broadcast_attack}'

LitCTF 2026

lit_xor_two_story

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python3
"""
LitCTF2026 — One-time pad reused for two messages (40 bytes each).

Players receive output.txt and README; they do not receive secret.py.
"""
from __future__ import annotations

import argparse
import os
from pathlib import Path

try:
from secret import M1_FLAG
except ImportError:
raise SystemExit(
"secret.py (organizer) is required to generate ciphertext; "
"players work from output.txt only."
)

# Public second message — duplicated in README for contestants.
M2_KNOWN = b"litctf2026_xor_keystream_reuse_40bytes!!"

assert len(M1_FLAG) == len(M2_KNOWN) == 40

def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))

def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write hex lines to file.",
)
args = parser.parse_args()

n = len(M1_FLAG)
k = os.urandom(n)
c1 = xor_bytes(M1_FLAG, k)
c2 = xor_bytes(M2_KNOWN, k)

lines = [
f"c1 = {c1.hex()}",
f"c2 = {c2.hex()}",
f"len = {n}",
]
text = "\n".join(lines) + "\n"
print(text, end="")
if args.write:
args.write.write_text(text, encoding="utf-8")

if __name__ == "__main__":
main()

# c1 = 5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31bf14ab7a0a82ccc108f8476f75c98a28
# c2 = 5f70a847ce123cc153283ca710ae7f042b8490a238eb2228970fad6a2694f2985dc5557e69e5f474
# len = 40

思路

题目中已知m2,c1,c2,且m1,m2都是40字节

加密流程:c1=m1 ^ k ,c2=m2 ^ k (这里是逐字节异或)

那么m1=c1 ^ c2 ^m2

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Util.number import *

c1 ="5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31bf14ab7a0a82ccc108f8476f75c98a28"
c2 ="5f70a847ce123cc153283ca710ae7f042b8490a238eb2228970fad6a2694f2985dc5557e69e5f474"
len = 40

m2 = b"litctf2026_xor_keystream_reuse_40bytes!!"
c1=bytes.fromhex(c1)
c2=bytes.fromhex(c2)

def xor_bytes(a: bytes, b: bytes,c:bytes) -> bytes:
return bytes(x ^ y ^z for x, y,z in zip(a, b,c))

m1=xor_bytes(c1,c2,m2)
print(m1)
#b'litctf{otp_reuse_never_twice_same_key__}'

lit_elgamal_handshake

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/usr/bin/env python3
"""
LitCTF2026 — ElGamal handshake (story)
Someone left debug logging on; the private exponent x was printed alongside ciphertext.
"""
from __future__ import annotations

import argparse
from pathlib import Path
from random import randrange

from Crypto.Util.number import bytes_to_long, getPrime, getRandomRange

try:
from secret import FLAG
except ImportError as e:
raise SystemExit("secret.py (FLAG) is required to encrypt.") from e

def generate_elgamal_keypair(bits: int = 512) -> tuple[int, int, int, int]:
p = getPrime(bits)
for _ in range(1000):
g = getRandomRange(2, min(6, p - 1))
if pow(g, (p - 1) // 2, p) != 1:
break
else:
raise RuntimeError("could not find suitable g")
x = randrange(2, p - 1)
y = pow(g, x, p)
return p, g, y, x

def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write captured output to this file (for organizers).",
)
args = parser.parse_args()

p, g, y, x = generate_elgamal_keypair(bits=512)
k = randrange(1, p - 2)
m = bytes_to_long(FLAG)
if m >= p:
raise ValueError("flag too large for chosen p — shorten FLAG")

c1 = pow(g, k, p)
c2 = (m * pow(y, k, p)) % p

lines = [
"=== Public key (p, g, y) ===",
f"p = {p}",
f"g = {g}",
f"y = {y}",
"",
"=== Ciphertext (c1, c2) ===",
f"c1 = {c1}",
f"c2 = {c2}",
"",
"# [DEBUG] prod accidentally logged the long-term secret:",
f"x = {x}",
]
text = "\n".join(lines) + "\n"
print(text, end="")
if args.write:
args.write.write_text(text, encoding="utf-8")

if __name__ == "__main__":
main()

# === Public key (p, g, y) ===
# p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
# g = 3
# y = 269130883529708333054320571854006406481346665463416017026083074488011546059928157925990665431751017523964760326934454181952822744463714981243407307134357

# === Ciphertext (c1, c2) ===
# c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627
# c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654

# # [DEBUG] prod accidentally logged the long-term secret:
# x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

思路

题目给了g是模p的二次非剩余,虽然和解答没什么关系。

已知:

$$\begin {cases} y=g^x \pmod p\ c_1=g^k \pmod p\ c_2=m*y^k \pmod p \end {cases}$$

则$c_2=mg^{xk}=mc_1^x \pmod p \Rightarrow m=c_2*(c_1^x)^{-1} \pmod p$

题目给的数据很多,但其实只需要用到p,c1,c2,x

解答

1
2
3
4
5
6
7
8
9
10
11
from Crypto.Util.number import long_to_bytes, inverse

p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651
c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627
c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654
x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884

A=pow(c1,-x, p)
m=pow(c2*A,1,p)
print(long_to_bytes(m))
#b'litctf{elgamal_leak_makes_happy_decrypt}'

lit_rsa_neighbor

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#!/usr/bin/env python3
"""
LitCTF2026 — RSA where q is 'far' along the prime line but still close enough to p for Fermat.
"""
from __future__ import annotations

import argparse
from pathlib import Path

import gmpy2
from Crypto.Util.number import bytes_to_long, getPrime

try:
from secret import FLAG, NEXT_PRIME_STEPS
except ImportError as e:
raise SystemExit(
"secret.py is required to generate output (FLAG, NEXT_PRIME_STEPS)."
) from e

E = 65537

def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write n, c to this file.",
)
args = parser.parse_args()

p = getPrime(512)
q = p
for _ in range(NEXT_PRIME_STEPS):
q = int(gmpy2.next_prime(q))

n = p * q
m = bytes_to_long(FLAG)
if m >= n:
raise ValueError("flag too large for n")

c = pow(m, E, n)

lines_players = [f"{n = }", f"{c = }", f"e = {E}"]
text = "\n".join(lines_players) + "\n"
print(text, end="")
if args.write:
args.write.write_text(text, encoding="utf-8")

if __name__ == "__main__":
main()

# n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911
# c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429
# e = 65537

思路

注意到p是512位的,q是p的下一个素数,说明p、q是十分接近的,用费马分解攻击即可

$$N=a^2-b^2=(a-b)(a+b)$$

$$p=a-b,q=a+b$$

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from Crypto.Util.number import *
import gmpy2
import math

n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911
c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429
e = 65537

def fermat_factor(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

p,q=fermat_factor(n)
phi=(p-1)*(q-1)
d=gmpy2.invert(e,phi)
m=pow(c,d,n)
flag=long_to_bytes(m)
print(flag)
#b'litctf{rsa_fermat_finds_close_primes}'

lit_tiny_key_aes

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python3
"""
LitCTF2026 — AES-128-ECB with a mostly fixed key (weak operational policy).
"""
from __future__ import annotations

import argparse
from pathlib import Path

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

try:
from secret import FLAG, UNKNOWN_KEY_SUFFIX
except ImportError as e:
raise SystemExit(
"secret.py is required to generate ciphertext (contains FLAG and key suffix)."
) from e

KEY_PREFIX = b"LitCTF2026!!!" # 13 bytes; 3 bytes brute-forced
assert len(KEY_PREFIX) + len(UNKNOWN_KEY_SUFFIX) == 16

def encrypt_aes_ecb_pkcs7(plaintext: bytes, key: bytes) -> bytes:
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(pad(plaintext, AES.block_size))

def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--write",
type=Path,
help="Write ciphertext hex to this file.",
)
args = parser.parse_args()

key = KEY_PREFIX + UNKNOWN_KEY_SUFFIX
c = encrypt_aes_ecb_pkcs7(FLAG, key)
line = f"c = {c!r}\n"
print(line, end="")
if args.write:
args.write.write_text(line, encoding="utf-8")

if __name__ == "__main__":
main()

# c = b"\x0c\xdb'`\xc91\xf7\x05\x91+\x0fM\xed\xbc\x9b\xf1\xd8D\xcd\xfd\x0c\xb9\xb6\xb2J<\x86\x19\x06K\xb3\xa2\xa4\x18\x87<v\xac\x1bbu#\xaa\xb5I\x7f\xd8\xd3"

思路

题目用AES-128的ECB模式进行加密,给了密钥的前13个字节,对后3个字节进行爆破即可

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from Crypto.Util.number import *
from Crypto.Cipher import AES
from tqdm import trange

KEY_PREFIX = b"LitCTF2026!!!"
c = b"\x0c\xdb'`\xc91\xf7\x05\x91+\x0fM\xed\xbc\x9b\xf1\xd8D\xcd\xfd\x0c\xb9\xb6\xb2J<\x86\x19\x06K\xb3\xa2\xa4\x18\x87<v\xac\x1bbu#\xaa\xb5I\x7f\xd8\xd3"

for i in trange (256**3):
low=i.to_bytes(3)
key=KEY_PREFIX+low

pt=AES.new(key,AES.MODE_ECB).decrypt(c)
if b"litctf{" in pt:
print(pt)
#litctf{aes_tiny_brut3_for_the_win!}

img


SecLeaf Q2 CTF 2026

military_grade_encryption

题目

1
U2VjTGVhZntiNDUzNjRfMXNfbjB0XzNuY3J5cHQxMG59

解答

base64 解得:SecLeaf{b45364_1s_n0t_3ncrypt10n}

Double Trouble

题目

1
2
526e4a7757584a756333737759544e66655452734d325666616a526d5957
64664d3245776148523166513d3d0a

解答

可以看出这是十六进制,按字节转换成ASCII后得到:

1
RnJwWXJuc3swYTNfeTRsM2VfajRmYWdfM2EwaHR1fQ==

base64解码得到:

1
FrpYrns{0a3_y4l3e_j4fag_3a0htu}

再枚举一下凯撒解密得到:

img