BKSEC TTV 2026 - các thử thách Cryptography

Trong sự kiện tuyển thành viên gen 10 của CLB BKSEC năm 2026 , mình tập trung vào các challenge cryptography và có giải được 3 chall từ easy đến medium . Sau đó it ngày thì mình cũng đã giải được chall thứ 4 là LFSR . Đến đợt vừa rồi khi có tìm hiểu thêm về bài toán RLWE thì mình mới tìm lại chal thứ 5 để giải nốt và quyết định tổng hợp các write up chi tiết vào bài viết này .
Có thể xem chi tiết về challenge và write up trong github của mình https://github.com/ltnanh/ctf_crypto/tree/main/bksec
Hidden Structure
- Category: Crypto
- Difficulty : Easy
Chal Overview
Đây là hệ mật mã hóa RSA với quá trình generate key khác thường
from secrets import randbits
from Crypto.Util.number import *
flag = bytes_to_long(b"BKSEC{fake_flag}") # you lose aura if you submit this... (we are watching)
B = 1 << 256
def r256():
x = randbits(256) | (1 << 255) | 1
return x
while True:
x, y = r256(), r256()
p = x * B + y
q = y * B + x
if isPrime(p) and isPrime(q):
break
e = 65537
N = p * q
c = pow(flag, e, N)
print(f"{N = }")
print(f"{e = }")
print(f"{c = }")
Nhiệm vụ của ta là decrypt ra flag từ ciphertex được cho
Attack Idea
Chắc chắn ta sẽ khai thác bất thường trong việc chọn các tham số của hệ mật:
Khi viết dưới dạng nhị phân, và sẽ có tính chất đối xứng: và . Điều này dẫn đến sẽ có cấu trúc bit đặc biệt. Ta có:
Với các số được viết dưới dạng nhị phân:
- (có 512 bit 0 ở cuối).
- (khoảng 512 đến 513 bit).
- (có 256 bit 0 ở cuối).
Ta thấy rằng khi tổng 3 giá trị lại, 256 bit cuối của chính là 256 bit cuối của . 256 bit đầu của gần như tương ứng với 256 bit đầu của . Từ đó ta có thể lấy được giá trị của , từ đó khôi phục giá trị của và cuối cùng là
Đầu tiên ta cần khôi phục . Lấy 256 bit cuối của chính là 256 bit cuối của . 256 bit đầu của gần bằng 256 bit đầu của vì có thể tạo ra carry sang phần high bits. Ta có thể brute-force một khoảng nhỏ quanh giá trị ước lượng.
Sau khi có các giá trị khả thi của (gọi là ), ta có .Từ đó tính tổng :
Nếu là một số chính phương , giá trị là hợp lệ.
Cuối cùng sau khi biết và , ta giải pt bậc 2 để tìm ,
Code Exploit
from math import isqrt
from Crypto.Util.number import long_to_bytes
def recover_xy(S, P):
D = S*S - 4*P
if D < 0:
return None
r = isqrt(D)
if r*r != D:
return None
x = (S + r) // 2
y = (S - r) // 2
return x, y
N = 107444638859099155759777187090231262569833054720517547475456101236477286061642951211232522980016935214117151901316067288134076019258357378153815894696105422994651894516549086845637403949534145390050746353299397783409065996987527927026314423191436300386280922075865669873754956335784354470237039709346406222789
e = 65537
c = 56143710973900761339774002971192037727375234874650436422245945684342980716505499471085786012340621409106405319356586450688993792522513823502401085339774194961300841598824881056390845294225756335102709315028822707701848497421372062559501439381841852116763699839771135406441292657937478625029095325072727620068
B = 1 << 256
bin_N = bin(N)[2:].zfill(1024)
# lấy low 256 bits của xy
low_xy = int(bin_N[-256:], 2)
# brute bits
for shift in range(-8, 9):
approx_high = int(bin_N[:256], 2) + shift
for mid in [0,1]:
xy = (approx_high << 256) | (mid << 255) | low_xy
rem = N - xy*(B*B + 1)
if rem <= 0:
continue
if rem % B != 0:
continue
x2_y2 = rem // B
S2 = x2_y2 + 2*xy
if S2 < 0:
continue
S = isqrt(S2)
if S*S != S2:
continue
res = recover_xy(S, xy)
if res is None:
continue
x, y = res
if x*y != xy:
continue
p = x*B + y
q = y*B + x
if p*q != N:
continue
phi = (p-1)*(q-1)
d = pow(e, -1, phi)
m = pow(c, d, N)
print("FLAG:", long_to_bytes(m))
exit()
print("not found")
FLAG: b'BKSEC{p4l1n_p0ly_1d3n717y_m47h}'
ZKRSA
- Category: Crypto
- Difficulty : Easy - Medium
Chal Overview
from Crypto.Util.number import getPrime, getRandomRange, bytes_to_long
from random import Random
A = 2**1000
B = 2**80
FLAG = "BKSEC{Blahaj}"
def keygen():
p = getPrime(512)
q = getPrime(512)
N = p * q
PHI = (p - 1) * (q - 1)
return N, PHI
def prove(z, N, PHI):
r = getRandomRange(0, A)
x = pow(z, r, N)
e = int(input("CHALLENGE ME!!! "))
if e >= B:
raise ValueError("I refuse to prove that D:<!!!")
y = r + (N - PHI) * e
print(x, e, y)
return (x, e, y)
def verify(z, x, e, y, N):
return 0 <= y < A and pow(z, y - N * e, N) == x
print("I can prove to you that I can factorize N without revealing any knowledge :D")
print("The protocol is so secured in fact, I will even give you the FLAG!!!")
N, PHI = keygen()
print(N)
r = Random(1337)
for _ in range(500):
try:
z = r.randrange(2, N)
x, e, y = prove(z, N, PHI)
assert verify(z, x, e, y, N)
except Exception as E:
print(f"ERR: {E}")
print("Here's your FLAG hehehe")
print(pow(bytes_to_long(FLAG), 65537, N))
Đây là 1 hệ ZKP dựa trên RSA. Server proof nó biết cách phân tích nhân tử của mô-đun RSA mà không trực tiếp để lộ các số nguyên tố hay giá trị .
Challenge :Có 500 lượt, mỗi lượt client có thể gửi 1 giá trị tới server và nhận lại giá trị :
Với là 2 số random tương ứng với mỗi lượt . Ta cần phải dựa vào sample này để mà factor và decrypt flag bị mã hóa bỏi RSA
Attack Idea
Ở mỗi vòng lặp, server trả về :
Đặt .
Ta thấy rằng, server chỉ cần giá trị ta có thể gửi 1 số âm có giá trị tuyệt đối rất lớn. Ví dụ: gửi ta sẽ nhận lại là 1 giá trị âm lớn:
Vì thường rất nhỏ so với . Vì vậy, ta có thể khôi phục chính xác 100% bằng cách chia làm tròn lên:
Từ ý tưởng đó , ta có cách tấn công như sau :
-
Ở lượt đầu tiên, ta gửi giá trị , từ đó có thể lấy được giá trị của :
-
Hàm verify ở serve sẽ ném ra lỗi, nhưng giá trị vẫn được gửi về và vòng lặp vẫn tiếp tục
-
Ở các lượt sau, ta gửi các giá trị tùy ý cho đến khi lấy được ciphertext
-
Sau khi có được từ vòng lặp đầu tiên , ta đã có . Khi này và là 2 nghiệm của phương trình bậc 2: . Ta chỉ cần dùng công thức nghiệm giải pt này để tìm ra và
-
Có , ta dễ dàng tính và khóa giải mã để decrypto flag .
Code exploit
from pwn import *
from Crypto.Util.number import long_to_bytes
import math
context.log_level = 'error'
HOST = '127.0.0.1'
PORT = 1337
A = 2**1000
r = remote(HOST, PORT)
r.recvline()
r.recvline()
N = int(r.recvline().strip())
print(f"N ={N}")
malicious_y = 0
enc_flag = 0
try:
for i in range(500):
r.recvuntil(b"CHALLENGE ME!!! ")
if i == 0:
e_challenge = -A
else:
e_challenge = 1
r.sendline(str(e_challenge).encode())
response = r.recvline().decode().strip().split()
x_val, e_val, y_val = map(int, response)
if i == 0:
malicious_y = y_val
r.recvuntil(b"Here's your FLAG hehehe\n")
flag_data = r.recvall().decode().strip()
if flag_data:
enc_flag = int(flag_data)
print(enc_flag)
else:
print("Không nhận được FLAG")
except Exception as e:
print(f"Server đóng kết nối hoặc lỗi: {e}")
finally:
r.close()
S = (-malicious_y + A - 1) // A
print(S)
p_plus_q = S + 1
delta = p_plus_q**2 - 4 * N
sqrt_delta = math.isqrt(delta)
p = (p_plus_q + sqrt_delta) // 2
q = (p_plus_q - sqrt_delta) // 2
print(f"p = {p}")
print(f"q = {q}")
if p * q == N:
PHI = (p - 1) * (q - 1)
e_rsa = 65537
d = pow(e_rsa, -1, PHI)
m = pow(enc_flag, d, N)
print(long_to_bytes(m).decode())
Đây là kết quả thành công trên local server
N=167629805948328480856136662463024641078544529691492272664474940169464728000546260009523173466811359968916536687879188204759866327925891179582731061156232738902731111791596842791987178269624500992452823641069225953237087383044968734888184746389875147369211197267744646914195310550864372010990131043741023412271
146783221816191892832299181433716702896421731754997132955358938245262339518821028341445414172362993797673815785106931873973143584407734686950165723251703969194223859104640716116796457521713336617903799526816984305863361249467510375811532386148051196710157885384565862626449466229103636438730600643208795535543
25906298710408090870795323312593156135881716487327475646693436001925545300645964041921123146237490299054641066748402079570334742717567596771513191072756879
p = 13345924443383651307825783053348900030561990589036994089184077754058942705750224512019827345001244357228276340291843668986253908689641003932364724809374513
q = 12560374267024439562969540259244256105319725898290481557509358247866602594895739529901295801236245941826364726456558410584080834027926592839148466263382367
BKSEC{fake_flag_for_testing}
ZKRSA Revenger
- Category: Crypto
- Difficulty : Medium
Chal Overview
from Crypto.Util.number import getPrime, getRandomRange, bytes_to_long, long_to_bytes
from random import Random
import os
import hashlib
A = 2**1000
B = 2**80
FLAG = b"BKSEC{Evil_Blahaj}"
def keygen():
p = getPrime(512)
q = getPrime(512)
N = p * q
PHI = (p - 1) * (q - 1)
return N, PHI
def prove(z, N, PHI):
r = getRandomRange(0, A)
x = pow(z, r, N)
e = int(input("CHALLENGE ME!!! "))
if e >= B:
raise ValueError("I refuse to prove that D:<!!!")
y = r + (N - PHI) * e
check = int(hashlib.sha256(str(y).encode()).hexdigest(), 16) % B
return (x, e, y, check)
def verify(z, x, e, y, N):
return 0 <= y < A and pow(z, y - N * e, N) == x
print("Your mathematics shenanigans (should) not work this time... My Blahaj hath fixedyour antics!!!")
print("No more knowledge. No more negativity. Truly (maybe) secured")
N, PHI = keygen()
print(N)
r = Random(int.from_bytes(os.urandom(8), 'big'))
for _ in range(500):
try:
z = r.randrange(2, N)
x, e, y, check = prove(z, N, PHI)
print(check) # This should be enough, right...?
except Exception as E:
print(f"ERR: {E}")
print("Here's your FLAG hehehe")
print(pow(bytes_to_long(FLAG), 65537, N))
Đây là phiên bản nâng cấp của challenge ZKRSA trước. Công thức tạo proof vẫn là , nhưng server không in trực tiếp nữa mà chỉ in một giá trị check tính từ hash của . Seed để sinh cũng được lấy từ os.urandom thay vì cố định. Vì vậy, cách gửi rồi chia ngược từ ở bài trước không còn dùng được.
Challenge: Ta vẫn có 500 lượt gửi giá trị tới server. Với mỗi lượt, server tính:
Trong đó được sinh mới ở mỗi lượt. Ta chỉ nhận được check hoặc thông báo lỗi; sau 500 lượt server mới gửi ciphertext của flag. Mục tiêu vẫn là tìm để giải mã RSA.
Attack Idea
Đặt , khi đó .
Việc sửa các bug của chal cũ khiến cho việc sử dụng toán học để khai thác trở nên bế tắc . Tuy nhiên , 1 ý tưởng rất hay hiện ra khi tôi để ý việc hash để tạo ra check. Server phải gọi str(y) trước khi băm nó , và 1 giới hạn của hàm str(y) trong Python chỉ ra bug của bài này . Với Python 3.11+ ở cấu hình mặc định, phép chuyển số nguyên sang chuỗi chỉ cho phép tối đa 4300 chữ số thập phân. Vì vậy str(y) ném ValueError nếu . Lỗi này nằm trong khối try...except, nên ta có một oracle từ phản hồi của server:
- Nếu , server in ra
check. - Nếu , server in ra
ERR.
Ta có thể dùng oracle này để so sánh với một giá trị đoán . Đặt và gửi:
Khi đó . Ta dùng để trường hợp luôn nằm dưới ngưỡng lỗi. Vì , còn lớn hơn rất nhiều, không làm thay đổi kết quả so sánh:
- Nếu , ta có , server in
check. - Nếu , ta có , server in
ERR.
Từ ý tưởng đó, ta dùng 500 lượt để binary search :
- Ban đầu, vì ; chọn vì đều là số nguyên tố 512 bit.
- Mỗi lượt lấy , gửi .
- Nếu nhận
ERR, đặt ; nếu nhậncheck, đặt .
Sau 500 lượt, khoảng cần tìm chỉ còn cỡ giá trị. Ta có thể brute từng giá trị còn lại để tìm
Code exploit
from pwn import *
from Crypto.Util.number import long_to_bytes
import math
context.log_level = 'error'
HOST = '127.0.0.1'
PORT = 1337
r = remote(HOST, PORT)
r.recvline()
r.recvline()
N = int(r.recvline().strip())
print(f"N: {N}")
low = math.isqrt(N) * 2
high = 2**513
try:
for i in range(500):
r.recvuntil(b"CHALLENGE ME!!! ")
mid = (low + high) // 2
e_val = -((10**4300 - 1) // mid)
r.sendline(str(e_val).encode())
response = r.recvline().decode().strip()
if "ERR" in response:
low = mid + 1
else:
high = mid
r.recvuntil(b"Here's your FLAG hehehe\n")
enc_flag = int(r.recvall().decode().strip())
print("Enc flag: ",enc_flag)
except Exception as E:
print(E)
finally:
r.close()
p = 0
q = 0
for S_candidate in range(low, high + 1):
p_plus_q = S_candidate + 1
delta = p_plus_q**2 - 4 * N
if delta < 0:
continue
sqrt_delta = math.isqrt(delta)
if sqrt_delta**2 == delta:
p = (p_plus_q + sqrt_delta) // 2
q = (p_plus_q - sqrt_delta) // 2
if p * q == N:
break
if p and q:
PHI = (p - 1) * (q - 1)
e_rsa = 65537
d = pow(e_rsa, -1, PHI)
m = pow(enc_flag, d, N)
print(long_to_bytes(m).decode())
Đây là kết quả thành công trên local server:
N:145969836915009028574884415219575953536366858889859356205542800445012138778425447433430313614471191573042943247039847180900962456896321305446838629594099890867237667420048577051350014585983463460901655558009198513004857165769770261027305485378749318791174847807561607256956580207610294822304775420574328597859
Enc flag: 112464274008268880605434356398753809883465308334599653876167892486876139832193670508490084518595728094457599189032901987219007080818043707274744430752113529399094996908964109352705072602691654039862086507316771400071154180553222046073605971076713364775178406587509920671003448338664291790775272746012258247888
BKSEC{Evil_Blahaj}
LFSR
Category : Crypto
Challenge overview
Source code
import secrets
import threading
import sys
import os
def kill_switch():
print("Time expired!")
sys.exit()
SECRET_FLAG = os.environ.get("FLAG", "BKSEC{REDEACT}").encode()
Ns = [14, 32, 24, 48, 8, 8, 8, 8, 10]
MASKS = [1959, 3487505359, 12175963, 144894747199363, 39, 101, 99, 43, 579]
Filters = [
43673535323473607050899647551732188151,
69474900172976843852504521249820447513188207961992185137442753975916133181030,
28448620439946980695145546319125628439828158154718599921182092785732019632576,
16097126481514198260930631821805544393127389525416543962503447728744965087216,
7283664602255916497455724627182983825601943018950061893835110648753003906240,
55629484047984633706625341811769132818865100775829362141410613259552042519543,
4239659866847353140850509664106411172999885587987448627237497059999417835603,
106379335904610565198575784689340408012917012758379923896044369424798179675586
]
extract = lambda x,b: sum(((x >> p) & 1) << i for i, p in enumerate(b))
blur = lambda x,i: (Filters[i] >> x) & 1
class LFSR:
def __init__(self, n, key, mask):
self.n = n
self.state = key & ((1 << n) - 1)
self.mask = mask
def __call__(self):
b = self.state & 1
self.state = (self.state >> 1) | (
((self.state & self.mask).bit_count() & 1) << (self.n - 1)
)
return b
class Cipher:
def __init__(self, key):
self.lfsrs = []
for i in range(len(Ns)):
self.lfsrs.append(LFSR(Ns[i], key, MASKS[i]))
key >>= Ns[i]
def bit(self):
x = blur(extract(self.lfsrs[0].state, [5, 9, 1, 0, 4, 11, 13]), 0)
y = self.lfsrs[1].state & 1
z = blur(extract(self.lfsrs[2].state, [20, 2, 16, 11, 1, 23, 22, 8]), 1)
w = blur(extract(self.lfsrs[3].state, [1, 46, 21, 7, 43, 0, 27, 39]), 2)
v = blur(extract(self.lfsrs[4].state, [1, 3, 7, 4, 5, 0, 6, 2]), 3)
u = blur(self.lfsrs[5].state, 4) ^ blur(self.lfsrs[6].state, 5) ^ blur(self.lfsrs[7].state, 6)
t = blur(extract(self.lfsrs[8].state, [5, 8, 9, 3, 1, 0, 2, 4]), 7)
for lfsr in self.lfsrs: lfsr()
return x ^ y ^ z ^ w ^ v ^ u ^ t
def stream(self):
while True:
b = 0
for i in reversed(range(8)):
b |= self.bit() << i
yield b
def encrypt(self, pt: bytes):
return bytes([x ^ y for x, y in zip(pt, self.stream())])
key = secrets.randbits(160)
cipher = Cipher(key)
tk = secrets.token_bytes(15)
print("Token:", tk.hex())
time_guard = threading.Timer(60, kill_switch)
time_guard.start()
print("ct:", cipher.encrypt(b"\x00"*80+tk).hex())
if input("Gimme Token: ") == tk.hex():
print("Here is your flag:", SECRET_FLAG)
Thử thách cho 1 hệ mã hóa Cipher kết hợp 9 bộ LFSR với độ dài khác nhau với
Mỗi lfsr (class LFSR) gồm :
- state: Trạng thái hiện tại của thanh ghi.
- mask: Xác định các vị trí (taps) để thực hiện phép XOR nhằm tạo ra bit feedback.
- chu kì của các bộ lfsr này sẽ tạo bit feed back như truyền thống
Main cipher (class Cipher) gộp các lfst tạo keystream phi tuyến tính (hàm bit())
-
extract(x, b) : Trích xuất các bit tại các vị trí cụ thể b từ state x để tạo thành một số nguyên mới.
-
blur(x, i) : Sử dụng số nguyên vừa trích xuất làm chỉ số (index) để lấy một bit từ danh sách Filters.
-
với mỗi bộ lfsr tại 1 clock với state tương ứng sẽ thông qua các các hàm này để trích xúât ra một bit tương ứng để tham giá xor tạo feedforward bit ==> Keystream có tính phi tuyến
Main challenge thì server sẽ encrypt 1 token và gửi ta ciphertext , nhiệm vụ của ta là decrypt ra token và gửi về cho server để lấy flag
Quan sát challenge & hệ mã
Known plaintext attack: server chèn 80 bytes 00 đầu vào token trước khi giải mã. Với mật mã dòng (plaintext xor keystream) , 80 bytes đầu của ciphertext chính là của keystream
=> Ta có thể lập 1 hệ phương trình 640 ẩn. Nếu ta có thể biểu diễn được feed forward của lfsr bằng 1 quan hệ tuyến tính giữa các biến bit => ta có thể giải hệ phương trình tuyến tính bằng Gauss
Quan sát chu kì của các bộ lfsr
Có state của các bộ lfsr , tôi sẽ tính đa thức đặc trưng và chu kì của chúng
import time
Ns = [14, 32, 24, 48, 8, 8, 8, 8, 10]
MASKS = [1959, 3487505359, 12175963, 144894747199363, 39, 101, 99, 43, 579]
def simulate_period(n, mask, timeout=10):
start_state = 1
state = start_state
period = 0
start_time = time.time()
while True:
state = (state >> 1) | (((state & mask).bit_count() & 1) << (n - 1))
period += 1
if state == start_state:
return period
# Chỉ check thời gian mỗi 100.000 vòng lặp
if period % 100000 == 0:
if time.time() - start_time > timeout:
return f"Timeout, Đã chạy được {period} bước"
for i, (n, mask) in enumerate(zip(Ns, MASKS)):
print(f"LFSR {i} (N={n:<2}) ", end=" ", flush=True)
actual_period = simulate_period(n, mask, 10)
print(f" => Chu kỳ: {actual_period}")
LFSR 0 (N=14) => Chu kỳ: 16383
LFSR 1 (N=32) => Chu kỳ: Timeout, Đã chạy được 34800000 bước
LFSR 2 (N=24) => Chu kỳ: 16646017
LFSR 3 (N=48) => Chu kỳ: 63
LFSR 4 (N=8 ) => Chu kỳ: 63
LFSR 5 (N=8 ) => Chu kỳ: 255
LFSR 6 (N=8 ) => Chu kỳ: 255
LFSR 7 (N=8 ) => Chu kỳ: 255
LFSR 8 (N=10) => Chu kỳ: 1023
Ta thấy lfsr3 và lfsr4 đều có chu kì là 63 => bit w , bit u có chu kì là 63 => bit w⊕v có chu kì là 63 => các bit w⊕v tạo thành chu kì f0, f1, … , f63 => Tạo thành 63 biến trong hệ phương trình 640 ẩn vì tại mỗi clock thì 1 trong các biến này sẽ xor tạo key bit
Tương tự với lfsr5 ,6,7 thì bit u có chu kì là 255 , tạo thành 255 biến trong hệ
Biểu diễn đại số cho feedforward của các lfsr
Các con số khổng lồ trong mảng Filters trông có vẻ ngẫu nhiên. nhưng thực chất chúng chính là Bảng chân trị (Truth Table) .Khi hàm extract ghép 8 bit lại thành số nguyên X => (Filters[i] >> X) & 1 chính là hành động tra cứu bit thứ X . Biến đổi Möbius (Fast Algebraic Transform) giúp chuyển bảng chân trị sang đa thức đại số
def generate_anf(lfsr_idx, filter_val, extract_positions):
k = len(extract_positions)
tt = [(filter_val >> i) & 1 for i in range(1 << k)]
anf = list(tt)
for i in range(k):
for j in range(1 << k):
if (j & (1 << i)) != 0:
anf[j] ^= anf[j ^ (1 << i)]
terms = []
if anf[0] == 1:
terms.append("1")
degree = 0
for j in range(1, 1 << k):
if anf[j] == 1:
term_vars = []
current_degree = bin(j).count('1')
degree = max(degree, current_degree)
for bit_idx in range(k):
if (j >> bit_idx) & 1:
state_pos = extract_positions[bit_idx]
term_vars.append(f"s_{state_pos}")
terms.append(" * ".join(term_vars))
if not terms:
equation = "0"
else:
equation = " + ".join(terms)
print(f"[*] LFSR {lfsr_idx}")
print(f"Degree: {degree}")
print(f"f(S) = {equation}\n")
chạy code sẽ ra kết quả với lfsr 2 là hoàn toàn tuyến tính
[*] LFSR 2
Degree: 1
f(S) = s_20 + s_2 + s_1 + s_23 + s_8
Feedforward của lfsr 2 có thể biểu diễn tuyến tính , ta sẽ thêm 24 ẩn bit của lfsr 2 vào hệ phương trình
Ngoài ra lfsr 1 implement theo cách chuẩn của lfsr tức là cũng tuyến tính . Ta sẽ thêm 32 ẩn bit vào hệ phương trình
Hệ đã xây được
Ta có 1 hệ gồm 640 phương trình với 374 ẩn số bao gồm:
- 32 ẩn của hệ LFSR 1 ()
- 24 ẩn của hệ LFSR 2 ()
- 63 ẩn của hệ LFSR 3 và 4 ()
- 255 ẩn của hệ LFSR 5, 6, 7 ()
Với mỗi clock i, hệ phương trình sẽ có dạng : . Ta chỉ cần mô phỏng lại cipher để xác định hệ số của các ẩn tại từng clock
Xử lí phi tuyến
Điều khó khăn ở đây là ta hoàn toàn ko biết RHS của hệ vì còn vướng Lfsr 0 và 8, biểu thức của 2 hệ này phi tuyến và nhiều ẩn khiến cho việc đưa vào hệ gần như ko thể . Tôi nghĩ đến việc brute 2 initial state của LFSR 0 và LFSR 8 còn lại, gen stream bit của 2 bộ đó qua 640 clock và XOR với keystream sinh ra stream . Case brute đúng thì hệ sẽ có nghiệm , tuy nhiên ta phải đối mặt với bài toán về thời gian . LFSR 0 có 14 bit state, LFSR 8 có 10 bit state => để đoán 24 bit sẽ có 16 triệu trường hợp, 16 triệu lần giải hệ . Implement của tôi cho thấy cần vài tiếng để hoàn thành việc này .
Kỹ thuật Meet-in-the-Middle
Vậy nên để tối ưu hóa, ta sử dụng kỹ thuật Meet-in-the-Middle: Với là keystream (640), là ma trận của hệ (640×374), là vector nghiệm (374), là stream của LFSR 0, là stream của LFSR 8, ta có:
-
Ta tính là left kernel của : ()
-
Nhân 2 vế pt trên với :
Từ đẳng thức trên , ta có thể :
-
Tạo 1 hash map, duyệt qua case của LFSR 0 và lưu vào bảng hash các giá trị .
-
Sau đó brute qua case của LFSR 8, với mỗi case tính và kiểm tra xem có giá trị đó trong hash table không. Nếu có thì đó chính là 2 giá trị và cần tìm.
-
Việc dùng bảng hash mỗi lần tìm kiếm chỉ tốn O(1), nhanh hơn rất nhiều so với việc brute cả 24 bit cùng lúc.
Decryption
Khi đã có initial state của LFSR 0 và LFSR 8, ta chỉ cần gen ra RHS của hệ phương trình và giải hệ đó .Với bộ nghiệm có được:
- Với bit u và w⊕v theo chu kì, ta chỉ cần gen tiếp các bit đó
- Với LFSR 1 và 2, các nghiệm tương ứng chính là initial state của 2 hệ đó, chỉ cần gen tiếp stream của 2 hệ này.
Ta đã có thể gen toàn bộ keystream của hệ 9 LFSR và decrypt được phần token.
Exploit
Dưới đây là toàn bộ code sage dùng để giải challenge
from sage.all import *
from pwn import *
context.log_level = 'error'
HOST = "localhost"
PORT = 1337
r = remote(HOST, PORT)
r.recvuntil(b"ct: ")
ct_hex = r.recvline().strip().decode()
Ns = [14, 32, 24, 48, 8, 8, 8, 8, 10]
MASKS = [1959, 3487505359, 12175963, 144894747199363, 39, 101, 99, 43, 579]
FILTERS = [
43673535323473607050899647551732188151,
69474900172976843852504521249820447513188207961992185137442753975916133181030,
28448620439946980695145546319125628439828158154718599921182092785732019632576,
16097126481514198260930631821805544393127389525416543962503447728744965087216,
7283664602255916497455724627182983825601943018950061893835110648753003906240,
55629484047984633706625341811769132818865100775829362141410613259552042519543,
4239659866847353140850509664106411172999885587987448627237497059999417835603,
106379335904610565198575784689340408012917012758379923896044369424798179675586
]
E0 = [5, 9, 1, 0, 4, 11, 13]
E2 = [20, 2, 16, 11, 1, 23, 22, 8]
E8 = [5, 8, 9, 3, 1, 0, 2, 4]
ct_bytes = bytes.fromhex(ct_hex)
known_bits = [(byte >> i) & 1 for byte in ct_bytes[:80] for i in reversed(range(8))]
Z = vector(GF(2), known_bits)
def clock_sym(state, mask):
fb = sum([state[i] for i in range(len(state)) if (mask >> i) & 1])
return state[1:] + [fb]
print("Xây dựng Ma trận Tuyến tính (M) cho 374 biến ,640 eqn\n")
state_L1 = [vector(GF(2), 374, {i: 1}) for i in range(32)]
state_L2 = [vector(GF(2), 374, {32+i: 1}) for i in range(24)]
# Bóc tách cấu trúc tuyến tính của L2
tt2 = [(FILTERS[1] >> j) & 1 for j in range(256)]
C2_const = tt2[0]
C2_coeffs = [tt2[1<<i] ^^ C2_const for i in range(8)]
M = matrix(GF(2), 640, 374)
C2_vec = vector(GF(2), 640)
for i in range(640):
row = vector(GF(2), 374)
row += state_L1[0] # L1
row += sum([C2_coeffs[k] * state_L2[E2[k]] for k in range(8)]) # L2
C2_vec[i] = C2_const
row[56 + (i % 63)] = 1 # V63
row[119 + (i % 255)] = 1 # V255
M[i] = row
state_L1 = clock_sym(state_L1, MASKS[1])
state_L2 = clock_sym(state_L2, MASKS[2])
H = M.left_kernel().basis_matrix()
print(f"Left kernel H kích thước {H.nrows()} x {H.ncols()}\n")
# to gen feed forward stream for lfsr 0,8 and 1,2
def gen_lfsr_stream(n, mask, filter_val, ext_pos, seed):
state = seed
stream = vector(GF(2), 640)
for i in range(640):
idx = 0
for j, p in enumerate(ext_pos):
if (state >> p) & 1: idx |= (1 << j)
stream[i] = (filter_val >> idx) & 1
fb = bin(state & mask).count('1') & 1
state = (state >> 1) | (fb << (n - 1))
return stream
print("Meet-in-the-Middle")
hash_L0 = {}
for seed0 in range(1, 1 << 14):
stream0 = gen_lfsr_stream(14, MASKS[0], FILTERS[0], E0, seed0)
Y0 = H * stream0
hash_L0[tuple(Y0)] = seed0
print(f" Đã có hashtable {len(hash_L0)} trạng thái của L0.")
seed_L0, seed_L8 = None, None
T = H * (Z + C2_vec)
for seed8 in range(1, 1 << 10):
stream8 = gen_lfsr_stream(10, MASKS[8], FILTERS[7], E8, seed8)
Y8 = H * stream8
target_Y0 = Y8 + T
if tuple(target_Y0) in hash_L0:
seed_L0 = hash_L0[tuple(target_Y0)]
seed_L8 = seed8
print(f" Tìm ra seed L8 khớp" )
print(f" Seed L0 = {seed_L0}")
print(f" Seed L8 = {seed_L8}\n")
break
print("Hoàn thiện hệ pt , giải và trích xuất seed/period seq")
stream0 = gen_lfsr_stream(14, MASKS[0], FILTERS[0], E0, seed_L0)
stream8 = gen_lfsr_stream(10, MASKS[8], FILTERS[7], E8, seed_L8)
RHS = Z + C2_vec + stream0 + stream8
try:
X = M.solve_right(RHS)
seed_L1 = sum(int(X[i]) << i for i in range(32))
seed_L2 = sum(int(X[32 + i]) << i for i in range(24))
seq_63 = [int(X[56 + i]) for i in range(63)]
seq_255 = [int(X[119 + i]) for i in range(255)]
print(f" Seed L1 = {seed_L1}")
print(f" Seed L2 = {seed_L2}")
print(" seq 63 = ",seq_63)
print(" seq 255 = ",seq_255)
print()
except ValueError:
print("Hệ pt vô nghiệm")
sys.exit()
print("Decypt flag token")
extract = lambda x,b: sum(((int(x) >> p) & 1) << i for i, p in enumerate(b))
blur = lambda x,i: (FILTERS[i] >> int(x)) & 1
class LFSR:
def __init__(self, n, key, mask):
self.n = n
self.state = int(key) & ((1 << n) - 1)
self.mask = int(mask)
def __call__(self):
b = self.state & 1
fb = int(self.state & self.mask).bit_count() & 1
self.state = (self.state >> 1) | (fb << (self.n - 1))
return b
class Cipher:
def __init__(self, s0, s1, s2, s8, seq63, seq255):
self.lfsr0 = LFSR(Ns[0], s0, MASKS[0])
self.lfsr1 = LFSR(Ns[1], s1, MASKS[1])
self.lfsr2 = LFSR(Ns[2], s2, MASKS[2])
self.lfsr8 = LFSR(Ns[8], s8, MASKS[8])
self.seq63 = seq63
self.seq255 = seq255
self.clock = 0
def bit(self):
x = blur(extract(self.lfsr0.state, E0), 0)
y = self.lfsr1.state & 1
z = blur(extract(self.lfsr2.state, E2), 1)
t = blur(extract(self.lfsr8.state, E8), 7)
w_v = int(self.seq63[self.clock % 63])
u = int(self.seq255[self.clock % 255])
self.lfsr0()
self.lfsr1()
self.lfsr2()
self.lfsr8()
self.clock += 1
return x ^^ y ^^ z ^^ w_v ^^ u ^^ t
def stream(self):
while True:
b = 0
for i in reversed(range(8)):
b |= self.bit() << i
yield b
def decrypt(self, ct: bytes):
return bytes([int(c) ^^ int(k) for c, k in zip(ct, self.stream())])
cipher = Cipher(seed_L0, seed_L1, seed_L2, seed_L8, seq_63, seq_255)
decrypted_bytes = cipher.decrypt(ct_bytes)
token = decrypted_bytes[80:]
print(f"FLAG TOKEN : {token.hex()}")
r.recvuntil(b"Gimme Token: ")
r.sendline(token.hex().encode())
flag = r.recvall(timeout=5).decode().strip()
print(flag)
r.close()

Lord of the error ring
- category : Crypto
- Difficulty : Hard
1. Challenge overview
Source code
import os
import random
import ast
proof.arithmetic(False)
flag = os.environ.get("FLAG","BKSEC{testing}")
p = 8380417
F = GF(p)
R.<x> = F[]
fs = []
degree = [8,16,32,64]
errs = random.sample(range(p), 3)
print(errs)
for d in degree:
f = R.irreducible_element(d)
if f not in fs:
fs.append(f)
f = prod(fs)
def parse_poly(user_in):
coeffs = ast.literal_eval(user_in)
if not isinstance(coeffs, (list, tuple)):
raise ValueError
if not all(isinstance(c, int) for c in coeffs):
raise ValueError
return R([c % p for c in coeffs])
def poly_crt(res, fs):
M = prod(fs)
a = R(0)
for i, fi in enumerate(fs):
Mi = M // fi
Ni = Mi.inverse_mod(fi)
a += res[i] * Mi * Ni
return a % M
def poly_sample(lb, ub):
es = [R([random.randint(lb,ub) for _ in range(d)]) for d in degree]
return poly_crt(es, fs)
def error_sample(errs):
es = [R([random.choice(errs) for _ in range(d)]) for d in degree]
return poly_crt(es, fs)
print("Welcome to RLWE challenge!")
print("I will let u sample at most 10 samples this time :DDDDD")
print("num sample:")
num_sample = int(input("> "))
if num_sample > 10:
print("You are too greedy !!!")
exit()
a_s = [poly_sample(0, p - 1) for _ in range(num_sample)]
s = poly_sample(0, p - 1)
e_s = [error_sample(errs) for _ in range(num_sample)]
samples = [(list(a % f), list((a * s + e) % f)) for a, e in zip(a_s, e_s)]
print("Here is your polynomial f:")
print(list(f))
print("Here are your samples:")
print(samples)
print("Submit your guess for my secret :<<<<")
try:
guess = R(parse_poly(input("> ")))
print('Guess:', guess)
if guess == s:
print("Congrats! Here is your flag: " + flag)
except:
print("Don't be that desperate :v")
exit()
Ta có thể thấy hệ mã trong challenge là 1 biến thể base trên Ring LWE
- hệ mã RLWE được xây trên vành đa thức với p = 8380417 . Thay vì dùng đa thức thì hệ mã chọn ra 4 đa thức bất khả quy với bậc lần lượt là 8,16,32,64 và ghép lại .Thay vì chọn nhiễu nhỏ như là RLWE thông thường , chal random 4 vecto nhiễu thành phần trên sau đó ghép lại bằng CRT để thành , và các hệ số của mỗi đa thức được chọn từ 1 trong 3 số được random trên của tập
errs - Về phần thử thách , dựa trên công thức của RLWE , chal gen khóa bí mật sau đó random ra nhiều nhất 10 cặp đa thức () để tính ra . Server sẽ cho ta 10 sample này và bắt ta khôi phục lại secret để lấy được flag
Attack Idea
Ta sẽ tận dụng các điểm khác thường của hệ mã này so với bài toán RLWE gốc để tìm hướng khai thác . Dễ thấy là ta có thể tìm các giá trị trên các cành đa thức nhỏ hơn sau đó construct lại thành bằng CRT . Tiếp theo là việc ko dùng random nhiễu nhỏ mà các nhiễu thành phần có các hệ số chỉ nhận 1 trong 3 giá trị .
Trước tiên thì ta cần phải factor được đa thức mà server gửi đến thành 4 đa thức thành phần , việc này khác dễ dàng khi dùng hàm factor trong sagemath . Hơn nữa thì như source code server thì hàm random ra các đa thức con là deterministic , tức là các cũng như là cố định , ta ko cần đợi từ server .
p = 8380417
F = GF(p)
R.<x> = F[]
f_coeffs = [2020240, 265213, 8152448, 1535330, 2302708, 188378, 4856268, 706694, 3888509, 892985, 7354552, 8172644, 1821287, 6012562, 6214552, 4620009, 269958, 2713660, 6902035, 3946856, 6610444, 6339, 4376878, 2650537, 6107091, 7884244, 8134050, 3585005, 8215254, 3058088, 4326325, 5780968, 5620643, 2045529, 1853649, 5820996, 7252971, 942616, 4123236, 1999542, 29601, 1823190, 7009514, 7392312, 6110635, 662596, 7578109, 7314550, 4536439, 2713935, 65615, 433806, 7852665, 8277309, 4578221, 7331417, 8115130, 1817320, 5330933, 2820207, 6925175, 5269501, 6986417, 4270840, 1541104, 5120143, 7500981, 7954515, 6482335, 4292987, 6993323, 2535234, 3576325, 1074488, 201808, 2324864, 2397710, 2824968, 1576746, 4847844, 2163136, 6229964, 3964077, 6820186, 3800330, 1077806, 7453108, 779773, 7713008, 4192884, 2125242, 2268313, 813588, 2250814, 5876952, 7155218, 2157270, 3818794, 5683869, 5492251, 3832114, 210892, 8145760, 6901089, 4085414, 7192824, 1194351, 4847602, 2522996, 5620900, 4856679, 7441652, 56020, 65652, 17006, 8379720, 8379664, 8380226, 4, 4, 1]
f = R(f_coeffs)
factors = f.factor()
fs = [fac[0] for fac in factors]
fs.sort(key=lambda poly: poly.degree())
for i, fi in enumerate(fs):
print(f"f{i+1} , degree =", fi.degree())
print(list(fi))
print()

Về phần implement , ta sẽ phải tính toán trên từng hệ số của đa thức , vậy nên tốt nhất là nên chuyển đổi đa thức thành vecto cũng như phép nhân đa thức thành phép nhân với ma trận để dễ lập hệ . Sau đây là hàm để biến phép nhân với đa thức thành nhân ma trận A
# convert mul a(x) to mul matrix A
def poly_to_matrix(a_poly, mod_poly, degree):
cols = []
for j in range(degree):
term = (a_poly * (x**j)) % mod_poly
c = term.list()
c += [0] * (degree - len(c))
cols.append(c)
return matrix(F, cols).transpose()
Phase 1 : s mod f1 và errs
Để khôi phục lại , đương nhiên đầu tiên ta sẽ đi từ đa thức nhỏ nhất với bậc 8 . Đa thức nhiễu là 1 thách thức lớn , tuy nhiên với việc từng hệ số trong đa thức chỉ có 3 giá trị có thể trong tập errs, ta đặt ẩn 3 giá trị này là => mỗi vecto nhiễu ( 8x1) là 1 pattern và ta có thể nghĩ đến việc brute force pattern này chỉ với hơn 3000 case . Chiến lược tìm diễn ra như sau : Ta có 10 sample , ta sẽ brute force pattern của . Với mỗi case pattern của ta sẽ:
-
Biễu diễn vecto qua công thức , khi này thì mỗi hệ số của vecto là bậc nhất của
-
Tiếp theo thì với các sample còn lại ta có
- Khi này thì mỗi hệ số của vecto là bậc nhất của
- Hơn nữa với việc mỗi hệ số trong vecto chỉ có 1 trong 3 giá trị , điều này có nghĩa là => Dễ thấy đây là 1 pt bậc 3 3 ẩn
- Một phương trình đa thức bậc 3 với 3 ẩn số chỉ có tổng cộng 20 đơn thức => Ta có thể tuyến tính hóa thành 20 ẩn
-
Từ idea này , với mỗi sample i , ta có thể lập được 8 phương trình với mỗi hệ số , mỗi pt 20 ẩn => Ta chỉ cần 4 sample để lập thành hệ 32x20 quá đủ để giải ra được .
Khi mà pattern đó là đúng , hệ sẽ có nghiệm và ta sẽ có đc tập errs , từ đó khôi phục dễ dàng . Dưới đây là implement của phase 1
# SOLVE PHASE1
def solve_phase1(fi, degree, samples):
print(f"\nSolving phase 1 with f1 of deg {degree}")
samples_5 = samples[:5]
A_matrices = []
B_vectors = []
for a_list, b_list in samples_5:
A_matrices.append(poly_to_matrix(R(a_list) % fi, fi, degree))
b_coeffs = (R(b_list) % fi).list()
B_vectors.append(vector(F, b_coeffs + [0] * (degree - len(b_coeffs))))
A0_inv = A_matrices[0].inverse()
v0 = A0_inv * B_vectors[0]
M0 = A0_inv
T_matrices = [A_matrices[i] * M0 for i in range(1, 5)]
V_vectors = [B_vectors[i] - A_matrices[i] * v0 for i in range(1, 5)]
PR.<c1, c2, c3> = PolynomialRing(F, 3)
monomials = [PR(1), c1, c2, c3, c1^2, c1*c2, c1*c3, c2^2, c2*c3, c3^2,c1^3, c1^2*c2, c1^2*c3, c1*c2^2, c1*c2*c3, c1*c3^2,c2^3, c2^2*c3, c2*c3^2, c3^3]
C_var = [c1, c2, c3]
patterns = [p for p in itertools.product([0, 1, 2], repeat=degree)
if set(p) == {0, 1, 2} and p.index(0) < p.index(1) and p.index(1) < p.index(2)]
errs = []
s1 = []
for pat in patterns:
e0_sym = vector(PR, [C_var[idx] for idx in pat])
equations = []
for i in range(4):
ei_sym = V_vectors[i-1] + T_matrices[i-1] * e0_sym
for k in range(degree):
E = ei_sym[k]
equations.append((E - c1) * (E - c2) * (E - c3))
M_lin = matrix(F, 32, 20)
for row_idx, eq in enumerate(equations):
for col_idx, mon in enumerate(monomials):
M_lin[row_idx, col_idx] = eq.monomial_coefficient(mon)
ker = M_lin.right_kernel()
if ker.dimension() > 0:
for vec in ker.basis():
if vec[0] != 0:
vec = vec / vec[0]
ans_c1, ans_c2, ans_c3 = vec[1], vec[2], vec[3]
# check valid solution of c1, c2, c3
if vec[4] == ans_c1**2 and vec[7] == ans_c2**2:
errs = [int(ans_c1), int(ans_c2), int(ans_c3)]
e0_val = vector(F, [ans_c1 if idx==0 else (ans_c2 if idx==1 else ans_c3) for idx in pat])
s1_val = M0 * (B_vectors[0] - e0_val)
s1 = list(s1_val) + [0] * (degree - len(list(s1_val)))
break
if errs:
break
if not errs:
print("Can't find errs and s mod f1")
sys.exit(1)
print("Solved phase 1")
print(f"errs = {errs}")
print(f"s mod f1 = {s1}")
return errs, s1
errs,s1 = solve_phase1(f1, 8, samples)

Phase 2,3,4
Việc có thể tìm ra và tìm ra tập errs là 1 bước tiến lớn của bài toán , tuy nhiên thì việc tìm trên các vành đa thức với càng lớn (16,32,64) vẫn là thử thách khó nhằn . Việc đi từ Đi từ bậc thấp lên bậc cao dường như là không thể vì định lý Số dư Trung Hoa đảm bảo các phần dư là hoàn toàn độc lập về mặt đại số => không rò rỉ bất kỳ một bit thông tin nào về . Tôi cũng ko thể tìm ra được cách lập hệ nào để có thể giải luôn được ra vecto cũng như các vecto nhiễu . với có bậc 16 , ta vẫn có thể brute force 1 vector nhiễu , tuy nhiên việc này sẽ thực sự bất khả thi với bậc 32 và 64 . Tôi cũng đã tìm cách tôi ưu cân bằng giữa brute force và giải hệ nhưng việc càng lấy nhiều sample , có nhiều phương trình đồng nghĩa với số ẩn từ nhiễu sẽ tăng lên theo hoàn toàn dập tắt ý tưởng . Vậy nên idea cuối cùng là cố đưa bài toán về nghiệm nhỏ => dùng lattice và các thuật toán lattice reduction để giải nghiệm . Chi tiết cách làm sẽ như sau , tôi sẽ lấy ví dụ giải bậc 32
-
Mỗi hệ số trong vecto có thể biễu diễn
- với và là các biến thuộc (và không đồng thời bằng 1) => Ta có các biến x,y này là nghiệm nhỏ
-
để luôn các biến của vecto vào sẽ gây ra chênh lệnh độ lớn giữa các biến , ta sẽ phải tìm cách để bỏ ra khỏi hệ , và tôi sử dụng left kernel để làm điều này . Ta lấy Sample 0 và Sample 1 (tổng cộng 64 hệ số). Biểu diễn dưới dạng ma trận:
-
Ta tính left kernel của ma trận , sau đó nhân cả 2 vế của pt trên với :
-
có rank là 32 => theo lí thuyết sẽ có 64 - 32 = 32 vecto hàng => có size 32x64 => là vecto 32x1 => Ta có 1 hệ 32 phương trình 128 ẩn (mỗi <=> 1 ẩn + 1 ẩn )=> Đây chính là bài toán lattice họ knapsack, multi modular subset sum . Ta có thể dùng template sau để giải
# MULTI MODULAR KNAPSACK SOLVER (FOR PHASE 2,3,4) # Extract and validate the solution vector from the reduced basis def extract_and_validate(reduced_basis, num_var, num_eq, A, S, p): for row in reduced_basis: if all(row[num_var + 1 + j] == 0 for j in range(num_eq)): if row[num_var] == -1/2: e_cand = [int(val + 1/2) for val in row[:num_var]] elif row[num_var] == 1/2: e_cand = [int(1/2 - val) for val in row[:num_var]] else: continue if all(bit in [0, 1] for bit in e_cand): valid = True if all(e_cand[i] + e_cand[i + num_var//2] <= 1 for i in range(num_var//2)): for j in range(num_eq): if (sum(e_cand[i] * A[j][i] for i in range(num_var)) % p) != (S[j] % p): valid = False break if valid: return e_cand return None # Solve the multi modular knapsack problem (S = Ax (modp)) def solve_multi_modular_subset_sum(A, S, p): num_eq = len(A) num_var = len(A[0]) W = 2**100 M = Matrix(QQ, num_var + num_eq + 1, num_var + num_eq + 1) for i in range(num_var): M[i, i] = 1 for j in range(num_eq): M[i, num_var + 1 + j] = W * A[j][i] for j in range(num_eq): M[num_var + j, num_var + 1 + j] = W * p for i in range(num_var): M[num_var + num_eq, i] = 1/2 M[num_var + num_eq, num_var] = 1/2 for j in range(num_eq): M[num_var + num_eq, num_var + 1 + j] = W * S[j] print(f"Lattice size {M.nrows()}x{M.ncols()}") print("Running LLL") reduced_M = M.LLL() sol = extract_and_validate(reduced_M, num_var, num_eq, A, S, p) if sol: return sol else: print("LLL fail, try using BKZ(block_size=20)") reduced_M = M.BKZ(block_size=20) return extract_and_validate(reduced_M, num_var, num_eq, A, S, p) return None -
Cuối cùng thì việc tìm lại vecto trở nên dễ dàng khi tìm được vecto nhiễu
Khi implement , số phương trình của hệ cần được xem xét trước khi đưa vào LLL để tránh size lattice quá to , nhất là với . Ở vd trên với bậc 32 , tôi lấy hết 2 sample tạo thành => số pt = số hàng ma trận - rank ma trận = 32 => lattice size 161x161 . Tuy nhiên , với bậc 64 nếu lấy vả 2 sample , ta sẽ có 64 pt 256 ẩn dẫn đến size lattice là 321x321 quá lớn , vậy nên việc lấy 30 hàng của ma trận để hệ gồm 30 pt là hợp lí.
def solve_branch_lattice(phase ,fi, degree, samples, errs, num_eq):
print(f"\nSolving phase {phase} with f{phase} of deg {degree}")
a0_f = R(samples[0][0]) % fi
b0_f = R(samples[0][1]) % fi
a1_f = R(samples[1][0]) % fi
b1_f = R(samples[1][1]) % fi
c1, c2, c3 = errs[0], errs[1], errs[2]
delta2 = (c2 - c1) % p
delta3 = (c3 - c1) % p
A0 = poly_to_matrix(a0_f, fi, degree)
A1 = poly_to_matrix(a1_f, fi, degree)
b0_vec = vector(F, b0_f.list() + [0] * (degree - len(b0_f.list())))
b1_vec = vector(F, b1_f.list() + [0] * (degree - len(b1_f.list())))
A1_sub = A1[:num_eq, :]
b1_sub = b1_vec[:num_eq]
A_big = block_matrix([[A0], [A1_sub]])
b_big = vector(F, list(b0_vec) + list(b1_sub))
L = A_big.left_kernel().basis_matrix()
c1_vec = vector(F, [c1] * (degree + num_eq))
#targer vecto s of multi knapsack problem
s_target = L * b_big - L * c1_vec
W_X = L * delta2
W_Y = L * delta3
# matrix of multi knapsack problem A_knap = W_X | W_Y
A_knap = block_matrix([[W_X, W_Y]])
A_list = [[int(A_knap[j, i]) for i in range(2 * (degree + num_eq))] for j in range(num_eq)]
s_list = [int(val) for val in s_target]
solution = solve_multi_modular_subset_sum(A_list, s_list, p)
if solution:
print(f"Found solution")
X_0 = solution[:degree]
Y_0 = solution[degree + num_eq : 2 * degree + num_eq]
e0 = []
for i in range(degree):
val = (c1 + X_0[i] * delta2 + Y_0[i] * delta3) % p
e0.append(val)
s_vec = A0.inverse() * (b0_vec - vector(F, e0))
s = list(s_vec) + [0] * (degree - len(list(s_vec)))
print(f"Solved phase {phase}")
print(f"s mod f{phase} =",s)
return s
else:
print(f"Lattice fail")
return None
Việc giải ra và diễn ra khá nhanh
s2 = solve_branch_lattice(2, f2, 16, samples, errs, num_eq=16)
s3 = solve_branch_lattice(3, f3, 32, samples, errs, num_eq=32)

Đến phase 4 , LLL chạy khá nhanh, tuy nhiên kết quả lại là no solution , tôi nhận ra rằng thuật toán LLL có thể ko hiệu quả trên lattice lớn . Vậy nên tôi đã backup dùng BKZ vốn hiệu quả hơn (và thời gian lâu hơn), và cuối cùng thì tôi đã có thể tìm dc
s4 = solve_branch_lattice(4, f4, 64, samples, errs, num_eq=30)

Kết hợp thành s
Bước cuối cùng là từ kết hợp thành dùng CRT (code server cho sẵn) , và gửi secret cho server để lấy flag
