本文最后更新于:2023年8月28日 晚上
Noisy Chinese remaindering
本文主要介绍 noisy Chinese remaindering 问题,以及在SekaiCTF中的一道相关的题目。
问题重述
Problem (Noisy Chinese remaindering): 令 0 ≤ N ≤ B 0\le N\le B 0 ≤ N ≤ B ,且 p 1 , ⋯ , p n p_1,\cdots,p_n p 1 , ⋯ , p n 是互素的整数。给定n n n 个集合S 1 , ⋯ , S n S_1,\cdots,S_n S 1 , ⋯ , S n ,其中S i = { r i , j } 1 ≤ j ≤ m S_i=\{r_{i,j}\}_{1\le j\le m} S i = { r i , j } 1 ≤ j ≤ m 包括N m o d p i N\bmod p_i N m o d p i 和m − 1 m-1 m − 1 个噪声(Z p i \Z_{p_i} Z p i 中随机整数),要求恢复N N N ,已知解是唯一的。
求解思路
令P = ∏ i = 1 n p i P=\prod_{i=1}^np_i P = ∏ i = 1 n p i ,L i ≡ 1 ( m o d p i ) L_i\equiv1(\bmod p_i) L i ≡ 1 ( m o d p i ) 且 L i ≡ 0 ( m o d ∏ j ≠ i p j ) L_i\equiv0(\bmod\prod_{j\neq i}p_j) L i ≡ 0 ( m o d ∏ j = i p j ) ,那么根据中国剩余定理,
N ≡ ∑ i = 1 n ( N m o d p i ) L i ( m o d P ) N\equiv\sum\limits_{i=1}^n(N\bmod p_i)L_i\;(\bmod P)
N ≡ i = 1 ∑ n ( N m o d p i ) L i ( m o d P )
再线性化这个问题,令δ i , j = 1 \delta_{i,j}=1 δ i , j = 1 当 N ≡ r i , j ( m o d p i ) N\equiv r_{i,j}\;(\bmod p_i) N ≡ r i , j ( m o d p i ) ,否则等于0. 那么可以得到:
N ≡ ∑ i = 1 n ∑ j = 1 m δ i , j r i , j L i ( m o d P ) N\equiv\sum\limits_{i=1}^n\sum\limits_{j=1}^m\delta_{i,j}r_{i,j}L_i\;(\bmod P)
N ≡ i = 1 ∑ n j = 1 ∑ m δ i , j r i , j L i ( m o d P )
这其实是说 N N N 是 r i , j L i r_{i,j}L_i r i , j L i 的一个小的子集和。
如果按下面的方式造格子:
L = ( P 0 … … 0 r 1 , 1 L 1 B 0 … 0 r 1 , 2 L 1 0 B ⋱ ⋮ ⋮ ⋮ ⋱ ⋱ 0 r n , m L n 0 … 0 B ) L=\begin{pmatrix}P&0&\ldots&\ldots&0\\r_{1,1}L_1&B&0&\ldots&0\\r_{1,2}L_1&0&B&\ddots&\vdots\\\vdots&\vdots&\ddots&\ddots&0\\r_{n,m}L_n&0&\ldots&0&B\end{pmatrix}
L = ⎝ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎜ ⎛ P r 1 , 1 L 1 r 1 , 2 L 1 ⋮ r n , m L n 0 B 0 ⋮ 0 … 0 B ⋱ … … … ⋱ ⋱ 0 0 0 ⋮ 0 B ⎠ ⎟ ⎟ ⎟ ⎟ ⎟ ⎟ ⎟ ⎞
那么可以验证,
( k , δ 1 , 1 , δ 1 , 2 , ⋯ , δ n , m ) L = ( N , δ 1 , 1 B , δ 1 , 2 B , … , δ n , m B ) (k,\delta_{1,1},\delta_{1,2},\cdots,\delta_{n,m})L=(N,\delta_{1,1}B,\delta_{1,2}B,\ldots,\delta_{n,m}B)
( k , δ 1 , 1 , δ 1 , 2 , ⋯ , δ n , m ) L = ( N , δ 1 , 1 B , δ 1 , 2 B , … , δ n , m B )
并且目标向量被期望是最短向量。
SekaiCTF Noisier CRC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import secretsfrom Crypto.Util.number import *from Crypto.Cipher import AESfrom hashlib import sha256from flag import FLAG isIrreducible = [True for i in range (1 << 17 )]def init (): for f in range (2 , 1 << 17 ): if isIrreducible[f]: ls = [0 ] cur_term = f while cur_term < (1 << 17 ): ls = ls + [x ^ cur_term for x in ls] cur_term <<= 1 for g in ls[2 :]: isIrreducible[g] = False def getCRC16 (msg, gen_poly ): assert (1 << 16 ) <= gen_poly < (1 << 17 ) msglen = msg.bit_length() msg <<= 16 for i in range (msglen - 1 , -1 , -1 ): if (msg >> (i + 16 )) & 1 : msg ^= (gen_poly << i) return msgdef oracle (secret, gen_poly ): l = int (13.37 ) res = [secrets.randbits(16 ) for _ in range (l)] res[secrets.randbelow(l)] = getCRC16(secret, gen_poly) return resdef main (): init() key = secrets.randbits(512 ) cipher = AES.new(sha256(long_to_bytes(key)).digest()[:16 ], AES.MODE_CTR, nonce=b"12345678" ) enc_flag = cipher.encrypt(FLAG) print (f"Encrypted flag: {enc_flag.hex ()} " ) used = set ({}) for _ in range (int (133.7 )): gen_poly = int (input ("Give me your generator polynomial: " )) assert (1 << 16 ) <= gen_poly < (1 << 17 ) if not isIrreducible[gen_poly]: print ("Invalid polynomial" ) exit(1 ) if gen_poly in used: print ("No cheating" ) exit(1 ) used.add(gen_poly) print (oracle(key, gen_poly)) main()
题目允许我们传133个不同数,并且都得对应一个次数16的不可约多项式f i ( x ) f_i(x) f i ( x ) 。然后计算CRC16(key, f i ( x ) f_i(x) f i ( x ) ),并添加12个噪声给我们。需要恢复key。
这里首先要注意计算CRC16相当于一次代数运算。我们将一个数看成是一个多项式,是将这个数进行二进制表示,对应于一个多项式的系数。CRC16(key, f i ( x ) f_i(x) f i ( x ) )等价于计算 k e y ( x ) ∗ x 16 ( m o d f i ( x ) ) key(x)*x^{16}(\bmod f_i(x)) k e y ( x ) ∗ x 1 6 ( m o d f i ( x ) ) 对应的数。
但我们依然可以建立起类似下面的方程组:
N ≡ ∑ i = 1 n ∑ j = 1 m δ i , j r i , j L i ( m o d P ) N\equiv\sum\limits_{i=1}^n\sum\limits_{j=1}^m\delta_{i,j}r_{i,j}L_i\;(\bmod P)
N ≡ i = 1 ∑ n j = 1 ∑ m δ i , j r i , j L i ( m o d P )
但不一样的是,对r i , j L i r_{i,j}L_i r i , j L i 模 P P P 后进行乘以δ i , j \delta_{i,j} δ i , j 求和方程最高次数不增。因此如果不考虑最后512项,求和后的多项式系数都必须为0,从而建立起线性方程组。需要注意的是,∑ j = 1 m δ i , j ≡ 1 ( m o d 2 ) \sum\limits_{j=1}^m\delta_{i,j}\equiv 1(\bmod 2) j = 1 ∑ m δ i , j ≡ 1 ( m o d 2 ) 可以多增加n n n 个方程。
最后在方程的解空间里爆破求解。
exp written by shallow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 from pwn import *from functools import reducefrom Crypto.Util.number import *from Crypto.Cipher import AESfrom hashlib import sha256 io = remote('chals.sekai.team' ,'3006' ) io.recvuntil(b'Encrypted flag: ' ) flag = io.recvline()[:-1 ] PR.<x> = PolynomialRing(GF(2 ))def to_poly (m ): temp = bin (m)[2 :] temp = [int (i) for i in temp][::-1 ] return PR(temp)def to_bin (m ): temp = list (m)[::-1 ] temp = [str (i) for i in temp] return int ('' .join(temp) , 2 )def getres (p ): io.recvuntil(b'generator polynomial: ' ) io.sendline(str (p).encode()) res = eval (io.recvline()[:-1 ]) return resdef find_prime (): plist = [] for i in range (2 **16 , 2 **17 ): temp = to_poly(i) if temp.is_irreducible(): plist.append(i) return plist pnum = 133 plist = find_prime()[:pnum] datalist = []for i in range (133 ): datalist.append(getres(plist[i])) plist = [to_poly(i) for i in plist] data1 = []for i in range (133 ): temp = [(to_poly(j) * inverse_mod(x^16 , plist[i])%plist[i]) for j in datalist[i]] data1.append(temp) datalist = data1from functools import reduce pmul = reduce(lambda x, y: x * y, plist)def to_vec (data , p ): temp = data * (pmul//p) * inverse_mod(pmul//p , p) % pmul return vector(GF(2 ) , (list (temp) + [0 ] * (16 *pnum - len (list (temp))))[513 :]) keysize = 512 eqnum = 16 *pnum - keysize - 1 error = 13 M = Matrix(GF(2 ) , 0 , eqnum)for i in range (pnum): for j in range (error): v = to_vec(datalist[i][j] , plist[i]) M = M.stack(v) M2 = Matrix(GF(2 ), pnum*error , pnum)for i in range (pnum): for j in range (error): M2[i*error+j , i] = 1 M1 = block_matrix([[M , M2]]) v = vector(GF(2 ) ,[0 ]*(eqnum) + [1 ]*pnum) vlist = M1.left_kernel().matrix() solv = M1.solve_left(v)def check_sol (v ): res = [] for i in range (133 ): temp = [i for i in v[i*13 :i*13 +13 ]] if temp.count(1 ) == 1 : res.append(temp.index(1 )) else : return None return res def getsol (vlist , solv ): vlen = vlist.nrows() for i in range (2 **vlen): temp = solv choice = i for j in range (vlen): if choice & 1 : temp += vlist[j] choice //= 2 res = check_sol(temp) if res != None : return res res = getsol(vlist , solv) key = 0 for i in range (133 ): a = datalist[i][res[i]] p = plist[i] key += a* (pmul//p) * inverse_mod(pmul//p , p) % pmulprint (key)print (flag) key = to_bin(key) cipher = AES.new(sha256(long_to_bytes(key)).digest()[:16 ], AES.MODE_CTR, nonce=b"12345678" ) enc_flag = bytes .fromhex(flag.deocde()) flag = cipher.decrypt(enc_flag)print (flag)