SekaiCTF2026

TL; DR

在期末周的间隙打了 SekaiCTF, 作为Leo/need吃 看到 STAGE OF SEKAI 的时候还是很惊喜的)

oneline6ryp7o

how hard can six seven be

1
assert __import__('re').match('SEKAI{[67]{67}}$',flag:=input()) and not int.from_bytes(flag.encode())%~(6+~7)**67

模数

1
~{(6+~7)**67} = 2^67 - 1

令最小为 "SEKAI{" + "6" * 67 + "}"

从 base 往上搜索,从低位往上每轮从 6 加到 7 ,然后算增量能否得出即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
M = 2**67 - 1

base = "SEKAI{" + "6" * 67 + "}"
target = (-int.from_bytes(base.encode())) % M

body = ""
for i in range(67):
bit_position = (8 * (67 - i)) % 67
body += "7" if (target >> bit_position) & 1 else "6"

flag = f"SEKAI{{{body}}}"

assert __import__('re').match('SEKAI{[67]{67}}$', flag)
assert not int.from_bytes(flag.encode()) % ~(6 + ~7) ** 67

print(flag)

# SEKAI{6777676667666666677676776776777766777777777776777767777776677666666}

orbital-strike

An orbital strike is an attack directed at a planetary surface from a weapon system stationed in space (orbit).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from Crypto.Util.number import getPrime, getRandomRange
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

FLAG = open('flag.txt','rb').read()

def lcg(a,b,p,x):
while 1: yield (x := (a*x+next(b)) % p)

P, p = getPrime(256), getPrime(0x137)
X, A, B, a, b = [getRandomRange(0,m) for m in (P,P,p,p,p)]

# what's better than one lcg? TWO lcgs!
moons = lcg(a,iter([b]*14),p,B)
planets = lcg(A,moons,P,X)
orbit = [next(planets) for _ in range(14)]
star = AES.new(X.to_bytes(32),AES.MODE_ECB).encrypt(pad(FLAG,16)).hex()

print(f"{orbit = }")
print(f"{star = }")

双重 LCG 第一眼想到了今年 karmalCTF 的 RBG 系列

先用参数 a, b, p, B 生成 moons ,再在模 P 下用一次 LCG 混淆 moons,得到外层 orbit 要求恢复 内层的planets 种子 X,解密 FLAG


代数结构梳理

  • moons

LCG 增量层是常数 b

  • planets

我们知道 要去恢复

变量太多,先消元

syzygy

结合这里的变量命名和线性系统关系,找到了如下的一个处理思路

syzygy定理是什么

化用本题中,这里模 p P 下的线性关系可以如此整理:先进行差分,再

那么满足

带入 而一般 不会整除 p ,那么我们针对规约的 ,得到了一组

从而建立一个 多取一组结果做 Resultant ,GCD 即可恢复 p, 恢复 p 后再对多项式做一个 GCD 即可恢复公共根 a ;

此时我们可以精确地针对 进行建模了


后续先考虑一个一个类似的思路,我们用 syzygy 恢复内层后,再考虑外层的差分

这里就发现,相关的差分,还涉及到模 p 下的关系,Lift 处理比较麻烦。

回看我们已有的数据, 获取了相关 的信息,又因为

展开 ,针对 利用方程组

进行规约,即可简单地恢复 ,再一个 那么就 恢复 P, 求逆元恢复 A, 然后逆推 X 即可

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
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
SEKAI{orbital_strike_like_miku_miku_beam!!!}#!/usr/bin/env sage
from sage.all import *

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

