NSSCTF -- [tangcuxiaojkuai] -- Matrix
Matrix_warm_up
task.sage
from Crypto.Util.number import *
from random import randint
from secret import flag
p = getPrime(256)
print("p =", p)
print("c =", randint(1, p) * vector(Zmod(p), [i * getPrime(128) for i in flag]))
analysis
其中\(a\in[1,p-1]\),\(b_i\in[2^{127}, 2^{128}-1](i=1,2,\cdots,n)\)
显然,我们现在只有准确的\(\vec{c}\)的数据,要依赖于\(\vec{c}\)建造格,使得\(\vec{m}'\)落在其中的格点上.
注意到这个格并不是一个方阵,这是因为\(a^{-1}\pmod{p}\)并不是一个模\(p\)意义下的小数字,所以我们没有必要规约他。而这种行>列的非方阵格会带来一个问题——格基并不是线性无关的,秩只有\(n\),因此\(LLL\)会得到一行全\(0\),故\(\vec{m}'\)会出现在第二行。
另外\(LLL\)算法的使用条件就是行>列且定义域\(\mathbb{Z}\)上。
exp.sage
p = ...
c = ...
c = vector(ZZ, list(c))
n = len(c)
P = p * identity_matrix(ZZ, n)
L = block_matrix([[c, P]])
L = L.transpose().LLL()
print(f"LLL done.")
m = L[1]
if m < 0:
m = -m
# print(factor(m[0])) # 2 * 3 * 13 * 204509365093829812703971289674546783117
# print(factor(m[0])[0]) # (2, 1) --> (base, e)
for mi in m:
print(chr(mi // factor(mi)[-1][0]), end="")
# NSSCTF{F1nd_@_s3cR3t_NUMBER_1s_eAsy_4_U}
Matrix1
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
n = 10
p = getPrime(256)
M1 = Matrix(Zmod(p), n, n, pad(flag[:len(flag)//2], n^2))
M2 = Matrix(Zmod(p), n, n, pad(flag[len(flag)//2:], n^2))
A = random_matrix(Zmod(p), n, n)
B = M1 * A * M2^(-1)
print("p =", p)
print("A =", A.list())
print("B =", B.list())
analysis
\((2)\)可以获得\(由100\)个齐次线性方程组成的齐次线性方程组,其中有\(200\)个未知数(\(m_{1,i},m_{2,j}\))。令
则\((2)\)可以转化为\(LM^{T}=O\),\(M\)是其解空间中的一个短向量,因为\(m_{i\ j,k}\in[0,255]\),相对于\(p\)而言足够小。因此,可以先求解\(\mathscr{L}(L)\)的核,再规约找短向量的方式求解得到\(M\)。
设我们得到的核为\(S\),其中
L.right_kernel().basis()返回基础解系时,\(sagemath\)是把这些解向量作为行向量组合成一个新的矩阵,那么就有\(LS^T=O\pmod{p}\),转化一下就是\(S=\left[\begin{matrix}I_{100} & K_{100×100}\end{matrix}\right]\)。
为了满足规约要求,我们令\(M\)落在以\(S\)为格基的格上面。设\(\vec{c}_{1×100}\cdot S_{100×200}=M_{1×200}\),需要注意,\(LLL\)算法依赖于欧式范数,因此\(S\)要建立在\(\mathbb{Z}\)上进行的,我们设前\(100\)个元素是我们想要的\(M_1\),即\(\vec{c}\cdot \left[\begin{matrix}I_{100} & K\end{matrix}\right]=\left[\begin{matrix}M_1 & \vec{c}\cdot M_1\end{matrix}\right]\),其中\(c\cdot M_1\)与\(M_2\)一定不会相等,此时需要利用上\(\pmod{p}\)的条件了。
建立\(S'_{200×200} = \left[\begin{matrix}I_{100} & K_{100×100}\\O_{100} & p\cdot I_{100}\end{matrix}\right]\),此时有\(\vec{c}_{1×200}\cdot S'=\left[\begin{matrix}M_1 & M_2\end{matrix}\right]\),此时就是\(M_2=M_1\cdot K-kp\),严格符合其中的关系式,对\(S'\)进行规约找到足够短的向量\(M\)即可。
exp.sage
from Crypto.Util.number import *
import subprocess
from re import findall
def flatter(M):
# compile https://github.com/keeganryan/flatter and put it in $PATH
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = subprocess.check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))
n = 10
p = ...
A = ...
B = ...
A = Matrix(Zmod(p), n, n, A)
B = Matrix(Zmod(p), n, n, B)
L = Matrix(Zmod(p), n**2, 2 * n**2)
# M1 * A - B * M2 = O (mod p)
for i in range(n):
b = B.row(i)
for j in range(n):
a = A.column(j)
for k in range(n):
L[n * i + j, n * i + k] = a[k]
L[n * i + j, n**2 + n * j + k] = -b[k]
S = Matrix(ZZ, L.right_kernel().basis())
# print(XY.dimensions()) # (100, 200)
# print(L * M.transpose()) # O
bottom = block_matrix([[zero_matrix(ZZ, n**2, n**2), p * identity_matrix(ZZ, n**2)]])
S = S.stack(bottom)
M = flatter(S)[0]
print("LLL done.")
M1 = M[:100]
M2 = M[100:]
# flag-first half
flag = b''
for c in M1.list():
flag += bytes([abs(c)])
# flag-second half
M2 = M2.list()
for i in range(n):
for j in range(n):
flag += bytes([abs(M2[i + n * j])])
print(flag)
# NSSCTF{LLL111lll_1n_th3_k3rn3L!}
Matrix1_v2
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
n = 5
p = getPrime(512)
M1 = Matrix(Zmod(p), n, n, pad(flag[:len(flag)//2],n^2))
M2 = Matrix(Zmod(p), n, n, pad(flag[len(flag)//2:],n^2))
A = random_matrix(Zmod(p), n, n)
B = M1*A*M2
print("p =", p)
print("A =", A.list())
print("B =", B.list())
analysis
与Matrix1相比,由于\(M_2\)没有求逆操作,所以没有办法移项变成只含一次项的线性操作。由于 \(M_1\)、\(M_2\) 都是 flag 编码矩阵,其中数值都在 \(0 \sim 255\),所以它们相乘的二次项是一个 \(0 \sim 65025\) 的量,相对于 \(512\) 位的 \(p\) 依然是一个极小量。因此,核心思路是换元:将矩阵乘法中的二次项换元成一次项,利用格规约求出所有二次项。
我们可以令 \(y^{(i,j)}_{u,v} = m_{1_{i,u}} m_{2_{v,j}}\)。这样对于每一个固定的 \((i,j)\),都会产生 \(n^2 = 25\) 个未知数,总共有 \(n^2 \times n^2 = 625\) 个未知数。由于 \(A\) 是已知的,这实际上构成了 \(25\) 个带有 \(625\) 个未知数的子集和问题(Subset Sum / Knapsack)。
我们可以利用 CVP(最近向量问题)思想,将所有 \(n^2\) 个等式收集起来,构造一个维度为 \(651 \times 651\) 的大格(\(625\) 个变量 \(+ 25\) 个模数约束 \(+ 1\) 个目标向量)。通过设定极大的权重 \(K\),强迫等式严格成立,进而规约出长度极短的解向量,也就是所有的二次项 \(y^{(i,j)}_{u,v}\)。
恢复 \(M_1\) 与 \(M_2\):
得到所有的二次项后,注意到 \(B\) 中同一行的所有数值其实是被相同的 \(M_1\) 行向量和不同的 \(M_2\) 的值加密的。具体来说,对于固定的 \(i\)(行)和 \(u\)(列),我们收集所有的 \(j\) 和 \(v\) 对应的二次项:
因为 \(y^{(i,j)}_{u,v} = m_{1_{i,u}} m_{2_{v,j}}\),所以:
由于 \(M_2\) 中的值由 flag 字符和随机填充组成,其整体的最大公约数极大可能为 \(1\)。因此,直接对其求 GCD 即可精准恢复出 \(m_{1_{i,u}}\)。
恢复整个 \(M_1\) 后,利用矩阵求逆即可恢复 \(M_2\):
exp.sage
import subprocess
from re import findall
def flatter(M):
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = subprocess.check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))
n, p = 5, ...
A = ...
B = ...
A, B = Matrix(Zmod(p), n, n, A), Matrix(Zmod(p), n, n, B)
K = 2**512
L = identity_matrix(ZZ, n**4 + n**2 + 1)
for k in range(n**2):
for l in range(n**2):
L[k * n**2 + l, n**4 + k] = K * int(A.list()[l])
L[n**4 + k, n**4 + k] = K * p
L[-1, n**4 + k] = -K * int(B.list()[k])
M = flatter(L)
print("LLL done.")
Y = next(row for row in M if abs(row[-1]) == 1)
Y = Y if Y[-1] == 1 else -Y
M1 = Matrix(Zmod(p), n, n, [reduce(gcd, [abs(Y[(i*n+j)*n**2 + u*n+v]) for j in range(n) for v in range(n)]) for i in range(n) for u in range(n)])
M2 = (M1 * A).inverse() * B
print(bytes(map(int, M1.list())))
print(bytes(map(int, M2.list())))
# b'NSSCTF{M@k3_N0nq\xbbx\xca\x96&\xf7E\xaa\xee'
# b'_L1ne@r_L1ne@r!}\x97\x9a_\x94\x82\xd5\xbd:E'
# NSSCTF{M@k3_N0n_L1ne@r_L1ne@r!}
Matrix2
task.sage
from Crypto.Util.number import *
from Crypto.Cipher import AES
from random import randint
from hashlib import md5
from secret import flag
n = len(flag)
p = getPrime(256)
secret = [randint(1, p) for i in range(n)]
def encrypt(n, p, msg):
while(1):
L = []
R = []
for i in range(n):
temp = random_vector(Zmod(p), n)
L.append(temp)
R.append(msg[i] * temp)
L = Matrix(Zmod(p), L).T
R = Matrix(Zmod(p), R).T
if(L.rank() == n and R.rank() == n):
return (R * L^(-1)) ^ (bytes_to_long(b"Tequila")) # ^ 表示幂操作,而非按位异或
print("p =", p)
print("C =", encrypt(n, p, vector(Zmod(p), secret)).list())
print("enc =", AES.new(key=md5(str(sum(secret)).encode()).digest(), nonce=b"Tiffany", mode=AES.MODE_CTR).encrypt(flag))
analysis
令\(B\equiv R\cdot L^{-1}\pmod{p}\),则有
其中\(s_i\)为\(B\)的特征值,\(\vec{v_i}\)是属于\(s_i\)的特征向量。由[[第七章 线性变换]]易知,存在可逆矩阵\(X\)使得\(X^{-1}BX=S(S=\left[\begin{matrix}s_1 & & & & \\ & s_2 & & & \\ & & & \ddots & \\ & & & & s_n\end{matrix}\right])\),于是有:
由于\(S\)是对角矩阵,那么\(S^e=\left[\begin{matrix}s_1^e & & & & \\ & s_2^e & & & \\ & & & \ddots & \\ & & & & s_n^e\end{matrix}\right]\),进而可以推断\(C\)的特征值就是\(S^e\),那么最后的关键一步就是通过求解\(C\)的特征值得到\(s_i^e\)之后还原得到\(s_i\)。即\(n\)为素数的\(RSA\)。
exp.sage
from Crypto.Cipher import AES
from Crypto.Util.number import bytes_to_long
import hashlib
p = ...
C = ...
enc = b'\xc6\x81\xac\x05\x88\x9b[U\xa6xuI@\xc0\r\x88\xf3\x0e\x82|\x8d\xa2\x84rs\xdc\xadDj-\x9a\x99\xf2\x17\x9c'
e = bytes_to_long(b"Tequila")
# print(len(C))
n = 35
# C = Matrix(Zmod(p), n, n, C)
C = Matrix(GF(p), n, n, C)
eigenvalues = C.eigenvalues()
# print(eigenvalues)
secret = []
d = pow(e, -1, p - 1)
for se in eigenvalues:
si = int(pow(se, d, p))
secret.append(si)
cipher = AES.new(key=hashlib.md5(str(sum(secret)).encode()).digest(), nonce=b'Tiffany', mode=AES.MODE_CTR)
flag = cipher.decrypt(enc)
print(flag)
# b'NSSCTF{A_S1mPl3_e1g3nVa1ue5_tr1ck!}'
Matrix3
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
n = 10
p = getPrime(256)
M = Matrix(Zmod(p), n, n, pad(flag,n^2))
e = Matrix(Zmod(p), n, n, pad(b"",n^2))
W = random_matrix(Zmod(p), n, n)
X = random_matrix(Zmod(p), n, n)
Y = random_matrix(Zmod(p), n, n)
Z = random_matrix(Zmod(p), n, n)
C = W*X*M*Y*Z+e
print("p =", p)
print("W =", W.list())
print("X =", X.list())
print("Y =", Y.list())
print("Z =", Z.list())
print("C =", C.list())
analysis
\(LWE\)类型的问题为\(C=A\vec{x}+\vec{e}\),但是针对于这道题目而言,\(M\)本身是一个\(10×10\)的矩阵,并且还右乘了矩阵\(B\)。
如果尝试直接进行化简,那么就会有下面的问题存在\(A^{-1}CB^{-1}\equiv M+A^{-1}\vec{e}B^{-1}\pmod{p}\)。此时,噪声项变为了\(A^{-1}\vec{e}B^{-1}\),它不再满足于远小于\(p\)的条件。
我们参考Matrix1的求解策略,将上述条件分解称为线性方程组:
这就意味着,如果我们把\(M\)和\(C\)按照从左到右、从上到下的顺序展平为长度为\(100\)的行向量\(\vec{m}\)和\(\vec{c}\),那么\(M_{k_1,k_2}\)对\(C_{i,j}\)的贡献系数就是\(a_{i,k_1}\cdot b_{k_2,j}\)。
据此构建出系数矩阵\(K\),使得在模\(p\)意义下满足:
由于\(\vec{m}\)和\(\vec{e}\)的每个元素都在\([0,255]\)之间,他们都是极短的向量,我们将上述等式转化为整数环上的严格等式:
由此,我们构造格:
exp.sage
from re import findall
from subprocess import check_output
def flatter(M):
# compile https://github.com/keeganryan/flatter and put it in $PATH
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))
p = ...
W = ...
X = ...
Y = ...
Z = ...
C = ...
n = 10
W = Matrix(Zmod(p), n, n, W)
X = Matrix(Zmod(p), n, n, X)
Y = Matrix(Zmod(p), n, n, Y)
Z = Matrix(Zmod(p), n, n, Z)
C = Matrix(Zmod(p), 1, n**2, C)
A = W * X
B = Y * Z
A = A.lift() # Zmod(p) --> ZZ
B = B.lift()
C = C.lift().list()
K = Matrix(ZZ, n**2, n**2)
for i in range(n):
for j in range(n):
row = i * n + j
for k1 in range(n):
for k2 in range(n):
col = k1 * n + k2
K[row, col] = A[i, k1] * B[k2, j]
"""
[I(100) K.T O(100×1)]
[O(100×1) pI(100) O(100×1)]
[O(1×100) -C weight]
"""
L = block_matrix(ZZ, [
[identity_matrix(ZZ, n**2), K.transpose(), zero_matrix(ZZ, n**2, 1)],
[zero_matrix(ZZ, n**2, n**2), p * identity_matrix(ZZ, n**2), zero_matrix(ZZ, n**2, 1)],
[zero_matrix(ZZ, 1, n**2), Matrix(ZZ, 1, n**2, C), 255]
])
L = flatter(L)
print("LLL done.")
for row in L:
if abs(row[-1]) == 255:
if row < 0:
row = -row
m = row[:n**2]
print(bytes(m))
# b'NSSCTF{fl@tt3n_4nd_LWE!}'
Matrix4
task.sage
from Crypto.Util.number import *
from random import randint
from secret import flag
n = len(flag)
p = getPrime(256)
def encrypt(n, p, msg):
while(1):
L = [msg]
R = [randint(1, p) * msg]
for i in range(n-1):
temp = random_vector(Zmod(p), n)
L.append(temp)
R.append(randint(1, p)*temp)
L = Matrix(Zmod(p), L).T
R = Matrix(Zmod(p), R).T
if(L.rank() == n and R.rank() == n):
return (R*L^(-1))^(bytes_to_long(b"Tequila"))
print("p =", p)
print("C =", encrypt(n,p,vector(Zmod(p),flag)).list())
analysis
特征向量与特征值为:\(A\cdot\vec{v}=\lambda\cdot\vec{v}\),这正对应着我们这道题目的数学逻辑,其中矩阵\(C'\)的特征值为\(k_1,k_2,\cdots,k_n\),并且在Matrix2中我们分析得到\(C\)本身的特征值为\(k_1^e,k_2^e,\cdots,k_n^e\)。
其对应的特征向量就是\(K\cdot L\),为什么这里有一个\(K\),是因为由特征值求解特征向量的时候,我们求解的向量可能并非是我们想要的目标向量,而是目标向量的整数倍,而我们期望获得的就是其中的\(\vec{m}\)或者说是矩阵\(L\),且顺序并非我们期望的那样,我们只能说我们想要的目标向量的整数倍是求解出来的特征向量矩阵的一个向量。
Matrix2就是flag就是隐藏在特征值中,但是这道题目,我们需要其中的特征向量了。
若已知\(k\cdot\vec{m}\equiv\vec{c}\pmod{p}\),我们有以下两种方式求解\(\vec{m}\):
- \(k\cdot m_i\equiv c_i\pmod{p}\rightarrow m_i=k_i\cdot p + k^{-1}\cdot c_i\),建立\(L=\left[\begin{matrix}c_1 & p & 0 & \cdots & 0 & 0\\c_2 & 0 & p & \cdots & 0 & 0\\c_3 & 0 & 0 & \cdots & 0 & 0\\\vdots & \vdots & \vdots & \ddots & \vdots & \vdots\\c_{n-1} & 0 & 0 & \cdots & p & 0\\c_n & 0 & 0 & \cdots & 0 & p\end{matrix}\right]_{n×(n+1)};\vec{b}=\left[\begin{matrix}k^{-1}\\k_1\\k_2\\\vdots\\k_{n-1}\\k_{n}\end{matrix}\right]\rightarrow L\cdot\vec{b}=\left[\begin{matrix}m_1\\m_2\\m_3\\\vdots \\m_{n-1}\\m_{n}\end{matrix}\right]\)
flag=NSSCTF{***},那么我们可以针对于我们得到的每一个特征向量,直接利用同余关系进行求解\(k\),判定条件为求解出的\(k\)是否相同或v[1] == v[2],筛选出\(k\cdot\vec{m}\)后,乘以逆元即可。
p = ...
C = ...
# print(len(C))
n = 42
C = Matrix(Zmod(p), n, n, C)
# values = C.eigenvalues()
result = C.eigenvectors_right()
# print(len(result[0])) # 3 --> (lambda, vectors, multipplicity)
vectors = [vector(Zmod(p), v[1][0]) for v in result]
# print(vectors)
"""
# Solve one
m = []
for v in vectors:
if v[1] == v[2]:
k = v[1] * pow(ord('S'), -1, p)
k_inv = pow(k, -1, p)
for i in v:
mi = i * k_inv % p
m.append(mi)
print(bytes(m))
"""
# Solve two
for v in vectors:
L = zero_matrix(ZZ, n, n + 1)
for i in range(n):
L[i, 0] = v[i]
L[i, i + 1] = p
M = L.transpose().LLL()
print("LLL done!")
for m in M:
if abs(m[0]) == ord('N'):
if m < 0:
m = -m
for mi in m:
print(chr(mi), end="")
exit()
# b'NSSCTF{AnotHer_S1mPl3_EIGeNVecT0Rs_tr1ck!}'
Matrix5
task.sage
from Crypto.Util.number import *
from random import randint
from secret import flag
n = len(flag)-4
p = getPrime(256)
def encrypt(n, p, msg):
while(1):
L = []
R = []
for i in range(n):
temp = vector(Zmod(p), [randint(1,p) for _ in range(n-5)] + list(msg[i:i+5]))
L.append(temp)
R.append(randint(1, p) * temp)
L = Matrix(Zmod(p), L).T
R = Matrix(Zmod(p), R).T
if(L.rank() == n and R.rank() == n):
return (R*L^(-1)) ^ (randint(1, p))
print("p =", p)
print("C =", encrypt(n,p,vector(Zmod(p),flag)).list())
analysis
这道题目很像Matrix4,\(C'/C\)对应的特征值为\(L\)与\(R\)的过渡参数,同时,对于重复地进行不塌缩(满秩)的线性变换,特征向量生成的空间本身并不会发生改变。那么问题就转化为:如果我们得到了目标特征向量的\(k\)倍,该如何进行求解?这里由于flag分散在了所有的特征向量中,我们无法通过flag的格式一次性确定所有的\(k\),因此只能使用Matrix4中提到的方法一了,不过需要注意的是\(a_{i,j}\in[1,p-1]\),这会导致我们要求解的目标向量过长,不过我们可以单拎出来,只求解最后五个元素。
其中还有一个小问题就是,我们得到的特征向量是乱序的,我们需要利用前一个目标向量与后一个目标向量存在\(4\over5\)重复的字符这个特性进行一下排序搜索。
exp.sage
from tqdm import tqdm
p = ...
C = ...
# print(len(C)) # 2209
n = 47
C = Matrix(Zmod(p), n, n, C)
result = C.eigenvectors_right()
vectors = [vector(Zmod(p), v[1][0]) for v in result]
"""
# Solve one
flag = []
for i in tqdm(range(n)):
v = vectors[i]
L = Matrix(ZZ, 5, 6)
for i in range(5):
L[i, 0] = v[n - 5 + i]
L[i, i + 1] = p
M = L.transpose().LLL()
for m in M:
if abs(m[-1]) >= 9 and abs(m[-1]) <= 126: # abs(m[0]) >= 9 and abs(m[0]) <= 126
if m < 0:
m = -m
flag.append(m)
charset = [bytes(block) for block in flag]
temp = b'NSSC'
ans = b'NSSC'
for i in range(n):
for j in charset:
if j.startswith(temp):
ans += bytes([j[-1]])
temp = bytes(j[1:])
print(ans)
"""
# solve two
flag = b'NSSCTF{'
for i in range(n - 3):
for v in vectors:
left = (v[-5] - v[-4]) * pow(flag[-4] - flag[-3], -1, p) % p
right = (v[-4] - v[-3]) * pow(flag[-3] - flag[-2], -1, p) % p
if left == right:
k = left
flag += bytes([v[-1] * pow(k, -1, p) % p])
print(flag)
# b'NSSCTF{U_h4Ve_C0mp1et3d_H@lF_0f_MATRIX_cha1LenGe5!}'
其实这里后面也想了一下,
Matrix4的方法二应该也是行得通的,利用\(4\over5\)重复字符进行差分选取,例如我们要找到第一个向量,并且求出\(k\)。我们可以利用NSSCTF{}中的S出现了两次,找到第一个向量,之后根据\(k_i\cdot [m_i\cdots m_{i+5}]\)其中存在已知的四个字符,\(k\equiv (v[1]-v[0])\cdot(m[1]-m[0])^{-1}\equiv (v[2]-v[1])\cdot(m[2]-m[1])^{-1}\pmod{p}\),如果通过右边的同余式求出来的\(k\)是相等的,那么就意味着这是下一个目标向量了,但是这里的差分比例可能会重复,对于这道题目就是,当临时结尾是enGe5,会与et3d_发生碰撞,最好的方法就是增加差分条件来降低差分碰撞的概率。
这个方法适用于\(L\)至少包含\(3\)个小元素的情况。
Matrix6
task.sage
from Crypto.Util.number import *
from random import randint
from secret import flag
polyeval = lambda poly, x: sum(a * pow(x, i, n) for a, i in poly) % n
p = getPrime(256)
q = getPrime(256)
n = p * q
e = 1337
m = bytes_to_long(flag)
nums = 10
g = [(randint(1, n), i) for i in range(nums)]
F = []
C = []
for i in range(nums + 1):
f = [(randint(1, n), (randint(1, n))) for i in range(nums)]
F.append(f)
C.append(polyeval(g, polyeval(f, m)))
print("n =", n)
print("c =", pow(m, e, n))
print("F =", F)
print("C =", C)
analysis
polyeval(poly, x):接收一个列表poly(包含系数和指数的元组(a, i))和一个变量x,计算逻辑为\(\sum(a\cdot x^i)\pmod{n}\)
g:定义了一个最高次幂为\(9\)的秘密多项式,其中的系数都是未知的,在\(1\)~\(n\)之间。
F:变换集合,循环执行了\(11\)次,每次循环生成一个列表\(f\),包含\(10\)个(随机数, 随机数)的元组。\(f\)定义了一个包含\(10\)项的表达式,其系数和指数均为随机大数。
将明文\(m\)代入\(f\)得到一个中间值,再将中间值代入\(g\)计算,结果存入列表\(C\)中。
这道题目如果没有其中中间值的这一步干扰,那么我们从\(10\)组\((x,y)\)中就能通过拉格朗日插值法求解出未知的\(10\)个系数,但是当前我们没有办法直接获取得到中间值。
这就是一个\(P[x]_9\)线性空间中的一个多项式元素,我们可以将\(m\)视作常数,则上述关于\(m\)的多项式就转化为了关于\((g_0,g_1,\cdots,g_9)\)的线性方程,\(11\)个线性方程组成线性方程组。
上述推导中我将其转化为一个线性齐次方程组的问题,也可以将其转化为非线性齐次方程组的问题,\(L\)矩阵是此时的增广矩阵。
由于这\(11\)个点是由同一个\(9\)次多项式求值得到的,这意味着这\(11\)个行向量线性相关。因此,在正确的明文\(m\)下,该矩阵的行列式必然为\(0\):
\((1)\)是一个正确\(m\)作为输入下的多项式,接下来就是\(RSA\)的使用了,由\(RSA\)的相关数据可知:
\((1);(2)\)这两个多项式的根均为\(m\),将未知的\(m\)泛化为变量\(x\),可以求解两个多项式的最大公因式,之后的求根,公因式的次数不会太高,在计算机上完成求解是现实的,但是\((1)\)的计算过程中由于指数太大了(\(b_{k,j}\in[1,n-1]\)),直接表示出来是不现实的,但是可以进行商换降次:
通过这个替换,原本极其复杂的\(f_k(m)\)被转化为了一个关于未知数\(m\)的、最高次幂不超过\(e-1\)(即\(1336\)次)的多项式,降次后的多项式记为\(R_k(m)\)。
在这个多项式环中,任何\(x^e\)被替换为常数\(c\),将\(F\)代入:
正确的\(m\)一定是多项式\(D(x)\equiv0\pmod{n}\)的一个根。
exp.sage
from Crypto.Util.number import long_to_bytes
n = ...
c = ...
F = ...
C = ...
e = 1337
num = 10
Z_n = Zmod(n)
P.<x> = PolynomialRing(Z_n)
Q.<y> = P.quotient(x**e - c)
L = []
for k in range(num + 1):
# f_k(y) = a_{k,j} * c**(q) * y**(r)
f_k_y = sum([
Z_n(a) * Z_n(pow(int(c), i // e, int(n))) * y**(i % e)
for a, i in F[k]
])
row = [f_k_y**j for j in range(num)]
row.append(Q(C[k]))
L.append(row)
L = Matrix(Q, L)
det_y = L.det()
D_x = P(det_y.lift())
a = x**e - c
b = D_x
while b != 0:
b = b.monic()
a, b = b, a % b
# print(a)
print(long_to_bytes(int(-a[0])))
# b'NSSCTF{L@graNge_InterP0lati0n_w1Th_QUoT1enT}'
Matrix7
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
def F(A):
res = Matrix(Zmod(p), n, n)
for i in range(len(f)):
res += A^(i+1) * f[i]
return res
n = 10
p = getPrime(256)
f = [Matrix(Zmod(p), n, n, pad(flag, n^2))] + [random_matrix(Zmod(p), n, n) for i in range(n-1)]
C = []
for i in range(10):
A = random_matrix(Zmod(p), n, n)
C.append([A.list(), F(A).list()])
print("p =", p)
print("C =", C)
analysis
f:
F(A):
已知\(10\)组\((A, F(a))\),求解\(F\)。
针对于\(n\)组\((x,y)\)的点对,我们可以确定\(9\)次未知系数的多项式;对于\(n\)组\((f(x),g(x))\)的点对也是如此;而这道题目也是同样的道理,将\(f\)视作系数,已知\(10\)组点对,插值求解系数即可。
多项式插值问题,用矩阵去表示的话我们可以表示为范德蒙矩阵:
求解线性方程组即可还原得到\(F\),进而恢复\(M\)。
exp.sage
p = ...
C = ...
n = 10
As = []
Cs = []
for temp in C:
A = Matrix(Zmod(p), n, n, temp[0])
As.append(A)
Cs.append(Matrix(Zmod(p), n, n, temp[1]))
L = []
R = []
for i in range(n):
row = [As[i]**(j+1) for j in range(n)]
L.append(row)
R.append([Cs[i]]) # R.append(Cs[i])
L = block_matrix(L)
R = block_matrix(R)
F = L.solve_right(R)
M = F.submatrix(0, 0, n, n)
flag = bytes([int(mi) for mi in M.list()])
print(flag)
# NSSCTF{M@tRiX_L4granGe_InterP0lati0n_1Sn't_H4rD}
Matrix8
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
def F(A):
res = Matrix(Zmod(p), n, n)
for i in range(len(f)):
res += A^(i + 1)*f[i]
return res
n = 7
p = getPrime(256)
f = [Matrix(Zmod(p), n, n, pad(flag, n^2)) for i in range(n)]
C = []
for i in range(1):
A = random_matrix(Zmod(p), n, n)
C.append([A.list(), F(A).list()])
print("p =", p)
print("C =", C)
analysis
这道题目与Matrix7逻辑基本一致,主要的不同在于:
- \(n=7\),但是这次我们只有一组\((A, F(A))\)。
f = [Matrix(Zmod(p), n, n, pad(flag, n^2)) for i in range(n)]的生成方式保证了系数\(f_i\)存在相同的部分,即为\(\left[\begin{matrix}m_1 & m_2 & \cdots & m_7\\m_8 & m_9 & \cdots & m_{14}\\\vdots & \vdots & \ddots & \vdots\\m_{43} & m_{44} & \cdots & a_{49}\end{matrix}\right]\),随机的部分由填充的随机字节urandom(length - len(msg))决定的,但是大部分都是相同的。
对于待求系数而言,右乘系数,\(F\)对于\(F(A)\)的影响是按照列独立的,设\(x_{i,k}^{(v)}\)为\(f[i]\)的第\(k\)行、第\(v\)列的元素,方程可以写成:
对于每一列\(v\),有\(n^2\)个字节的未知数,对于每一列的系统,可以提取出系数矩阵\(M_{small}(7×49)\),以及目标向量\(Y_v\)(即\(F(A)\)的第\(v\)列),需要求解:
这里分析一下维度,\(X\)是\(49×1\)的\(49\)维列向量,\(Y_v\)是一个\(7\)维的列向量,相当于我们只有\(7\)个方程,要去解\(49\)个未知数,是欠定方程组,因此我们需要尝试用\(LLL\)去规约,不能直接解线性方程组去求解。
其中的\(X\)的元素都是\([0,127]\),符合常规题目的条件。
为了使得上界与目标向量相差更大,可以对\(L\)中的第二行(分块矩阵)进行权重调整,因为在目标向量中呈现的是\(0\),规约得到\(X\)。
\(sagemath\)分块矩阵的构建需要也要保证块是行列对应的,不能第一行\(3\)个块,第二行\(4\)个块。
exp.sage
from tqdm import tqdm
p = ...
C = ...
n = 7
W = 2**256
A = Matrix(Zmod(p), n, n, C[0][0])
FA = Matrix(Zmod(p), n, n, C[0][1])
As = [A**(i+1) for i in range(n)]
M_small = matrix(ZZ, n, n**2)
for u in range(n):
for i in range(n):
for k in range(n):
M_small[u, i * n + k] = ZZ(As[i][u, k])
flag = [0] * n**2
for v in tqdm(range(n)):
Y_v = vector(ZZ, n)
for u in range(n):
Y_v[u] = ZZ(FA[u, v])
dim = n**2 + n + 1
L = block_matrix([
[identity_matrix(ZZ, n**2), zero_matrix(ZZ, n**2, n), zero_matrix(ZZ, n**2, 1)],
[W * M_small, W * p * identity_matrix(ZZ, n), -W * Y_v],
[zero_matrix(ZZ, 1, n**2), zero_matrix(ZZ, 1, n), Matrix(ZZ, 1, 1, [1])]
])
L = L.transpose().LLL()
for row in L:
if abs(row[-1]) == 1:
if all(row[n**2 + j] == 0 for j in range(n)):
X = row[:n**2]
if row[-1] == -1:
X = -X
for k in range(n):
flag[k * n + v] = int(X[k])
print(bytes(flag))
# b'NSSCTF{Y3t_4n0Th3r_L@graNge_InterP0lati0n_TasK}'
Matrix8_v2
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
def F(A):
res = Matrix(Zmod(p), n, n)
for i in range(len(f)):
res += A^(i+1)*f[i]
return res
n = 15
p = getPrime(256)
f = [Matrix(Zmod(p), n, n, pad(flag,n^2)) for i in range(n)]
C = []
for i in range(1):
A = random_matrix(Zmod(p), n, n)
C.append([A.list(),F(A).list()])
print("p =", p)
print("C =", C)
analysis
Matrix8_v2版本相较于Matrix8而言,维度增大了,那么就意味着按照这个方法进行规约的矩阵的维度是\(n^2+n+1=241\),也是能够接收的一个规约维度,逐列规约\(n=15\)次。
exp.sage
from tqdm import tqdm
import subprocess
from re import findall
def flatter(M):
# compile https://github.com/keeganryan/flatter and put it in $PATH
z = "[[" + "]\n[".join(" ".join(map(str, row)) for row in M) + "]]"
ret = subprocess.check_output(["flatter"], input=z.encode())
return matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\\d+", ret)))
p = ...
C = ...
n = 15
W = 2**256
A = Matrix(Zmod(p), n, n, C[0][0])
FA = Matrix(Zmod(p), n, n, C[0][1])
As = [A**(i+1) for i in range(n)]
M_small = matrix(ZZ, n, n**2)
for u in range(n):
for i in range(n):
for k in range(n):
M_small[u, i * n + k] = ZZ(As[i][u, k])
flag = [0] * n**2
for v in tqdm(range(n)):
Y_v = vector(ZZ, n)
for u in range(n):
Y_v[u] = ZZ(FA[u, v])
dim = n**2 + n + 1
L = block_matrix([
[identity_matrix(ZZ, n**2), zero_matrix(ZZ, n**2, n), zero_matrix(ZZ, n**2, 1)],
[W * M_small, W * p * identity_matrix(ZZ, n), -W * Y_v],
[zero_matrix(ZZ, 1, n**2), zero_matrix(ZZ, 1, n), Matrix(ZZ, 1, 1, [1])]
])
L = flatter(L.transpose())
for row in L:
if abs(row[-1]) == 1:
if all(row[n**2 + j] == 0 for j in range(n)):
X = row[:n**2]
if row[-1] == -1:
X = -X
for k in range(n):
flag[k * n + v] = int(X[k])
print(bytes(flag))
# NSSCTF{d1m3nSi0n_1s_n0T_A_pr0B13m_FOr_th1S_T@sK}
Matrix9
task.sage
from Crypto.Util.number import *
from Crypto.Cipher import AES
from hashlib import md5
from secret import flag
n = 10
A = random_matrix(GF(2), n, n)
PR.<x> = PolynomialRing(GF(2))
B = PR.random_element(degree=n)(A)
print("B =", B.list())
print("enc =", AES.new(key=md5(str(A).encode()).digest(), nonce=b"Tiffany", mode=AES.MODE_CTR).encrypt(flag))
analysis
\(A\in M_n(\mathbb{F}_2)\),整到题目的目的就是恢复这个\(n×n\)的随机矩阵。
PR.<x>:定义了了\(\mathbb{F}_2\)上单位多项式环,其系数只能是\(0/1\)。
B = PR.random_element(degree=n):生成一个次数为\(n=10\)的随即多项式\(P(x)\in\mathbb{F}_2[x]\),形式为:
(A):将矩阵\(A\)代入多项式\(P(x)\)中,在数学上,这意味着把自变量\(x\)替换为矩阵\(A\),常数项\(c_0\)替换为\(c_0\cdot I\):
已知\(B\),该如何求解\(A\)?
- 由于多项式\(P(x)\)的次数为\(10\),且系数都在\(\mathbb{F}_2\)中,那么可能的多项式总数只有\(2^{11}=2048\)种,如果要求最高次项次数为\(1\),则只有\(2^{10}=1024\)种,可以直接爆破多项式\(P(x)\),进而求解\(A\)。
- 因为\(B=P(A)\),\(A\)和B必然是可交换的,即:\(AB=BA=AP(A)=c_nA^{n+1}+c_{n-1}A^n+\cdots+c_1A^2+c_0A\),一旦爆破固定了某个\(P(x)\),方程\(B=P(A)\)结合\(AB=BA\)就能组成一个关于\(A\)的线性方程组。由于\(n=10\)非常小(\(A\)只有\(100\)个未知比特),通过可交换矩阵的中心化子\(subspace\)可以解出\(A\).
中心化子
群\(G\)中元素\(z\)的中心化子是\(G\)中与\(z\)可交换的所有元素的集合:\(C_G(z)=\{x\in G,xz=zx\}\)。
同样地,群\(G\)的子群\(H\)的中心化子是\(G\)中与\(H\)的每个元素都可交换的所有元素的集合:\(C_G(H)=\{x\in G,\forall h\in H, xh=hx\}\)。
中心化子总是包含群的群中心,并且被包含在相应的正规化子中。在阿贝尔群中,中心化子就是整个群。
这道题目中所说的中心化子其实就是该线性变换的核,因为在\(\mathbb{F}_2\)中,矩阵可交换意味着:
我们需要用变换后的基来表示出线性变换\(M=XB+BX(XB-BX)\),也就是克罗内克基,怎么去模拟出线性变换\(\mathscr{M}\)所对应的矩阵,用\(n×n\)矩阵对每个位置单独的变换v[i]=1,存入\(M\)的每一列即可。
exp.sage
from Crypto.Cipher import AES
from hashlib import md5
import itertools
B = ...
enc = ...
n = 10
MS = MatrixSpace(GF(2), n, n)
B = MS(B) # B = Matrix(GF(2), n, n, B)
M = matrix(GF(2), n**2, n**2) # 100 × 100
"""
克罗内克基
枚举基底的映射结果来构造变换矩阵,模拟 XB+BX 这一线性变换
"""
for i in range(n**2):
v = [0] * (n**2)
v[i] = 1
X = MS(v) # Matrix(GF(2), n, n, v)
# XB + BX
res = X * B + B * X
M[:, i] = vector(GF(2), res.list())
ker = M.right_kernel()
dim = ker.dimension()
print(f"dim = {dim}")
basis = ker.basis()
for coeffs in itertools.product([0, 1], repeat=dim):
# 以零空间的基为基础,尝试其所有的系数组合
X = vector(GF(2), n**2)
for c, b in zip(coeffs, basis):
if c == 1:
X += b
A = MS(X.list())
key = md5(str(A).encode()).digest()
cipher = AES.new(key=key, nonce=b"Tiffany", mode=AES.MODE_CTR)
flag = cipher.decrypt(enc)
if flag.startswith(b'NSSCTF{'):
print(flag)
break
# b'NSSCTF{U31nG_C0mmut@t1vE_and_Bru7eF0rCe!}'
Matrix9_v2
task.sage
from Crypto.Util.number import *
from os import urandom
from secret import flag
def pad(msg, length):
return msg + urandom(length - len(msg))
n = 10
p = getPrime(256)
A = Matrix(Zmod(p), n, n, pad(flag,n^2))
PR.<x> = PolynomialRing(Zmod(p))
B = PR.random_element(degree=n)(A)
print("p =", p)
print("B =", B.list())
analysis
Matrix9_v2相较于Matrix9而言,区别就在于参数矩阵以及多项式环的构建由\(\mathbb{F}_2\)变为了\(GF(p)\),由于上一题中,对于\(kernel\)的线性组合系数在\(\mathbb{F}_2\)中,爆破是现实的,而这道题目就不行了,但是我们知道\(A\)仍在\(XB-BA\)的\(kernel\)中,且相较于模\(p\)而言非常短。所以我们可以沿着原来的解题思路进行,不过在最后一步我们不再进行爆破基的系数,而是进行规约。
我们将未知矩阵\(A\)展平为一个长度为\(n^2=100\)的一维向量\(\vec{a}=\left[\begin{matrix}a_1 & a_2 & \cdots & a_{100}\end{matrix}\right]\in\mathbb{Z}\),因为\(A\)的每个元素都是字节且在\(XB-BX\)的右核中,设在\(\mathbb{Z}_p\)上求解出的基底为\(\vec{v_1},\vec{v_2},\cdots,\vec{v_d}(d=10)\),由此有:
这意味着在整数域\(\mathbb{Z}\)上,必然存在一组整数系数\(k_1,k_2,\cdots,k_d\)以及一组代表模\(p\)周期性的整数\(k_1,\cdots,k_{n^2}\),使得:
其中\(\vec{e_i}\)是标准的单位行向量,将\(a\)作为最短向量,建立\(SVP\)问题的格基:
注意这里\(R\)行>列,非满秩的,那么规约出的前面的向量是存在零向量\(\vec{0}\)的。
这里发现并不能完全规约出来我们需要的\(\vec{a}\),其中存在一些不正确的元素,
exp.sage
p = ...
B = ...
n = 10
MS = MatrixSpace(Zmod(p), n, n)
B = MS(B)
M = matrix(Zmod(p), n**2, n**2)
for i in range(n**2):
v = [0] * n**2
v[i] = 1
X = MS(v)
res = X * B - B * X
M[:, i] = vector(Zmod(p), res.list())
ker = M.right_kernel()
basis = ker.basis()
dim = ker.dimension()
# print(f"dim = {dim}") # dim = 10
L = block_matrix(ZZ, [
[Matrix(ZZ, basis)],
[p * identity_matrix(ZZ, n**2)]
])
L = L.LLL()
print("LLL done.")
for row in L:
if abs(row[1]) == ord('S'): # if abs(row[0]) == ord('N'):
# print(bytes([int(mi) for mi in row.list()])) # ValueError: bytes must be in range(0, 256)
for mi in row:
mi = abs(mi)
if mi > 32 and mi < 128:
print(chr(mi), end="")
else:
print("*", end="")
# *SSCTF{St1l*LLL111lllI*I_1n_th3_k*rn3L!}
# Right flag: NSSCTF{St1llLLL111lllIII_1n_th3_k3rn3L!}
这说明存在比\(\vec{a}\)更短的向量,原本想采用中心化的优化方法尝试优化一下(\(a_i=a_i'+127\)):
虽然并没有将\(\vec{v}\)的长度降下来,但是\(LLL\)算法的执行步骤就是在施密特正交化以及求解欧式范数下的最短向量,最终我们规约的结果就是\(\vec{a'}\),但是发现仍然不是最终正确的\(flag\)。
exp-pro.sage
p = ...
B = ...
n = 10
MS = MatrixSpace(Zmod(p), n, n)
B = MS(B)
M = matrix(Zmod(p), n**2, n**2)
for i in range(n**2):
v = [0] * n**2
v[i] = 1
X = MS(v)
res = X * B - B * X
M[:, i] = vector(Zmod(p), res.list())
ker = M.right_kernel()
basis = ker.basis()
dim = ker.dimension()
scale = 10000
temp = 127
L = matrix(ZZ, dim + n**2 + 1, n**2 + 1)
for i in range(dim):
L[i, :n**2] = vector(ZZ, [ZZ(x) for x in basis[i]])
for i in range(n**2):
L[dim + i, i] = p
for i in range(n**2):
L[dim + n**2, i] = temp
L[dim + n**2, n**2] = scale
L = L.LLL()
print("LLL done.")
for row in L:
if abs(row[n**2]) == scale:
shift = 1 if row[n**2] == scale else -1
print(bytes([int(row[i] * shift + temp) for i in range(n**2)]))
print(bytes([254 - int(row[i] * shift + temp) for i in range(n**2)]))
# ~SSCTF{St1l\x9cLLL111lllIyI_1n_th3_kcrn3L!}
# Right flag: NSSCTF{St1llLLL111lllIII_1n_th3_k3rn3L!}
另外就是利用\(Cayley-Hamilton\)定理去进行求解:文章列表 - NSSCTF
\(Cayley-Hamilton\)
设 \(A\) 是数域 \(P\) 上的 \(n×n\) 方阵,其特征多项式为\(f(λ)=det(λI−A)=λn+b1λn−1+…+bn−1λ+bn\)
则将矩阵 \(A\) 代入其特征多项式,得到的结果为零矩阵:\(f(A)=An+b1An−1+…+bn−1A+bnI=0\)
换句话说,矩阵本身是其特征多项式的“根”。

NSSCTF -- [tangcuxiaojkuai] -- Matrix
浙公网安备 33010602011771号