# Simple Crypto - 0x06(2023 HW - LFSR) ## Background * [Python – List XOR](https://www.geeksforgeeks.org/python-list-xor/) > ```python > from funtools import reduce > test_list = [4, 6, 2, 3, 8, 9] > res = reduce(lambda x, y: x ^ y, test_list) # The output is 2 > ``` * [Numpy矩陣乘法,但不是乘法,而是XOR的元素](https://www.qiniu.com/qfans/qnso-67006518#comments) > ```python > import numpy as np > m1 = np.array([[1, 0, 0], [0, 0, 0], [0, 0, 0]]) > m2 = np.array([[1, 0, 1], [0, 0, 1], [1, 1, 1]]) > mr = np.empty((m2.shape[0], m1.shape[1]), dtype = np.int64) > for i in range(mr.shape[0]): > for j in range(mr.shape[1]): > mr[i, j] = np.sum(m1[:, j] ^ m2[i, :]) > print(mr) > ``` * [使用 Python 來認識矩陣](https://pyradise.com/使用-python-來認識矩陣-915376207187) * [[Day07]Learning Numpy - 建立、合併、分割 - CheetSheet for Numpy](https://ithelp.ithome.com.tw/articles/10203624) * Sage ```bash $ sudo apt install sagemath -y # wsl/unix base可以直接安裝,如果是windows要下載sage binary,有1.4GB $ sage -n # 開起sage notebook,也就是可以用sage kernel運行jupyter $ sage <.py/.sage file> # 用sage運行腳本 $ sage # 直接開啟sage interactive shell ``` ## Recon 這一題和前面的triLFSR不一樣的地方在於他只有一層的LFSR,但他只會每個70個才會給一個state,換句話說我們只能拿到$S_{71*0+70},\ S_{71*1+70},\ S_{71*2+70},\ S_{71*3+70}...$(從0開始算),而前面256個拿到的State最後會和flag進行XOR,只有最後70個是最純粹的State * What we have 我們有的東西就是Companion Matrix,因為題目有給taps,所以可以建出上課提到的矩陣;另外我們還有最後出現的70個State,雖然是每格70個出現一次,換句話說就是$State_{71*256+70},\ State_{71*257+70},\ State_{71*258+70},\ ...State_{71*325+70}$(從0開始算) * Goal 既然我們知道了State的公式為$s_m = p_0s_0 + p_1s_1 + … + p_{m-1}s_{m-1}$,也就是companion matrix的最後一列$*$那64個initial state就會是新的state,換句話說,繼續往下做,其實就只是把companion matrix多乘幾次,然後還是一樣乘以initial state,然後我們只要取得companion matrix乘完之後的最後一列,就是下一個新的state的特徵,如下圖所示: ![](https://hackmd.io/_uploads/HkwyVkGx6.jpg) 在Round 0時,companion matrix的最後一列當然就是$S_{64}$的特徵,再往下做,也就是Round 1時,companion matrix的平方後,再取最後一列就是$S_{65}$的特徵,而題目給我們的ouptut[0]以state來說就是第70個(以0來說),所以companion matrix的7次方,再取最後一列,以此類推,我們陸續算到output[256](這是第一個沒有和flag XOR的bit),也就是companion matrix的$71*256+7=18183$次方再取最後一列,就是$S_{71*256+70}$的特徵,自此開始,我們就可以開始把這些特徵存起來,存滿64個後,再取反矩陣,乘上原本得到的那64個state,就可以得到一開始的initial state * 完整的對應關係如下圖 ![](https://hackmd.io/_uploads/SJcl-JGep.jpg) ## Exploit 1. 陷阱1: 此題所有的運算接在mod 2底下運算,包含內積和反矩陣,所以需要用sage的語法幫助我們快速算出答案(真的差很多,如果是手刻不用sage,至少要花一小時,但用了sage,只需要10秒,真香啊!!!) * 在Modular 2的情況下內積,乘法會對應到AND,而加法對應到XOR,在sage中語法如下 ```sage sage: a = Matrix([[1,1,0],[0,1,0],[0,0,1]]) sage: b = Matrix([[1,1,1],[1,1,0],[1,1,1]]) sage: a * b [2 2 1] [1 1 0] [1 1 1] sage: a * b % 2 [0 0 1] [1 1 0] [1 1 1] ``` * 在sage中要計算modular下的inverse matrix,語法如下 ```sage sage: a = Matrix([[1, 2], [3,4]]) sage: b = Matrix(IntegerModRing(7), a).inverse() sage: b [5 1] [5 3] sage: a * b [1 0] [0 1] ``` 2. 陷阱2: 其實也不算陷阱,反正就是要很細心處理每一個state和for loop中的i之間的關係變化,其實上面就有完整演練一遍,只要按圖施工保證成功 3. 陷阱3: 在sage中,如果要表達XOR是用`^^`表示,而非python常見的`^`,因為這在sage中代表次方 4. 小技巧: 如果不知道實作的方式對不對,可以直接用原本題目給的code,寫死已知的initial state,然後把output印出來後照著原本設計的邏輯,看能不能還原initial state :::spoiler Whole Exploit with Sage ```python from tqdm import trange import numpy as np class LFSR: def __init__(self, tap, state): self._tap = tap self._state = state def getbit(self): f = sum([self._state[i] for i in self._tap]) & 1 x = self._state[0] self._state = self._state[1:] + [f] return x def verification(taps, key): randomness = LFSR(taps, key) output = [] for _ in range(256 + 64): for __ in range(70): randomness.getbit() output.append(randomness.getbit()) return output[:256], output[256:] def get_flag(cipher_flag, output): flag = "" plaintext_hex = '' for idx, i in enumerate(range(len(cipher_flag))): flag += str(output[i] ^^ cipher_flag[i]) if (idx+1) % 8 == 0: plaintext_hex += hex(int(flag, 2))[2:] flag = "" return bytes.fromhex(plaintext_hex).decode("cp437") if __name__ == '__main__': f = [0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0] # Initialization taps = [0, 2, 17, 19, 23, 37, 41, 53] init_state_size = 64 cipher_text_xor_flag, cipher_text = f[:len(f)-70], f[len(f)-70:] cipher_text = Matrix(np.array(cipher_text[:init_state_size]).reshape((init_state_size, 1)).tolist()) # Create companion Matrix a = np.eye(init_state_size-1, dtype = int) # 創造對角矩陣 b = np.zeros((init_state_size-1, 1), dtype=int) # 創造最左邊全為0的行 c = np.array([1 if i in taps else 0 for i in range(init_state_size)]) # 創造最後一列的taps comp_matrix = Matrix(np.vstack([np.hstack([b, a]), c]).tolist()) # 全部組合起來 # 做內積的運算 _comp_matrix = comp_matrix # _comp_matrix代表會變動的companion matrix real_comp_matrix = np.empty(init_state_size, dtype=int) count = 256 arr_merge = True for i in trange(71*319+6+1): _comp_matrix = comp_matrix * _comp_matrix % 2 # 因為是在mod 2底下處理,所以不是普通的dot運算,乘法對應到AND,加法對應到XOR if i == 71 * count + 5: real_comp_matrix = np.vstack([real_comp_matrix, _comp_matrix[-1]]) count += 1 # 計算在模2情況下的反矩陣 inv_real_comp_matrix = Matrix(IntegerModRing(2), real_comp_matrix[1:]).inverse() # 算出initial state init_state = inv_real_comp_matrix * cipher_text % 2 init_state = list(init_state.numpy().reshape(1, init_state_size)[0]) print("Initial State = ", init_state) output, check = verification(taps, [0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1]) assert list(cipher_text.numpy().reshape(1, 64)[0]) == check # 如果assert通過,代表找到正確的initial state然後就可以反算flag print(get_flag(cipher_text_xor_flag, output)) ``` ::: Flag: `FLAG{Lf5r_15_50_eZZzZzZZZzzZzzz}`