【入门建议收藏】密码学学习笔记之线性分析入门篇——EzSPN
前言
上一篇了解了一下差分分析,這次我們結合一道CTF題目聊一聊線性分析
同屬于選擇明文的差分分析不同,線性分析屬于已知明文攻擊方法,它通過尋找明文和密文之間的一個“有效”的線性逼近表達式,將分組密碼與隨機置換區分開來,并再此基礎上進行密鑰恢復攻擊。
在正式介紹線性分析之前,我們還是要介紹一下相關的基礎概念,參考《分組密碼的攻擊方法與實例分析》一書
基礎概念
首先還是給出和差分分析一樣的一個迭代分組密碼的加密流程
迭代分組密碼的加密流程
內積
線性掩碼
線性逼近表達式
線性殼
迭代分組密碼的一條 i 輪線性殼是指一對掩碼(β0,βi),其中β0 是輸入掩碼,βi 是輸出掩碼。
線性特征
線性殼的線性概率
線性特征的線性概率
線性區分器
先給出一個命題,對{0,1}^n 上的隨機置換R,任意給定掩碼α,β,α ≠ 0,β ≠ 0, 則 LP(α,β ) = 0,? 即 偏差 ε?(α,β) = 0
如果我們找到了一條 r-1 輪線性逼近表達式 (α,β),其線性概率 LP(α,β) ≠ 0,即偏差 ε(α,β) ≠ 0。則利用該線性逼近表達式可以將 r-1 輪的加密算法與隨即置換區分開來,利用該線性區分器就可以對分組密碼進行密鑰恢復攻擊。假設攻擊 r 輪加密算法,為獲得第 r 輪的輪密鑰的攻擊步驟如下。
步驟1
尋找一個 r – 1輪的線性逼近表達式 (α,β) ,設其偏差為ε(α,β),使得 |ε(α,β)|較大。
步驟2
根據區分器的輸出,攻擊者確定要恢復的第 r 輪輪密鑰 k_r(或者其部分比特):設攻擊的密鑰比特長度為 l,對每個可能的候選密鑰gk_i, 0 ≤ i ≤ 2^l -1,設置相應的 2^l 個計數器λ_i,并初始化。
步驟3
均勻隨機地選取明文 X,在同一個未知密鑰 k 下加密,(一般是讓擁有密鑰地服務端幫你加密)獲得相應地密文 Z, 這里選擇明文地數目為 m ≈ c ·(1/ε ^2),c 為某個常數。
步驟4
對一個密文Z,我們用自己猜測地第 r 輪輪密鑰 gk_i(或其部分比特)對其進行第 r 輪解密得到Y_{r-1},然后我們計算線性逼近表達式 α x · X ⊕ β · Y_{r-1} 是否為0,若成立,則給相應計數器λ_i 加 1
步驟5
將 2^l 個計數器中|λ/m – 1/2| 最大地指所對應地密鑰 gk_i(或其部分比特)作為攻擊獲得地正確密鑰值。
Remark 針對步驟1中,我們如何去尋找一個高概率地 r -1 輪線性逼近表達式呢?例如針對一個S盒,我們可以選擇窮舉所有的輸入、并獲得相應的輸出,然后窮舉輸入掩碼、輸出掩碼,去獲取這個S盒的相關線性特性。
下面就根據一道CTF中出現的賽題來解釋分析上述過程。
實例-[NPUCTF2020]EzSPN
task.py
import os from binascii import hexlify, unhexlify import Crypto.Random.random as random from secret import flagSZ = 8 coef = [239, 163, 147, 71, 163, 75, 219, 73] sbox = list(range(256)) random.shuffle(sbox) sboxi = [] for i in range(256):sboxi.append(sbox.index(i))def doxor(l1,l2):return [x[0]^x[1] for x in zip(l1,l2)]def trans(blk):res = []for k in range(0, SZ, 8):bits = [bin(x)[2:].rjust(8,'0') for x in blk[k:k+8]]for i in range(8):res.append(int(''.join([x[(i+1) % 8] for x in bits]),2))res[k:k+8] = [(coef[i] * res[k+i]) % 256 for i in range(8)]return resdef encrypt_block(pt, ks):cur = doxor(pt, ks[:SZ])cur = [sbox[x] for x in cur]cur = trans(cur)cur = [sboxi[x] for x in cur]cur = doxor(cur, ks[SZ:])return curdef encrypt(pt, k):x = 0 if len(pt)%SZ==0 else (SZ-len(pt)%SZ)pt += [x]*xct = ''for i in range(0, len(pt), SZ):res = encrypt_block([x for x in pt[i:i+SZ]], k)ct += ''.join(["{:02x}".format(xx) for xx in res])return ctdef doout(x):if len(x) % 16:x = (16 - len(x) % 16) * "0" + xreturn xdef doin(x):return list(unhexlify(x))def genkeys():return list(os.urandom(2*SZ))if __name__ == "__main__":print(sbox)key = genkeys()ct = encrypt(flag, key)print(ct)while True:pt = doin(input())print(doout(encrypt(pt, key)))題目提供一個交互,能夠加密你的輸入并返回密文。所以這里是否可以采用差分分析的攻擊方法呢,但這里我們討論的線性分析,所以我們就用線性分析來做這道題目里。
那么看到這里所用加密系統的流程。
關鍵函數是encrypt_block(pt, ks),
首先是一個明文和一半的密鑰異或,然后是進入S盒(題目每次隨機生成并提供S盒具體內容),然后trans一下,再進入一個逆S盒,最后再和另一半的密鑰異或。
看到這個trans函數,這里有一個int(‘’.join([x[(i+1) % 8] for x in bits]),2),這個(i+1%8)有點類似位移的效果,然后是乘以了一個系數,但這個系數已經定死了,所以沒有什么關系。
首先測一個這個trans函數的一個位移效果,大概類似這樣
然后整個流程圖(畫的丑了點)
那么針對這道題,我們利用線性分析攻擊手法,
**步驟1,**我們去分析S盒的線性特性以找到使得線性表達式偏差大的掩碼對(α,β)。我們窮舉所有的S盒的可能的輸入并計算出相應的輸出,然后窮舉所有的輸入、輸出掩碼的組合,然后根據其是否符合滿足線性逼近表達式 α · X ⊕ β · F(X,K) = 0 來更新計數器,最后我們計算相應的偏差表offset
def linearSbox():global linearInput for i in range(256):si = sbox[i]for j in range(256):for k in range(256):a = bitxor(i, j) # 線性估計輸入b = bitxor(si, k) # 線性估計輸出 if a == b:offset[j][k] += 1for i in range(256):offset[i] = [abs(x - 128) / 256 for x in offset[i]]for linearOutput in range(256):cur = [x[linearOutput] for x in offset]linearInput.append(cur.index(max(cur)))其中j是輸入掩碼,k是輸出掩碼。
這里的offset以輸入掩碼為行,輸出掩碼為列,所以這里的cur是獲取同一輸出掩碼下不同輸入掩碼的偏差
并且在linearInput中記錄下同一輸入掩碼下使得線性表達式偏差最大的輸出掩碼。
這里我們簡單看一下輸出的offset表
可以看到列表的第一行很特殊,除了第一個是0.5這個極大的值以外,其他都是0。因為在輸入掩碼為0、輸出掩碼也為0的話,肯定是能夠滿足的情況下,針對所有的輸入、輸出肯定是都能滿足線性逼近表達式的,所以偏差達到了最大,0.5。但是換做其他輸出掩碼的話,針對所有的輸出,其最后的表達式的結果分布得很均勻,所以偏差為0。
再舉第二行第一列的0.03131…這個值的含義:就是針對所有的256個輸入,當輸入掩碼為1,輸出掩碼為1時的線性逼近表達式的偏差。即有 256 * (0.03+0.5) ≈ 135 個輸入滿足(或者不滿足)這個線性逼近表達式。
有了S盒的線性特性之后,步驟2是設置一個計數器counter,然后我們開始進入步驟3搜集明文密文對。就隨機選擇明文,發送給服務端,然后接收服務端返回的密文即可。
接著開始步驟4,由于這里有兩輪加密,然后我們在步驟1已經生成了關于單個S盒的一個offset表,這個就相當于是 1 輪線性特征了,那么我們接下來就按照線性分析的攻擊思路,
這里我們按字節猜測密鑰,首先拿一個密文的第一個字節,異或第一個字節的密鑰(枚舉)后從S逆盒出去,然后把系數coef除掉,再根據P盒,把這個值換到相應的位置,然后這里我們根據S盒的線性特征選取合適的輸出、輸入掩碼對我們的這對輸入進行測試。若滿足線性逼近表達式,則True計數加一,否則False計數加一。
最后步驟5,針對每個字節我們都測一萬對,取結果偏差最大的key值。這樣就完成了密鑰每個字節的猜測。
結合代碼再細細講一遍
def calcOffset(pt, ct, j, guessed_key): # 猜測第j段子密鑰pt = list(unhexlify(pt))ct = list(unhexlify(ct))ct[j] ^= guessed_keyct[j] = sbox[ct[j]] # sbox即為sboxi的逆ct[j] = (ct[j] * coef[j]) % 256u1 = bitxor(pt[0], linearInput[1 << ((6 - j) % 8)])u2 = bitxor(ct[j], 0b10000000)if u1 == u2:return Trueelse:return Falsedef linearAttack():key2 = []for i in range(8): # 第二輪子密鑰的第i段count = [0 for _ in range(256)]for guessed_key in range(256):print("[+] Cracking key...({}-{})".format(i, guessed_key))for j in range(10000):if calcOffset(plain[j], cipher[j], i, guessed_key) == True:count[guessed_key] += 1bias = [abs(x - 5000) / 10000 for x in count]key2.append(bias.index(max(bias)))return key2linearAttack()中我們主要關注那個循環。這里調用了calcOffset函數,傳入了一對明文密文對(plain[j], cipher[j])、索引(i)、以及猜測的密鑰(guessed_key)。
在calcOffset中,首先用密文異或了guessed_key,然后根據值獲取從Sbox的輸入,然后乘以了coef(這里已經是原來coef的逆了),接著是u2 = bitxor(ct[j], 0b10000000),用了0b10000000作為掩碼,即選取了密文的第一個bit。然后選取 linearInput[1 << ((6 – j) % 8)] 做為明文第一個字節的掩碼。
為什么只選明文的第一個字節,以及其掩碼的選取這里可能看的不是很直觀,因為這里其實包含了兩部分。
我們需要回去注意到P盒。當這里j=0,即爆破第一個字節的密鑰時,我們選取的是密文的第1個字節,掩碼選的是第1個bit,那么這是從P盒輸入的哪個bit傳來的呢,從P盒我們可以看到這是P盒輸入的第一個字節的第2bit傳來的,那么P盒輸入的第2bit,實際上就是S盒輸出的第2bit,也就是S盒的輸出掩碼為0b01000000,也就是1 << 6。而這個 linearInput 存的就是輸出掩碼為某個值的使得線性逼近表達式偏差最大的輸入掩碼。
linearAttack()中我們主要關注那個循環。這里調用了calcOffset函數,傳入了一對明文密文對(plain[j], cipher[j])、索引(i)、以及猜測的密鑰(guessed_key)。
在calcOffset中,首先用密文異或了guessed_key,然后根據值獲取從Sbox的輸入,然后乘以了coef(這里已經是原來coef的逆了),接著是u2 = bitxor(ct[j], 0b10000000),用了0b10000000作為掩碼,即選取了密文的第一個bit。然后選取 linearInput[1 << ((6 – j) % 8)] 做為明文第一個字節的掩碼。
為什么只選明文的第一個字節,以及其掩碼的選取這里可能看的不是很直觀,因為這里其實包含了兩部分。
我們需要回去注意到P盒。當這里j=0,即爆破第一個字節的密鑰時,我們選取的是密文的第1個字節,掩碼選的是第1個bit,那么這是從P盒輸入的哪個bit傳來的呢,從P盒我們可以看到這是P盒輸入的第一個字節的第2bit傳來的,那么P盒輸入的第2bit,實際上就是S盒輸出的第2bit,也就是S盒的輸出掩碼為0b01000000,也就是1 << 6。而這個 linearInput 存的就是輸出掩碼為某個值的使得線性逼近表達式偏差最大的輸入掩碼。
當我們選取密文第二個字節的時候,根據P盒,第二個字節第1個bit來自于明文第一個字節第3個bit,也就是0b00100000,也就是1 << 5,同理可推
所以這里只用選明文的第一個字節,密文不同字節的第一個比特,且輸入掩碼為 linearInput[1 << ((6 – j) % 8)]。
另外可能還有一個問題,S盒的輸入不是明文啊,應該是明文異或了第一個段密鑰。確實,但是試想一下,在線性逼近表達式的這個式子中,密鑰部分相互異或后,最后無非就是0或者1,而且由于密鑰固定,所以針對固定的輸入掩碼,這個值是固定的。對于返回結果沒有影響或者只是一個取反的效果。而我們是根據偏移來判斷的,所以正并不影響結果,key的具體的值在這里也就沒什么影響。
舉個例子,如果輸入掩碼是0b00110011,輸入掩碼是0b00001111,那么由于密鑰的存在,線性逼近表達式就是這么寫:X_2 ⊕ X_3 ⊕ X_6⊕ X_7 ⊕ K_2 ⊕ K_3⊕ K_6⊕ K_7 = Y_4⊕ Y_5 ⊕ Y_6 ⊕ Y_7
其中K_2 ⊕ K_3⊕ K_6⊕ K_7 這部分無非就是等于 0 或者 1,對于結果沒有影響或者只是一個取反的效果,對偏差則沒有影響。
爆破出第二部分key的每一個字節之后,我們就可以逆算法流程。由于S盒是雙射的,用明文、密文、第二部分key就可以去恢復第一部分的key,然后再寫一個解密函數就可以解決這道問題了,
來自出題人的exp:
import os, sys from binascii import hexlify, unhexlify from pwn import *SZ = 8 offset = [[0 for i in range(256)] for i in range(256)] #Sbox線性估計offset linearInput = [] sbox, sboxi, plain, cipher = [], [], [], [] enc_flag = None coef = [15, 11, 155, 119, 11, 99, 83, 249]def getData(ip, port):global enc_flag, sbox, sboxiio = remote(ip, port)sbox_str = io.recvline()sbox = eval(sbox_str)for i in range(256):sboxi.append(sbox.index(i))enc_flag = io.recvline().strip().decode("utf8")for i in range(10000):print("[+] Getting data...({}/10000)".format(i))pt = hexlify(os.urandom(8)).decode("utf8")plain.append(pt)io.sendline(pt)ct = io.recvline().strip().decode("utf8")print(ct,pt)cipher.append(ct)io.close()def doxor(l1, l2):return [x[0] ^ x[1] for x in zip(l1, l2)]# 線性層 def trans(blk):res = []for k in range(0, SZ, 8):cur = blk[k:k+8]cur = [(cur[i] * coef[i]) % 256 for i in range(8)]bits = [bin(x)[2:].rjust(8, '0') for x in cur]bits = bits[-1:] + bits[:-1]for i in range(8):res.append(int(''.join([x[i] for x in bits]), 2))return resdef bitxor(n, mask):bitlist = [int(x) for x in bin(n & mask)[2:]]return bitlist.count(1) % 2# Sbox線性估計 def linearSbox():global linearInput for i in range(256):si = sbox[i]for j in range(256):for k in range(256):a = bitxor(i, j) # 線性估計輸入b = bitxor(si, k) # 線性估計輸出 if a == b:offset[j][k] += 1for i in range(256):offset[i] = [abs(x - 128) / 256 for x in offset[i]]for linearOutput in range(256):cur = [x[linearOutput] for x in offset]linearInput.append(cur.index(max(cur)))def calcOffset(pt, ct, j, guessed_key): # 猜測第j段子密鑰pt = list(unhexlify(pt))ct = list(unhexlify(ct))ct[j] ^= guessed_keyct[j] = sbox[ct[j]] # sbox即為sboxi的逆ct[j] = (ct[j] * coef[j]) % 256u1 = bitxor(pt[0], linearInput[1 << ((6 - j) % 8)])u2 = bitxor(ct[j], 0b10000000)if u1 == u2:return Trueelse:return Falsedef linearAttack():key2 = []for i in range(8): # 第二輪子密鑰的第i段count = [0 for _ in range(256)]for guessed_key in range(256):print("[+] Cracking key...({}-{})".format(i, guessed_key))for j in range(10000):if calcOffset(plain[j], cipher[j], i, guessed_key) == True:count[guessed_key] += 1bias = [abs(x - 5000) / 10000 for x in count]key2.append(bias.index(max(bias)))return key2def getkey(key2):ct = list(unhexlify(cipher[0]))pt = list(unhexlify(plain[0]))cur = doxor(ct, key2)cur = [sbox[x] for x in cur]cur = trans(cur)cur = [sboxi[x] for x in cur]key = doxor(cur, pt) + key2return keydef decrypt_block(ct, key):cur = doxor(ct, key[SZ:])cur = [sbox[x] for x in cur]cur = trans(cur)cur = [sboxi[x] for x in cur]cur = doxor(cur, key[:SZ])return curdef decrypt(ct, key):pt = b''for i in range(0, len(ct), SZ * 2):block_ct = list(unhexlify(ct[i : i + SZ * 2]))block_pt = decrypt_block(block_ct, key)pt += bytes(block_pt)return ptif __name__ == "__main__":getData('node4.buuoj.cn', 27125)linearSbox()key2 = linearAttack()key = getkey(key2)print(key)flag = decrypt(enc_flag, key)print(flag)那么對于這道簡單的兩輪分組密碼的線性分析就到此告一段落了,但對于分組密碼的學習不會到此為止,因為,還有高階差分、截斷差分、不可能差分……
最后
我有收集整理了一些學習的資料與工具,有想要得朋友,可以給個關注私我,發給你
【資料了解】
總結
以上是生活随笔為你收集整理的【入门建议收藏】密码学学习笔记之线性分析入门篇——EzSPN的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【域渗透】教你怎么利用DCSync导出域
- 下一篇: 【网络安全】NFS服务安全加固