HGAME2020 Week4 Writeup

完結撒花


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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章