solved lake ctf
This commit is contained in:
1
2025/lake/crypto/sage_shit/.python-version
Normal file
1
2025/lake/crypto/sage_shit/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
1
2025/lake/crypto/sage_shit/keys.json
Normal file
1
2025/lake/crypto/sage_shit/keys.json
Normal file
File diff suppressed because one or more lines are too long
7
2025/lake/crypto/sage_shit/pyproject.toml
Normal file
7
2025/lake/crypto/sage_shit/pyproject.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[project]
|
||||
name = "sage-shit"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = []
|
||||
206
2025/lake/crypto/sage_shit/solve.sage
Normal file
206
2025/lake/crypto/sage_shit/solve.sage
Normal file
@@ -0,0 +1,206 @@
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
# --- 1. Setup Parameters ---
|
||||
q = 251
|
||||
n = 16
|
||||
k = 2
|
||||
|
||||
# --- 2. Helper Functions ---
|
||||
|
||||
def negacyclic_matrix(poly_coeffs):
|
||||
"""Converts a polynomial to a negacyclic matrix."""
|
||||
mat = []
|
||||
# In the challenge, polynomial mult involves convolution and subtraction
|
||||
# This corresponds to standard negacyclic structure for x^n + 1
|
||||
p = list(poly_coeffs)
|
||||
for i in range(n):
|
||||
row = [0] * n
|
||||
for j in range(n):
|
||||
if i - j >= 0:
|
||||
row[j] = p[i - j]
|
||||
else:
|
||||
row[j] = -p[i - j + n] # Negate wrap-around
|
||||
mat.append(row)
|
||||
# The challenge uses numpy convolve which results in a specific layout.
|
||||
# Let's verify orientation.
|
||||
# v0 * v1 in challenge is sum(convolve(a,b)...)
|
||||
# We want Matrix * Vector = Result.
|
||||
# The matrix constructed above represents multiplication BY poly_coeffs
|
||||
return Matrix(ZZ, mat)
|
||||
|
||||
def get_secret_key(A_polys, t_polys):
|
||||
# Construct the large A matrix (Block matrix)
|
||||
# A has shape 2x2. Each element is a polynomial.
|
||||
# We convert each polynomial to a 16x16 matrix.
|
||||
# A_big will be 32x32.
|
||||
|
||||
# A_polys structure: [[poly00, poly01], [poly10, poly11]]
|
||||
M00 = negacyclic_matrix(A_polys[0][0])
|
||||
M01 = negacyclic_matrix(A_polys[0][1])
|
||||
M10 = negacyclic_matrix(A_polys[1][0])
|
||||
M11 = negacyclic_matrix(A_polys[1][1])
|
||||
|
||||
# Block matrix construction
|
||||
A_big = block_matrix([[M00, M01], [M10, M11]])
|
||||
|
||||
# Flatten t into a single vector of size 32
|
||||
t_vec = vector(ZZ, A_polys[0][0]).parent().zero_vector() # dummy init
|
||||
t_flat = []
|
||||
for row in t_polys:
|
||||
t_flat.extend(row)
|
||||
t_vec = vector(ZZ, t_flat)
|
||||
|
||||
# --- Lattice Embedding ---
|
||||
# We want to solve s * A_big = t (approx)
|
||||
# Note: In the challenge code: t = A * s + e
|
||||
# So A_big * s_vec = t_vec (mod q)
|
||||
|
||||
# Lattice Basis Construction (Kannad Embedding)
|
||||
# Rows:
|
||||
# [ I_32 | A_big^T | 0 ]
|
||||
# [ 0 | q*I_32 | 0 ]
|
||||
# [ 0 | -t | 1 ]
|
||||
|
||||
dim = 2 * n
|
||||
# Identity
|
||||
B0 = identity_matrix(dim)
|
||||
# A Transpose (because A*s = t, we put columns of A into rows of lattice basis part)
|
||||
# Wait, if we use row vectors v*M, we need appropriate orientation.
|
||||
# Let's stick to: we want vector (s, e, 1)
|
||||
# A_big * s - t = -e (mod q)
|
||||
# => A_big * s + q*k - t = -e
|
||||
|
||||
L = matrix(ZZ, 2*dim + 1, 2*dim + 1)
|
||||
|
||||
# Top Left: Identity for s
|
||||
L.set_block(0, 0, identity_matrix(dim))
|
||||
|
||||
# Top Middle: A_big Transpose (mapping s to A*s)
|
||||
# We use Transpose because Sage lattices are row-span.
|
||||
# A row (s) * (A^T) = (A*s)^T
|
||||
L.set_block(0, dim, A_big.transpose())
|
||||
|
||||
# Middle Middle: q * Identity (modulus reduction)
|
||||
L.set_block(dim, dim, q * identity_matrix(dim))
|
||||
|
||||
# Bottom Middle: -t vector
|
||||
L.set_block(2*dim, dim, matrix(ZZ, 1, dim, [-x for x in t_flat]))
|
||||
|
||||
# Bottom Right: 1 (constant for embedding)
|
||||
L[2*dim, 2*dim] = 1
|
||||
|
||||
print("Running LLL...")
|
||||
L_reduced = L.LLL()
|
||||
|
||||
# Search for the short vector. It should look like (s, -e, 1) or -(s, -e, 1)
|
||||
for row in L_reduced:
|
||||
if row[2*dim] == 1: # Check the embedding constant
|
||||
s_recovered = row[:dim]
|
||||
print("Found candidate secret key!")
|
||||
return s_recovered
|
||||
elif row[2*dim] == -1:
|
||||
s_recovered = [-x for x in row[:dim]]
|
||||
print("Found candidate secret key (inverted)!")
|
||||
return s_recovered
|
||||
|
||||
return None
|
||||
|
||||
# --- 3. Decryption Routine ---
|
||||
|
||||
def poly_mul_mod(p1, p2):
|
||||
"""Re-implementation of the challenge multiplication for decryption"""
|
||||
res = [0] * (2 * n)
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
res[i+j] += p1[i] * p2[j]
|
||||
|
||||
# Reduction mod x^16 + 1
|
||||
out = [0] * n
|
||||
for i in range(len(res)):
|
||||
if i < n:
|
||||
out[i] = (out[i] + res[i])
|
||||
else:
|
||||
out[i - n] = (out[i - n] - res[i]) # x^16 = -1
|
||||
return [x % q for x in out]
|
||||
|
||||
def decrypt(u, v, s):
|
||||
# u is a list of lists (k x n)
|
||||
# v is a list (n)
|
||||
# s is the recovered secret (k x n flattened)
|
||||
|
||||
# Reconstruct s into polys
|
||||
s_polys = [list(s[i*n : (i+1)*n]) for i in range(k)]
|
||||
|
||||
# Calculate s^T * u
|
||||
# u is shape (k, n). s is shape (k, n).
|
||||
# Dot product = sum(poly_mul(s[i], u[i]))
|
||||
|
||||
dot_prod = [0]*n
|
||||
for i in range(k):
|
||||
prod = poly_mul_mod(s_polys[i], u[i])
|
||||
dot_prod = [(x + y) % q for x, y in zip(dot_prod, prod)]
|
||||
|
||||
# m_noisy = v - s*u
|
||||
m_noisy = [(v_val - d_val) % q for v_val, d_val in zip(v, dot_prod)]
|
||||
|
||||
# Rounding
|
||||
# Center is roughly q/2 = 126.
|
||||
# If close to 0 -> 0. If close to 126 -> 1.
|
||||
bits = []
|
||||
limit = q // 4
|
||||
for val in m_noisy:
|
||||
# Distance to 0
|
||||
d0 = min(val, q - val)
|
||||
# Distance to q/2
|
||||
target = (q + 1) // 2
|
||||
d1 = min(abs(val - target), q - abs(val - target))
|
||||
|
||||
if d1 < d0:
|
||||
bits.append(1)
|
||||
else:
|
||||
bits.append(0)
|
||||
return bits
|
||||
|
||||
# --- 4. Main Execution ---
|
||||
|
||||
# Load Data
|
||||
with open("keys.json", "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
A = data['A']
|
||||
t = data['t']
|
||||
u_list = data['u']
|
||||
v_list = data['v']
|
||||
|
||||
# Recover S
|
||||
s_vec = get_secret_key(A, t)
|
||||
|
||||
# Decrypt all batches
|
||||
all_bits = []
|
||||
for u_batch, v_batch in zip(u_list, v_list):
|
||||
# u_batch is shape (k, n). v_batch is shape (n) (actually it's list of lists in json?)
|
||||
# Wait, check JSON structure.
|
||||
# v is list of lists. u is list of lists of lists.
|
||||
# But decrypt function inputs: u_list is list of u. v_list is list of v.
|
||||
# In encrypt: u_list.append(u), u is numpy array.
|
||||
|
||||
# Let's handle the specific JSON structure provided in the prompt
|
||||
# u in json is [ [row1, row2], [row1, row2] ... ]
|
||||
# v in json is [ poly, poly ... ]
|
||||
|
||||
decrypted_bits = decrypt(u_batch, v_batch, s_vec)
|
||||
all_bits.extend(decrypted_bits)
|
||||
|
||||
# --- 5. Bits to Flag ---
|
||||
# The challenge code: [int(c) for char in flag for c in format(ord(char), '08b')]
|
||||
# This groups 8 bits per char.
|
||||
flag = ""
|
||||
for i in range(0, len(all_bits), 8):
|
||||
byte = all_bits[i:i+8]
|
||||
if len(byte) < 8: break
|
||||
# Convert list of 0/1 to integer
|
||||
char_code = int("".join(map(str, byte)), 2)
|
||||
flag += chr(char_code)
|
||||
|
||||
print(f"Flag: {flag}")
|
||||
Reference in New Issue
Block a user