Here we explain the intended solution for the cryptography+reversing challenge the_swirl from our recent CTF, which no team managed to solve during the competition.
The goal of the challenge was to break a Ring‑LWE key-encapsulation mechanism (KEM) by exploiting leakage from a botched NTT‑based implementation of the multiplication in the underlying polynomial ring $R = \mathbb F_q[X]/(X^{256}+1)$, where $q=11777$.
The task consisted of a Python script, eerily similar to the source code of the significantly easier whistler challenge, and a compiled shared library which contains an AVX2‑vectorized implementation of multiplication in $R$. The server starts by printing a public key and an encryption of the flag, then enters an infinite loop of responding to a simple ping‑pong protocol which involves completing a decapsulation and responding with an encrypted message under the resulting key.
First, how do the obvious ideas fail?
Playing around with the code for a bit
should make you notice fairly quickly that
the new mul()
function gives
wrong results for some inputs.
To find out why and how, we have to dig
into the code, most of which — to everyone's
great dismay — looks somewhat like this:
// ...
10ee: c5 b5 e4 e6 vpmulhuw ymm4,ymm9,ymm6
10f2: c5 b5 d5 f6 vpmullw ymm6,ymm9,ymm6
10f6: c4 c1 4d d5 f6 vpmullw ymm6,ymm6,ymm14
10fb: c4 c1 4d e4 f7 vpmulhuw ymm6,ymm6,ymm15
1100: c5 dd f9 e6 vpsubw ymm4,ymm4,ymm6
1104: c4 c1 5d fd f7 vpaddw ymm6,ymm4,ymm15
1109: c4 e2 5d 3a e6 vpminuw ymm4,ymm4,ymm6
110e: c5 fd f9 f4 vpsubw ymm6,ymm0,ymm4
1112: c5 fd fd c4 vpaddw ymm0,ymm0,ymm4
1116: c4 c1 4d fd e7 vpaddw ymm4,ymm6,ymm15
111b: c4 e2 5d 3a e6 vpminuw ymm4,ymm4,ymm6
1120: c4 c1 7d f9 f7 vpsubw ymm6,ymm0,ymm15
1125: c4 e2 7d 3a c6 vpminuw ymm0,ymm0,ymm6
112a: c5 fd 7f 04 24 vmovdqa YMMWORD PTR [rsp],ymm0
112f: c5 fd 6f 44 24 40 vmovdqa ymm0,YMMWORD PTR [rsp+0x40]
1135: c5 b5 e4 f0 vpmulhuw ymm6,ymm9,ymm0
1139: c5 b5 d5 c0 vpmullw ymm0,ymm9,ymm0
113d: c4 c1 7d d5 c6 vpmullw ymm0,ymm0,ymm14
1142: c4 c1 7d e4 c7 vpmulhuw ymm0,ymm0,ymm15
1147: c5 cd f9 f0 vpsubw ymm6,ymm6,ymm0
114b: c4 c1 4d fd c7 vpaddw ymm0,ymm6,ymm15
1150: c4 e2 4d 3a f0 vpminuw ymm6,ymm6,ymm0
1155: c5 f5 f9 c6 vpsubw ymm0,ymm1,ymm6
1159: c5 f5 fd ce vpaddw ymm1,ymm1,ymm6
115d: c4 c1 7d fd f7 vpaddw ymm6,ymm0,ymm15
1162: c4 e2 4d 3a f0 vpminuw ymm6,ymm6,ymm0
1167: c4 c1 75 f9 c7 vpsubw ymm0,ymm1,ymm15
116c: c4 e2 75 3a c8 vpminuw ymm1,ymm1,ymm0
1171: c4 c1 35 e4 c2 vpmulhuw ymm0,ymm9,ymm10
1176: c4 41 35 d5 d2 vpmullw ymm10,ymm9,ymm10
117b: c4 41 2d d5 d6 vpmullw ymm10,ymm10,ymm14
1180: c4 41 2d e4 d7 vpmulhuw ymm10,ymm10,ymm15
1185: c4 c1 7d f9 c2 vpsubw ymm0,ymm0,ymm10
118a: c4 41 7d fd d7 vpaddw ymm10,ymm0,ymm15
118f: c4 c2 7d 3a c2 vpminuw ymm0,ymm0,ymm10
1194: c5 6d f9 d0 vpsubw ymm10,ymm2,ymm0
1198: c5 ed fd d0 vpaddw ymm2,ymm2,ymm0
119c: c4 c1 2d fd c7 vpaddw ymm0,ymm10,ymm15
11a1: c4 c2 7d 3a c2 vpminuw ymm0,ymm0,ymm10
11a6: c4 41 6d f9 d7 vpsubw ymm10,ymm2,ymm15
11ab: c4 c2 6d 3a d2 vpminuw ymm2,ymm2,ymm10
// ...
However, there are some hints. For one, the symbols in the shared library tell you what kind of algorithm it is:
$ nm -D compute.so
0000000000002913 T inv_ntt
0000000000004748 T mul
0000000000001020 T ntt
It should then take at most some reading around to learn that NTT‑based polynomial multiplication means computing \[ f\cdot g = \mathrm{NTT}^{-1}\big(\mathrm{NTT}(f) \odot \mathrm{NTT}(g)\big) \,\text, \] where $\odot$ is coefficient‑wise multiplication. (This is an instance of the much more general theme that Fourier transforms turn convolutions into pointwise products. For beginners, I can recommend this 3Blue1Brown video on the topic.)
Among the three functions in the library,
it is to be expected that mul()
would be the easiest to understand:
Besides calls to NTT and inverse NTT,
it only contains the pointwise multiplications.
By staring at the code, one should be able to
deduce that a single multiplication looks like this:
; pointwise multiplication ymm2 = ymm0 * ymm1
vpmulhuw ymm2, ymm0, ymm1
vpmullw ymm1, ymm0, ymm1
vpmullw ymm1, ymm1, ymm14
vpmulhuw ymm1, ymm1, ymm15
vpsubw ymm2, ymm2, ymm1
vpaddw ymm1, ymm2, ymm15
vpminuw ymm2, ymm2, ymm1
The abundance of w
suffixes
in the code strongly suggests that
the polynomial coefficients are packed
into the ymm
vector registers
as 2‑byte integers.
Moreover, we see that
ymm15
and ymm14
are initialized with the constants
\[
\mathtt{ymm15} = \mathtt{0x2e01} = 11777 = q
\\
\mathtt{ymm14} = \mathtt{0xd201} = -11775 = 2-q
\]
at the beginning of the function.
From the absence of explicit modular arithmetic
and the presence of the constant $2-q = q^{-1}\bmod {2^{16}}$,
connoisseurs may recognize this piece of code as a
Montgomery multiplication:
It computes the low and high halves of the product,
finds a multiple of $q$ to subtract from the product
so that the low half becomes zero,
and returns the high half of the result,
thus effectively computing
$x{\cdot}y/2^{16}\bmod q$.
(Montgomery arithmetic is generally a common choice in vectorized code,
since typical vector instruction sets do not feature divisions with remainder.)
Having understood this, one should probably attempt to (perhaps just mentally) replace all occurrences of this multiplication routine in the code with a macro of some sort, in order to make it easier to focus on the higher-level structure of the computation. However, attempting to do so reveals that the inverse NTT uses a different multiplication algorithm! One way to recognize this is just from the constants that are being loaded in the beginning:
$ objdump -sd compute.so | grep -A200 '<[^@]*>:' | grep '>:\|mov.*eax'
0000000000001020 <ntt>:
102f: b8 01 2e 00 00 mov eax,0x2e01
103e: b8 01 d2 00 00 mov eax,0xd201
0000000000002913 <inv_ntt>:
2922: b8 01 2e 00 00 mov eax,0x2e01
2931: b8 ff 2d 00 00 mov eax,0x2dff
0000000000004748 <mul>:
4949: b8 01 2e 00 00 mov eax,0x2e01
4958: b8 01 d2 00 00 mov eax,0xd201
Notably, $\mathtt{0x2dff}$ is $q-2$ rather than $2-q$, suggesting that perhaps some part of the Montgomery multiplication happens with negated signs. Identifying and comparing the relevant code sequence appearing in the inverse NTT with the previously found multiplication routine confirms this:
ntt() and mul()
| inv_ntt()
|
---|---|
|
|
ymm14
have subtractions in place of additions and vice versa.
However, this variant of Montgomery multiplication has a little
problem: When adding some multiple of $q$ that clears
the low half, there is almost always a carry into the high half.
Noticing that ymm13
holds the constant
$\mathtt{0xffff}=-1$
in inv_ntt()
,
this is what the new
vpsubw ymm1, ymm1, ymm13
instruction takes care of.
However, there is a single exceptional case
that does not require adding the carry:
When the low half is already zero.
In that case, the result of the Montgomery multiplication
will be off by one.
This is the source of the observed occasional failures
of the mul()
function!
Now, the most likely reason for the low half of a product to be zero is when one of the multiplicands was already equal to zero. Thus, we get the following rule, which should hold with overwhelming probability for random inputs:
If the overall polynomial multiplication was computed correctly,
then
no multiplications by zero happened inside the inverse NTT.
It may seem daunting to learn anything useful from this, but as it turns out things are actually fairly simple. The first step of the inverse NTT is to take $128$ pairs of two coefficients $(v_i,v_j)$ and apply a Gentleman–Sande butterfly to each of them, which is the following linear map: [source]
Here, $\tau$ is a nonzero constant that varies with the location $(i,j)$. This is exactly what we need: If $\tau(v_i-v_j)$ is computed correctly, then we learn that $v_i\neq v_j$. (Keep in mind that this operation happens — and hence, this observation holds true — for $128$ separate pairs of coefficients.)
Coming back to the bigger picture, recall that each of the coefficients $v_i$ entering the inverse NTT is itself a product of an unknown coefficient $\hat s_i$ of the NTT'd secret $\hat s$ with a coefficient $\hat c_i$ of the NTT'd ring element $\hat c$ contained in the ciphertext. Thus, the statement $v_i\neq v_j$ actually says $\hat s_i\hat c_i \neq \hat s_j\hat c_j$, and we can compute $\hat c_i$ and $\hat c_j$ by simply applying the NTT to $c$.
Turning such "disequations" into equations is conceptually easy: Just collect enough of them. With each query, we learn $\hat s_i\hat c_i \neq \hat s_j\hat c_j$, which is equivalent to $\hat s_j \neq \hat s_i\hat c_i/\hat c_j$ assuming $\hat c_j\neq0$. Since $\hat c_i,\hat c_j$ are random, we expect to observe all $q{\,-\,}1$ possible values $\hat c_i/\hat c_j$ eventually — the single exception is the impossible constant $\hat s_j/\hat s_i$, again assuming $\hat s_i\neq0$ for simplicity.
This gives rise to the following strategy for learning linear equations between the coefficients of the NTT'd secret $\hat s$: Query the server with a random, honestly generated ciphertext. Each time, for each of the $128$ pairs $(v_i,v_j)$ of coefficients entering a GS butterfly in the first layer of the inverse NTT, eliminate $\hat c_i/\hat c_j$ from the list of possible values of $\hat s_j/\hat s_i$. Repeat until there is only one possible ratio $\hat s_j/\hat s_i$ left for each of the $128$ pairs.
After some $250{,}000$ queries (which should take no more than a couple minutes when communication is properly pipelined — and yes, we did test this between two machines on opposite sides of the planet), we should have obtained exact linear equations between $128$ pairs of coefficients, effectively cutting the number of unknowns in half. Does this help? The previously employed dreamteam "lattice estimator + ChatGPT" is cautiously optimistic:
Here's the strategy for halving the dimension of the Ring‑LWE problem using the known linear relations: Write $\hat S\in \mathbb F_q^{128\times 256}$ for the matrix whose rows are a basis of the space of possible NTT'd secrets $\hat s$. Apply the inverse NTT to all rows (this can be written as a matrix product $\hat S\cdot M_\mathtt{INTT}$) to get a new matrix $S\in\mathbb F_q^{128\times 256}$. By construction, if $A$ is the matrix associated to the element $a\in R$ contained in the public key, the rows of $S\cdot A$ span a lattice of dimension $128$, embedded in $\mathbb Z^{256}$, that contains $s\cdot a\in R$.
At this point, we can basically chop off a bunch of redundant columns of everything and apply the uSVP attack (see for instance these nice slides) to an essentially half‑dimensional instance; see the code below for the hairy details. Solving the resulting lattice problem typically takes between 30 and 50 minutes on my laptop and seems to succeed much more often than not. (Almost all of the time is spent on BKZ‑reducing a $215$‑dimensional lattice with block size $45$.)
pwn.py
#!/usr/bin/env python3
import socket, sys, random, hashlib, collections
from Crypto.Cipher import AES
n = 256
q = 11777
w = 8
################################################################
import ctypes, struct
lib = ctypes.CDLL('./compute.so')
pck = lambda v: struct.pack(f'@{n}H', *(c%q for c in v))
def wrap(lib_fun):
def fun(f):
lib_fun(buf := ctypes.create_string_buffer(pck(f), int(n*2)))
return list(struct.unpack(f'@{n}H', bytes(buf)))
return fun
ntt = wrap(lib.ntt)
################################################################
from vuln import sample, add, pck, unp, mul, center, extract, ppoly, pbits, hbits, mkaes, encaps, decaps
sub = lambda a,b: [(x-y)%q for x,y in zip(a,b)]
################################################################
sock = socket.socket()
sock.settimeout(5)
sock.connect((sys.argv[1], int(sys.argv[2])))
class ufail(Exception): pass
def get():
r = b''
while b'\n' not in r:
try: tmp = sock.recv(1)
except socket.error as e: tmp = b''
if not tmp: raise ufail(r)
r += tmp
return r.decode().strip()
pk_a = list(struct.unpack(f'<{n}H', bytes.fromhex(get().removeprefix('pk[0]: '))))
pk_b = list(struct.unpack(f'<{n}H', bytes.fromhex(get().removeprefix('pk[1]: '))))
pk = pk_a, pk_b
ct_c = list(struct.unpack(f'<{n}H', bytes.fromhex(get().removeprefix('ct[0]: '))))
ct_r = list(map('01'.index, get().removeprefix('ct[1]: ')))
ct_t = bytes.fromhex(get().removeprefix('ct[2]: '))
cipher = bytes.fromhex(get().removeprefix('flag: '))
ct = ct_c, ct_r, ct_t
def oracle(num=1):
sent = []
for _ in range(num):
bits,(c,r,t) = encaps(pk)
m1 = mkaes([1]+bits).encrypt(b'ping')
sock.sendall((struct.pack(f'<{n}H', *c).hex() + '\n').encode())
sock.sendall(''.join(map(str, r)).encode() + b'\n')
sock.sendall(t.hex().encode() + b'\n')
sock.sendall(m1.hex().encode() + b'\n')
sent.append((bits, (c,r,t)))
for bits,(c,r,t) in sent:
m2 = bytes.fromhex(get())
ok = mkaes([2]+bits).decrypt(m2) == b'pong'
yield ((c,r,t), ok)
################################################################
num_queries = 0
ratios = {i: set(range(q)) for i in range(n) if i%32<16}
while True:
for (c,r,t),ok in oracle(1000):
num_queries += 1
if not ok:
continue # bad sample, god knows what happened to it
cmid = ntt(c)
for i in ratios.keys():
j = i + 16
if not cmid[j]:
continue
# s[i]*c[i] - s[j]*c[j] != 0
# s[i]*c[i] != s[j]*c[j]
# s[j]/s[i] != c[i]/c[j]
ratio = cmid[i] * pow(cmid[j],-1,q) % q
ratios[i].discard(ratio)
print(f'{num_queries:7} {sum(len(r)>1 for r in ratios.values()):3}', [None if not r else list(r)[0] if len(r) == 1 else -len(r) for i,r in sorted(ratios.items())])
if all(len(r) <= 1 for r in ratios.values()):
break
fails = sum(not r for r in ratios.values())
if fails:
print(f'\x1b[33mfailed to leak {fails} equation{"s" if fails>1 else ""}\x1b[0m')
if fails > 3:
exit(1)
ratios = {i: list(rs)[0] for i,rs in ratios.items() if rs}
################################################################
shat_basis = []
for i,r in ratios.items():
v = [0]*n
v[i] = 1
v[i+16] = r
shat_basis.append(v)
for i in range(n):
if not any(v[i] for v in shat_basis):
v = [0]*n
v[i] = 1
shat_basis.append(v)
################################################################
from sage.all import *
load('solve.sage')
s = solve(pk_a, pk_b, shat_basis) # <-------
if s is None:
print(f'\x1b[1m!!\x1b[0m \x1b[31mno solution found!\x1b[0m')
exit(1)
print(f'\x1b[1m!!\x1b[0m \x1b[32m{s}\x1b[0m')
bits = decaps(s, pk, ct)
flag = mkaes([0]+bits).decrypt(cipher)
print()
print(f'--> \x1b[32m{flag}\x1b[0m')
print()
solve.sage
#!/usr/bin/env sage
proof.all(False)
n = 256
q = 11777
w = 8
################################################################
P.<T> = GF(q)[]
R.<t> = P.quotient(T^n + 1)
sample = lambda: [bin(getrandbits(w)).count('1') - w//2 for _ in range(n)]
add = lambda a,b: [c.lift_centered() for c in R(list(a))+R(list(b))]
sub = lambda a,b: [c.lift_centered() for c in R(list(a))-R(list(b))]
mul = lambda a,b: [c.lift_centered() for c in R(list(a))*R(list(b))]
################################################################
import ctypes, struct
lib = ctypes.CDLL('./compute.so')
pck = lambda v: struct.pack(f'@{n}H', *(c%q for c in v))
unp = lambda bs: struct.unpack(f'@{n}H', bytes(bs))
def wrap(lib_fun):
def fun(f):
lib_fun(buf := ctypes.create_string_buffer(pck(f), int(n*2)))
return list(struct.unpack(f'@{n}H', bytes(buf)))
return fun
ntt = wrap(lib.ntt)
nttmat = matrix(GF(q), [ntt(v) for v in (ZZ^n).gens()])
inttmat = ~nttmat
lift = lambda vec: vector(ZZ, (GF(q)(c).lift_centered() for c in list(vec)))
################################################################
import time
def solve(a, b, shatbas):
amat = matrix(ZZ, [mul(a, list((GF(q)^n).gens()[i])) for i in range(n)])
assert (lambda r: vector(GF(q), r) * amat == vector(GF(q), mul(a,r)))([randrange(q) for _ in range(n)])
shatlat = matrix(GF(q), shatbas)
slat = shatlat * inttmat
#-------------------------------
slatr = slat[:, :1]
amatr = amat[:, :amat.ncols()*5//6]
embw = 1
#-------------------------------
modqpart = slatr.augment(-slat*amatr)
ech = modqpart.echelon_form()
assert ech[:,:ech.nrows()] == 1
trafo = modqpart.solve_left(ech)
wdth = slatr.ncols() + amatr.ncols()
hght = slatr.nrows()
assert wdth >= hght
modq = matrix(wdth-hght, hght).augment(q * identity_matrix(wdth-hght))
modql, modqr = modq[:,:slatr.ncols()], modq[:,slatr.ncols():]
lat0 = block_matrix(ZZ, [
[ trafo*slatr, trafo*(-slat*amatr), matrix(slat.nrows(),1), ],
[ matrix(1,slatr.ncols()), matrix(b[:amatr.ncols()]), matrix([[embw]]), ],
[ modql, modqr, matrix(modq.nrows(),1), ],
])
print('dim =', lat0.dimensions())
print('det =', lat0.det())
print('log(|det|) =', lat0.det().abs().log(2).n())
# print(lat0)
# root Hermite factor
rhf = lambda vec: ((vec.norm() / lat0.det().abs()**(1/lat0.ncols()))**(1/lat0.ncols())).n()
t0 = time.time()
lat = matrix(filter(bool, lat0.LLL()))
print(time.time() - t0, '\x1b[35mLLL done\x1b[0m')
print('dim =', lat.dimensions())
# print(lat)
for beta in [45]:
t0 = time.time()
lat = lat.BKZ(block_size=beta, fp='rr', precision=200)
print(time.time() - t0, f'\x1b[35mBKZ-{beta} done\x1b[0m')
for vec in lat.rows():
if vec[-1] < 0:
vec = -vec
print(f'rhf:{rhf(vec)} nrm:{vec.norm().n()} {vec}')
if rhf(vec) < 1:
print(f'\x1b[36mrhf:{rhf(vec)} {vec.norm().n()} {vec}\x1b[0m')
comb = lat0.change_ring(GF(q)).solve_left(vec)
print(f'\x1b[1m??\x1b[21m \x1b[34m{comb}\x1b[0m')
assert comb[:slat.nrows()+1] * lat0[:slat.nrows()+1,:] == vec
s = lift(comb[:slat.nrows()] * trafo * slat)
print(f'\x1b[1m??\x1b[21m \x1b[33m{s}\x1b[0m')
if lift(sub(mul(s, a), b)).norm()^2 < (w+1)//2 * n:
print(f'\x1b[32mrhf:{rhf(vec)} {vec.norm().n()} {vec}\x1b[0m')
return list(s)
$ time ./pwn.py 167.235.158.19 4422
1000 128 [-11124, -11124, -11117, -11118, -11125, -11123, -11121, -11119, -11120, -11116, -11118, -11122, -11131, -11129, -11123, -11131, -11123, -11126, -11123, -11114, -11125, -11118, -11121, -11117, -11116, -11118, -11119, -11128, -11116, -11115, -11119, -11118, -11124, -11118, -11115, -11125, -11120, -11121, -11125, -11116, -11119, -11114, -11121, -11118, -11120, -11127, -11123, -11116, -11119, -11116, -11115, -11125, -11119, -11125, -11123, -11118, -11126, -11125, -11122, -11112, -11119, -11120, -11114, -11119, -11125, -11119, -11123, -11120, -11122, -11124, -11118, -11124, -11126, -11117, -11119, -11120, -11121, -11114, -11120, -11123, -11115, -11127, -11133, -11117, -11116, -11123, -11130, -11119, -11120, -11120, -11122, -11126, -11119, -11122, -11123, -11116, -11126, -11128, -11119, -11120, -11121, -11122, -11115, -11120, -11127, -11121, -11110, -11125, -11121, -11121, -11121, -11119, -11120, -11118, -11117, -11117, -11123, -11120, -11120, -11121, -11119, -11117, -11119, -11123, -11120, -11120, -11117, -11121]
2000 128 [-10528, -10525, -10520, -10517, -10519, -10526, -10516, -10515, -10520, -10510, -10521, -10512, -10530, -10533, -10525, -10525, -10512, -10530, -10514, -10514, -10519, -10523, -10518, -10529, -10508, -10518, -10515, -10519, -10511, -10508, -10517, -10511, -10527, -10509, -10513, -10521, -10525, -10511, -10515, -10520, -10522, -10500, -10536, -10521, -10535, -10518, -10518, -10517, -10516, -10505, -10506, -10527, -10517, -10514, -10542, -10510, -10525, -10518, -10502, -10512, -10515, -10516, -10514, -10520, -10525, -10517, -10514, -10537, -10516, -10532, -10514, -10524, -10520, -10521, -10520, -10520, -10526, -10516, -10523, -10524, -10502, -10525, -10536, -10526, -10514, -10527, -10531, -10503, -10522, -10520, -10525, -10516, -10522, -10516, -10515, -10523, -10526, -10522, -10514, -10514, -10524, -10508, -10505, -10522, -10515, -10526, -10508, -10517, -10513, -10527, -10524, -10513, -10513, -10517, -10509, -10507, -10526, -10516, -10516, -10518, -10515, -10508, -10524, -10511, -10520, -10518, -10520, -10528]
3000 128 [-9953, -9942, -9938, -9919, -9925, -9949, -9945, -9924, -9936, -9924, -9937, -9923, -9960, -9947, -9924, -9924, -9932, -9941, -9929, -9927, -9920, -9944, -9941, -9954, -9928, -9936, -9947, -9934, -9927, -9924, -9948, -9931, -9956, -9936, -9921, -9935, -9946, -9923, -9938, -9923, -9945, -9921, -9956, -9924, -9948, -9940, -9941, -9937, -9936, -9928, -9913, -9944, -9921, -9931, -9939, -9911, -9943, -9931, -9906, -9918, -9928, -9941, -9921, -9939, -9943, -9952, -9924, -9946, -9925, -9948, -9939, -9938, -9927, -9938, -9938, -9946, -9932, -9938, -9945, -9941, -9927, -9925, -9942, -9943, -9932, -9933, -9936, -9933, -9942, -9932, -9932, -9930, -9944, -9937, -9930, -9951, -9947, -9925, -9928, -9918, -9944, -9932, -9935, -9934, -9944, -9960, -9923, -9925, -9941, -9938, -9929, -9939, -9932, -9937, -9928, -9938, -9933, -9943, -9932, -9933, -9929, -9925, -9934, -9919, -9938, -9937, -9939, -9961]
// ...
260000 1 [5320, 10257, 4096, 6890, 9083, 7912, 10732, 2864, 1845, 9883, 9538, 7131, 8788, 11690, 1074, 9894, 8892, 2120, 2385, 3126, 9852, 8389, 3392, 3789, 8012, 4327, 9191, 5249, 716, 4321, 10155, 6309, 6429, 9346, 1256, 5844, 3456, 11467, 1310, 10726, 6644, 3638, 6206, 2032, 10754, 8362, 8807, 5338, 2979, 1526, 5609, 5391, 9269, 9237, 8177, 777, 9645, 10453, 9839, 11054, 8295, 11635, 3314, 9639, 3530, 2156, 3835, 8398, 1280, 9238, 10248, 7273, 5322, 7510, 6809, 1129, 6276, 2100, 6914, 9238, 6443, 2177, 7440, 3198, 1797, 9167, 10563, 387, 463, 3009, 5627, 9985, 4899, 9838, 7498, 48, 4348, 9644, -2, 9673, 11307, 8737, 7841, 9038, 163, 11256, 6525, 2805, 4659, 3456, 10488, 8221, 5658, 784, 552, 9743, 11439, 6535, 9693, 9163, 778, 5012, 9810, 11039, 3404, 4454, 3257, 3897]
261000 1 [5320, 10257, 4096, 6890, 9083, 7912, 10732, 2864, 1845, 9883, 9538, 7131, 8788, 11690, 1074, 9894, 8892, 2120, 2385, 3126, 9852, 8389, 3392, 3789, 8012, 4327, 9191, 5249, 716, 4321, 10155, 6309, 6429, 9346, 1256, 5844, 3456, 11467, 1310, 10726, 6644, 3638, 6206, 2032, 10754, 8362, 8807, 5338, 2979, 1526, 5609, 5391, 9269, 9237, 8177, 777, 9645, 10453, 9839, 11054, 8295, 11635, 3314, 9639, 3530, 2156, 3835, 8398, 1280, 9238, 10248, 7273, 5322, 7510, 6809, 1129, 6276, 2100, 6914, 9238, 6443, 2177, 7440, 3198, 1797, 9167, 10563, 387, 463, 3009, 5627, 9985, 4899, 9838, 7498, 48, 4348, 9644, -2, 9673, 11307, 8737, 7841, 9038, 163, 11256, 6525, 2805, 4659, 3456, 10488, 8221, 5658, 784, 552, 9743, 11439, 6535, 9693, 9163, 778, 5012, 9810, 11039, 3404, 4454, 3257, 3897]
262000 1 [5320, 10257, 4096, 6890, 9083, 7912, 10732, 2864, 1845, 9883, 9538, 7131, 8788, 11690, 1074, 9894, 8892, 2120, 2385, 3126, 9852, 8389, 3392, 3789, 8012, 4327, 9191, 5249, 716, 4321, 10155, 6309, 6429, 9346, 1256, 5844, 3456, 11467, 1310, 10726, 6644, 3638, 6206, 2032, 10754, 8362, 8807, 5338, 2979, 1526, 5609, 5391, 9269, 9237, 8177, 777, 9645, 10453, 9839, 11054, 8295, 11635, 3314, 9639, 3530, 2156, 3835, 8398, 1280, 9238, 10248, 7273, 5322, 7510, 6809, 1129, 6276, 2100, 6914, 9238, 6443, 2177, 7440, 3198, 1797, 9167, 10563, 387, 463, 3009, 5627, 9985, 4899, 9838, 7498, 48, 4348, 9644, -2, 9673, 11307, 8737, 7841, 9038, 163, 11256, 6525, 2805, 4659, 3456, 10488, 8221, 5658, 784, 552, 9743, 11439, 6535, 9693, 9163, 778, 5012, 9810, 11039, 3404, 4454, 3257, 3897]
263000 0 [5320, 10257, 4096, 6890, 9083, 7912, 10732, 2864, 1845, 9883, 9538, 7131, 8788, 11690, 1074, 9894, 8892, 2120, 2385, 3126, 9852, 8389, 3392, 3789, 8012, 4327, 9191, 5249, 716, 4321, 10155, 6309, 6429, 9346, 1256, 5844, 3456, 11467, 1310, 10726, 6644, 3638, 6206, 2032, 10754, 8362, 8807, 5338, 2979, 1526, 5609, 5391, 9269, 9237, 8177, 777, 9645, 10453, 9839, 11054, 8295, 11635, 3314, 9639, 3530, 2156, 3835, 8398, 1280, 9238, 10248, 7273, 5322, 7510, 6809, 1129, 6276, 2100, 6914, 9238, 6443, 2177, 7440, 3198, 1797, 9167, 10563, 387, 463, 3009, 5627, 9985, 4899, 9838, 7498, 48, 4348, 9644, 10722, 9673, 11307, 8737, 7841, 9038, 163, 11256, 6525, 2805, 4659, 3456, 10488, 8221, 5658, 784, 552, 9743, 11439, 6535, 9693, 9163, 778, 5012, 9810, 11039, 3404, 4454, 3257, 3897]
dim = (215, 215)
det = 128523355212312984740482083941262861951183664818213117392658355763911676483262820940871293704572209997969344568368573664129974370880969790012879529100120020972774362199763666440288493137415545233266753570155201424002582071152741974278571893757836092110540902092387401351720488413371942895068455348678668372414388266339767150349996223748142190450930689
log(|det|) = 1163.03686375978
16.31879711151123 LLL done
dim = (215, 215)
1571.3907492160797 BKZ-45 done
rhf:0.996645908137457 nrm:20.6397674405503 (0, 0, 0, -1, -1, -2, 0, -1, -1, -4, 0, 2, 1, 0, -1, 2, 0, -1, -2, 1, 2, 0, 0, -1, 0, -2, -1, 0, 3, 2, -1, 4, -1, 1, 2, 3, -2, -1, 1, 0, -1, 0, 1, 0, 1, 0, 0, -3, -1, -1, 2, 2, -1, 2, 0, 0, 1, 0, 0, 0, -2, -2, -3, 0, 0, 1, 0, 1, -1, 1, -1, 1, -1, 0, 1, -1, 1, 2, 2, 0, -2, -1, -2, 0, -1, 0, -2, 0, 0, -1, 1, 1, -3, 2, 0, 1, -1, 0, 0, -1, -2, 0, 1, 1, 3, -1, 0, -1, -2, -1, -1, -2, -1, 1, -1, 0, -1, 1, -1, 0, 0, 1, 1, 1, -1, 0, 1, -2, -3, -1, 0, 2, 0, 3, -2, 1, -2, -2, -1, 1, 0, -1, 0, -3, 1, -3, 0, 0, 3, -2, -3, 2, -1, -1, -2, 0, 1, 1, 0, -1, -1, 2, -2, -2, -2, 2, -1, -3, -1, 0, 0, -1, 0, 2, 1, 2, 2, -1, -3, 1, -2, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, -1, 1, 0, -1, -1, 1, 0, 1, 2, 1, 0, 1, 0, 0, 2, -2, 0, -2, -1, -1, 0, 0, -2, 1)
rhf:0.996645908137457 20.6397674405503 (0, 0, 0, -1, -1, -2, 0, -1, -1, -4, 0, 2, 1, 0, -1, 2, 0, -1, -2, 1, 2, 0, 0, -1, 0, -2, -1, 0, 3, 2, -1, 4, -1, 1, 2, 3, -2, -1, 1, 0, -1, 0, 1, 0, 1, 0, 0, -3, -1, -1, 2, 2, -1, 2, 0, 0, 1, 0, 0, 0, -2, -2, -3, 0, 0, 1, 0, 1, -1, 1, -1, 1, -1, 0, 1, -1, 1, 2, 2, 0, -2, -1, -2, 0, -1, 0, -2, 0, 0, -1, 1, 1, -3, 2, 0, 1, -1, 0, 0, -1, -2, 0, 1, 1, 3, -1, 0, -1, -2, -1, -1, -2, -1, 1, -1, 0, -1, 1, -1, 0, 0, 1, 1, 1, -1, 0, 1, -2, -3, -1, 0, 2, 0, 3, -2, 1, -2, -2, -1, 1, 0, -1, 0, -3, 1, -3, 0, 0, 3, -2, -3, 2, -1, -1, -2, 0, 1, 1, 0, -1, -1, 2, -2, -2, -2, 2, -1, -3, -1, 0, 0, -1, 0, 2, 1, 2, 2, -1, -3, 1, -2, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, -1, 1, 0, -1, -1, 1, 0, 1, 2, 1, 0, 1, 0, 0, 2, -2, 0, -2, -1, -1, 0, 0, -2, 1)
?? (0, 10281, 9815, 3519, 4588, 1719, 2586, 2236, 1782, 2232, 1760, 9808, 8285, 1496, 2514, 752, 4650, 6841, 6267, 5090, 10482, 5714, 4721, 10948, 6712, 5131, 7847, 595, 2245, 1016, 7147, 4556, 1232, 9319, 388, 9170, 6109, 6141, 8242, 8524, 4833, 10737, 3725, 2541, 10949, 1056, 5449, 5940, 3044, 8543, 9710, 7453, 4517, 194, 2306, 3911, 6029, 10337, 283, 6096, 1533, 765, 6818, 7329, 6384, 6570, 2142, 9103, 1582, 7978, 9926, 6336, 645, 2230, 7923, 7758, 640, 1490, 11003, 7084, 9732, 4982, 11765, 2734, 1553, 4631, 991, 6472, 659, 10225, 7292, 10374, 7176, 9077, 2177, 6415, 11148, 6727, 9782, 11497, 9743, 9991, 810, 6403, 5250, 10224, 636, 6859, 4703, 11251, 5131, 5751, 8924, 10747, 6864, 2359, 10324, 1147, 4930, 10079, 6052, 2180, 4890, 1747, 9678, 1355, 4036, 6942, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
?? (0, 0, -2, 2, 0, 1, 0, 0, 1, 2, -1, 2, 2, -2, 0, 0, 1, 0, -2, 0, 1, 1, -1, 3, 0, 2, 0, 2, -3, 2, 2, 1, 2, 0, -1, -2, 1, 2, 1, 1, 1, 2, -1, 1, -2, 1, 1, -1, 2, 2, 1, 0, -2, -1, 1, 1, -1, 0, -1, 1, 0, -1, 0, 1, 0, 0, 1, -1, 0, -3, 0, -2, 1, 3, -1, -1, 0, -1, 1, 1, -2, 0, 2, 3, 1, -1, 1, -1, -1, 0, 1, -1, 1, -2, -1, 0, 0, 0, 0, -2, 2, -1, -1, 0, 0, 0, 1, -1, -2, 0, 0, 0, 1, 0, 1, 0, 0, -2, 0, -4, -1, 1, 0, -1, -3, 1, 0, 4, 1, 0, 1, -1, 1, -3, -3, 1, 2, -2, 0, -1, -1, -2, 2, -1, -1, 1, -1, 2, 1, -2, 1, -2, 0, 2, -1, -1, 1, 1, -1, 0, 2, 0, 0, 1, -1, 1, 1, -1, -1, 0, -2, 1, -1, -1, -2, -1, 0, -1, 2, 1, -1, 2, 1, 0, 1, 1, 0, 1, -2, 0, -1, 1, 0, 0, -3, 0, -3, -1, -1, -2, 0, -1, 0, -1, 1, -1, -1, 1, 0, -3, 0, 1, -1, 0, 1, 1, -1, -3, -1, 1, 1, 0, 0, -2, -1, 1, 0, 1, -1, 0, 0, 0, -1, 0, 0, 1, -2, -1, 0, 1, -1, 0, 0, -2, -3, 1, -1, -1, 1, 2, -2, -2, 0, 0, 1, 0)
rhf:0.996645908137457 20.6397674405503 (0, 0, 0, -1, -1, -2, 0, -1, -1, -4, 0, 2, 1, 0, -1, 2, 0, -1, -2, 1, 2, 0, 0, -1, 0, -2, -1, 0, 3, 2, -1, 4, -1, 1, 2, 3, -2, -1, 1, 0, -1, 0, 1, 0, 1, 0, 0, -3, -1, -1, 2, 2, -1, 2, 0, 0, 1, 0, 0, 0, -2, -2, -3, 0, 0, 1, 0, 1, -1, 1, -1, 1, -1, 0, 1, -1, 1, 2, 2, 0, -2, -1, -2, 0, -1, 0, -2, 0, 0, -1, 1, 1, -3, 2, 0, 1, -1, 0, 0, -1, -2, 0, 1, 1, 3, -1, 0, -1, -2, -1, -1, -2, -1, 1, -1, 0, -1, 1, -1, 0, 0, 1, 1, 1, -1, 0, 1, -2, -3, -1, 0, 2, 0, 3, -2, 1, -2, -2, -1, 1, 0, -1, 0, -3, 1, -3, 0, 0, 3, -2, -3, 2, -1, -1, -2, 0, 1, 1, 0, -1, -1, 2, -2, -2, -2, 2, -1, -3, -1, 0, 0, -1, 0, 2, 1, 2, 2, -1, -3, 1, -2, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, -1, 1, 0, -1, -1, 1, 0, 1, 2, 1, 0, 1, 0, 0, 2, -2, 0, -2, -1, -1, 0, 0, -2, 1)
!! [0, 0, -2, 2, 0, 1, 0, 0, 1, 2, -1, 2, 2, -2, 0, 0, 1, 0, -2, 0, 1, 1, -1, 3, 0, 2, 0, 2, -3, 2, 2, 1, 2, 0, -1, -2, 1, 2, 1, 1, 1, 2, -1, 1, -2, 1, 1, -1, 2, 2, 1, 0, -2, -1, 1, 1, -1, 0, -1, 1, 0, -1, 0, 1, 0, 0, 1, -1, 0, -3, 0, -2, 1, 3, -1, -1, 0, -1, 1, 1, -2, 0, 2, 3, 1, -1, 1, -1, -1, 0, 1, -1, 1, -2, -1, 0, 0, 0, 0, -2, 2, -1, -1, 0, 0, 0, 1, -1, -2, 0, 0, 0, 1, 0, 1, 0, 0, -2, 0, -4, -1, 1, 0, -1, -3, 1, 0, 4, 1, 0, 1, -1, 1, -3, -3, 1, 2, -2, 0, -1, -1, -2, 2, -1, -1, 1, -1, 2, 1, -2, 1, -2, 0, 2, -1, -1, 1, 1, -1, 0, 2, 0, 0, 1, -1, 1, 1, -1, -1, 0, -2, 1, -1, -1, -2, -1, 0, -1, 2, 1, -1, 2, 1, 0, 1, 1, 0, 1, -2, 0, -1, 1, 0, 0, -3, 0, -3, -1, -1, -2, 0, -1, 0, -1, 1, -1, -1, 1, 0, -3, 0, 1, -1, 0, 1, 1, -1, -3, -1, 1, 1, 0, 0, -2, -1, 1, 0, 1, -1, 0, 0, 0, -1, 0, 0, 1, -2, -1, 0, 1, -1, 0, 0, -2, -3, 1, -1, -1, 1, 2, -2, -2, 0, 0, 1, 0]
--> b'hxp{iF_y0u_g4z3_l0Ng_1Nt0_a_L4tT1c3__Th3_l4Tt1c3_g4z3S_b4Ck_4t_Y0u}'
real 29m2.043s
user 29m1.685s
sys 0m2.114s