1082 words
5 minutes
LitCTF 2026 WriteUp
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 argparseimport osfrom pathlib import Path
try: from secret import M1_FLAGexcept 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故有
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 argparsefrom pathlib import Pathfrom random import randrange
from Crypto.Util.number import bytes_to_long, getPrime, getRandomRange
try: from secret import FLAGexcept 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所以我们有:
exp.py:
from Crypto.Util.number import long_to_bytes, inverse
# === Public key (p, g, y) ===p = 9000784855376359808051354825193962042770028561343848432778443672755982397391267124312572697249531643069409873722736348916207732622884411596948807031140651g = 3y = 269130883529708333054320571854006406481346665463416017026083074488011546059928157925990665431751017523964760326934454181952822744463714981243407307134357# === Ciphertext (c1, c2) ===c1 = 5245857426274383693193378669425243235151460522527004924092730024427525619244222247576829782077334810173274945751493387545849499010408499951268967774043627c2 = 6059939492718262451327758167005534191200936922719178843825888167191062504030471358635203794720371216217447404436172970111033824674731063386612549785069654# [DEBUG] prod accidentally logged the long-term secret:x = 633366293219022684108628483753423657477324253833657141033762971761747669344649667887002347907882241246119223126492863291886751205505360049793728851371884inv = inverse(pow(c1, x, p), p)m = c2 * inv % pprint(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 argparsefrom pathlib import Path
import gmpy2from Crypto.Util.number import bytes_to_long, getPrime
try: from secret import FLAG, NEXT_PRIME_STEPSexcept 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 = 65537q是由p多步gmpy2.next_prime出来的,两数直接不会差很大,直接费马分解即可。
exp.py:
from math import gcdfrom gmpy2 import invert, irootfrom Crypto.Util.number import *
n = 139637440016232025690294457609899605991056011052010466558411851317943636600860419882966079629826706361935550982744312593243181819999590825159611186779613601241742349986440676188542381451066058816661317621009248513651083772907520139375108426466691332559612971244160246310746215067136490772061317571744230078911c = 81172369642931859390486697024961350889751244109623802937988620847486863147682579984823958801948701482096140632580173113959531836503723522945335985723867818778699337807630592078265626995722998378992215523352858561923474395550395284015986525513984910021995657780411466237306614109262460764382539311725297619429e = 65537MAX_SIZE = 100000# Fermat 无穷递降法n0 = iroot(n, 2)[0] + 1m = 0for i in range(MAX_SIZE): nn = (n0 + i) ** 2 - n if iroot(nn, 2)[1]: m = iroot(nn, 2)[0] breakassert gcd((n0 + m), n) == n0 + mp = n0 + mq = n // pphi = (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 argparsefrom pathlib import Path
from Crypto.Cipher import AESfrom Crypto.Util.Padding import pad
try: from secret import FLAG, UNKNOWN_KEY_SUFFIXexcept 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-forcedassert 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 trangefrom Crypto.Cipher import AESfrom 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()) breakelse: 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/ Some information may be outdated









