# ==== ==== ==== ==== == Parameters == ==== ==== ==== ==== n = 64 q = 65537 k = 2 l = 2 eta = 2 tau = 20 gamma1 = 8192 gamma2 = 256 beta = tau * eta
NOISE_BOUND = 15
TARGET_MSG = "Please give me the flag" FLAG = os.environ.get("FLAG")
# ==== ==== ==== ==== == Polynomial arithmetic == ==== ==== ==== ==== classPoly: """Polynomial in Z_q[x]/(x^n+1), stored with coefficients in 0..q-1."""
def__init__(self, coeffs=None): if coeffs isNone: self.coeffs = [0] * n else: self.coeffs = [c % q for c in coeffs] iflen(self.coeffs) != n: raise ValueError(f"Length must be {n}")
def__add__(self, other): return Poly([(a + b) % q for a, b inzip(self.coeffs, other.coeffs)])
def__sub__(self, other): return Poly([(a - b) % q for a, b inzip(self.coeffs, other.coeffs)])
def__mul__(self, other): """Convolution modulo x^n+1 and q.""" res = [0] * (2 * n) for i inrange(n): ifself.coeffs[i] == 0: continue for j inrange(n): res[i + j] += self.coeffs[i] * other.coeffs[j]
final = [0] * n for i inrange(2 * n - 1): idx = i % n val = res[i] % q if i < n: final[idx] = (final[idx] + val) % q else: final[idx] = (final[idx] - val) % q return Poly(final)
c_coeffs = [0] * n positions = rng.sample(range(n), tau) for pos in positions: c_coeffs[pos] = 1if rng.randint(0, 1) == 0else -1 return Poly(c_coeffs)
# ==== ==== ==== ==== == Key generation == ==== ==== ==== ==== defkeygen(): A = [[sample_uniform_poly() for _ inrange(l)] for _ inrange(k)] s1 = [sample_small_poly() for _ inrange(l)] t = matrix_vec_mul(A, s1) return A, t, s1
# ==== ==== ==== ==== == Signature == ==== ==== ==== ==== defsign(msg, A, s1): whileTrue: y = sample_y() w = matrix_vec_mul(A, y) w1_list = [poly_high_bits(p, gamma2) for p in w] c = generate_challenge(msg, w1_list) cs1 = poly_vec_scalar_mul(c, s1) z = poly_vec_add(y, cs1)
z_centered = [coeff for p in z for coeff in p.centered_list()] ifmax(abs(v) for v in z_centered) >= gamma1 - beta: continue
noise = [ [random.randint(-NOISE_BOUND, NOISE_BOUND) for _ inrange(n)] for _ inrange(l) ] r = [Poly([y[i].coeffs[j] + noise[i][j] for j inrange(n)]) for i inrange(l)] return c, z, r
# ==== ==== ==== ==== == Verification == ==== ==== ==== ==== defverify(msg, c, z, A, t): z_centered = [coeff for p in z for coeff in p.centered_list()] ifmax(abs(v) for v in z_centered) >= gamma1 - beta: returnFalse
Az = matrix_vec_mul(A, z) ct = poly_vec_scalar_mul(c, t) w_prime = [Az[i] - ct[i] for i inrange(k)] w1_prime = [poly_high_bits(p, gamma2) for p in w_prime] c_prime = generate_challenge(msg, w1_prime) return c == c_prime
if choice == "1": print("Public key (A, t):", flush=True) print("A:", flush=True) for row in A: for poly in row: print(poly.to_int_list(), flush=True) print("t:", flush=True) for poly in t: print(poly.to_int_list(), flush=True)
elif choice == "2": msg = input("Message to sign: ").strip() if msg == TARGET_MSG: print("[-] Sorry, cannot sign the target message!", flush=True) continue
c, z, r = sign(msg, A, s1) print("Signature:", flush=True) print("c:", c.coeffs, flush=True) print("z:", flush=True) for poly in z: print(poly.centered_list(), flush=True) print("r (debug):", flush=True) for poly in r: print(poly.centered_list(), flush=True)
elif choice == "3": print("Submit your signature for: " + TARGET_MSG, flush=True) msg = TARGET_MSG print("Enter c (list of int, length 64):", flush=True) c_data = input().strip() try: c_coeffs = [int(x) for x in c_data.strip("[]").split(",")] c = Poly(c_coeffs) except Exception: print("Invalid format.", flush=True) continue
z_vec = [] for i inrange(l): print(f"Enter z[{i}] (64 ints):", flush=True) z_data = input().strip() try: z_coeffs = [int(x) for x in z_data.strip("[]").split(",")] z_vec.append(Poly(z_coeffs)) except Exception: print("Invalid format.", flush=True) break
iflen(z_vec) != l: continue
if verify(msg, c, z_vec, A, t): print("[+] Signature valid! Here is your flag: " + FLAG, flush=True) else: print("[-] Invalid signature.", flush=True) break
from sage.allimport gcd, is_prime, prod, proof from Crypto.Util.number import bytes_to_long, getPrime from Crypto.Random.random import sample from secret import flag
proof.arithmetic(False)
e = 65537 n = 10000 k = 10
s = set() whilelen(s) < n: s.add(getPrime(50))
sops = sorted(s)
defget_p(): whileTrue: p = 2 * prod(sample(sops, k)) - 1 if is_prime(p) and gcd(p - 1, e) == 1: return p
from fpylll import IntegerMatrix, LLL from sage.allimport PolynomialRing, ZZ
text = open("ce_shi_zhuan_yong_.py").read() N = int(re.findall(r"N = (\d+)", text)[-1]) p_high = int(re.findall(r"p_high = (\d+)", text)[-1]) a = int(re.findall(r"a = (\d+)", text)[-1]) c = int(re.findall(r"c = (\d+)", text)[-1]) outputs = ast.literal_eval(re.findall(r"outputs = (\[.*\])", text)[-1])
# Coppersmith: f(x) = (p_high << 502) + x, with |x| < X. A = p_high << 502 X = 1 << 502 m, t = 25, 25 dim = m + t + 1 M = IntegerMatrix(dim, dim)
for i inrange(m): # N^(m-i) * (x+A)^i scale = N ** (m - i) for j inrange(i + 1): M[i, j] = scale * math.comb(i, j) * A ** (i - j) * X**j
for j inrange(t + 1): # x^j * (x+A)^m row = m + j for k inrange(m + 1): M[row, k + j] = math.comb(m, k) * A ** (m - k) * X ** (k + j)
# A lower delta keeps this near-boundary instance lightweight enough locally. LLL.reduction(M, delta=0.75) R = PolynomialRing(ZZ, "x") x = R.gen() p = None for row inrange(12): h = sum(ZZ(M[row, j]) // X**j * x**j for j inrange(dim) if M[row, j]) for root, _ in h.roots(): candidate = A + int(root) if0 <= root < X and N % candidate == 0: p = candidate break if p isnotNone: break
from Crypto.Util.number import getPrime, inverse, GCD from hashlib import sha256, sha512 from secret import FLAG import secrets import socketserver import signal
self.send(f"[round {idx}/{ROUNDS}]".encode()) self.send(f"r = {r.hex()}".encode()) self.send(b"I have committed to my move. Now your turn.") self.send(f"commitment: {commitment}".encode())
data = self.readline(b"your move [rock/scissors/paper]: ") player = self.parse_move(data) if player isNone: self.send(b"Invalid move. Use rock, scissors, or paper.") returnFalse
self.send(f"I played {MOVES[dealer]}.".encode()) self.send(f"You played {MOVES[player]}.".encode()) returnself.beats(player, dealer)
self.send(b"Welcome to NepCTF 2026") self.send(f"Beat me in RPS game for {ROUNDS} rounds.".encode()) n, e = COM.parameters() self.send(f"parameters: n = {n}, e = {e}".encode())
for i inrange(1, ROUNDS + 1): ifnotself.play_round(i): self.send(b"You lose.") self.request.close() return self.send(b"You win this round.")
self.send(b"You win the game!") self.send(b"flag: " + FLAG) self.request.close()
source = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" for i in source: bi = to_bytes(i) for j in source: bj = to_bytes(j) for k in source: bk = to_bytes(k) if sha256(bi + bj + bk + suffix).hexdigest().encode() == target_hash: return bi + bj + bk
#!/usr/bin/env python3 """ Vector Blind RAG - Challenge Server Provides the query oracle for the encrypted RAG system.
Usage: python server.py [port] Default port: 8080
Requires: server_key.json (generated by generate.py, KEEP SECRET) challenge_data.json (generated by generate.py, public) """
import json import sys from http.server import HTTPServer, BaseHTTPRequestHandler from socketserver import ThreadingMixIn
# ─── Matrix arithmetic over Z_p ──────────────────────────────────────────────
defmat_vec_mul(vec, mat, p): """Row vector (1 x k) times matrix (k x m) modulo p. Returns row vector (1 x m).""" ifnot mat: return [] m = len(mat[0]) result = [0] * m for i, vi inenumerate(vec): if vi == 0: continue row = mat[i] for j inrange(m): result[j] = (result[j] + vi * row[j]) % p return result
# ─── Query encryption (THE VULNERABLE FUNCTION) ──────────────────────────────
defencrypt_query(q, M1_inv, M2_inv, p): """Encrypt a query vector for ASPE-based search.
Note: query vector is not split before encryption. This is a deliberate simplification over the full ASPE protocol where q would be randomly split as q = q1 + q2 and each half encrypted separately with M1^{-1} and M2^{-1}. """ t1 = mat_vec_mul(q, M1_inv, p) t2 = mat_vec_mul(q, M2_inv, p) return t1, t2
# ─── Data loading ────────────────────────────────────────────────────────────
defload_data(): """Load challenge data and server secret keys.""" try: withopen("challenge_data.json", "r") as f: challenge = json.load(f) except FileNotFoundError: print("ERROR: challenge_data.json not found. Run generate.py first.") sys.exit(1)
try: withopen("server_key.json", "r") as f: keys = json.load(f) except FileNotFoundError: print("ERROR: server_key.json not found. Run generate.py first.") sys.exit(1)
p = int(challenge["p"]) n = challenge["n"] database = challenge["database"]
M1_inv = [[int(x) for x in row] for row in keys["M1_inv"]] M2_inv = [[int(x) for x in row] for row in keys["M2_inv"]]
q_raw = body.get("q") if q_raw isNoneornotisinstance(q_raw, list): self._send_error(400, f"missing 'q' field; must be a list of {self.N} integers") return iflen(q_raw) != self.N: self._send_error(400, f"expected q of length {self.N}, got {len(q_raw)}") return
try: q = [int(x) for x in q_raw] except (ValueError, TypeError): self._send_error(400, "q must contain valid integers") return
t1, t2 = encrypt_query(q, self.M1_INV, self.M2_INV, self.P) self._send_json(200, { "t_q": [[str(x) for x in t1], [str(x) for x in t2]], })
# ─── Main ────────────────────────────────────────────────────────────────────
defmain(): p, n, database, M1_inv, M2_inv = load_data()
RequestHandler.P = p RequestHandler.N = n RequestHandler.DATABASE = database RequestHandler.M1_INV = M1_inv RequestHandler.M2_INV = M2_inv
port = int(sys.argv[1]) iflen(sys.argv) > 1else8080 server = ThreadingHTTPServer(("0.0.0.0", port), RequestHandler) print(f"Vector Blind RAG oracle listening on port {port}") print(f" GET / - health check") print(f" GET /database - download encrypted database") print(f" POST /query - submit query vector, get encrypted token")
import requests import sympy as sp from hashlib import sha256 from Crypto.Cipher import AES
url = "https://omzgzwv2-zl0e-tp6o-pfdr-6a5dc4ab30760-neptunus.nepctf.com" response = requests.get(f"{url}/database") data = response.json()
p = int(data["p"]) n = int(data["n"]) database = data["database"]
M1_inv = [] M2_inv = [] for i inrange(64): print(i + 1) q = ["0"] * n q[i] = "1"
res = requests.post(f"{url}/query", json={"q": q}).json() M1_inv.append([int(x) for x in res["t_q"][0]]) M2_inv.append([int(x) for x in res["t_q"][1]])
for i inrange(21): print(i + 1) # 转化为行向量 sp 的 Int 类型需要额外转换 c1 = sp.Matrix([int(x) for x in database[i]["c_v"][0]]).T c2 = sp.Matrix([int(x) for x in database[i]["c_v"][1]]).T
v1 = (c1 * M1_inv_T).applyfunc(lambda x: x % p) v2 = (c2 * M2_inv_T).applyfunc(lambda x: x % p)
v = [(v1[i] + v2[i]) % p for i inrange(n)]
key_bytes = b"".join((int(x) % 2**256).to_bytes(32, "little") for x in v) key = sha256(key_bytes).digest()
#!/usr/bin/env python3 """ LeakyRAG CTF — Server "可搜索加密"的向量数据库,实际上分数泄漏导致向量可被完全重建。 """ import json import os import secrets from http.server import HTTPServer, BaseHTTPRequestHandler
import numpy as np
DIM = 64 FLAG = os.environ.get("FLAG", "flag{l34ky_v3ct0r_s34rch_1s_n0t_3ncrypt10n}")
# ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== # "可搜索加密" Embedding — 公开且确定性 # 比值编码:前 63 维编码字符,第 64 维为参考。 # v [i] / v [63] = exp((char_i - 128) / 64) # 归一化不改变比值 → 重建向量后本地解码即得 flag。 # ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== defembed(text: str) -> np.ndarray: data = text.encode() v = np.ones(DIM, dtype=np.float64) n = min(len(data), DIM - 1) for i inrange(n): ratio = np.exp((data[i] - 128) / 64.0) v[i] = ratio return v / np.linalg.norm(v)
defdecode(v_norm: np.ndarray) -> str: """从归一化向量恢复文本""" v = np.array(v_norm, dtype=np.float64) v = v / np.linalg.norm(v) ref = v[-1] chars = [] for i inrange(DIM - 1): ratio = v[i] / ref char_code = int(round(np.log(ratio) * 64 + 128)) if32 <= char_code <= 126: chars.append(chr(char_code)) else: break return''.join(chars)
# ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== # 文档库 # ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== definit_docs(): docs = {} # 普通文档 samples = [ "The quick brown fox jumps over the lazy dog", "Machine learning is revolutionizing the world", "Python is a popular programming language", "The capital of France is Paris", "Quantum computing uses qubits instead of bits", "Blockchain is a decentralized ledger technology", "Neural networks are inspired by biological neurons", "The Earth orbits around the Sun", "Encryption is the process of encoding information", "Docker containers provide isolated environments", ] for i, text inenumerate(samples): doc_id = f"doc_{i:03d}" docs[doc_id] = { "text": text, "vector": embed(text), "protected": False, }
if __name__ == "__main__": port = int(os.environ.get("PORT", 8080)) server = HTTPServer(("0.0.0.0", port), Handler) print(f"SecureRAG running on port {port}") server.serve_forever()
分析
这题和上一道 Blind RAG 非常像,也是构造向量。
可以通过暴露的接口查询自定义向量和目标文本向量之间的余弦相似度。一开始我想的是如果把查询向量构造为
,再对 snippet 值做匹配,就能得到 flag 向量的第一个分量。以此类推发 64 次请求,就能完整还原 flag 向量,再通过源码里的 decode() 函数还原为文本。
不过 top_k 最高是 20,如果没匹配到就完蛋。但细看源码,相关过滤机制是 top_k = min(body.get("top_k", 5), 20),最终输出 results[:top_k],并没有对负值做校验。所以传 top_k = -1 就能查到除最后一项外的所有条目。实际上手测试了一下,发现 flag 在某些维度竟然还真是垫底的。但是项目有上传功能,只需要上传 64 份在对应维度相似度最低的文档,让 flag 不再垫底,就能还原出 flag 向量,进而解题。
对于上传文档的构造,先看源码的实现:
1 2 3 4 5 6 7 8
defembed(text: str) -> np.ndarray: data = text.encode() v = np.ones(DIM, dtype=np.float64) n = min(len(data), DIM - 1) for i inrange(n): ratio = np.exp((data[i] - 128) / 64.0) v[i] = ratio return v / np.linalg.norm(v)
即某个字符对应的 ASCII 码较小,对应位置的向量分量就小。那么对于第 i 维,把对应位置的字符设为 \x01,其它设为 \x7F,就能让上传的文档在指定维度垫底。