orbit = [
46157012221654917396851254347154820393060391878580715960476654689260395468184,
36926194633341127588542680684095293820802193681748458943524916140809713523560,
16005201943847263206512436577001283414470030273089746675203830598137794555134,
28937919714389596084610407023450127584695575606301484773390370819366639643218,
11459012353705334109041842799942754581703065868230253271729711591416155557180,
31030059279554219046464541926833857543445131889728181065565033726460506326840,
20987315604501021667042879662101693092441980938033961081037347214532349371248,
76741461130245451493723909055453557280102065396647043801270629949855565452326,
84258885183671683674472390974667571532577240974449641001706593550302243268268,
59535034089467707172052245359810812420431903279354584714432674122159502991956,
7115679899033391144975170596669540596311296590450661546000723388170577963715,
35572951991838484594163260879328705523576344587262461128887804475450813563036,
85569022704397114858282741078883377190544624744955636482627379979792474136036,
5047270986830280372910174287287823507537624765267582560460157826800286170460,
]
star = bytes.fromhex(
"1664ff83cbca2643b357bcdc8c3d6e1548615a18cec73e734a82e163b32a9b0c367c61bab01140a04ac8eda8b007d1d6"
)

def max_bits(row):
return max(abs(ZZ(x)).nbits() for x in row)

def recover_short_syzygies(D):
H = Matrix(ZZ, [D[:11], D[1:12], D[2:13]])
rels = [list(r) for r in H.right_kernel_matrix().LLL().rows()]
short = [r for r in rels if max_bits(r) <= 96]
if len(short) < 2:
raise RuntimeError("not enough short syzygies")
return rels, short

def recover_inner_modulus_and_multiplier(short):
R = PolynomialRing(ZZ, "T")
T = R.gen()
polys = [R(sum(ZZ(c) * T**i for i, c in enumerate(rel))) for rel in short]

g = ZZ(0)
for i in range(len(polys)):
for j in range(i + 1, len(polys)):
res = abs(ZZ(polys[i].resultant(polys[j])))
if res:
g = res if g == 0 else gcd(g, res)

large_primes = [ZZ(q) for q, _ in factor(g) if ZZ(q).nbits() > 300 and is_prime(q)]
if not large_primes:
raise RuntimeError("resultant gcd did not expose the 311-bit prime")
p = max(large_primes, key=lambda q: q.nbits())

Rp = PolynomialRing(GF(p), "T")
gp = Rp(polys[0])
for poly in polys[1:]:
gp = gcd(gp, Rp(poly))
gp = gp.monic()
if gp.degree() != 1:
raise RuntimeError(f"expected a linear gcd over GF(p), got degree {gp.degree()}")
a = ZZ(-gp[0])
return p, a

def recover_inner_differences(D, short, p, a):
# Variables are E_1..E_13 and quotient witnesses n_1..n_12 for
# E_{i+1} - a*E_i = p*n_i.
rows = []
for rel in short:
row = [ZZ(0)] * 25
for k, c in enumerate(rel):
row[k + 1] = ZZ(c)
rows.append(row)

row = [ZZ(0)] * 25
for k, c in enumerate(rel):
row[k + 2] = ZZ(c)
rows.append(row)

for i in range(12):
row = [ZZ(0)] * 25
row[i + 1] = 1
row[i] = -a
row[13 + i] = -p
rows.append(row)

reduced = Matrix(ZZ, rows).right_kernel_matrix().LLL()
for basis_row in reduced.rows():
base = [ZZ(x) for x in basis_row[:13]]
for sign in (1, -1):
E = [sign * x for x in base]
if not any(E) or not all(-p < x < p for x in E):
continue
if not all((E[i + 1] - a * E[i]) % p == 0 for i in range(12)):
continue

g = ZZ(0)
for i in range(1, 13):
for j in range(i + 1, 13):
val = (D[i] - E[i]) * D[j - 1] - (D[j] - E[j]) * D[i - 1]
if val:
g = abs(val) if g == 0 else gcd(g, abs(val))

if g.nbits() == 256 and is_prime(g) and max(orbit) < g < 2**256:
return E, g

raise RuntimeError("could not identify the inner difference vector and outer modulus")

