Codegate CTF 2024 writeup and upsolve

  • ~7.78K 字
  1. 1. TheseWalls [7 Solves] upsolve
    1. 1.1. chall
    2. 1.2. solve
      1. 1.2.1. p0の場合
      2. 1.2.2. そのほかの場合
  2. 2. Resonance [2 solve] upsolve

TheseWalls [7 Solves] upsolve

chall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from secret import P, s, o, wrong, flag
from Crypto.Cipher import AES
import random, os, math

assert P * o == P.curve()(0)
assert all(math.gcd(o, w) == 1 for w in wrong + [s])
assert all(P * s != P * w for w in wrong)

key = os.urandom(32)
enc_flag = AES.new(key, AES.MODE_CTR, nonce=bytes(12)).encrypt(flag.encode())

print(f"{enc_flag.hex() = }\n{o = }")

key = int.from_bytes(key)
for i in range(32 * 8):
P *= [random.choice(wrong), s][(key >> i) & 1]
print(P.xy())

solve

全てのパラメータがP, s, o, wrong, flagに格納されて何もわからん状態です。

とりあえず、出力から、パラメータ(modも含め)リカバリーできるのでやっていきます。

1
2
3
4
5
6
Ps = [eval(i) for i in open("output.txt").readlines()[2:]]

PR.<A, B> = PolynomialRing(ZZ)
polys = [P[0]^3 + A*P[0] + B - P[1]^2 for P in Ps]
B = Ideal(polys).groebner_basis()
print(B)

これで簡単に復元できます。(グレブナー基底便利)

1
[A + 42437997016440510106912234059113681926759694788505021671714484006715784960406807326337661210536581463443414294141888675487947253004723175974512262085287744401565107056353588589043894055035481272184124372125833792185916608297835874449349134631410453080264719245505015377542883012707703282190702361267129303085, B + 459126200632887849752040227678358886031820038366770963884159263686486524398810848612517185164748716420888346292004054958933940038260166360595786900336024132100453336182502034941923082615662562466591523855797071255915619970830634702916789796993677327554098916574862778060640867963470961569155246282541122528, 102426836944425277819174256940128868455066517869185115488781035394164877927042198980921397582250905694209313963017944880679709788694772632564808279062807699845761214703586660326307989151472431126842544189892251409194318824261554131269698538462346988910692541060196190265045309883144365795763912362099350625483]

modが合成数でどうしようかと思っていた矢先に、わざとdivision error起こして素因数分解する手法を思い出し、やってみると、

1
ZeroDivisionError: Inverse of 3562548874780288796769030192977 does not exist (characteristic = 102426836944425277819174256940128868455066517869185115488781035394164877927042198980921397582250905694209313963017944880679709788694772632564808279062807699845761214703586660326307989151472431126842544189892251409194318824261554131269698538462346988910692541060196190265045309883144365795763912362099350625483 = 3562548874780288796769030192977*28750998384756811025752129590555132573338276387761714818239168529696001090482641836251161799832248142910954339498915727248648688248809529111284480071606360371923389075510517968865199800431013435656000983440622625813948412147843001184329335317021013178557815932219648144379013979)

よって、1つの因数3562548874780288796769030192977がわかります。

ここで、更に因数分解を進めると以下になります。

1
2
3
4
5
p0 = 3562548874780288796769030192977
p1 = 3692983360407686094702508373879
p2 = 2717597692908121319788497985451
p3 = 324094280281900209908870811008292068290746348301400744740589987
n == p_ * p1 ^ 2 * p2 ^ 3 * p3 ^ 2

ここで、p0はSingular curveなのでSingular Curve Point Decompression Attackが刺さります。

ここで、他の素数p1,p2,p3は位数がp+1の型なのでMOV attack、なので、p進整数の2つを用いて解きます。

p0の場合

以下で解きます。

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
x = GF(p)["x"].gen()
f = x ** 3 + a2 * x ** 2 + a4 * x + a6
roots = f.roots()

# Singular point is a cusp.
if len(roots) == 1:
alpha = roots[0][0]
u = (Gx - alpha) / Gy
v = (Px - alpha) / Py
return int(v / u)

# Singular point is a node.
if len(roots) == 2:
if roots[0][1] == 2:
alpha = roots[0][0]
beta = roots[1][0]
elif roots[1][1] == 2:
alpha = roots[1][0]
beta = roots[0][0]
else:
raise ValueError("Expected root with multiplicity 2.")

