Crypto CTF 2021 WriteUps
This article is automatically translated by LLM, so the translation may be inaccurate or incomplete. If you find any mistake, please let me know.
You can find the original article here .
During the ongoing AIS3, I participated in Crypto CTF 2021 and ranked 19th. The competition lasted only 24 hours, so I only solved the easier and medium-difficulty problems. Even with more time, I might have only solved 1-2 more problems.
Problems marked with *
were solved after the competition.
easy
Farm
The key is the product of any 14 elements in , and the encryption part is to first base64 encode the flag, then convert it to elements of , multiply each element by the key, and convert it back to base64 characters to form the ciphertext.
The solution is to brute force 64 keys and see which key can convert back to normal base64.
import string, base64
ALPHABET = string.printable[:62] + "\\="
F = list(GF(64))
enc = "805c9GMYuD5RefTmabUNfS9N9YrkwbAbdZE0df91uCEytcoy9FDSbZ8Ay8jj"
for key in F[1:]:
flag = ""
for c in enc:
i = ALPHABET.index(c)
ii = F.index(F[i] / key)
flag += ALPHABET[ii]
if flag.endswith("=="):
print(base64.b64decode(flag))
break
As the flag suggests, this is just a substitution cipher, but in a finite field version.
KeyBase
This problem is an AES-CBC challenge. First, you are given a flag encrypted with a fixed key and IV, and an oracle that allows you to provide any 32 bytes of plaintext to be encrypted with the same key and IV. After encryption, the oracle gives you the first 14 bytes of the key and the second block (last 16 bytes) of the ciphertext.
Since the first 14 bytes of the key are known, you can brute force 65536 combinations to find the key, treating the key as known. However, the challenge lies in not knowing the IV. To obtain the IV, encrypt known plaintext using the oracle, brute force the key to decrypt the second block of the ciphertext, and XOR it with the second block of the plaintext to get the first block of the ciphertext. With the first block of plaintext and ciphertext, brute force the key to deduce the IV, and then decrypt the flag with the IV and key.
from Crypto.Cipher import AES
import string
enc_flag = bytes.fromhex(
"96bacca946f4b43a9c1ea88f7213ff002ada02e260b4378b983f312aa451fe36"
)
key_prefix = bytes.fromhex("c006db7407cb60af7d599a85bb8e")
pt = b"aaaabbbbaaaabbbb"
ct = bytes.fromhex("6f380eea6c17a694780bf694ac8341b3")
def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def encrypt(msg, iv, key):
aes = AES.new(key, AES.MODE_CBC, iv)
return aes.encrypt(msg)
def decrypt(enc, iv, key):
aes = AES.new(key, AES.MODE_CBC, iv)
return aes.decrypt(enc)
def decrypt_ecb(enc, key):
aes = AES.new(key, AES.MODE_ECB)
return aes.decrypt(enc)
for a in range(256):
for b in range(256):
key = key_prefix + bytes([a, b])
xpt = decrypt_ecb(ct, key)
enc1 = xor(xpt, pt)
iv = xor(decrypt_ecb(enc1, key), pt)
flag = decrypt(enc_flag, iv, key)
if all(20 <= x < 127 for x in flag):
print(flag)
Symbols
This problem gives you an image with many mathematical symbols, resembling a substitution cipher.
My solution was to use Detexify to look up the LaTeX commands for the symbols. Noticing that the first character of each command corresponds to the flag character, I found CCTF{Play_with_LaTeX}
.
medium-easy
Rima
This problem converts the flag into a 0-1 array and encodes it using some shifting additions. The code is hard to read, so I used z3 to solve it directly.
from Crypto.Util.number import *
from sage.all import Integer, next_prime
from z3 import *
with open("g.enc", "rb") as f:
g = bytes_to_long(f.read())
with open("h.enc", "rb") as f:
h = bytes_to_long(f.read())
gg = list(map(int, Integer(g).digits(5)))
hh = list(map(int, Integer(h).digits(5)))
f = [Int(f"f_{i}") for i in range(8 * 32 - 1)]
f0 = list(f)
f.insert(0, 0)
for i in range(len(f) - 1):
f[i] += f[i + 1]
a = next_prime(len(f))
b = next_prime(a)
g, h = [[_ for i in range(x) for _ in f] for x in [a, b]]
c = next_prime(len(f) >> 2)
for _ in [g, h]:
for __ in range(c):
_.insert(0, 0)
for i in range(len(_) - c):
_[i] += _[i + c]
sol = Solver()
for x in f0:
sol.add(Or(x == 0, x == 1))
sol.add(And([x == y for x, y in zip(g, gg)]))
sol.add(And([x == y for x, y in zip(h, hh)]))
assert sol.check() == sat
m = sol.model()
f = [m.eval(x).as_long() for x in f0]
f = int("".join(map(str, f))[::-1], 2)
print(long_to_bytes(f))
Hyper Normal
This problem encodes the flag into a matrix over and shuffles it. Assuming the flag characters are , the matrix becomes:
Then the rows are randomly shuffled.
Next, it loops , generates a vector , and calculates:
where and are the -th row vectors of matrices and . After generating , it shuffles the rows of and outputs it as the encrypted flag.
My solution uses the property to brute force the possible values of , intersecting multiple rows to deduce , and then solving for the flag.
However, my solution had some issues, and I had to adjust which sets to union to get the correct flag.
# fmt: off
W = [] # fill this
# fmt: on
from sage.all import *
from functools import reduce
from operator import and_
import random
p = 8443
def transpose(x):
result = [[x[j][i] for j in range(len(x))] for i in range(len(x[0]))]
return result
def vsum(u, v):
assert len(u) == len(v)
l, w = len(u), []
for i in range(l):
w += [(u[i] + v[i]) % p]
return w
def sprod(a, u):
w = []
for i in range(len(u)):
w += [a * u[i] % p]
return w
def encrypt(msg):
l = len(msg)
hyper = [ord(m) * (i + 1) for (m, i) in zip(list(msg), range(l))]
V, W = [], []
for i in range(l):
v = [0] * i + [hyper[i]] + [0] * (l - i - 1)
V.append(v)
random.shuffle(V)
# V = [V[1],V[0],V[3],V[2]]
print(V)
for _ in range(l):
R, v = [random.randint(0, 126) for _ in range(l)], [0] * l
for j in range(l):
v = vsum(v, sprod(R[j], V[j]))
W.append(v)
print(R, v)
random.shuffle(transpose(W))
return W
l = len(W)
Z = GF(p)
W = Matrix(Z, W)
ccand = []
for k in range(l):
cand = [set() for _ in range(l)]
for i in range(1, 126):
v = vector(x / i for x in W[k])
for j in range(len(v)):
cand[j].add(v[j])
ccand.append(cand)
# print(ccand[0][0] & ccand[1][0] & ccand[2][0])
# print(ccand[0][1] & ccand[1][1] & ccand[2][1])
# print(ccand[0][2] & ccand[1][2] & ccand[2][2])
# print(ccand[0][3] & ccand[1][3] & ccand[2][3])
tccand = transpose(ccand)
flag = ""
for i in range(l):
# tccand[i] = tccand[i][1::2]
# without this line: CCTF{0w_f1Nd_th3_4lL_3I9EnV4Lu35_iN_FiN173_Fi3lD5 ???}
# with this line: CCTF{H0w_f1Nd_th3_4l _3I9EnV4Lu35_iN_FiN173_Fi3lD5!???}
# so we have the full flag
ss = tccand[i][0]
j = 1
while len(ss) > 1:
ss = ss & tccand[i][j]
j += 1
if len(ss) > 0:
c = list(ss)[0] // (i + 1)
else:
c = ord(" ")
flag += chr(c)
print(flag)
# CCTF{H0w_f1Nd_th3_4lL_3I9EnV4Lu35_iN_FiN173_Fi3lD5!???}
The expected solution seems related to eigenvalues, but it's unclear how to proceed...
Hamul
This problem is an RSA challenge. The prime generation steps are as follows:
while True:
p, q = getPrime(nbit), getPrime(nbit)
P = int(str(p) + str(q))
Q = int(str(q) + str(p))
PP = int(str(P) + str(Q))
QQ = int(str(Q) + str(P))
if isPrime(PP) and isPrime(QQ):
break
Then n = PP * QQ
is used as the RSA modulus to encrypt the flag.
First, the lengths of str(p)
and str(q)
are either 19 or 20, so and . Using similar equations, it becomes a polynomial root-finding problem , which I solved using Coppersmith's method.
from Crypto.Util.number import *
n = 98027132963374134222724984677805364225505454302688777506193468362969111927940238887522916586024601699661401871147674624868439577416387122924526713690754043
c = 42066148309824022259115963832631631482979698275547113127526245628391950322648581438233116362337008919903556068981108710136599590349195987128718867420453399
# https://github.com/defund/coppersmith/blob/master/coppersmith.sage
load("~/workspace/coppersmith.sage")
for plen in [20, 19]:
for qlen in [20, 19]:
Plen = plen + qlen
Qlen = plen + qlen
Z = Zmod(100 * n + 1)
PZ = PolynomialRing(Z, "p,q")
p, q = PZ.gens()
P = p * 10 ^ qlen + q
Q = q * 10 ^ plen + p
PPP = P * 10 ^ Qlen + Q
QQQ = Q * 10 ^ Plen + P
f = PPP * QQQ - n
sol = small_roots(f, (2 ^ 65, 2 ^ 65), m=7, d=3)
if len(sol) > 0:
p, q = sol[0]
PPP = PPP(p, q)
QQQ = QQQ(p, q)
assert PPP * QQQ == n
d = inverse_mod(65537, (PPP - 1) * (QQQ - 1))
m = power_mod(c, d, n)
print(long_to_bytes(m))
exit()
medium
Tuti
This problem splits the flag into two halves and , satisfying the following equation, where is a constant:
Rewriting it as a polynomial root problem, it can be factored as:
where and are constants satisfying . Since and are positive, the condition can only be .
Rewriting this equation, it becomes . By factoring , all solutions for and can be found.
from Crypto.Util.number import *
k = 246389259423689900229483388850933720271363907782961941639413620688310657308991195363798484778005049234253341752674411282663124201840620584781830032437353134292733496201534415265400175632719346764031781179041636
# xy-x+y-b=0
# (y-1)(x+1)=b-1
b = 992752253935874779143952218845275961347009322164731344882417010624071055636710540798045985678351986133613
# big factors are factored using FactorDB
f1 = 1357459302115148222329561139218955500171643099
f2 = 17258104558019725087
f3 = 2035395403834744453
assert (b - 1) % f1 == 0
assert (b - 1) % f2 == 0
assert (b - 1) % f3 == 0
fs = list(factor(b // f1 // f2 // f3)) + [(f3, 1), (f2, 1), (f1, 1)]
def divs(f):
# copied from sage
output = [1]
for p, e in f:
prev = output[:]
pn = 1
for i in range(e):
pn *= p
output.extend(a * pn for a in prev)
output.sort()
return output
for d in divs(fs):
x, y = d - 1, (b - 1) // d + 1
assert (y - 1) * (x + 1) == b - 1
flag = long_to_bytes(x) + long_to_bytes(y)
if b"CCTF" in flag:
print(flag)
break
Salt and Pepper
This problem involves two unknowns, salt and pepper, both of length 19. You are given md5(salt)
and sha(pepper)
. The goal is to find an input containing the specified username
and password
and determine sha1(pepper + input_password + md5(salt + input_username))
.
The solution uses a Length Extension Attack with HashPump. However, HashPump requires data
. I commented out this check and recompiled it to get the desired result.
from pwn import *
from hashpumpy import hashpump
# ./hashpump -s '5f72c4360a2287bc269e0ccba6fc24ba' -a 'n3T4Dm1n' -k 19
s1 = "95623660d3d04c7680a52679e35f041c"
d1 = b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x98\x00\x00\x00\x00\x00\x00\x00n3T4Dm1n"
# ./hashpump -s '3e0d000a4b0bd712999d730bc331f400221008e0' -a 'P4s5W0rd95623660d3d04c7680a52679e35f041c' -k 19
s2 = "83875efbe020ced3e2c5ecc908edc98481eba47f"
d2 = b"\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x98P4s5W0rd"
io = remote("02.cr.yp.toc.tf", 28010)
io.sendlineafter(b"Options", b"L")
io.sendlineafter(b"comma: ", d1.hex() + "," + d2.hex())
io.sendlineafter(b"hash: ", s2)
io.interactive()
Triplet
This problem requires finding 3 sets of RSA (at least 160 bits) and providing such that and .
First, deduce , where . This implies , and for , , so needs to be minimized.
A simple method is to choose three primes , , and , and set , , , making smaller. Then factor to find .
from Crypto.Util.number import *
def phi(k):
p, q = k
return (p - 1) * (q - 1)
def lcm(a, b):
return a * b // GCD(a, b)
p = 880418646898378491477543405137962536180977254166879
q = 2 * p + 1
r = 2 * q + 1
k1 = (p, q)
k2 = (q, r)
k3 = (p, r)
l = lcm(lcm(phi(k1), phi(k2)), phi(k3))
print(",".join(map(str, k1)))
print(",".join(map(str, k2)))
print(",".join(map(str, k3)))
mn = min(phi(k1), phi(k2), phi(k3))
print(l + 1)
d = (
83818189125408135328873033373544374818199783983
* 34388019287720328558025059428347529
* 10803954241
)
assert d < mn
assert (l + 1) % d == 0
e = (l + 1) // d
assert e < mn
assert e * d % phi(k1) == 1
assert e * d % phi(k2) == 1
assert e * d % phi(k3) == 1
print(f"{e},{d}")
# CCTF{7HrE3_b4Bie5_c4rRi3d_dUr1nG_0Ne_pr39naNcY_Ar3_triplets}
Onlude
This problem encodes the flag into an matrix over . There are two matrices and , with decomposed into .
Encryption is done using . Additionally, three matrices , , and are provided.
First, calculate from and . Then deduce and . With and , decryption is straightforward.
p = 71
with open("output.txt") as f:
lines = [line.strip() for line in f.readlines() if "=" not in line]
vecs = [[int(x) for x in line[1:-1].split(" ") if x] for line in lines]
E = matrix(GF(p), vecs[:11])
LUL = matrix(GF(p), vecs[11:22])
L1S2L = matrix(GF(p), vecs[22:33])
R1S8 = matrix(GF(p), vecs[33:44])
U = L1S2L * LUL ^ -1
S2 = LUL * U
R = (R1S8 * S2 ^ -4) ^ -1
A = U ^ -1 * E - R
print(A)
alphabet = "=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$!?_{}<>"
flag = ""
for k in range(100):
i, j = 5 * k // 11, 5 * k % 11
if A[i, j] == 0:
break
flag += alphabet[A[i, j]]
print(flag)
Improved
This problem involves a strange hash function, and the goal is to find a collision. Parameter generation produces two primes and , then calculates , , and . Next, compute , , and . The final parameters are .
The hash function uses , , and . The final hash result is .
PS: is restricted to
Although the problem seems complex, it is easily solved. Any element in has an order of , so calculating yields , and another root . Since is even, , resulting in a collision.
from pwn import remote
import ast
io = remote("05.cr.yp.toc.tf", 14010)
io.sendlineafter(b"Options", b"G")
io.recvuntil(b"Parameters = ")
n, f, v = ast.literal_eval(io.recvlineS().strip())
assert f % 2 == 0
g = pow(48763, n, n * n)
x = pow(g, f // 2, n * n)
y = n * n - x
assert pow(x, f, n * n) == 1
assert pow(y, f, n * n) == 1
print(x)
print(y)
io.sendlineafter(b"Options", b"R")
io.sendlineafter(b"comma: ", f"{x},{y}".encode())
print(io.recvallS())
Maid
This problem generates two primes and such that , then calculates and the private key .
The flag is encrypted using traditional RSA , but the encryption and decryption oracles use the Rabin cryptosystem. The encryption function is simply , but the decryption function is hidden, likely similar to .
The goal is to factor and . First, use the encryption oracle to calculate gcd and obtain . The key to obtaining lies in the decryption oracle.
Initially, I tried decrypting -1, assuming is odd, yielding . Adding 1 gives , which worked locally but failed remotely due to the hidden decryption function.
Later, I used to find a value, initially thought to be , but it was . This allowed me to obtain and and retrieve the flag.
from pwn import *
from Crypto.Util.number import *
from gmpy2 import isqrt
from functools import reduce
# io = process(["python", "maid.py"])
io = remote("04.cr.yp.toc.tf", 38010)
io.sendlineafter(b"Options", b"S")
io.recvuntil(b" = ")
enc_flag = int(io.recvlineS().strip())
def encrypt(msg):
io.sendlineafter(b"Options", b"E")
io.sendlineafter(b"encrypt: ", str(msg).encode())
io.recvuntil(b" = ")
return int(io.recvlineS().strip())
def decrypt(msg):
io.sendlineafter(b"Options", b"D")
io.sendlineafter(b"decrypt: ", str(msg).encode())
io.recvuntil(b" = ")
return int(io.recvlineS().strip())
pub = GCD(
GCD(2 ** 4000 - encrypt(2 ** 2000), 2 ** 4002 - encrypt(2 ** 2001)),
GCD(2 ** 4004 - encrypt(2 ** 2002), 2 ** 4006 - encrypt(2 ** 2003)),
)
print(pub)
p2 = reduce(GCD, [decrypt(2 ** i) ** 2 - 2 ** i for i in range(1991, 2015, 2)])
p = isqrt(p2)
assert p > 100
assert pub % p == 0
q = pub // p // p
assert p * p * q == pub
d = pow(65537, -1, (p - 1) * (q - 1))
print(long_to_bytes(pow(enc_flag, d, p * q)))
Here is the decryption function posted by the problem creator on Discord:
def decrypt(c, privkey):
m_p = pow(c, (privkey + 1) // 4, privkey)
i = (c - pow(m_p, 2)) // privkey
j = i * inverse(2*m_p, privkey) % privkey
m = m_p + j * privkey
if 2*m < privkey**2:
return m
else:
return privkey**2 - m
Ferman
This problem has no source code. It provides an equation and uses to encrypt the flag. The goal is to solve the equation.
Initially, I had no idea, but factoring revealed . Repeatedly testing multiple parameters yielded the same result. Although Sage's solver couldn't solve , it solved .
Given solutions for , use the Brahmagupta–Fibonacci identity to extend solutions to , then repeatedly extend to .
from Crypto.Util.number import *
e = 65537
a = 769
b = 90
# (p - a)**2 + (q - b)**2 == k**7
k = 533349483431826854866479442416204129077526048835547852310509534957185
c = 4478819143432789024587861603523572305269547479550443133641110109373270566470025946722977115602647046295004476694988416461505550664119915082335497331912881526446940124404687029541487759747406116312872601161581904176763818623358120927587871262018474674411074996384180525486478668863914557062661081721929081678057785839028975581815732964462013512812566725502749216649190469493027431158255717939171221374546082898410798277258418126725070247145397363980604758633071972900958843430904130
p, q = var("p q")
assume(p, "integer")
assume(q, "integer")
sol = solve(p ** 2 + q ** 2 == k, p, q)
sol = [(Integer(p), Integer(q)) for p, q in sol if p > 0 and q > 0]
def mul(r, s):
# https://en.m.wikipedia.org/wiki/Brahmagupta%E2%80%93Fibonacci_identity
a, b = r
c, d = s
x1 = a * c - b * d
y1 = a * d + b * c
x2 = a * c + b * d
y2 = a * d - b * c
return (abs(x1), abs(y1)), (abs(x2), abs(y2))
def sqr(r):
return mul(r, r)[0]
for r in sol:
r2 = sqr(r)
r4 = sqr(r2)
for r3 in mul(r, r2):
for r7 in mul(r4, r3):
p, q = r7
assert p ^ 2 + q ^ 2 == k ^ 7
p += a
q += b
if isPrime(p) and isPrime(q):
print(p, q)
n = p * q
phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)
m = power_mod(c, d, n)
print(long_to_bytes(m))
exit()
I later realized this implementation worked by luck, as directly raising a solution to the 7th power doesn't guarantee all solutions for . The correct approach should multiply all solutions for to obtain solutions for , then multiply solutions for to get , and so on...
Tiny ECC
This problem requires providing two primes and (at least 128 bits), and two numbers and . It then selects two random points on two elliptic curves over or and gives you two ECDLPs to solve, yielding the flag.
My method was simple and brute-force: find such that both curves' orders are 2^40-smooth. This took a few minutes.
from Crypto.Util.number import *
def gp(b):
while True:
p = getPrime(b)
if isPrime(2 * p + 1):
return p, 2 * p + 1
while True:
p, q = gp(128)
a, b = 2, 3
mxp = max([p for p, e in factor(EllipticCurve(GF(p), [a, b]).order())])
mxq = max([p for p, e in factor(EllipticCurve(GF(q), [a, b]).order())])
if mxp < 2 ^ 40 and mxq < 2 ^ 40:
print(p, q)
break
print(log(mxp, 2).n(), log(mxq, 2).n())
The remaining part is solved using Sage's built-in Pohlig-Hellman:
from pwn import remote
import ast
p = 301712731189529370473291776359599603783
q = 2 * p + 1
Ep = EllipticCurve(GF(p), [2, 3])
Eq = EllipticCurve(GF(q), [2, 3])
io = remote("01.cr.yp.toc.tf", 29010)
io.sendlineafter(b"Options", b"C")
io.sendline(str(p).encode())
io.sendlineafter(b"Options", b"A")
io.sendline(b"2,3")
io.sendlineafter(b"Options", b"S")
io.recvuntil(b"P = ")
P = Ep(*ast.literal_eval(io.recvlineS().strip()))
io.recvuntil(b"k*P = ")
kP = Ep(*ast.literal_eval(io.recvlineS().strip()))
io.recvuntil(b"Q = ")
Q = Eq(*ast.literal_eval(io.recvlineS().strip()))
io.recvuntil(b"l*Q = ")
lQ = Eq(*ast.literal_eval(io.recvlineS().strip()))
print("solve p")
k = discrete_log(kP, P, operation="+")
print(k)
print("solve q")
l = discrete_log(lQ, Q, operation="+")
print(l)
io.sendline((str(k) + "," + str(l)).encode())
io.interactive()
# CCTF{ECC_With_Special_Prime5}
Additionally, an interesting alternative solution sets , making the ECDLP on the singular curve . Since can be mapped to the additive group of or via , the DLP becomes simple division.
from pwn import remote
import ast
p = 301712731189529370473291776359599603783
q = 2 * p + 1
Fp = GF(p)
Fq = GF(q)
def phi(P, F):
return F(P[0]) / F(P[1])
def solve_dlp(P, G, F):
# xG = P
pG = phi(G, F)
pP = phi(P, F)
return pP / pG
io = remote("01.cr.yp.toc.tf", 29010)
io.sendlineafter(b"Options", b"C")
io.sendline(str(p).encode())
io.sendlineafter(b"Options", b"A")
io.sendline(f"{p*q},{p*q}".encode())
io.sendlineafter(b"Options", b"S")
io.recvuntil(b"P = ")
P = ast.literal_eval(io.recvlineS().strip())
io.recvuntil(b"k*P = ")
kP = ast.literal_eval(io.recvlineS().strip())
io.recvuntil(b"Q = ")
Q = ast.literal_eval(io.recvlineS().strip())
io.recvuntil(b"l*Q = ")
lQ = ast.literal_eval(io.recvlineS().strip())
print("solve p")
k = solve_dlp(kP, P, Fp)
print(k)
print("solve q")
l = solve_dlp(lQ, Q, Fq)
print(l)
io.sendline((str(k) + "," + str(l)).encode())
io.interactive()
# CCTF{ECC_With_Special_Prime5}
*Elegant Curve
This problem is similar to the previous one but requires selecting two primes and such that , and the curves' can differ. The previous brute-force method works but is inefficient.
This time, I generated an anomalous curve , then used next prime to find . Then, I brute-forced to find smooth curves.
The generation script is as follows:
from Crypto.Util.number import *
from subprocess import check_output
import json
def nextPrime(p):
assert p > 2
p += 2
while not isPrime(p):
p += 2
return p
ECGEN_PATH = "" # Prebuilt binary of ecgen: https://github.com/J08nY/ecgen
def gen_anomalous(bits):
r = check_output([ECGEN_PATH, "--anomalous", "--fp", str(bits)])
curve = json.loads(r)[0]
return int(curve["a"], 16), int(curve["b"], 16), int(curve["field"]["p"], 16)
while True:
a, b, p = gen_anomalous(160)
q = nextPrime(p)
mxq = max([p for p, e in factor(EllipticCurve(GF(q), [a, b]).order())])
if mxq < 2 ^ 40:
print(a, b)
print(p, q)
break
print(log(mxq, 2).n())
With the required parameters, solving the problem is straightforward:
from pwn import remote, process
import ast
a, b = (
823375435824563939268788611110428744408176719161,
128395071361889866257668408194030595816566744161,
)
p, q = (
890662999704698873790151198748536866796172800897,
890662999704698873790151198748536866796172800963,
)
Ep = EllipticCurve(GF(p), [a, b])
Eq = EllipticCurve(GF(q), [a, b])
assert Ep.order() == p
def smart_attack(P, G, p):
# https://crypto.stackexchange.com/a/70508
E = G.curve()
Eqp = EllipticCurve(Qp(p, 2), [ZZ(t) + randint(0, p) * p for t in E.a_invariants()])
P_Qps = Eqp.lift_x(ZZ(G.xy()[0]), all=True)
for P_Qp in P_Qps:
if GF(p)(P_Qp.xy()[1]) == G.xy()[1]:
break
Q_Qps = Eqp.lift_x(ZZ(P.xy()[0]), all=True)
for Q_Qp in Q_Qps:
if GF(p)(Q_Qp.xy()[1]) == P.xy()[1]:
break
p_times_P = p * P_Qp
p_times_Q = p * Q_Qp
x_P, y_P = p_times_P.xy()
x_Q, y_Q = p_times_Q.xy()
phi_P = -(x_P / y_P)
phi_Q = -(x_Q / y_Q)
k = phi_Q / phi_P
return ZZ(k)
def recvpoint(io, name):
io.recvuntil(f"{name} = {{".encode())
return ast.literal_eval(io.recvlineS().strip()[:-1])
# io = process(["python", "elegant_curve.py"])
io = remote("07.cr.yp.toc.tf", 10010)
io.sendlineafter(b"Options", b"S")
io.sendlineafter(b"first", f"{a},{b},{p}".encode())
io.sendlineafter(b"second", f"{a},{b},{q}".encode())
G = Ep(recvpoint(io, "G"))
H = Eq(recvpoint(io, "H"))
rG = Ep(recvpoint(io, "r * G"))
sH = Eq(recvpoint(io, "s * H"))
r = smart_attack(rG, G, p)
s = discrete_log(sH, H, operation="+")
print(r, s)
io.sendline(f"{r},{s}".encode())
io.interactive()
# CCTF{Pl4yIn9_Wi7H_ECC_1Z_liK3_pLAiNg_wiTh_Fir3!!}
medium-hard
Wolf
This problem uses AES-GCM to encrypt the flag and provides an oracle to encrypt arbitrary information. The key is that the IV is fixed, so it has the same issue as CTR mode reuse nonce. XORing two ciphertexts and then XORing the known plaintext reveals the flag.
from pwn import remote
def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))
io = remote("01.cr.yp.toc.tf", 27010)
io.sendlineafter(b"Options", "G")
io.recvuntil(b"encrypt(flag) = ")
enc_flag = bytes.fromhex(io.recvlineS().strip())
io.sendlineafter(b"Options", "T")
io.sendlineafter(b"encrypt:", "a" * 128)
io.recvuntil(b"enc = ")
enc_a = bytes.fromhex(io.recvlineS().strip())
print(xor(b"a" * 128, xor(enc_flag, enc_a)))
Frozen
This problem has two parameters ( is 128 bits). During key generation, it calculates 5 numbers , with public key being the high 96 bits of and private key being the low 32 bits. is an unknown random number.
The sign function groups the msg into four-byte numbers , then calculates q = next_prime(max(M))
. The signature is . The goal is to sign a given random string correctly to get the flag.
First, , so:
Eliminating from two equations yields a polynomial in and . Solving it with Coppersmith's method reveals , allowing the complete private key to be calculated and the msg to be signed, revealing the flag.
from Crypto.Util.number import *
from pwn import remote
from sage.matrix.matrix2 import Matrix
import ast
def resultant(f1, f2, var):
# Copied from https://jsur.in/posts/2021-07-19-google-ctf-2021-crypto-writeups#h1
return Matrix.determinant(f1.sylvester_matrix(f2, var))
io = remote("03.cr.yp.toc.tf", 25010)
io.sendlineafter(b"Options", b"S")
io.recvuntil(b"p = ")
p = int(io.recvlineS().strip())
io.recvuntil(b"r = ")
r = int(io.recvlineS().strip())
io.sendlineafter(b"Options", b"P")
io.recvuntil(b"pubkey = ")
pubkey = ast.literal_eval(io.recvlineS().strip())
print(p, r)
print([hex(x) for x in pubkey])
ks = [pow(r, c + 1, p) for c in range(0, 5)]
Z = Zmod(p)
PP = PolynomialRing(Z, "s,x1,x2")
s, *xs = PP.gens()
f1 = ks[0] * s - (pubkey[0] + xs[0])
f2 = ks[1] * s - (pubkey[1] + xs[1])
f = PP.remove_var(s)(resultant(f1, f2, s))
print(f)
load("~/workspace/coppersmith.sage")
xs = small_roots(f, (2 ** 40, 2 ** 40), m=4, d=2)[0]
s = Z(pubkey[0] + xs[0]) / ks[0]
assert s == Z(pubkey[1] + xs[1]) / ks[1]
print(s)
U = [pow(r, c + 1, p) * s % p for c in range(0, 5)]
S = [int(bin(u)[2:][-32:], 2) for u in U]
print([hex(x) for x in S])
def sign(msg, privkey, d):
msg = msg.encode("utf-8")
l = len(msg) // 4
M = [bytes_to_long(msg[4 * i : 4 * (i + 1)]) for i in range(l)]
q = int(next_prime(max(M)))
sign = [M[i] * privkey[i] % q for i in range(l)]
return sign
io.sendlineafter(b"Options", b"F")
io.recvuntil(b"example: ")
msg = io.recvlineS().strip()
sig = sign(msg, S, 32)
io.sendline(",".join(map(str, sig)))
print(io.recvlineS())
The flag mentions Lattice, but according to others, it's unnecessary. The problem includes an unused oracle providing a fixed msg and signature. Since the msg is known, can be deduced, and the private key can be inferred.
LINDA
This problem is rather dull. It has four parameters , with being random, , and .
Encryption involves , , and , where are random and unrelated to the previous ones.
This problem resembles an ElGamal-like encryption system, but the key is that is 2^40-smooth. Directly solving DLP for allows decryption.
from Crypto.Util.number import *
p = 512156828103365901048559505103237592010730651992953680223000172094095757203886681225415426450387040031253612295400192069437985566674257501701743368686395531
u = 278331424202715529007235225803083105831898177238768088959896273495587346471193489462465390715976004916539200837444755811532762335472120306642106779851779895
v = 256568421561256620412008753530664775781745915184311927713911832406042827934333342438038088202080460367979454347035971495645785213472938457309904032230294302
w = 291075584404341211571864039572762475831612407000784732318354539887997569888845019102264165940156056600333692694871997403708476839090241830333179131216949773
ca, cb, cc = (
287555353447321752440570170565916634951105240901098656962539161570704086923490274350537706807786632105173520254109700863175418950150190301275461066194290273,
61323835393791353232128681044112245885676777670178659696073015541712435637029773641799576432645407470518574659302988690944012552307615441231640649288656467,
24320377817820710179555605271052810810335284586923974087878926553551229844983654115310868607990402619355506091875700785511303195656702348392817435407241231,
)
Z = Zmod(p)
r = discrete_log(Z(ca), Z(u))
print(r)
s = discrete_log(Z(cb), Z(v))
print(s)
wrs = Z(w) ** (r + s)
m = Z(cc) / wrs
print(long_to_bytes(m))
*RSAphantine
This problem involves solving a system of Diophantine equations to find two primes and decrypt the flag.
Initially, I tried using Sage's Groebner basis, but it ran indefinitely, so I didn't solve it.
After the competition, I learned that . Factoring reveals , and the second equation yields .
from Crypto.Util.number import *
def nextPrime(x):
if x % 2 == 0:
x += 1
else:
x += 2
while not isPrime(x):
x += 2
return x
k1 = 47769864706750161581152919266942014884728504309791272300873440765010405681123224050402253883248571746202060439521835359010439155922618613520747411963822349374260144229698759495359592287331083229572369186844312169397998958687629858407857496154424105344376591742814310010312178029414792153520127354594349356721
k2 = 89701863794494741579279495149280970802005356650985500935516314994149482802770873012891936617235883383779949043375656934782512958529863426837860653654512392603575042842591799236152988759047643602681210429449595866940656449163014827637584123867198437888098961323599436457342203222948370386342070941174587735051
k3 = 47769864706750161581152919266942014884728504309791272300873440765010405681123224050402253883248571746202060439521835359010439155922618613609786612391835856376321085593999733543104760294208916442207908167085574197779179315081994735796390000652436258333943257231020011932605906567086908226693333446521506911058
c = 486675922771716096231737399040548486325658137529857293201278143425470143429646265649376948017991651364539656238516890519597468182912015548139675971112490154510727743335620826075143903361868438931223801236515950567326769413127995861265368340866053590373839051019268657129382281794222269715218496547178894867320406378387056032984394810093686367691759705672
# k3 - k1 = y^6 + x^3 = (y^4 - x*y^2 + x^2)*(y^2 + x)
s1 = 3133713317731333
s2 = 28413320364759425177418147555143516002041291710972733253944530195017276664069717887927099709630886727522090965378073004342203980057853092114878433424202989
assert s1 * s2 == k3 - k1
def solve_xy():
P = PolynomialRing(QQ, "x,y")
x, y = P.gens()
I = P.ideal([y ^ 2 + x - s1, y ^ 4 - x * y ^ 2 + x ^ 2 - s2])
V = I.variety()
for sol in V:
yield (Integer(sol[x]), Integer(sol[y]))
def solve_z(x, y):
P = PolynomialRing(ZZ, "z")
z = P.gen()
f = x ^ 4 + y ^ 5 + x * y * z - k2
rs = f.roots()
if len(rs) > 0:
return Integer(rs[0][0])
for x, y in solve_xy():
z = solve_z(x, y)
if z is None:
continue
print(x, y, z)
assert 2 * z ** 5 - x ** 3 + y * z == k1
assert x ** 4 + y ** 5 + x * y * z == k2
assert y ** 6 + 2 * z ** 5 + z * y == k3
p = nextPrime(x ** 2 + z ** 2 + y ** 2 << 76)
print(p)
q = nextPrime(z ** 2 + y ** 3 - y * x * z ^^ 67)
print(q)
n, e = p * q, 31337
d = inverse_mod(e, (p - 1) * (q - 1))
print(long_to_bytes(power_mod(c, d, n)))
Post-competition, I found that Mathematica can solve this in minutes.
In[4]:= FindInstance[{2z^5-x^3+y*z==k1,x^4+y^5+x*y*z==k2,y^6+2z^5+z*y==k3}, {x, y, z}, Integers]
Out[4]= {{x -> -97319611529501810510904538298668204056042623868316550440771307534558768612892,
> y -> 311960913464334198969500852124413736815,
> z -> 29896806674955692028025365368202021035722548934827533460297089}}
You can run it for free on your computer using Docker's wolframresearch/wolframengine, as tio.run only allows 60 seconds.
*Double Miff
This problem involves a strange curve with unknown parameters. It defines a strange addition and provides three points , with the flag being the x-coordinates of and .
First, find by substituting points into the polynomial, eliminating variables, and using gcd to find a multiple of . Then use factordb to find .
from sage.matrix.matrix2 import Matrix
def resultant(f1, f2, var):
# Copied from https://jsur.in/posts/2021-07-19-google-ctf-2021-crypto-writeups#h1
return Matrix.determinant(f1.sylvester_matrix(f2, var))
P.<a, b> = ZZ[]
def point(x, y):
return a * x * (y ** 2 - 1) - b * y * (x ** 2 - 1)
PQ = point(
540660810777215925744546848899656347269220877882,
102385886258464739091823423239617164469644309399,
)
QQ = point(
814107817937473043563607662608397956822280643025,
961531436304505096581595159128436662629537620355,
)
PP = point(
5565164868721370436896101492497307801898270333,
496921328106062528508026412328171886461223562143,
)
print(gcd(resultant(QQ, PP, a), resultant(QQ, PQ, a)))
# p = 1141623079614587900848768080393294899678477852887
Solving for is impossible as they don't matter. The polynomial gcd reveals they are linearly related.
The goal is to reverse the addition formula to halve the points. This involves listing two polynomials, eliminating variables, and solving for the roots.
from Crypto.Util.number import *
from sage.matrix.matrix2 import Matrix
def resultant(f1, f2, var):
# Copied from https://jsur.in/posts/2021-07-19-google-ctf-2021-crypto-writeups#h1
return Matrix.determinant(f1.sylvester_matrix(f2, var))
p = 1141623079614587900848768080393294899678477852887
F = GF(p)
PQ = (
F(540660810777215925744546848899656347269220877882),
F(102385886258464739091823423239617164469644309399),
)
QQ = (
F(814107817937473043563607662608397956822280643025),
F(961531436304505096581595159128436662629537620355),
)
PP = (
F(5565164868721370436896101492497307801898270333),
F(496921328106062528508026412328171886461223562143),
)
def half(P):
x3, y3 = P
PP.<x1, y1> = GF(p)[]
f1 = (1 + x1 ^ 2) * (1 - y1 ^ 2) * x3 - 2 * x1 * (1 + y1 ^ 2)
f2 = (1 + y1 ^ 2) * (1 - x1 ^ 2) * y3 - 2 * y1 * (1 + x1 ^ 2)
return [r for r, _ in resultant(f1, f2, y1).univariate_polynomial().roots()]
for x in half(PP):
print(long_to_bytes(x))
for x in half(QQ):
print(long_to_bytes(x))
*ecchimera
This problem involves an elliptic curve over , with the flag being the discrete log results of two points. First, factor into