Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
1082 words
5 minutes
LitCTF 2026 WriteUp
2026-05-23
统计加载中...

Crypto#

lit_xor_two_story#

题目:

#!/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
c1=M1k,c2=M2kc_1=M_1\oplus k,c_2=M_2\oplus k

故有

M1=c1k=c1(c2M2)M_1=c_1\oplus k=c1\oplus(c_2\oplus M_2)

exp.py

from Crypto.Util.strxor import strxor
M2_KNOWN = b"litctf2026_xor_keystream_reuse_40bytes!!"
c1 = bytes.fromhex("5f70a847ce12759e156e3cad1aa9530a119386a02ffc1c31bf14ab7a0a82ccc108f8476f75c98a28")
c2 = bytes.fromhex("5f70a847ce123cc153283ca710ae7f042b8490a238eb2228970fad6a2694f2985dc5557e69e5f474")
k = strxor(c2, M2_KNOWN)
M1_FLAG = strxor(c1, k)
print(M1_FLAG.decode())
# litctf{otp_reuse_never_twice_same_key__}

lit_elgamal_handshake#

题目:

#!/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
ygx(modp),c1gk(modp),c2myk(modp)y\equiv g^x\pmod{p},c_1\equiv g^k\pmod{p},c_2\equiv m*y^k\pmod{p}

所以我们有:

yk=(gx)k,mc2(yk)1=c2(c1x)(modp)y^k=(g^x)^k,m\equiv c_2\cdot(y^k)^{-1}=c_2\cdot(c_1^x)\pmod{p}

exp.py

from Crypto.Util.number import long_to_bytes, inverse
# === 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
inv = inverse(pow(c1, x, p), p)
m = c2 * inv % p
print(long_to_bytes(m).decode())
# litctf{elgamal_leak_makes_happy_decrypt}

lit_rsa_neighbor#

题目:

#!/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

q是由p多步gmpy2.next_prime出来的,两数直接不会差很大,直接费马分解即可。

exp.py

from math import gcd
from gmpy2 import invert, iroot
from Crypto.Util.number import *
n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911
c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429
e = 65537
MAX_SIZE = 100000
# Fermat 无穷递降法
n0 = iroot(n, 2)[0] + 1
m = 0
for i in range(MAX_SIZE):
nn = (n0 + i) ** 2 - n
if iroot(nn, 2)[1]:
m = iroot(nn, 2)[0]
break
assert gcd((n0 + m), n) == n0 + m
p = n0 + m
q = n // p
phi = (p - 1) * (q - 1)
d = invert(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m).decode())
# litctf{rsa_fermat_finds_close_primes}

lit_tiny_key_aes#

题目:

#!/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"

爆破后三位密钥就行。

exp.py

from tqdm import trange
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
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 x in trange(1 << 24):
suffix = x.to_bytes(3, "big")
key = KEY_PREFIX + suffix
pt = AES.new(key, AES.MODE_ECB).decrypt(C)
try:
msg = unpad(pt, AES.block_size)
except ValueError:
continue
if msg.startswith(b"litctf{"):
print("key =", key)
print("suffix =", suffix)
print("flag =", msg.decode())
break
else:
print("not found")
# 22%|██▏ | 3645953/16777216 [00:18<01:06, 196630.17it/s]
# key = b'LitCTF2026!!!7\xa2\x01'
# suffix = b'7\xa2\x01'
# flag = litctf{aes_tiny_brut3_for_the_win!}
LitCTF 2026 WriteUp
https://q1uju.cc/posts/litctf-2026-writeup/
Author
Q1uJu
Published at
2026-05-23
License
CC BY-NC-SA 4.0

Some information may be outdated