def recover_outer_multiplier(D, E, P):
vals = []
for i in range(1, 13):
vals.append(((D[i] - E[i]) * inverse_mod(D[i - 1], P)) % P)
if len(set(vals)) != 1:
raise RuntimeError("outer multiplier candidates disagree")
return ZZ(vals[0])

def decrypt_flag(p, a, P, A, E):
x1, x2 = ZZ(orbit[0]), ZZ(orbit[1])
y2_mod_p = (x2 - A * x1) % P
e1_residue = (inverse_mod(a, p) * E[1]) % p

for E1 in (ZZ(e1_residue), ZZ(e1_residue) - p):
y1_mod_p = (y2_mod_p - E1) % P
X = ((x1 - y1_mod_p) * inverse_mod(A, P)) % P
try:
flag = unpad(AES.new(int(X).to_bytes(32, "big"), AES.MODE_ECB).decrypt(star), 16)
except ValueError:
continue

# Verify the recovered key/state against the first outer recurrence.
if (A * X + y1_mod_p) % P == x1:
return X, flag

raise RuntimeError("AES decryption failed for both E1 lifts")

def main():
D = [ZZ(orbit[i + 1]) - ZZ(orbit[i]) for i in range(len(orbit) - 1)]
rels, short = recover_short_syzygies(D)
print("[+] ZZ kernel basis max coefficient bits:", [max_bits(r) for r in rels])
print("[+] using short syzygies:", [max_bits(r) for r in short])

p, a = recover_inner_modulus_and_multiplier(short)
print("[+] p =", p)
print("[+] a =", a)

E, P = recover_inner_differences(D, short, p, a)
A = recover_outer_multiplier(D, E, P)
print("[+] P =", P)
print("[+] A =", A)

X, flag = decrypt_flag(p, a, P, A, E)
print("[+] X =", X)
print("[+] flag =", flag.decode())

if __name__ == "__main__":
main()
# SEKAI{orbital_strike_like_miku_miku_beam!!!}

iihash

Fast hash is good hash. A secret seed makes it even better, right?

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
import random
import signal

signal.alarm(600)
import xxhash

MIN_PAYLOAD_LEN = 256
TARGET_DIGEST = b"Give me the flag"


class XXH3Challenge:
def __init__(self):
self.seed = random.getrandbits(64)

def _read_hex_data(self, prompt: str) -> bytes:
try:
hex_str = input(prompt).strip()
return bytes.fromhex(hex_str)
except ValueError:
print("[-] Invalid hex format.")
return b""

def hash_mode(self):
data = self._read_hex_data("[*] Enter data (hex): ")
if not data:
return
if len(data) <= MIN_PAYLOAD_LEN:
print(
f"[-] Data length must be strictly greater than {MIN_PAYLOAD_LEN} bytes."
)
return

h = xxhash.xxh3_128(data, seed=self.seed).hexdigest()
print(f"[+] Hash: {h}")

def flag_mode(self):
data = self._read_hex_data("[*] Enter data (hex): ")
if not data:
return
if len(data) <= MIN_PAYLOAD_LEN:
print(
f"[-] Payload length must be strictly greater than {MIN_PAYLOAD_LEN} bytes."
)
return

if xxhash.xxh3_128(data, seed=self.seed).digest() == TARGET_DIGEST:
print("[+] Target verified.", open("flag.txt").read())
exit()
else:
print("[-] Verification failed. Digest does not match target.")

def run(self):
while True:
print(" 1. Hash")
print(" 2. Get Flag")
print(" 3. Exit")
choice = input("> ").strip()
if choice == "1":
self.hash_mode()
elif choice == "2":
self.flag_mode()
elif choice == "3":
print("[*] Goodbye.")
break
else:
print("[-] Invalid choice.")


if __name__ == "__main__":
challenge = XXH3Challenge()
challenge.run()

##needle-in-a-multivariate-sekai

*apbq-rsa-iv