完結撒花
Crypto - ToyCipher_Linear
題目:
Why encryption based on XOR and Rotation is easy to break?
還有加解密的腳本:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os, binascii
from secret import flag
def rotL(x, nbits, lbits):
mask = 2**nbits - 1
return (x << lbits%nbits) & mask | ( (x & mask) >> (-lbits % nbits) )
def rotR(x, nbits, rbits):
return rotL(x, nbits, -rbits%nbits)
def keySchedule(masterkey):
roundKeys = [ ( rotR(masterkey, 64, 16*i) % 2**16 ) for i in range(12) ]
return roundKeys
def f(x, roundkey):
return rotL(x, 16, 7) ^ rotL(x, 16, 2) ^ roundkey
def ToyCipher(block, mode='enc'):
'''Feistel networks'''
roundKeys_ = ROUNDKEYS
if mode == 'dec':
roundKeys_ = roundKeys_[::-1]
L, R = (block >> 16), (block % 2**16)
for i in range(12):
_R = R
R = L ^ f( R, roundKeys_[i] )
L = _R
return (R << 16) | L
def pad(data, blocksize):
pad_len = blocksize - (len(data) % blocksize)
return data + bytes( [pad_len] * pad_len )
def unpad(data, blocksize):
pad_len = data[-1]
_data = data[:-pad_len]
assert pad(_data, blocksize)==data, "Invalid padding."
return _data
def encrypt(plaintext):
'''ECB mode'''
plaintext = pad(plaintext, BLOCKSIZE)
ciphertext = b''
for i in range( len(plaintext) // BLOCKSIZE ):
block = plaintext[i*BLOCKSIZE:(i+1)*BLOCKSIZE]
block = int.from_bytes(block, byteorder='big')
E_block = ToyCipher(block)
ciphertext += E_block.to_bytes(BLOCKSIZE, byteorder='big')
return ciphertext
def decrypt(ciphertext):
'''ECB mode'''
plaintext = b''
for i in range( len(ciphertext) // BLOCKSIZE ):
block = ciphertext[i*BLOCKSIZE:(i+1)*BLOCKSIZE]
block = int.from_bytes(block, byteorder='big')
D_block = ToyCipher(block, 'dec')
plaintext += D_block.to_bytes(BLOCKSIZE, byteorder='big')
plaintext = unpad(plaintext, BLOCKSIZE)
return plaintext
masterkey = os.urandom(8)
masterkey = int.from_bytes(masterkey, byteorder='big')
ROUNDKEYS = keySchedule(masterkey)
BLOCKSIZE = 4
cipher = encrypt(b'just a test')
print(cipher)
print(decrypt(cipher))
print(encrypt(flag))
# b'\x91a\xb1o\xed_\xb2\x8c\x00\x1b\xdfp'
# b'just a test'
# b'\xe6\xf9\xda\xf0\xe18\xbc\xb4[\xfb\xbe\xd1\xfe\xa2\t\x8d\xdft:\xee\x1f\x1d\xe2q\xe5\x92/$#DL\x00\x1dD5@\x01W?!7CQ\xc16V\xb0\x14q)\xaa2'
感謝出題人在校內羣裏提供的資料,要不然一點思路都沒有
先大概分析了一下代碼。RotL函數會將x左移lbits位,並將多出來的部分接在x的右側。例如設x=110101,則RotL(x,6,4)的結果爲011101。RotR函數則是右移rbits位,操作與RotL函數類似。
根據資料和題目名,在網上搜索線性攻擊,發現線性攻擊當中涉及到隨機置換,而腳本中似乎不涉及
搜索針對Feistel網絡結構的攻擊,無果
仔細閱讀資料,發現提問者設計了一個基於旋轉和異或操作的加密方法(與此題類似),之後還給出了根據明文和密文恢復密鑰的方法
c1 + k1 = p3 1 + k1 = 1 k3 = 1
c0 + k0 = p2 ==> 0 + k0 = 0 ==> k2 = 0 (A)
c3 + k3 = p1 1 + k3 = 0 k1 = 0
c2 + k2 = p0 1 + k2 = 1 k0 = 0
進一步分析,本題的加/解密過程本質還是若干個位的異或操作,而旋轉改變了位與位之間的對應關係。只要找到解密過程中明文每一個位是由哪些位異或而得到的(明文bit與密文bit、密鑰bit的對應關係),就可以得到一個異或方程組,通過已知的明文和密文就能把密鑰恢復出來,進而解密出flag
因爲加解密過程是ECB模式,因此接下來只需要對單個block進行分析即可
一開始準備手動模擬一遍,後來意識到任務量巨大,於是仿照解密邏輯寫了個腳本,尋找並顯示對應關係:
from copy import deepcopy
def rotL(x, lbits):
return x[lbits:] + x[:lbits]
roundkeys = []
for i in range(12):
rndkey = []
for j in range(16):
out = 'k{k_index}_{k_bit}'.format(k_index=i, k_bit=j)
rndkey.append(out)
roundkeys.append(rndkey)
block = []
for i in range(32):
b = []
b.append(i)
block.append(b)
L, R = block[:16], block[16:]
for i in range(12):
_R = deepcopy(R)
currkey = deepcopy(roundkeys[i])
tmp = sum(R, [])
rtl2 = rotL(tmp, 2)
rtl7 = rotL(tmp, 7)
for j in range(16):
L[j].append(currkey[j])
L[j].append(rtl2[j])
L[j].append(rtl7[j])
R = deepcopy(L)
L = deepcopy(_R)
for i in range(16):
for j in R[i]:
times = R[i].count(j)
if times % 2 == 0:
for k in range(times):
R[i].remove(j)
else:
for k in range(times - 1):
R[i].remove(j)
print(R[i])
for i in range(16):
for j in L[i]:
times = L[i].count(j)
if times % 2 == 0:
for k in range(times):
L[i].remove(j)
else:
for k in range(times - 1):
L[i].remove(j)
print(L[i])
理論上通過這個腳本可以得到明文的第i位與密文密鑰的對應關係,並且這個關係不會因爲加密內容和密鑰的不同而改變
也就是對於明文M,密文C和密鑰K(roundkeys),無論三者如何變化,總有
M[0]=C[a]^C[b]^...^K[c]^K[d]^...
M[1]=C[e]^C[f]^...^K[g]^K[h]^...
M[2]=C[i]^C[j]^...^K[k]^K[l]^...
M[...]=...
而腳本要尋找的就是a,b,c,d,…的值並將其顯示出來
之後就可以通過已知的M和已知的C求出K,進而解出flag
進一步分析,將上述每一條表達式中C[x] ^ C[x] ^ …的結果記爲midC,K[x] ^ K[x] ^ …的結果記爲midK,則上述式子可以改寫爲
M[0]=midC_0^midK_0
M[1]=midC_1^midK_1
M[2]=midC_2^midK_2
M[...]=...
那麼選取兩組不同的明文和密文(從已知信息中選取兩個不同的block),通過剛纔的方法可以得到兩個不同的midC_0,如果兩者異或的結果等於兩者對應M[0]相異或的結果,則可以驗證剛纔的分析結論
然而可能是因爲寫出來的腳本有問題,選取b’just’, b’ a t’(因爲題中BLOCKSIZE = 4)和其對應的密文進行上述操作,無法驗證剛纔的結論
之後仔細閱讀題目代碼,發現如果將f函數中的
return rotL(x, 16, 7) ^ rotL(x, 16, 2) ^ roundkey
改爲
return rotL(x, 16, 7) ^ rotL(x, 16, 2)
也可以達到計算midC的效果,並且由於是直接修改題目,所以出錯的機率要低一些
複製題目代碼,刪掉 ^ roundkey以及加密flag的部分,然後插入以下語句:
midC1 = encrypt(C[:4])
midC1 = int.from_bytes(midC1, 'big')
print(bin(midC1))
midC2=encrypt(C[4:8])
midC2 = int.from_bytes(midC2, 'big')
print(bin(midC2))
得到
midC1 = 0b1000000100100011010011011011110
midC2 = 0b0001010100001011111010111011110
計算midC1 ^ midC2 = 1242845952
直接將M[:4] (b’just’)和M[4:8] (b’ a t’)轉爲數字後異或,得到的結果也是1242845952,說明這一思路是正確的
因爲M=midC ^ midK,所以midK=M^midC。將M[:4] (b’just’)與midC1異或,得到midK=719639978,而這個midK對於題中的所有密文block都是適用的
之後將加密後的flag
b'\xe6\xf9\xda\xf0\xe18\xbc\xb4[\xfb\xbe\xd1\xfe\xa2\t\x8d\xdft:\xee\x1f\x1d\xe2q\xe5\x92/$#DL\x00\x1dD5@\x01W?!7CQ\xc16V\xb0\x14q)\xaa2'
拆成13個長度爲4的block,分別計算每個塊的midC,並將其與midK異或即可得到flag
最終flag:hgame{r0TAT!on_&&-x0r 4Re-b0tH~l1neaR_0pEr4t1On5}
希望有師傅能幫我找一下之前的腳本錯在哪兒,不勝感激!
2020.02.14