Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4Mobile wallpaper 5Mobile wallpaper 6
10580 words
53 minutes
QCTF 2025 WriteUp
2025-12-27

复现平台:A1CTF

打星号(*)的是赛时未解出赛后复现的,打感叹号(!)是看了官方 WriteUp 有所修改的,打连接符(-)的是没复现出来咕咕咕的。

Crypto#

Week 1#

Basic Number theory#

题目:

from Crypto.Util.number import *
from secret import flag
def gift(m, prime):
return pow(m, (prime + 1) // 2, prime)
m = bytes_to_long(flag)
p = getPrime(256)
q = getPrime(256)
print(f'p = {p}')
print(f'q = {q}')
print(f'gift1 = {gift(m, p)}')
print(f'gift2 = {gift(m, q)}')
# p = ...
# q = ...
# gift1 = ...
# gift2 = ...
gift1mp+12(modp)mp12m(modp)m(mp)(modp)gift2mq+12(modq)m(mq)(modq)gift1±m(modp),gift2±m(modq)\begin{aligned} gift_1\equiv m^{\frac{p+1}{2}}\pmod{p}\equiv m^{\frac{p-1}{2}}*m\pmod{p}\equiv m*(\frac{m}{p})\pmod{p}\\ gift_2\equiv m^{\frac{q+1}{2}}\pmod{q}\equiv m*(\frac{m}{q})\pmod{q}\\ gift_1\equiv \pm{m}\pmod{p},gift_2\equiv \pm{m}\pmod{q} \end{aligned}

CRT一下穷举四种可能就好了。

exp.py

from sympy.ntheory.residue_ntheory import crt
from Crypto.Util.number import long_to_bytes
p = ...
q = ...
gift1 = ...
gift2 = ...
gift1_ = [gift1, -gift1]
gift2_ = [gift2, -gift2]
for i in gift1_:
for k in gift2_:
s, _ = crt([p, q], [i, k])
flag = long_to_bytes(s)
if b'flag' in flag:
print(flag.decode())
break
# flag{Th3_c0rner5t0ne_0f_C2ypt0gr@phy}

Strange Machine#

题目:

from secret import msg_len, offset, plaintext
from Crypto.Util.number import long_to_bytes
from random import randbytes
from base64 import b64encode
from pwn import xor
import os
flag = os.getenv('FLAG')
def banner():
print("你发现了一个奇怪的机器,它对你的消息进行了加密。")
print("你截获了这个机器第一次的密文,同时可以继续使用该机器进行加密。")
print("注意:所有密文输出都经过 base64 编码,方便你复制分析。\n")
def menu():
print(f"1. 加密消息")
print(f"2. 校验明文是否正确")
print(f"3. 退出")
class Key:
def __init__(self):
self.seed = randbytes(msg_len)
def generate(self):
self.seed = self.seed[offset:] + self.seed[:offset]
return self.seed
def pad(msg):
pad_len = msg_len - len(msg)
return msg + pad_len * long_to_bytes(pad_len)
def encrypt(msg, key):
cipher = xor(pad(msg), key)
return b64encode(cipher)
def main():
banner()
key = Key()
cur_key = key.generate()
cipher1 = encrypt(plaintext, cur_key)
print(f'[*] 首次密文(base64):{cipher1}\n')
while True:
try:
menu()
choice = int(input(f"[?] 请输入你的选择:"))
if choice == 1:
msg = input(f"[?] 请输入要加密的消息(长度小于等于{msg_len}): ").encode()
if len(msg) > msg_len:
print(f"[!] 输入消息过长,最长为 {msg_len} 字节\n")
continue
cur_key = key.generate()
cipher = encrypt(msg, cur_key)
print(f"[*] 你的消息已加密(密文): {cipher}\n")
continue
if choice == 2:
msg = input(f"[?] 请输入待校验的明文: ").encode()
if msg == plaintext:
print(f"[*] 这是你的flag: {flag}\n")
break
print("[!] 校验错误!\n")
continue
if choice == 3:
print("再见~\n")
break
print("[!] 无效输入\n")
except Exception:
print("[!] 出现错误!\n")
break
if __name__ == "__main__":
main()

1 没有限制次数,可以多加密几次一样的明文,已知明文攻击,推出offset再推出key然后和首次密文异或求解,然后选 2 校验明文即可,带中文了交互脚本写的有点麻烦,手拿数据手输了。

exp.py

from base64 import b64decode
from pwn import xor
C1_b64 = "sHACJ0klMagdhYDm53eTNw=="
known_b64_list = [
"29T2vjOCBs8oHm4WYCH+RA==",
"Hm4WYCH+RNvU9r4zggbPKA==",
"9r4zggbPKB5uFmAh/kTb1A==",
"FmAh/kTb1Pa+M4IGzygebg==",
]
msg_len = 16
C1 = b64decode(C1_b64)
known_list = [b64decode(x) for x in known_b64_list]
K_list = [xor(c, b'0' * msg_len) for c in known_list]
offset = 0
for i in range(len(K_list) - 1):
idx = (K_list[i] + K_list[i]).find(K_list[i + 1])
offset = idx if idx != -1 else 0
print(f"{offset = }")
K1 = K_list[0][-offset:] + K_list[0][:-offset]
P_padded = xor(C1, K1)
pad_len = P_padded[-1]
plaintext = P_padded[:-pad_len].decode()
print(plaintext)
# offset = 9
# Oh,you find it!
# flag{094dc384-ba84-4fdf-871d-8e4a3ed761c0}

beyondHex#

题目:

这是什么东西? 807G6F429C7FA2200F46525G1350AB20G339D2GB7D8

多了个G,加上题目名字超过 16 进制,猜测是 17 进制。

exp.py

from Crypto.Util.number import long_to_bytes
s = "807G6F429C7FA2200F46525G1350AB20G339D2GB7D8"
print(long_to_bytes(int(s, 17)).decode())
# flag{welc0me_t0_?CTF!}

certificate#

题目:

private.pem

-----BEGIN RSA PRIVATE KEY----- MIICewIBAAKBgQCdA0+pqynKvc3/BH5ojnUXBMdWy9Lzi9TwSaOgiJ6ky//QBrWG CNan98CYNcoKux2yOtHKIjxUrPh+LdgjmW+/paWPJyrnoQw5SqD+FvqNTjG7Akvx +TUXyMflL9qodrWBEbl/xmN01Qbivo36+U1mFDB+6LENk/3BwWXHVj0DvQIhAL7t Vc3/3jEFe+paWKNoTV++2B8D1T+ii7ZYsy3kU1yNAoGA???????????????????? ???????????????????????????????????????????????????????????????? ???????????????????????????????????????????????????????????????? ??????????????????????kCQQ?????????????????????????????????????? ????????????????????????????????????????????????AkE????????????? ???????????????????????????????????????????????????????????????? ?????????wJAJrKFhI5pl/oBN2BZqLTf+NacGqTFrzmbi7RVFaN43kXYXu11urXy LTncfJXBpRhtGFdsL31jiswhiYRp9yjT+wJB???????????????????????????? ??????????????????????????????????????????????????????????MCQQ?? ???????????????????????????????????????????????????????????????? ???????????????????? -----END RSA PRIVATE KEY-----

又又又又要经典的手撕私钥了,这边给个La佬tover佬的链接。

3082 027b # Begin Sequence: len=0x027b

0201 # Version: (len=0x01) 00

0281 81 # n: (len=0x81) 009d034fa9ab29cabdcdff047e688e751704c756cbd2f38bd4f049a3a0889ea4cbffd006b58608d6a7f7c09835ca0abb1db23ad1ca223c54acf87e2dd823996fbfa5a58f272ae7a10c394aa0fe16fa8d4e31bb024bf1f93517c8c7e52fdaa876b58111b97fc66374d506e2be8dfaf94d6614307ee8b10d93fdc1c165c7563d03bd

02 21 # e: (len=0x21) 00beed55cdffde31057bea5a58a3684d5fbed81f03d53fa28bb658b32de4535c8d

0281 80 # d: (len=0x80) …9

02 41 # p: (len=0x41) 0…

02 41 # q: (len=0x41) …

# 加上w解base64会乱

wJAJrKFhI5pl/oBN2BZqLTf+NacGqTFrzmbi7RVFaN43kXYXu11urXyLTncfJXBpRhtGFdsL31jiswhiYRp9yjT+wJB c09009aca161239a65fe804dd8166a2d37fe35a706a9316bce66e2ed154568de37917617bb5d6ead7c8b4e771f257069461b4615db0bdf58e2b30862611a7dca34fec090

# 所以去了w解base64

JAJrKFhI5pl/oBN2BZqLTf+NacGqTFrzmbi7RVFaN43kXYXu11urXyLTncfJXBpRhtGFdsL31jiswhiYRp9yjT+wJB 24026b285848e6997fa01376059a8b4dff8d69c1aa4c5af399b8bb45515a378de45d85eed75bab5f22d39dc7c95c1a5186d18576c2f7d638acc21898469f728d3fb024

2 40 # dp: (len=0x40) 26b285848e6997fa01376059a8b4dff8d69c1aa4c5af399b8bb45515a378de45d85eed75bab5f22d39dc7c95c1a5186d18576c2f7d638acc21898469f728d3fb

02 4 # dq …3

0241 0…

变成了dp泄露,e很大,上轮子梭。

exp.py

from Crypto.Util.number import *
from gmpy2 import *
n = ...
e = ...
c = ...
dp = ...
m = 2
p = gcd(pow(m, dp * e, n) - m, n)
q = n // p
assert p * q == n
phi = (p - 1) * (q - 1)
d = invert(e, phi)
m = pow(c, d, n)
print(long_to_bytes(m).decode())
# This is your flag:flag{4n_3xc3551v3ly_l4r93_publ1c_k3y_c4n_b3_pr0bl3m4t1c}

two Es#

题目:

from Crypto.Util.number import *
import random
from secret import flag
p, q = getPrime(512), getPrime(512)
n = p * q
e1 = random.getrandbits(32)
e2 = random.getrandbits(32)
m = bytes_to_long(flag)
c1 = pow(m, e1, n)
c2 = pow(m, e2, n)
print(f'{n = }')
print(f'{e1 = }')
print(f'{e2 = }')
print(f'{c1 = }')
print(f'{c2 = }')
'''
n = ...
e1 = ...
e2 = ...
c1 = ...
c2 = ...
'''

如题,两个e,共模攻击。

exp.py

from gmpy2 import gcdext, iroot
from Crypto.Util.number import long_to_bytes
n = ...
e1 = ...
e2 = ...
c1 = ...
c2 = ...
s, s1, s2 = gcdext(e1, e2) # 扩展欧几里得算法,求最大公约数
m = iroot((pow(c1, s1, n) * pow(c2, s2, n) % n), s)[0]
print(long_to_bytes(m).decode())
# flag{s01v3_rO0T_bY_7he_S4mE_m0dU1u5}

xorRSA#

题目:

from Crypto.Util.number import *
from secret import flag
p, q = getPrime(1024), getPrime(1024)
n = p * q
e = 65537
m = bytes_to_long(flag)
c = pow(m, e, n)
p_q = p ^ q
print(f'{n = }')
print(f'{e = }')
print(f'{c = }')
print(f'{p_q = }')
'''
n = ...
e = 65537
c = ...
p_q = ...
'''

泄露了p^q,给个DexterJie师傅的链接

exp.py

from Crypto.Util.number import *
import sys
sys.setrecursionlimit(3000)
N = ...
e = 65537
c = ...
gift = ...
# 低位往高位爆
def findp(p, q):
if len(p) == 1024:
pp = int(p,2)
if N % pp == 0:
print("p = ",pp)
print("q = ",N // pp)
qq = N // pp
d = inverse(65537,(pp-1)*(qq-1))
m = pow(c,d,N)
print(long_to_bytes(m))
return
else:
l = len(p)
pp = int(p,2)
qq = int(q,2)
if (pp ^ qq) % (2 ** l) == gift % (2 ** l) and pp * qq % (2 ** l) == N % (2**l):
findp('1' + p,'1' + q)
findp('1' + p,'0' + q)
findp('0' + p,'1' + q)
findp('0' + p,'0' + q)
findp('1','1')
# flag{U5e_PruN1ng_41g0rI7hm_tO_sEarch}

Week 2#

AES_mode#

题目:

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import binascii
from Crypto.Util.number import bytes_to_long
from secret import flag
import os
iv = flag.strip(b'flag{').strip(b'}')
key = os.urandom(16)
hint = os.urandom(4) * 8
print(bytes_to_long(hint)^bytes_to_long(key))
msg = b'Welcome to ?CTF! , I hope you can have fun!!!!!!'
def encrypto(message):
aes = AES.new(key,AES.MODE_CBC,iv)
return aes.encrypt(message)
print(binascii.hexlify(encrypto(msg))[-32:])
'''
...
b'...'
'''

hint部分老熟了,一道经典老题的泄露方式。key只有 16 位,而hint8 * 4 = 32位,异或后,生成的随机四字节会泄露,所以直接将第一个已知值转为字节,取前面 16 字节会发现是某四字节的四次重复,便可恢复hint并恢复key。然后题目给了CBC模式下加密的明文和最后一块的密文,有了key就可以一路倒推回去了。

exp.py

from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Util.strxor import strxor
from Crypto.Cipher import AES
hint_xor_key = ...
# print(long_to_bytes(hint_xor_key))
# b'\xca\xd8N\x97\xca\xd8N\x97\xca\xd8N\x97\xca\xd8N\x97Q\xe1\x80\x0b\x8c>y\x1f\x9a\xa0\xd8A\x0b\xb7c\xa4'
c3 = bytes.fromhex('...')
hint = b'\xca\xd8N\x97' * 8
key = long_to_bytes(hint_xor_key ^ bytes_to_long(hint))
msg = b'Welcome to ?CTF! , I hope you can have fun!!!!!!'
cipher = AES.new(key, AES.MODE_ECB)
c2 = strxor(msg[32:], cipher.decrypt(c3))
c1 = strxor(msg[16:32], cipher.decrypt(c2))
iv = strxor(msg[:16], cipher.decrypt(c1))
print("flag{" + iv.decode() + "}")
# flag{CBc_Us3s_Iv!=ECb}

Common RSA#

题目:

from Crypto.Util.number import *
from secret import flag
assert flag.startswith(b'flag{') and flag.endswith(b'}')
p, q = getPrime(512), getPrime(512)
n = p * q
e = 65537
m = bytes_to_long(flag)
c = pow(m, e, n)
hint = pow(p + q, 2, n)
print(f'{n = }')
print(f'{e = }')
print(f'{c = }')
print(f'{hint = }')
'''
n = ...
e = 65537
c = ...
hint = ...
'''

由题目给的hint可以很简单的通过平方和和n分解pq,然后分解出pq后发现gcd(e, p - 1)gcd(e, q - 1)都为 65537,e太大了,考虑 AMM 算法打出mpq都挺大,打一个就行了。

exp.py

# SageMath 10.7
import time
import random
from tqdm import trange, tqdm
from gmpy2 import iroot, invert
from Crypto.Util.number import *
def AMM(o, r, q):
start = time.time()
g = GF(q)
o = g(o)
p = g(random.randint(1, q))
while p ^ ((q-1) // r) == 1:
p = g(random.randint(1, q))
print('[+] Find p:{}'.format(p))
t = 0
s = q - 1
while s % r == 0:
t += 1
s = s // r
print('[+] Find s:{}, t:{}'.format(s, t))
k = 1
while (k * s + 1) % r != 0:
k += 1
alp = (k * s + 1) // r
print('[+] Find alp:{}'.format(alp))
a = p ^ (r**(t-1) * s)
b = o ^ (r*alp - 1)
c = p ^ s
h = 1
for i in range(1, t):
d = b ^ (r^(t-1-i))
if d == 1:
j = 0
else:
print('[+] Calculating DLP...')
j = - discrete_log(d, a)
print('[+] Finish DLP...')
b = b * (c^r)^j
h = h * c^j
c = c^r
result = o^alp * h
end = time.time()
print("Finished in {} seconds.".format(end - start))
print('Find one solution: {}'.format(result))
return result
def onemod(p,r):
t=p-2
while pow(t,(p-1) // r,p)==1:
t -= 1
return pow(t,(p-1) // r,p)
def solution(p,root,e):
g = onemod(p,e)
may = set()
for i in range(e):
may.add(root * pow(g,i,p)%p)
return may
def union(x1, x2):
a1, m1 = x1
a2, m2 = x2
d = gmpy2.gcd(m1, m2)
assert (a2 - a1) % d == 0
p1,p2 = m1 // d,m2 // d
_,l1,l2 = gmpy2.gcdext(p1,p2)
k = -((a1 - a2) // d) * l1
lcm = gmpy2.lcm(m1,m2)
ans = (a1 + k * m1) % lcm
return ans,lcm
def excrt(ai,mi):
tmp = zip(ai,mi)
return reduce(union, tmp)
n = ...
e = 65537
c = ...
hint = ...
p, q = None, None
for k in trange(1000000):
try:
sq = hint + k * n
res = iroot(sq, 2)
if res[0] ** 2 == sq:
sum = res[0]
D = sum ** 2 - 4 * n
diff = iroot(D, 2)[0]
p = (sum + diff) // 2
q = (sum - diff) // 2
if p * q == n:
break
except:
continue
cp = c % p
mp = AMM(cp, e, p)
mps = solution(p, mp, e)
for mpp in tqdm(mps):
ai = [int(mpp)]
mi = [p]
m = CRT_list(ai,mi)
flag = long_to_bytes(m)
if b'flag' in flag:
print(flag.decode())
exit(0)
# flag{1t_i5_N0T_4_c0mmOn_R5A!}

Furious BlackCopper#

题目:

from Crypto.Util.number import *
from secret import flag
p,q,r = [getPrime(1024) for _ in range(3)]
n = p*q*r
e = 65537
c = pow(bytes_to_long(flag),e,n)
print(f"n = {n}")
print(f"c = {c}")
print(f"hint1 = {p % (1<<20)}")
print(f"hint2 = {(p>>30) % (2**800)}")
'''
n = ...
c = ...
hint1 = ...
hint2 = ...
'''

就常规打,爆破 10 位就行。

exp.py

# SageMath 10.7
from tqdm import trange
from Crypto.Util.number import *
e = 65537
n = ...
c = ...
hint1 = ...
hint2 = ...
PR.<x> = PolynomialRing(Zmod(n))
for i in trange(2 ** 10):
pl = hint1 + i * 2 ** 20 + hint2 * 2 ** 30
f = x * 2 ** 830 + pl
res = f.monic().small_roots(X=2**194, beta=0.19, epsilon=0.02)
if len(res) != 0:
p = res[0] * 2 ** 830 + pl
dp = inverse(e, p - 1)
m = pow(c, dp, p)
print(long_to_bytes(int(m)).decode())
break
# 27%|█████████████████████▍ | 275/1024 [00:05<00:14, 51.46it/s]
# flag{C0pp3R_W0u1d_B3_T4sty_W1tH_F0rc3}

baby Elgamal#

题目:

from Crypto.Util.number import *
import random
from secret import flag
p = getPrime(512)
g = random.randint(2, p - 2)
x = random.getrandbits(32)
y = pow(g, x, p)
print(f'{p = }')
print(f'{g = }')
print(f'{y = }')
k = random.randint(2, p - 2)
m = bytes_to_long(flag)
c1 = pow(g, k, p)
c2 = m ^ pow(y, k, p)
print(f'{c1 = }')
print(f'{c2 = }')
'''
p = ...
g = ...
y = ...
c1 = ...
c2 = ...
'''

都给了g了,bsgs 直接打就行。

exp.py

from Crypto.Util.number import long_to_bytes
p = ...
g = ...
y = ...
c1 = ...
c2 = ...
N = 1 << 32
m = 1 << 16
baby = {}
cur = 1
for j in range(m):
baby[cur] = j
cur = (cur * g) % p
gm = pow(g, m, p)
gm_inv = pow(gm, -1, p)
cur = y
x = None
for i in range((N + m - 1) // m + 1):
if cur in baby:
x = i * m + baby[cur]
if x < N:
break
cur = (cur * gm_inv) % p
assert x is not None
s = pow(c1, x, p)
m_int = c2 ^ s
flag = long_to_bytes(m_int)
print(x, flag.decode())
# 1616680587 flag{31g4m41_D15cr373_10g}

findKey in middle#

题目:

from Crypto.Util.Padding import pad
from Crypto.Util.number import *
from random import getrandbits
from Crypto.Cipher import AES
from hashlib import sha256
from secret import flag
def f(x, y):
return (pow(3, x, p) * pow(5, y, p)) % p
def split_key(key):
x, y = getPrime(16), getPrime(16)
assert x * y > key
k1, k2 = key % x, key % y
return k1, k2, x, y
def aes_encrypt(key, flag):
aes = AES.new(key, AES.MODE_ECB)
return aes.encrypt(pad(flag, 16))
p = 1000000007
key = getrandbits(32)
k1, k2, mod1, mod2 = split_key(key)
x = f(k1, k2)
cipher = aes_encrypt(sha256(long_to_bytes(key)).digest()[:16], flag)
print(f'x = {x}')
print(f'mod = {(mod1, mod2)}')
print(f'cipher = {cipher}')
# x = ...
# mod = (..., ...)
# cipher = ...

简而言之密钥被两个模数拆分了,然后给了提示,要从提示推出两个拆分的密钥然后 crt 打出原密钥再派生出 AES 的密钥解密就行。

exp.py

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Util.number import long_to_bytes
from hashlib import sha256
p = ...
x = ...
mod1, mod2 = ..., ...
cipher = ...
pow3 = {}
v = 1
for i in range(mod1):
pow3[v] = i
v = (v * 3) % p
k1 = k2 = None
pow5 = 1
inv5 = pow(5, p - 2, p)
inv5_k = 1
for j in range(mod2):
t = (x * inv5_k) % p
if t in pow3:
k1 = pow3[t]
k2 = j
break
inv5_k = (inv5_k * inv5) % p
if k1 is None:
raise RuntimeError("未找到(k1,k2)")
def crt(a1, m1, a2, m2):
n1 = m2
n2 = m1
inv_n1 = pow(n1, -1, m1)
inv_n2 = pow(n2, -1, m2)
x = (a1 * n1 * inv_n1 + a2 * n2 * inv_n2) % (m1 * m2)
return x
key = crt(k1, mod1, k2, mod2)
assert key < mod1 * mod2
aes_key = sha256(long_to_bytes(key)).digest()[:16]
pt = unpad(AES.new(aes_key, AES.MODE_ECB).decrypt(cipher), 16)
print("k1 =", k1, "k2 =", k2)
print("key =", key)
print(pt.decode("utf-8"))
# k1 = 12050 k2 = 27449
# key = 1313232941
# flag{e31343dd-4795-4236-bbec-11b8410b5ce6}

strange random#

题目:

from Crypto.Util.number import *
import random
from sympy import prime
def sssstranger(p,q):
n = p*q
list=[]
for i in range(312):
x=random.getrandbits(32)
list.append(x)
print(list)
return n
p=getStrongPrime(512)
q=getStrongPrime(512)
r=getStrongPrime(512)
n1=sssstranger(p,q)
print(n1)
n2=sssstranger(q,r)
print(n2)
e=random.getrandbits(32)
m=bytes_to_long(b"xxx")
c=pow(m,e,n1*n2//q)
print(c)
'''
[...]
...
[...]
...
...
'''

前半是个 MT19937,randcrack 创出e,然后会发现后面epqr都不互素,可以取r来打flag

n=pqr,φ(n)=(p1)(q1)(r1)cme(modn)g:=gcd(e,r)=4e:=gh,ch1mg(modr)\begin{aligned} n=p*q*r,\varphi{(n)}=(p-1)*(q-1)*(r-1)\\ c\equiv m^e\pmod{n}\\ g:=gcd(e,r)=4\\ e:=g*h,c^{h^{-1}}\equiv m^g\pmod{r} \end{aligned}

在模r下缩小范围从m ** 4求解m

exp.py

# SageMath 10.7
from math import gcd
from Crypto.Util.number import inverse, long_to_bytes
from randcrack import RandCrack
outputs = [...]
n1 = ...
n2 = ...
c = ...
rc = RandCrack()
for v in outputs:
rc.submit(v)
e = rc.predict_getrandbits(32)
# print("predicted e =", e)
# predicted e = 1297450108
q = gcd(n1, n2)
p = n1 // q
r = n2 // q
N = (n1 * n2) // q
phi = (p - 1) * (q - 1) * (r - 1)
# print(gcd(e, p - 1), gcd(e, q - 1), gcd(e, r - 1), gcd(e, phi))
# 436 2 4 436
g = gcd(e, r - 1)
er = e // g
phir = r - 1
dr = inverse(er, phir)
cr = pow(c, dr, r)
PR.<x> = PolynomialRing(Zmod(r))
f = x ** g - cr
res = f.roots(multiplicities=False)
for i in res:
m = long_to_bytes(int(i))
if all(1 <= c <= 127 for c in m):
print(m.decode(errors="ignore"))
# flag{wwooooow_u_no_the_randdoooooo0m!!!}

Week 3#

Backpack#

题目:

import random
from secret import flag
assert flag.startswith(b'flag{') and flag.endswith(b'}')
assert len(flag) == 18
nbits = 100
a = [random.randint(1<<(nbits-1),1<<nbits) for i in range(len(flag))]
b = sum([i*j for i,j in zip(a,flag)])
print(f'{a = }')
print(f'{b = }')
'''
a = [...]
b = ...
'''

很简洁的题目,但是一开始思索了下是z3还是格攻击求解,但是约束应该都还是不太够,所以先通过flag{}的固定首尾搭配来降维。然后常规背包格打就行。

M=(1a[5]1a[6]1a[2]b)M=\begin{pmatrix} 1 & & & & a[5]\\ & 1 & & & a[6]\\ & & \ddots & & \vdots\\ & & & 1 & a[-2]\\ & & & & -b\\ \end{pmatrix}

exp.py

# SageMath 10.7
a = [...]
b = ...
b = b - ord('f') * a[0] - ord('l') * a[1] - ord('a') * a[2] - ord('g') * a[3] - ord('{') * a[4] - ord('}') * a[-1]
a = a[5:-1]
n = len(a) # 18
B = 1 << 0
M = Matrix(ZZ, n + 1)
for i in range(n):
M[i, i] = B
M[i, n] = a[i]
M[n, n] = -b
res = M.LLL()
flag = "flag{"
for i in res[0]:
flag += chr(i)
flag += "}"
print(flag)
# flag{342y_14771C3}

Great Block Cipher#

题目:

def encrypt(msg, key, iv):
assert len(msg) % 16 == 0
assert len(key) == 16 and len(iv) == 16
xor = lambda a, b:[i^j for i,j in zip(a,b)]
sbox = [...]
cipher = []
ks = [key[:4],key[4:8],key[8:12],key[12:]]
for i in range(16*4):
nk = 0
for j in range(4):
nk = int.from_bytes(ks[i+j],'big') ^ (3377808869 * nk % (1<<32))
ks.append(nk.to_bytes(4,'little'))
block_count = len(msg)//16
for block in range(block_count):
m = list(msg[16*block:16*(block+1)])
m = xor(m, iv)
m = xor(m, b''.join(ks[:4]))
for loop in range(16):
m = [sbox[i] for i in m]
for i in range(4):
a = bin(sum([m[4*i+j]*256**j for j in range(4)]))[2:].zfill(32)
b = []
d = i+3
c1, c2 = 32//d, 32%d
k = 0
for j in range(d):
if j < c2:
b.append(a[k:k+c1+1])
k += c1+1
else:
b.append(a[k:k+c1])
k += c1
c = ''
for j in range(c1+1):
for k in range(d):
if len(b[k]) > j:
c += b[k][j]
m[4*i],m[4*i+1],m[4*i+2],m[4*i+3] = int(c,2).to_bytes(4,'big')
if loop < 15:
a = []
b = [10, 13, 3, 11, 7, 9, 0, 15, 5, 12, 8, 4, 6, 14, 2, 1]
for i in range(4):
for j in range(4):
a.append(sum([b[4*i+k]*m[j+4*k] for k in range(4)])%256)
m = a
m = xor(m, b''.join(ks[4*(loop+1):4*(loop+2)]))
cipher.append(bytes(m))
iv = m
return b''.join(cipher)
def pad(msg):
l = (len(msg)//16+1)*16
return msg.ljust(l,chr(l-len(msg)).encode())
flag = b'flag{*********}'
what = b'www.bilibili.com/video/BV1ox4y1e71S'
key, iv = what[:16], what[-16:]
cipher = encrypt(pad(flag), key, iv)
print(cipher.hex())
# ...

题目给出的encrypt()函数自定义了一个分组对称加密算法,但是整体特征还是类似AES-CBC加密方式,只是用了自定义的S-box,按位重排函数,模 256 下的 4x4 矩阵混合以及自定义的密钥扩展方式。

大致流程是先将Blockiv异或,然后和key异或,接着进入 16 轮迭代:1. S-box 替换,2. bit 混合,3. 线性混合,4. 异或子密钥。

解密的话就是反向执行,ai 调了调,自己修了下出了(又像逆向题了emmm),唉我的 AES 是真菜。

exp.py

def invert_matrix_mod256(M):
"""
计算 4x4 矩阵在模 256 下的逆矩阵。
线性混合步骤的逆运算。
"""
n = 4
# 构造增广矩阵 [M | I]
A = [[M[i][j] % 256 for j in range(n)] + [1 if i == j else 0 for j in range(n)] for i in range(n)]
# 模 256 下的逆元(利用扩展欧几里得)
def inv(a):
a %= 256
t, nt = 0, 1
r, nr = 256, a
while nr:
q = r // nr
t, nt = nt, t - q * nt
r, nr = nr, r - q * nr
return None if r != 1 else t % 256
# 高斯消元法
row = 0
for col in range(n):
pivot = None
for r in range(row, n):
if inv(A[r][col]) is not None:
pivot = r
break
if pivot is None:
return None # 不可逆
# 交换主元行
A[row], A[pivot] = A[pivot], A[row]
ip = inv(A[row][col])
# 主元行归一化
for c in range(2 * n):
A[row][c] = (A[row][c] * ip) % 256
# 其他行消元
for r in range(n):
if r == row:
continue
f = A[r][col] % 256
if f:
for c in range(2 * n):
A[r][c] = (A[r][c] - f * A[row][c]) % 256
row += 1
# 取右半部分为逆矩阵
return [r[n:] for r in A]
def inv_bitmix_word(w, i):
"""
逆位混合函数,对单个 4 字节(32bit)word 逆变换。
参数:
w : list[int],长度为 4
i : 第 i 个 word 的索引 (0~3)
"""
d = i + 3
c1, c2 = 32 // d, 32 % d
lens = [c1 + 1 if j < c2 else c1 for j in range(d)]
# 根据加密阶段的分块顺序重建索引
idx = 0
bidx = []
for ln in lens:
bidx.append(list(range(idx, idx + ln)))
idx += ln
# 加密时按列交织,现在要按列逆序恢复
order = [bidx[r][c] for c in range(max(lens)) for r in range(d) if c < len(bidx[r])]
bits = bin(int.from_bytes(bytes(w), 'big'))[2:].zfill(32)
restored = ['0'] * 32
for pc, pa in enumerate(order):
restored[pa] = bits[pc]
return list(int(''.join(restored), 2).to_bytes(4, 'little'))
ciphertext = bytes.fromhex("...")
what = b'www.bilibili.com/video/BV1ox4y1e71S'
key, iv = what[:16], what[-16:]
xor = lambda a, b: [i ^ j for i, j in zip(a, b)]
# 逆 S-box
sbox = [...]
inv_sbox = [0] * 256
for i, v in enumerate(sbox):
inv_sbox[v] = i
# 密钥扩展,与 encrypt() 相同
ks = [key[:4], key[4:8], key[8:12], key[12:]]
for i in range(64):
nk = 0
for j in range(4):
nk = int.from_bytes(ks[i + j], 'big') ^ (3377808869 * nk % (1 << 32))
ks.append(nk.to_bytes(4, 'little'))
# 加密中使用的混合矩阵
b = [[10, 13, 3, 11], [7, 9, 0, 15], [5, 12, 8, 4], [6, 14, 2, 1]]
binv = invert_matrix_mod256(b)
# 分块解密
blocks = [ciphertext[i:i + 16] for i in range(0, len(ciphertext), 16)]
result = []
prev_iv = iv
for blk in blocks:
m = list(blk)
# 共 16 轮逆序进行
for round_idx in range(15, -1, -1):
# 1. XOR 当前轮的子密钥
m = xor(m, b''.join(ks[4 * (round_idx + 1):4 * (round_idx + 2)]))
# 2. 若不是最后一轮,则执行线性混合的逆运算
if round_idx < 15:
temp = [0] * 16
for j in range(4):
col = [m[j + 4 * k] for k in range(4)]
col = [sum(binv[i][k] * col[k] for k in range(4)) % 256 for i in range(4)]
for i in range(4):
temp[i * 4 + j] = col[i]
m = temp
# 3. 位重排逆变换
for i in range(4):
m[4 * i:4 * i + 4] = inv_bitmix_word(m[4 * i:4 * i + 4], i)
# 4. S-box 逆变换
m = [inv_sbox[x] for x in m]
# 5. 轮外 XOR 初始 key 部分 与 CBC iv
m = xor(m, b''.join(ks[:4]))
m = xor(m, prev_iv)
result.append(bytes(m))
prev_iv = blk # CBC 链接
plaintext = b''.join(result)
padlen = plaintext[-1]
print(plaintext[:-padlen].decode())
# flag{7h3_Ch4rm_0f_SyMm3tr1c_EnCrYpT1On!!!}

abc equation(!)#

题目:

import os
flag = os.getenv('FLAG')
if __name__ == "__main__":
a, b, c = int(input('a = ')), int(input('b = ')), int(input('c = '))
if a > 0 and b > 0 and c > 0:
f1 = 2744*a**3 + 10648*b**3 + 17984728*c**3 + 3161480*a**2 + 12256398*a*b + 9908932*b**2 + 115107342*a*c + 185540278*b*c + 52709471256*c
f2 = 10241*a**2*b + 16093*a*b**2 + 121961*a**2*c + 464002*a*b*c + 301169*b**2*c + 2282413*a*c**2 + 3586649*b*c**2 + 1698355526*c**2 + 1440958176*a + 2321077176*b + 538357358480
if f1 == f2:
print(flag)

题目主要就是解一个三元三次方程大于 0 的特解,由于不知道数多大,大概范围在哪,爆破成本很高,问题主要就来到了怎么降低计算量去求解了。

首先设:

F(a,b,c)=f1(a,b,c)f2(a,b,c)=0F(a,b,c)=f_1(a,b,c)-f_2(a,b,c)=0

题目要求就是要找这个方程的一组正整数解。我们可以考虑降维来看:

F(a,b,c)=A3(b,c)a3+A2(b,c)a2+A1(b,c)a+A0(b,c)=0F(a,b,c)=A_3(b,c)a^3+A_2(b,c)a^2+A_1(b,c)a+A_0(b,c)=0

这样对每个固定的(b, c),我们只需找一个a使得这个方程成立即可。

接下来可以用一下模数筛的思想:

如果F(a,b,c)0(modp)F(a,b,c)\equiv 0\pmod{p},则a必是此方程的解。那么可以对若干的小素数求出可能的余数解,然后CRT合并就可以得到对一个大模数下的方程的解,这样每个(b, c)只需验证少量a的同余类即可。

exp.py

# SageMath 10.7
from tqdm import tqdm
from itertools import product
def F(a,b,c):
f1 = 2744*a**3 + 10648*b**3 + 17984728*c**3 + 3161480*a**2 + 12256398*a*b + 9908932*b**2 + 115107342*a*c + 185540278*b*c + 52709471256*c
f2 = 10241*a**2*b + 16093*a*b**2 + 121961*a**2*c + 464002*a*b*c + 301169*b**2*c + 2282413*a*c**2 + 3586649*b*c**2 + 1698355526*c**2 + 1440958176*a + 2321077176*b + 538357358480
return f1 - f2
def Fa_mod_p(b,c,p):
R.<x> = GF(p)[]
return R( F(x, ZZ(b), ZZ(c)) )
def crt_combine(res_lists, mods):
M = 1
for m in mods:
M *= m
if any(len(S) == 0 for S in res_lists):
return [], M
cand = set()
for tpl in product(*res_lists):
r = 0
mod = 1
ok = True
for ri, mi in zip(tpl, mods):
try:
r = crt([r, ri], [mod, mi])
except Exception:
ok = False
break
mod *= mi
if ok:
cand.add(int(r % M))
return sorted(cand), M
# 太小再加
BMAX, CMAX = 400, 400
AMAX = 200000
# 不够可以加模数
PRIMES = [7, 11, 13, 17, 19, 23]
for b in tqdm(range(1, BMAX + 1)):
for c in range(1, CMAX + 1):
root_sets = []
for p in PRIMES:
P = Fa_mod_p(b,c,p)
rs = [int(r) for r in P.roots(multiplicities=False)] # a ≡ r (mod p)
if not rs:
root_sets = []
break
root_sets.append(rs)
if not root_sets:
continue
residues, M = crt_combine(root_sets, PRIMES)
if not residues:
continue
# 只在 a ≡ residue (mod M) 的序列上检查,步长 = M
for r in residues:
start = r if r > 0 else r + M
for a in range(start, AMAX + 1, M):
if a <= 0:
continue
if F(a,b,c) == 0:
print(f"FOUND a={a} b={b} c={c}")
raise SystemExit
# 30%|█████████████████████▉ | 355/1200 [00:17<00:41, 20.42it/s]
# FOUND a=1347 b=356 c=365
> ncat challenge.ilovectf.cn 30786
a = 1347
b = 356
c = 365
flag{16fe3697-ca92-4635-91ce-76d08c93f189}

观察了一下官方 WriteUp,居然是转到椭圆曲线上求解吗hhh,有点意外了,复现一下。(但是这个观察立方项系数发现是立方数然后还要想到放缩求平移常数实在有点emmmm想不到)

f1 == f2时,可以令:

{a=x37b=y1137c=z131+29\begin{cases} a=\frac{x-3}{7}\\ b=\frac{y}{11}-37\\ c=\frac{z}{131}+29 \end{cases}

则有原式等价于:

8x319x2y19xy2+8y319x2z46xyz19y2z19xz219yz2+8z3=08x^3-19x^2y-19xy^2+8y^3-19x^2z-46xyz-19y^2z-19xz^2-19yz^2+8z^3=0

然后可以使用EllipiticCurve_from_cubic将上述三次齐次式映射到一条有理数域的椭圆曲线,利用gens找到椭圆曲线的生成元,再通过椭圆曲线上的加法和数乘计算出该生成元的n倍点并在这些点中找到一个满足abc都为正整数的(x, y, z)

exp.py

# SageMath 10.7
F.<a, b, c> = ZZ[]
f1 = 2744*a**3 + 10648*b**3 + 17984728*c**3 + 3161480*a**2 + 12256398*a*b + 9908932*b**2 + 115107342*a*c + 185540278*b*c + 52709471256*c
f2 = 10241*a**2*b + 16093*a*b**2 + 121961*a**2*c + 464002*a*b*c + 301169*b**2*c + 2282413*a*c**2 + 3586649*b*c**2 + 1698355526*c**2 + 1440958176*a + 2321077176*b + 538357358480
f = f1 - f2
F2.<x, y, z> = QQ[]
g = f((x - 3) / 7, y / 11 - 37, z / 131 + 29)
F = EllipticCurve_from_cubic(g, morphism=True)
E = F.codomain()
G = E.gens()[0]
Finv = F.inverse()
for k in range(100):
ABC = Finv(k * G)
AA, BB, CC = ABC
d = max(AA.denominator(), BB.denominator(), CC.denominator())
AA, BB, CC = AA * d, BB * d, CC * d
AA, BB, CC = ((AA - 3) / 7, BB / 11 - 37, CC / 131 + 29)
if all([i.denominator() == 1 and i > 0 for i in [AA, BB, CC]]):
print(k)
print('a =', AA)
print('b =', BB)
print('c =', CC)
print(f(AA, BB, CC))
break

my crypto#

题目:

from Crypto.Util.number import *
from Crypto.Cipher import AES
from secret import keys, flag
assert all([isPrime(i) for i in keys])
a, b, c, d = keys
flag1 = flag[:16]
flag2 = flag[16:]
def getrandbits(nbits):
global a
a = (a * b + c) % d
res = a % (1 << (nbits % 32))
for i in range(nbits>>5):
a = (a * b + c) % d
res = (res << 32) + a
return res
def my_getPrime(nbits):
while 1:
p = getrandbits(nbits)
if isPrime(p):
return p
k = b''.join([long_to_bytes(i) for i in keys])
cipher = AES.new(k,AES.MODE_ECB)
c1 = cipher.encrypt(flag1)
print(c1.hex())
# b7a6aa2e92065c6ca0f641774daa6be7
p = my_getPrime(512)
q = my_getPrime(512)
n = p * q
d = my_getPrime(32)
e = pow(d, -1, (p-1)*(q-1))
m = bytes_to_long(flag2)
c2 = pow(m, e, n)
print(f'{n = }')
print(f'{e = }')
print(f'{c2 = }')
'''
n = ...
e = ...
c2 = ...
'''

分两个部分,前半是个 AES-ECB,关键是求keykeyabcd四个参数组成;后半部分是 RSA,用了自己写的 LCG 函数生成pqdAESkey中的四个参数同时以 LCG 的参数参与了后半部分的 RSA,后半部分的 RSA 可以直接 Wiener 板子打出d,就能求解后半部分 flag。然后有了dk可以分解n,求出pq,再把pq从高位往低位每 32 比特提取,可以得到 LCG 的状态组(但是pq的状态组之间不连续),然后通过 LCG 可以求得abcd四个参数就可以解前半部分 flag 了。

exp.py

from Crypto.Util.number import *
from Crypto.Cipher import AES
from sympy import *
class ContinuedFraction():
def __init__(self, numerator, denominator):
self.numberlist = []
self.fractionlist = []
self.GenerateNumberList(numerator, denominator)
self.GenerateFractionList()
def GenerateNumberList(self, numerator, denominator):
# 生成简单连分数
while numerator != 1:
quotient = numerator // denominator
remainder = numerator % denominator
self.numberlist.append(quotient)
numerator = denominator
denominator = remainder
def GenerateFractionList(self):
self.fractionlist.append([self.numberlist[0], 1])
for i in range(1, len(self.numberlist)):
numerator = self.numberlist[i]
denominator = 1
for j in range(i):
temp = numerator
numerator = denominator + numerator * self.numberlist[i - j - 1]
denominator = temp
self.fractionlist.append([numerator, denominator])
n = ...
e = ...
c2 = ...
res = ContinuedFraction(e, n)
for k, d in res.fractionlist:
# 判断哪一个是我们所需的d
if k == 0 or k == 1:
continue
elif (e * d - 1) % k == 0:
break
m = pow(c2, d, n)
flag2 = long_to_bytes(m).decode()
phi = (e * d - 1) // k
p, q = var("p q")
eq1 = Eq(p * q, n)
eq2 = Eq((p - 1) * (q - 1), phi)
p, q = solve([eq1, eq2], [p, q])[0]
assert p * q == n
state = [(p >> (32 * (15 - i))) & 0xffffffff for i in range(16)]
diff = []
for i in range(len(state) - 1):
diff.append(state[i + 1] - state[i])
d = diff[-1] * diff[-3] - diff[-2] ** 2
for i in range(2, len(diff) - 1):
d = gcd(d, diff[i] * diff[i - 2] - diff[i - 1] ** 2)
assert isPrime(int(d))
for i in range(2, len(diff) - 1):
b = (diff[i] * invert(diff[i - 1], d)) % d
b += d
binv = invert(b, d)
c = (state[1] - state[0] * b) % d
for i in range(len(state) - 1):
assert state[i + 1] == (b * state[i] + c) % d
a = (state[0] - c) * binv % d
c1 = bytes.fromhex("b7a6aa2e92065c6ca0f641774daa6be7")
while 1:
a = (a - c) * binv % d
cnt += 1
if cnt % 100000 == 0:
print(cnt)
if isPrime(int(a)):
k = b''.join([long_to_bytes(i, 4) for i in [a, b, c, d]])
cipher = AES.new(k, AES.MODE_ECB)
flag1 = cipher.decrypt(c1)
if b'flag{' in flag1:
print(flag1.decode() + flag2)
break
# a = 3597317387
# b = 4126288123
# c = 3523486777
# d = 3952641229
# flag{LCG_is_n0t_a_cryp7o_5ecure_PRNG}

期末考试(!)#

题目:

from secret import B, C
import numpy as np
p = 251
A = np.diag([np.random.randint(0,p) for i in range(6)])
B = np.matrix(B)
C = np.matrix(C)
print(B * C %p)
'''
[...]
'''
D = B * A * C %p
print('D =',D.tolist())
'''
D = [...]
'''
flag = bytes(B.reshape((1,36)).tolist()[0])
assert flag[:4] == b'flag'
print(flag)
# b'flag{you_need_to_solve_by_yourself!}' ## fakeflag

加密流程是在模 251 的域上取随机对角阵 A,选择可逆矩阵 B 以及 B 的逆矩阵 C。输出:

DBACmod251D\equiv BAC\mod{251}

flag就是 B 矩阵按行展平。可以验证一下 D 在域上是可对角化且特征值互异:

# SageMath 10.7
print(D.is_diagonalizable())
print(D.eigenvalues())
# True
# [182, 150, 132, 109, 68, 16]

首先这题要说一下自由度的问题:

A=diag(λ1,,λ6),B=[v1v2v6]A=diag(\lambda_1,\dots,\lambda_6),B=[v_1v_2\dots v_6]的列是 DD 的右特征向量,满足

Dvi=λivi,D=BAB1Dv_i=\lambda_iv_i,D=BAB^{-1}

其中有两类等价变换不会改变 DD

  1. **列缩放:**任取可逆对角矩阵 S=diag(s1,,s6)S=diag(s_1,\dots,s_6)

    B=BS,A=S1AS=A,BA(B)1=DB'=BS,A'=S^{-1}AS=A,B'A'(B')^{-1}=D

    含义:每列特征向量可乘任意非零标量,自由度 (F251×)6(\mathbb{F}^{\times}_{251})^6,规模为 250^6。

  2. **列置换:**任取置换矩阵 PP

    B=BP,A=P1AP=diag(λπ(1),,λπ(6)),BA(B)1=DB'=BP,A'=P^{-1}AP=diag(\lambda_{\pi(1)},\dots,\lambda_{\pi(6)}),B'A'(B')^{-1}=D

    含义:列顺序可任意换,同时对 AA 以相同置换重排对角元,自由度 6!。

也就是说 DD 唯一确定的是“每个特征空间的方向”,而不是具体的列向量的顺序。然后再看回这道题就会发现,是个有点巧妙的题了,flag{}刚好限制了 6 个列缩放自由度,只需要再爆破一下列置换自由度就行了。

exp.py

# SageMath 10.7
from itertools import permutations
# 取一条向量并规范化
def canon(v):
v = vector(F, v)
for x in v:
if x != 0:
return (1 / x) * v
return v
p = 251
D = Matrix(GF(p), [...])
spec = D.eigenvectors_right()
pairs = [(lam, canon(vs[0])) for lam, vs, mult in spec]
# 全排列列顺序
target_head = list(b"flag{")
for perm in permutations(range(6)):
# 组 B, A
cols = [pairs[i][1] for i in perm]
lams = [pairs[i][0] for i in perm]
B = Matrix(F, cols).transpose()
# 必须可逆
if B.rank() < 6:
continue
try:
C = B.inverse()
except ZeroDivisionError:
continue
A = diagonal_matrix(F, lams)
if B * A * C != D:
continue
# 固定右下角为 '}'
if B[5, 5] == 0:
continue
s5 = F(125) * (B[5, 5]) ** -1
B[:, 5] *= s5
# 固定首行为 b"flag{"
ok = True
for j in range(5):
if B[0, j] == 0:
ok = False
break
sj = F(target_head[j]) * (B[0, j]) ** -1
B[:, j] *= sj
if not ok:
continue
# 重新验证可逆与相似关系
try:
C = B.inverse()
except ZeroDivisionError:
continue
if B * A * C != D:
continue
if all(32 <= int(x) <= 126 for x in B.list()):
flag = bytes(int(x) for x in B.list())
print(flag.decode())
break
# flag{3Njoy_My_eZ_m4tr1x_5imi1aRi7y!}

参照官方exp码一个,主要是学习记录一下 SageMath 的diagonalization函数的使用。

exp.py

# SageMath 10.7
from itertools import permutations
D = Matrix(GF(251), [...])
A, E = D.diagonalization()
eye = [[1 if i == j else 0 for j in range(6)] for i in range(6)]
for m in permutations(range(6)):
P = []
for i in m:
P.append(eye[i])
P = Matrix(Zmod(251), P)
for n in range(32, 127):
B = E / P * diagonal_matrix([ord("f"), ord("l"), ord("a"), ord("g"), ord("{"), n])
flag = B.list()
if all([32 <= i <= 127 for i in flag]) and flag[-1] == 125:
print(bytes(flag).decode())
# flag{3Njoy_My_eZ_m4tr1x_5imi1aRi7y!}

粗心的刀客塔#

题目:

作战中,粗心的刀客塔在给干员们下达机密指令时漏发了部分内容,为了省事,他直接用原来的密钥重发了完整指令。可他万万没想到,这个看似简单的操作,却导致战术被普瑞赛斯全盘破解。

from Crypto.Util.number import *
from secret import flag
import string
assert all([32 <= i < 127 for i in flag])
p = getPrime(256)
q = getPrime(256)
n = p * q
e = 101
m = bytes_to_long(flag)
leak = pow(m >> 24, e, n)
c = pow(m, e, n)
print(f"{n = }")
print(f"{c = }")
print(f"{leak = }")
'''
n = ...
c = ...
leak = ...
'''

好简洁的题目,好舒服啊哈哈哈哈哈。根据描述以及这个代码内容,相关信息攻击,上板子!但是会有个问题,这个右移 24 位怎么解决呢,肯定不能爆破,有点大了。这个时候观察一下题目给的一个提示:assert all([32 <= i < 127 for i in flag]),那么我们爆破直接爆破两位字符就可以了(最后一位一定是})。

exp.py

# SageMath 10.7
from Crypto.Util.number import *
from tqdm import trange
from sys import exit
def franklinReiter(n, e, c1, c2, r):
PR.<x> = PolynomialRing(Zmod(n))
g1 = x ** e - c1
g2 = (x * 2 ** 24 + r) ** e - c2
def gcd(g1, g2):
while g2:
g1, g2 = g2, g1 % g2
return g1.monic()
return -gcd(g1, g2)[0]
e = ...
n = ...
c = ...
leak = ...
for i in trange(32, 127):
for j in range(32, 127):
r = i * 65536 + j * 256 + ord("}")
x = franklinReiter(n, e, leak, c, r)
m = int(x * 2 ** 24 + r)
try:
flag = long_to_bytes(m)
if b'flag{' in flag:
print(flag.decode())
raise SystemExit
except SystemExit:
print("Over!")
raise SystemExit
except:
continue
# 1%|▉ | 1/95 [00:00<00:14, 6.38it/s]
# flag{Franklin-Reiter_Re1at3d_Mes5age_4t7ack!!}
# Over!

Week 4#

Crazy List(-)#

题目:

import hashlib
from random import randint
from secret import flag
def crash(b):
assert type(b) == list
def T(b):
a = [abs(i) for i in b]
n, l, r = max(a) + 1, len(b), []
p = list(range(n))
p1 = p.copy()
for i in a:
p[i-1], p[i] = p[i], p[i-1]
F = set(i+1 for i in range(n-1) if p[i]>p[i+1])
p = [p.index(i) for i in range(n)]
S = set(i+1 for i in range(n-1) if p[i]>p[i+1])
while p != p1:
for i in range(n-1):
if p[i]>p[i+1]:
p[i], p[i+1] = p[i+1], p[i]
r.append(i+1)
return r, S, F
n, l = max(b) + 1, len(b)
p, r, B, S, F = list(range(n)), [], [[]], [set()], []
for i in b:
if {p[i-1],p[i]} in r:
F.append(set(j+1 for j in range(n-1) if p[j]>p[j+1]))
p, r = list(range(n)), []
B.append([])
S.append(set())
B[-1].append(i)
r.append({p[i-1],p[i]})
if abs(p[i-1]-p[i]) == 1:
S[-1].add(min(p[i-1],p[i])+1)
p[i-1], p[i] = p[i], p[i-1]
F.append(set(j+1 for j in range(n-1) if p[j]>p[j+1]))
check = True
while check:
check = False
for i in range(len(B)-1):
d = S[i+1].difference(F[i])
if len(d) != 0:
d1 = d.pop()
B[i], S[i], F[i] = T(B[i]+[d1])
B[i+1], S[i+1], F[i+1] = T([-d1]+B[i+1])
check = True
nB = []
for i in B:
nB.extend(i)
return nB
def hash_list(b):
b = crash(b)
h = hashlib.sha512(bytes(b)).digest()
return h
n = 10
nLen = 70
x1, x2 = [randint(1,n-1) for i in range(nLen)], [randint(1,n-1) for i in range(nLen)]
a = [randint(1,2*n-1) for i in range(1000)]
b = x1 + a + x2
y1, y2 = [randint(n+1,2*n-1) for i in range(nLen)], [randint(n+1,2*n-1) for i in range(nLen)]
xor = lambda a,b:bytes([i^j for i,j in zip(a,b)])
c1 = crash(y1 + a + y2)
c2 = xor(flag, hash_list(y1 + b + y2))
a = crash(a)
b = crash(b)
with open('output.txt','w') as f:
f.write(f'{a = }\n')
f.write(f'{b = }\n')
f.write(f'{c1 = }\n')
f.write(f'{c2 = }\n')

群论没学好笑的,这个有点太难了,先鸽一鸽害。

Myneighbors#

题目:

from secret import magical_num, flag
from Crypto.Util.Padding import pad
from Crypto.Util.number import *
from Crypto.Cipher import AES
import hashlib
p = 431
F.<i> = GF(p^2, modulus = x^2 + 1)
E = EllipticCurve(j=F(magical_num))
assert E.is_supersingular()
P = E(0).division_points(2)[1:]
neighbors = []
for idx in range(len((P))):
phi = E.isogeny(P[idx])
EE = phi.codomain()
neighbors.append(EE.j_invariant())
assert E.j_invariant() in neighbors
P = E(0).division_points(3)[1:]
shuffle(P)
phi = E.isogeny(P[0])
E = phi.codomain()
H = hashlib.md5(str(E.j_invariant()).encode()).hexdigest().encode()
key, iv = H[:16], H[16:]
aes = AES.new(key, AES.MODE_CBC, iv)
cipher = aes.encrypt(pad(flag, 16))
print(f'cipher: {cipher.hex()}')
#cipher: 49e90a91357fef12c54234b3cb553bb2fdd61f2af8c7e78b3d5ffdeac7022af0

本周签到题,真就只做出来这一道,哭了qaq。同源真难啊,先简单学习一下,后面系统学习总结一下。

加密代码首先选定了一条特定的超奇异曲线,从这条超奇异椭圆曲线出发,通过一个3-isogeny得到新曲线,并用新曲线的j-invariantmd5值切割为 AES 的keyiv来加密flag。解密枚举出满足约束条件的候选起点曲线E,并对每个候选枚举所有非平凡3-torsion子群生成的3-isogeny,找到能解出flag的那条就行。

exp.py

# SageMath 10.7
from sage.all import *
from hashlib import md5
from tqdm import trange
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
p = 431
F.<i> = GF(p^2, modulus=x^2 + 1)
cipher = bytes.fromhex("49e90a91357fef12c54234b3cb553bb2fdd61f2af8c7e78b3d5ffdeac7022af0")
found = False
for magical_num in trange(p):
E = EllipticCurve(j=F(magical_num))
if E.is_supersingular() and found == False:
P = E(0).division_points(3)[1:]
for p in P:
try:
phi = E.isogeny(p)
E1 = phi.codomain()
H = md5(str(E1.j_invariant()).encode()).hexdigest().encode()
key, iv = H[:16], H[16:]
aes = AES.new(key, AES.MODE_CBC, iv)
flag = aes.decrypt(cipher)
if b'flag{' in flag:
print(unpad(flag, 16).decode())
found = True
break
except:
continue
elif found:
break
else:
continue
# 14%|███████████▌ | 62/431 [00:00<00:00, 1200.25it/s]
# flag{I_@m_4_n31gh80r_0f_my53lf}

RSA紧缩术(*)#

题目:

from Crypto.Util.number import *
from secret import flag
p = getPrime(1024)
q = getPrime(1024)
n = p * q
phi = (p - 1) * (q - 1)
e = getPrime(128)
d = pow(e, -1, phi)
leak = (1145 * d + 1419 * p + 19810 * q) >> 200
m = bytes_to_long(flag)
c = pow(m, e, n)
print(f'{n = }')
print(f'{e = }')
print(f'{c = }')
print(f'{leak = }')
'''
n = ...
e = ...
c = ...
leak = ...
'''

变式 RSA 的私钥d高位泄露的 CopperSmith 攻击。

主要还是考虑将已知d的高位泄露想办法转化为已知p的高位泄露。leak = (1145 * d + 1419 * p + 19810 * q) >> 200,我们可以记:

Lhigh=leak2200,L=Lhigh+LlowL_{high}=leak*2^{200},L=L_{high}+L_{low}

由已知

Lhigh=1145d+1419p+19810qLlowL_{high}=1145d+1419p+19810q-L_{low}

左右同乘e,移项可得

eLhigh1419ep19810eq+eLlow1145=1145(ed1)=1145(kφ(n))=1145kn1145k(p+q1)k=eLhigh1145n+1419ep19810eq+eLlow1145+1145k(p+q1)1145n\begin{aligned} &e\cdot L_{high}-1419ep-19810eq+e\cdot L_{low}-1145=1145(ed-1)\\ =&1145(k\cdot \varphi(n))=1145kn-1145k(p+q-1)\\ \Rightarrow&k=\frac{e\cdot L_{high}}{1145n}+\frac{-1419ep-19810eq+e\cdot L_{low}-1145+1145k(p+q-1)}{1145n} \end{aligned}

而第二项根据位数分析一下可知应该很小,在0 - 1之间,我们可以考虑k = (e * L_high) // (1145 * n)。从上述式子还能找出一个等式:

epLhigh1419ep219810en+epLlow1145p1145k(npppn+p)=0ep\cdot L_{high}-1419ep^2-19810en+ep\cdot L_{low}-1145p-1145k(np-p^p-n+p)=0

由于e * p * L_low很小,我们省略这一项然后在实数域对p求解可以得到p的高位(但是不是我们常见意义的高位,并非说是p >> 200 << 200这种高位,只是p减去了一个小值p_low而已),然后 CopperSmith 打出p的低位就能分解n了。

exp.py

# SageMath 10.7
from tqdm import trange
from Crypto.Util.number import long_to_bytes
n = ...
e = ...
c = ...
leak = ...
L_high = leak << 200
k = (e * L_high) // (1145 * n)
RF.<ph> = RealField(2000)[]
f1 = e * ph * L_high - 1419 * e * ph ** 2 - 19810 * e * n - 1145 * ph - 1145 * k * (n * ph - ph ** 2 - n + ph)
for res, _ in f1.roots():
p_high = int(res)
PR.<pl> = PolynomialRing(Zmod(n))
f2 = pl + p_high
p_low = f2.small_roots(X=2**200, beta=0.4, epsilon=0.02)
if p_low:
p = int(p_low[0] + p_high)
q = n // p
assert p * q == n
phi = (p - 1) * (q - 1)
d = inverse_mod(e, phi)
m = pow(c, d, n)
print(long_to_bytes(int(m)).decode())
break
# flag{c0pp3r5mith_Shrinkage_Technique}

Xaleid◆scopiX(*)#

题目:

from Crypto.Util.number import *
import random
import os
flag = open('/home/ctf/flag').read()
basis = random.randint(2,(1<<128)-2)
prime = getPrime(128)
def kaleidoscope(msg):
r = basis
for i in msg:
r = r * prime % (1 << 128)
r = r ^ i
return r
def main(i):
key = os.urandom(random.randint(64,128))
print('Hello! Your gift:', kaleidoscope(key))
s1 = input('Tell me your name: ').encode()
print('Hello,', kaleidoscope(s1), '! Let\'s sign in!')
s2 = input('Your account: ').encode()
print('Your account:', kaleidoscope(s2))
s3 = int(input('Your password: '))
if s3:
if s3 == kaleidoscope(s1 + key + s2):
print('Sign in successfully!')
s4 = input('Your operation: ')
if s4 == 'flag':
if s2 == b'admin':
print(flag)
else:
print('Permission deny')
elif s4 == 'logout' and i:
main(i-1)
else:
print('Unknown operation')
else:
print('Password incorrect!')
if __name__ == '__main__':
main(1)

也算是第一次了解 FNV 了,上一次看到 FNV 还是在 R3 上,根本看不懂那个hhh。

本题服务器会在我们每一次发送字符串后返回哈希值。而basisprime是固定的,因此我们可以想办法把basisprime求出来。题目有一个较大的泄漏点就是没有检查s1是否为空字符串,所以我们可以发送空字符串s1直接得到basis值,然后发送任意字节比如"1",就可以得到((basis * prime) % (2 ** 128)) ^ ord("1"),这样就可以求出prime的值,考虑到basis可能为偶数,题解使用了gcdext去逆basis求解prime,当basis为偶数时穷举可能的prime

然后求出这两个已知信息后,易知s1 + key的哈希值就是key的哈希值,而且key + s2的哈希值是在key的基础上以key的哈希值作为basis算出的s2的哈希值。因此只要我们有key的哈希值和prime就可以在不知道key的实际值的情况下伪造key + s2的哈希值。题目给了两轮的机会,可以在第一轮验证时求出basisprime的值然后logout,在第二轮伪造出key + "admin"的哈希值获取flag

exp.py

from pwn import *
from gmpy2 import gcdext, is_prime
def kaleidoscope(basis, prime, msg):
r = basis
for i in msg:
r = r * prime % (1 << 128)
r = r ^ i
return r
r = remote("ctf.a1natas.com", 24524)
r.recvuntil(b'Hello! Your gift: ')
key = int(r.recvline().strip().decode())
r.sendafter(b'Tell me your name: ', b'\n')
basis = int(r.recvline()[7:-18].decode())
r.sendafter(b'Your account: ', b'1\n')
r.recvuntil(b'Your account: ')
s2 = int(r.recvline().strip().decode())
g = gcdext(basis, 1 << 128)
p_r= ((s2 ^ ord("1")) * g[1] % (1 << 128)) // g[0]
for i in range(g[0]):
prime = p_r + i * (1 << 128) // g[0]
if is_prime(prime) and prime.bit_length() == 128:
pswd = (key * prime % (1 << 128)) ^ ord("1")
r.sendline(str(pswd).encode())
r.sendline(b'logout')
r.recvuntil(b'Hello! Your gift: ')
key = int(r.recvline().strip().decode())
r.sendafter(b'Tell me your name: ', b'\n')
r.sendafter(b'Your account: ', b'admin\n')
r.recvuntil(b'Your password: ')
r.sendline(str(kaleidoscope(key, prime, b'admin')).encode())
r.sendafter(b'Your operation: ', b'flag\n')
print(r.recvline().decode())
# flag{ea93b1b3-8a01-4145-9b4d-2fde98400fae}

ezECC(-)#

题目:

from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from secrets import flag
import random
import hashlib
class Niederreiter_Cryptosystem:
def __init__(self, n, k):
self.F = GF(2^8,repr='int')
alpha = self.F.list()[1:]
self.C = codes.GeneralizedReedSolomonCode(alpha, k)
self.t = (n - k) // 2
self.pub, self.priv = self.gen(n, k)
def get_recover_ability(self):
return self.t
def get_key(self):
return self.priv
def gen(self, n, k):
H = self.C.parity_check_matrix()
P = Permutations(n).random_element().to_matrix()
S = random_matrix(self.F, n-k, n-k)
while S.det() == 0:
S = random_matrix(self.F, n-k, n-k)
Pub = S * H * P
return Pub, (S, H, P)
def encrypt(self, msg):
e = vector(self.F, [self.F._cache.fetch_int(x) for x in msg])
return self.pub * e
n, k = 255, 223
encryptor = Niederreiter_Cryptosystem(n, k)
key = [random.getrandbits(8) for _ in range(encryptor.get_recover_ability())] + [0] * (n - encryptor.get_recover_ability())
random.shuffle(key)
cipher = encryptor.encrypt(key)
S, H, P = encryptor.get_key()
key = hashlib.sha256(str(key).encode()).digest()
aes = AES.new(key, AES.MODE_ECB)
c = aes.encrypt(pad(flag, 16))
with open('output.txt', 'w') as file:
file.write('encrypted key: ' + str(cipher) + '\n')
file.write('S: ' + str(S.list()) + '\n')
file.write('P: ' + str(P.list()) + '\n')
file.write('encrypted flag: ' + str(c))

初看这题应该就要单纯的根据这个私钥解出原来的密文,题目也没有藏着掖着很直接的就给出了Niederreiter密码系统

复现好像有点问题一直不对hhh,先鸽了。

linear function?(*)#

题目:

import random
from Crypto.Util.number import *
P = ...
def pairing(a, b):
return (a * b) % P
def modexp(base, exp):
return pow(base, exp, P)
def fake_hash(msg: bytes) -> int:
return (bytes_to_long(msg)+q*114514114514)% P
q = 1# ?? #small number
g1 = pow(random.randint(2, P-1), (P-1)//q, P)
g2 = pow(random.randint(2, P-1), (P-1)//q, P)
x = random.randint(2, q-1)
y = random.randint(2, q-1)
pk1 = modexp(g1, x)
pk2 = modexp(g2, y)
flag = b""
m = fake_hash(flag)
c1 = modexp(m, y)
c2 = pow(pairing(m, g2), x, P)
print("g1 =", g1)
print("g2 =", g2)
print("pk1 =", pk1)
print("pk2 =", pk2)
print("c1 =", c1)
print("c2 =", c2)
'''
g1 = ...
g2 = ...
pk1 = ...
pk2 = ...
c1 = ...
c2 = ...
'''

考点是双线性配对,这是一类满足 e(ga,hb)=e(g,h)abe(g^a,h^b)=e(g,h)^{ab} 的映射,它允许将两个群元素中的指数线性地组合。本题中的pairing(a, b) = ab mod p是一个不安全的伪双线性配对,满足双线性性质:(ab)x=axbx(a\cdot b)^x=a^x\cdot b^x,但是暴露了指数结构,使得密文中的指数可以被代数拆解从而恢复明文。

题目给出了密文:

c1my(modP),c2(mg2)x(modP)c_1\equiv m^y\pmod{P},c_2\equiv (m\cdot g_2)^x\pmod{P}

其中m = fake_hash(flag) = (bytes_to_long(flag) + q * 114514114514) % P

由于pairing是线性的:

c2(mg2)xmxg2x(modP)c_2\equiv (mg_2)^x\equiv m^x\cdot g_2^x\pmod{P}

如果我们能得到x,就可以直接消去g2项:

mxc2/g2x(modP)m^x\equiv c_2/g_2^x\pmod{P}

而且计算阶发现g1g2的阶足够小且较为平滑(且很巧ord(g1) = 2 * ord(g2)),可以直接DLP打出xy,这样就能恢复出:

myc1(modP),mxc2/g2x(modP)m^y\equiv c_1 \pmod{P},m^x\equiv c_2/g_2^x\pmod{P}

然后通过计算发现d = gcd(x, y) = 3,我们可以通过扩展欧几里得找到uv使

ux+vy=dux+vy=d

这样我们便可以找到m ** 3,然后在模P下开三次方找到三个根,分别逆fake_hash筛出flag

exp.py

# SageMath 10.7
from Crypto.Util.number import long_to_bytes
P = ...
g1 = ...
g2 = ...
pk1 = ...
pk2 = ...
c1 = ...
c2 = ...
ZP = Zmod(P)
g1, g2, pk1, pk2, c1, c2 = ZP(g1), ZP(g2), ZP(pk1), ZP(pk2), ZP(c1), ZP(c2)
ord_g1 = g1.multiplicative_order()
ord_g2 = g2.multiplicative_order()
# ord(g1) == 2 * ord(g2)
q_sub = Integer(ord_g2)
x = discrete_log(pk1 % P, g1 % P, ord=q_sub*2)
y = discrete_log(pk2 % P, g2 % P, ord=q_sub)
QMAX = max(x, y) * 4
mx = c2 / (g2 ** x)
my = c1
d, u, v = xgcd(x, y)
md = (mx ** u) * (my ** v) # md = m^d
F = GF(P)
mdF = F(int(md))
roots = list(mdF.nth_root(3, all=True))
candidates_m = [ZP(int(r)) for r in roots]
for ridx, m in enumerate(candidates_m):
m_int = Integer(m)
for q_try in range(2, Q_MAX + 1):
val = (m_int - q_try * C) % P
msg = int_to_bytes(val)
if msg.startswith(b"flag{") and msg.endswith(b"}"):
print(msg.decode())
raise SystemExit
# flag{pair1n9_13_s000_fun}

官方的 WriteUp 就更暴力一些了,直接通过爆破去找xy,然后在模P下恢复到m的gcd(y, P - 1) = 3次方,再进行开方计算flag = long_to_bytes((r - q * 114514114514) % P)筛出真正的flag

exp.py

from sys import exit
from gmpy2 import gcd, invert
from Crypto.Util.number import long_to_bytes
from sympy.ntheory.residue_ntheory import nthroot_mod
def modexp(base, exp):
return pow(base, exp, P)
def force(base, value, p, q):
cur = 1
for e in range(q):
if cur == value:
return e
cur = (cur * base) % p
return None
P = ...
g2 = ...
pk2 = ...
c1 = ...
y = None
for q in range(100, 150):
rec_y = force(g2, pk2, P, q)
y = rec_y
if y != None:
M = (P - 1) // gcd(y, P - 1)
e = invert(y // gcd(y, P - 1), M)
m3 = pow(c1, e, P)
roots = nthroot_mod(m3, 3, P, all_roots=True)
for r in roots:
flag = long_to_bytes((r - q * 114514114514) % P)
if flag.startswith(b"flag{") and flag.endswith(b"}"):
print(flag.decode())
exit(1)
# flag{pair1n9_13_s000_fun}

异域症(*)#

题目:

from Crypto.Util.number import *
import os
from secret import flag
import hashlib
def mul(a, b, n):
return ((a[0]*b[0]+2*a[1]*b[2]+2*a[2]*b[1]) % n, (a[0]*b[1]+a[1]*b[0]+2*a[2]*b[2]) % n, (a[0]*b[2]+a[2]*b[0]+a[1]*b[1]) % n)
def powmod(a, b, n):
res = (1, 0, 0)
while b > 0:
if b & 1 == 1:
res = mul(res, a, n)
a = mul(a, a, n)
b >>= 1
return res
p = getPrime(512)
q = getPrime(512)
N = p * q
e = 13
mask = [os.urandom(64) for i in range(3)]
m = tuple(bytes_to_long(i) for i in mask)
hint = mask[2]
c = powmod(m, e, N)
xor = lambda a,b:[i^j for i,j in zip(a,b)]
cipher = bytes(xor(hashlib.sha512(b''.join(mask)).digest(),flag))
print(f'{N = }')
print(f'{e = }')
print(f'{c = }')
print(f'{hint = }')
print(f'{cipher = }')
'''
N = ...
e = 13
c = (..., ..., ...)
hint = b'...'
cipher = b'...'
'''

前两天 ISCTF 上碰到的下架的题目也是结式题,在这再仔细学习学习(不过这题好难,是环上的结式并非是域上的)。

首先本题得了解一下商环,在此暂时不展开说明。了解完仔细看看这个mul函数就会发现这是一个商环 R=FN[x]/(x32)R=\mathbb{F}_N[x]/(x^3-2) 上的乘法。而mR中的一个随机元素并且泄露了其a2分量,根据mc的各分量相等我们可以给出三个方程,未知数为a0a1

为了进一步简化问题,可以利用结式(Resultant)来消去一个未知数。结式是通过消去多项式中的一个变量,得到另一个关于剩余变量的方程。在本题中,环上无法使用resultant函数,可以利用 Sylvester 矩阵 来计算结式,并通过计算行列式来判断多项式是否有公共根。具体来说,通过计算多项式的 Sylvester 矩阵的行列式来得到结式,从而消去未知数,并通过得到的方程求解出 (a_0) 和 (a_1)。再结合hint就可以计算出m了。

exp.py

# SageMath 10.7
from Crypto.Util.number import *
from hashlib import sha512
def poly_gcd(f, g):
if f.degree() < g.degree():
f, g = g, f
while not g.is_zero():
f, g = g, f % g
return f
xor = lambda a, b: [i ^^ j for i, j in zip(a, b)]
N = ...
e = 13
c = (..., ..., ...)
hint = b'...'
cipher = b'...'
h = bytes_to_long(hint)
R.<s, t, x> = Zmod(N)[]
C = c[0] + c[1] * x + c[2] * x ** 2
m = s + t * x + h * x ** 2
c1 = m ** e - C
# 这里 c2 = c1 % (x ** 3 - 2) 有点小问题,还没弄明白,手搓了一个
c2 = c1
while c2.degree(x) >= 3:
for i in range(3, c2.degree(x) + 1):
c2 += 2 * c2.coefficient({x:i}) * (x ** (i - 3))
c2 -= c2.coefficient({x:i}) * (x ** i)
cc1 = c2.coefficient({x:0})
cc2 = c2.coefficient({x:1})
cc3 = c2.coefficient({x:2})
ccc1 = cc1.sylvester_matrix(cc2, R.gen(1)).det()
ccc2 = cc1.sylvester_matrix(cc3, R.gen(1)).det()
F2.<q> = Zmod(N)[]
ccc1 = ccc1(q, 0, 0)
ccc2 = ccc2(q, 0, 0)
mask1 = N - poly_gcd(ccc1, ccc2).monic()[0]
ccc3 = cc1(mask1, q, 0)
ccc4 = cc2(mask1, q, 0)
mask2 = N - poly_gcd(ccc3, ccc4).monic()[0]
flag = bytes(xor(sha512(long_to_bytes(int(mask1)) + long_to_bytes(int(mask2)) + hint).digest(), cipher))
print(flag.decode())
# flag{D0N'T_B3_Depr3s5i0n_bE_HOpefu1_4nd_HappY}
QCTF 2025 WriteUp
https://q1uju.cc/posts/qctf-2025---crypto---writeup/
Author
Q1uJu
Published at
2025-12-27
License
CC BY-NC-SA 4.0

Some information may be outdated