t = (alpha - beta).sqrt()
u = (Gy + t * (Gx - alpha)) / (Gy - t * (Gx - alpha))
v = (Py + t * (Px - alpha)) / (Py - t * (Px - alpha))
return int(v.log(u))

そのほかの場合

MOV attackとzer0pts ctf 2021 writeup (日本語)で解きます。

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
E = P.curve()
q = E.base_ring().order()
n = P.order()
assert gcd(n, q) == 1, "GCD of base point order and curve base ring order should be 1."

logging.info("Calculating embedding degree...")
k = get_embedding_degree(q, n, max_k)
if k is None:
return None

logging.info(f"Found embedding degree {k}")
Ek = E.base_extend(GF(q ** k))
Pk = Ek(P)
Rk = Ek(R)
for i in range(max_tries):
Q_ = Ek.random_point()
m = Q_.order()
d = gcd(m, n)
Q = (m // d) * Q_
if Q.order() != n:
continue

if (alpha := Pk.weil_pairing(Q, n)) == 1:
continue

beta = Rk.weil_pairing(Q, n)
logging.info(f"Computing {beta}.log({alpha})...")
l = beta.log(alpha)
return int(l)

あとは、適当にCRTで合わせておk

Resonance [2 solve] upsolve

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import itertools
import sys

p = 73743043621499797449074820543863456997944695372324032511999999999999999999999

x = var("x")
Fp2 = GF(p**2, name="z2", modulus=x**2 + 1)
z2 = Fp2.gen()
Fp4 = Fp2.extension(2, name="z4")
z4 = Fp4.gen()

E0 = EllipticCurve(Fp4, [1, 0])
E0.set_order((p**2 - 1) ** 2, num_checks=0)
E0_j_invariant = E0.j_invariant()


def H(preimage):
prev_j_invariant = E0_j_invariant
walk = set()
E = E0.isogeny(E0(0, 0)).codomain()

result = 0
# Lets surf through the graphs.
for idx, bit in enumerate(preimage):
sys.stdout.write(str(idx)+"->"+str(bit))
sys.stdout.flush()

f = E.division_polynomial(2)

kernels = [
cand
for cand in sorted([E.lift_x(x) for x in f.roots(multiplicities=False)])
if E.isogeny(cand).codomain().j_invariant() != prev_j_invariant
]
assert len(kernels) == 2
kernel = kernels[bit]

isogeny = E.isogeny(kernel)
domain = isogeny.domain()
domain_j_invariant = domain.j_invariant()
codomain = isogeny.codomain()
codomain_j_invariant = codomain.j_invariant()
assert domain_j_invariant != codomain_j_invariant

prev_j_invariant = domain_j_invariant
E = codomain

# Please, become a trailblazer.
assert codomain_j_invariant not in walk
walk.add(codomain_j_invariant)

sys.stdout.write(".")
sys.stdout.flush()

h = E.j_invariant()
walk.remove(h)

return h, walk


if __name__ == "__main__":
msg_cnt = 3

bit_lens = [int(sys.stdin.readline().strip()) for _ in range(msg_cnt)]
# The preimage must be plenty long.
assert all(bit_len >= 1024 for bit_len in bit_lens)

preimages = [
[
int(c)
for c in list(
format(abs(int(sys.stdin.readline().strip())), f"0{bit_lens[idx]}b")
)
]
for idx in range(2)
]
# Each preimage must be unique.
assert all(a != b for a, b in itertools.combinations(preimages, r=2))

# Takes long. Grab some coffee!
print(preimages)
hs, walks = zip(*map(H, preimages))
print()
print("hs", hs)
print(set(hs))
print("walks", walks)

# I need a triple collision, which makes it triple the difficulty.
# assert len(set(hs)) == 1
# Can you walk?
print([len(a & b) for a, b in itertools.combinations(walks, r=2)])
print([a & b for a, b in itertools.combinations(walks, r=2)])
assert all(len(a & b) <= msg_cnt for a, b in itertools.combinations(walks, r=2))
assert all(len(a) >= bit_lens[idx] - msg_cnt for idx, a in enumerate(walks))

# Here is your treat.
sys.stdout.write(open("flag.txt").read())
sys.stdout.flush()

同種写像を使って始点、終点が同じ、その他はほとんど異なる3つのpathを作れという問題

KLPT algrithm

1