---
# System prepended metadata

title: 2020程安 Crypto筆記01 (LFSR.HASH)
tags: [crypto, 程式安全]

---

# 2020程安 Crypto筆記01 (LFSR.HASH)
###### tags: `程式安全` `crypto`


## LFSR (Linear Feedback Shift Register)
- 給定一個initial state, 經過線性運算之後得到的結果成為下一輪的msb,並將lsb shift出作為output
![](https://i.imgur.com/9uN5TnQ.png =400x250)
- [Example](https://github.com/oalieno/Crypto-Course/tree/master/LFSR/Known-Plaintext-Attack)
![](https://i.imgur.com/cRCqeQs.png =400x200)
    
    > 1. init state = **s** = [s2, s1, s0] = [1, 0, 0]
    > 2. feedback = **p** = [p2. p1, p0] = [0, 1, 1]
    > 3. 線性運算
        s[n+1] = s[n]*p[2] ^ s[n-1]*p[1] ^ s[n-2]*p[0]

    ```
    Note:
        XOR = ADD (mod 2)
        MUL = AND 
    ```    
    - source code
    ```python=
    # generate.py
    import random
    from functools import reduce

    class LFSR:
        def __init__(self, init, feedback):
            self.state = init
            self.feedback = feedback
        @classmethod
        def random(cls, size):
            init = [random.choice([0, 1]) for i in range(size)] # 隨機生成init state s [s0, s1, ..., sn-1]
            feedback = [random.choice([0, 1]) for i in range(size)] # 隨機生成feedback p [p0, p1, ..., sn-1]
            return cls(init, feedback)
        def getbit(self): # 生成新bit
            nextbit = reduce(lambda x, y: x ^ y, [i & j for i, j in zip(self.state, self.feedback)]) 
            #  sn = s[0]*p0 + s[1]*p1 + ... + s[n-1]*pn-1
            self.state = self.state[1:] + [nextbit] # 2nd s = [s1, s2, ..., sn-1, sn]
            return nextbit
        def getbyte(self):
            b = 0
            for i in range(8):
                b = (b << 1) + self.getbit()
            return bytes([b])

    def xor(a, b):
        return bytes([i ^ j for i, j in zip(a, b)])

    FLAG = open('./flag', 'rb').read()
    assert FLAG.startswith(b'CTF{')

    lfsr = LFSR.random(16) # 生成16bit的state & feedback
    key = b''.join([lfsr.getbyte() for i in range(len(FLAG))]) # 產生長度跟FLAG一樣的key
    print(f'enc = {xor(FLAG, key).hex()}')
    ```
    - output
    ```python
    #output.txt
    enc = d0aa72cef8dab5baac
    ```
    - Solve
        :::info
        想法
        1. 已知init state(**s**)跟feedback(**p**)皆為**16bits**
        2. 已知FLAG前四個字元是'CTF{'
            FLAG ^ key = enc --> enc ^ FLAG = key
            故可以獲得key的前四個字元 = 4byte * 8 = **32bits**
        3. 已知的32bit key其實就是s0, ..., s31
        4. 2m bit的key可以回推m bit的feedback (解聯立)
            --> **berlekamp massey algo.**
        :::
        
        ```python=
        #solve.sage
        from sage.matrix.berlekamp_massey import berlekamp_massey

        class LFSR:
            def __init__(self, init, feedback):
                self.state = init
                self.feedback = feedback
            @classmethod
            def random(cls, size):
                init = [random.choice([0, 1]) for i in range(size)]
                feedback = [random.choice([0, 1]) for i in range(size)]
                return cls(init, feedback)
            def getbit(self):
                nextbit = reduce(lambda x, y: x ^^ y, [int(i) & int(j) for i, j in zip(self.state, self.feedback)])
                self.state = self.state[1:] + [nextbit]
                return nextbit
            def getbyte(self):
                b = 0
                for i in range(8):
                    b = (b << 1) + self.getbit()
                return bytes([b])

        def xor(a, b):
            return bytes([i ^^ j for i, j in zip(a, b)])

        def bytes2bits(x):
            return [int(i) for i in f'{int.from_bytes(x, "big"):0{len(x) * 8}b}']

        # recover feedback coefficients
        enc = bytes.fromhex('d0aa72cef8dab5baac') # FLAG跟KEY都有9個bytes
        key = xor(enc[:4], b'CTF{') # 還原key的前32bit
        s = [GF(2)(i) for i in bytes2bits(key)]
        feedback = berlekamp_massey(s).list() # 還原 [p0, p1,..., p15] 
                                              # Note: (x^2+ 2*x +3).list() = [3, 2, 1] (升冪)
        feedback = feedback[:-1] # 去掉x最高次的係數
        feedback = ([0] * 16 + feedback[:-1])[-16:] # 如果不夠前面補0

        # generate xor key to decrypt
        lfsr = LFSR(bytes2bits(key)[16:], feedback) # 由s16~s31計算接下來的state
        key = b''.join([lfsr.getbyte() for _ in range(5)]) # 計算剩下來的5byte key
        plain = xor(enc[4:], key) # 計算剩下5byte FLAG
        print(b'CTF{' + plain)
        ```
```python=
# todo
from sage.matrix.berlekamp_massey import berlekamp_massey as bp

def xor_every_bit(a, b):
	res = []
	for x,y in zip(a,b):
		res.append(x^y)
	return res
	
def byte2bit(byte_stream):
	bits_str = ''.join(format(byte, '08b') for byte in byte_stream)
	bits = [int(i) for i in bits_str]
	return bits

def solve_feedback(key_list):
    feedback = bp(key_list).list()
    return feedback[::-1]
	 
enc = 'd0aa72cef8dab5baac' # 9bytes： FLAG.key 都是9bytes
FLAG = b'CTF{'

# cal key_bit as list
enc = bytes.fromhex(enc)
key = xor_every_bit(enc[:4], FLAG)
key_bit = byte2bit(key)

p = solve_feedback(key_bit)
p = p[:-1]
p = [0]*(16-len(p)) + p
```
        
        
## HW1 COR
- 一層LFSR不夠,那就兩層吧XD
    - 再不夠就三層(X
- [Ex](https://edu-ctf.csie.org/challenges/COR/generate.py)
    1. LFSR(第一層)
    ```python=
    class LFSR:
        def __init__(self, init, feedback):
            self.state = init
            self.feedback = feedback
        def getbit(self):
            nextbit = reduce(lambda x, y: x ^ y, [i & j for i, j in zip(self.state, self.feedback)])
            self.state = self.state[1:] + [nextbit]
            return nextbit
            # nextbit = ((s0&p0 ^ s1&p1) ^ ...) ^ ...
            # state每次拿掉LSB,shift right 1 bit並在MSB補上nextbit
    ```
    
    
    2. MYLFSR(第二層)
    ```python= 13
    class MYLFSR:
    # input一個list,每個list都有2bytes(16bit),將其每個element都轉成bit array
    # 初始化l1, l2, l3的init跟feedback值
    def __init__(self, inits):
        inits = [[int(i) for i in f"{int.from_bytes(init, 'big'):016b}"] for init in inits]
        self.l1 = LFSR(inits[0], [int(i) for i in f'{39989:016b}'])
        self.l2 = LFSR(inits[1], [int(i) for i in f'{40111:016b}'])
        self.l3 = LFSR(inits[2], [int(i) for i in f'{52453:016b}'])
    
    # 取得nextbit x1,x2,x3, 再進行第二層LFSR 
    def getbit(self):
        x1 = self.l1.getbit()
        x2 = self.l2.getbit()
        x3 = self.l3.getbit()
        return (x1 & x2) ^ ((not x1) & x3)
    # 取得output
    def getbyte(self):
        b = 0
        for i in range(8):
            b = (b << 1) + self.getbit()
        return bytes([b])
    ```
    
    
    ```python=33
    FLAG = open('./flag', 'rb').read()
    assert len(FLAG) == 12 # FLAG lenth = 12
    assert FLAG.startswith(b'FLAG{')
    assert FLAG.endswith(b'}') # FLAG = 'FLAG{xxxxxx}'
    FLAG = FLAG[5:-1] # FLAG = 'xxxxxx'

    lfsr = MYLFSR([FLAG[0:2], FLAG[2:4], FLAG[4:6]])
    print([lfsr.getbit() for _ in range(100)])
    ```
- Summery
    ```
    把input拆成三份R1, R2, R3,分別做lfsr得到x1, x2, x3
    for i in range(100):
        output[i] = (x1 & x2) ^ ((~x1) & x3)
    ```
- 想法:雖然多了一層,但還是可以用correlation attack處理
    - Correlation Attack
        - 建立一個x1.x2.x3.output的table 
        `ex: output = (x1&x2) ^ ((~x1)&x3)`
            > x1 : 0.5
            > x2 : 0.75
            > x3 : 0.75

    | x1  | x2  | x3  | output |
    | --- | --- | --- | ------ |
    | 0   | 0   | 0   | 0      |
    | 0   | 0   | 1   | 1      |
    | 0   | 1   | 0   | 0      |
    | 0   | 1   | 1   | 1      |
    | 1   | 0   | 0   | 0      |
    | 1   | 0   | 1   | 0      |
    | 1   | 1   | 0   | 1      |
    | 1   | 1   | 1   | 1      |
    
    - 因此可以先嘗試用暴力枚舉R3,如果R3產生的x3跟output的相似度**接近0.75**,就有可能是正確的R3,同理可以暴力枚舉R2,找出可能的R2
        ```python=
        # cal similarity between 2 lists
        def correlation(a,b):
            cnt = 0
            for i, j in zip(a,b):
                if (i == j):
                    cnt += 1
            return cnt
            
        # brute force R3 (by cmp x3 with output)
        for R3 in list(product([0,1], repeat=16)): #產生所有可能性                     R3 = [R3[i] for i in range(16)]
            lfsr = MYLFSR([[0]*16, [0]*16, R3])
            x3 = []
            for i in range(100):
                x3.append(lfsr.l3.getbit())
            if (correlation(x3, output) >= 70): #概率訂為70%
                print(f'R3 : {R3}')
                R3_ = chr(int('0b' + ''.join([str(i) for i in R3[:8]]), 2)) + chr(int('0b' + ''.join([str(i) for i in R3[8:]]), 2))
                print(f'R3_ : {R3_}')
        ```
            
    - 最後再爆破R1,利用R2跟R3算出out跟題目給的output做比較
    ```python=
    for i in printable:
        for j in printable:
            FLAG = bytes(i.encode()) + bytes(j.encode())
            lfsr = MYLFSR([FLAG, R2_.encode(), R3_.encode()])
            x1 = [lfsr.l1.getbit() for _ in range(100)]
            x2 = [lfsr.l2.getbit() for _ in range(100)]
            x3 = [lfsr.l3.getbit() for _ in range(100)]
            out = [((x1[i] & x2[i]) ^ ((not x1[i]) & x3[i])) for i in range(100)]
            if correlation(out, output) == 100:
                print('FLAG{' + FLAG.decode() + 'uihj' + '}')
    ```
## Hash
### Hash: 不可逆的單向函式,將任意長度資料轉換成固定長度資料(大集合mapping到小集合)
### Collision:
- 因為hash是大集合mapping到小集合,所以理論上如果資料夠多,產生的hash value會重複
    > i.e. ∃ p1, p2 ∈ P ,
    > s.t. H(p1) = H(p2) = q ∈ Q

- 現在很多hash function是基於Merkle–Damgård構造的演算法
    > ex. SHA1. md5. SHA256
    
    ![](https://i.imgur.com/OanOw4U.png =400x250)

    
- Hash function的特點是
    1. 給一個p,很容易推算出唯一的q (q = H(p))
    2. 但如果給一個q,要逆推回p卻很難,更不用說要找到有同hash值的p1,p2
        ```
        因此要算出p的方法只有兩種
        [法I] 暴力破解  >> need time
        [法II] 對所有可能值做Hash,建立一個對應的table >> need space
        --> 有沒有更快又省空間的辦法...?
        ```
### 彩虹表: 用空間換取時間
:::info
對於Q = H( P ),另外建立一個algo. R, s.t P = R( Q )
由p0開始建立一個hash chain 
∀ i < n, H(pi) = qi, R(qi) = pi+1, n取決於想要換多少時間
並記錄下每個(p0, pn)
ex. **p0** -> q0 -> p1 -> q1 -> p2 ... -> qn-1 -> **pn**
```
if給一個q想要找p
1. 首先比對有沒有pn = R(q)
2. 如果有,則p = pn-1 (因為H(pn-1) = q)
   如果沒有,則繼續比對pn-1是否=R(qn-1) = R(H(pn-1)) = R(H(R(q)))
3. 如果有,則p = pn-2
   沒有則以此類推
```
:::
    

### Length Extension Attack (長度擴展攻擊)
- Ex. MD5
        1. 把message分成好幾個message block B0-Bm,每個block 512bit(64byte)
        2. 如果最後一個block長度不足448(56byte),就加上'1+n個0'補足到448,而最後512-448=64bit(8byte)則是放message長度
            ```
            ex. secret + msg = 50byte
                --> little endian = 0x3200000000000000
            --> 50byte + 48bit(10...0) + 64bit(0x320...0)                     ```
        4. 每個block再分成16個32bit的組M0-M15
        5. 利用一個固定的128bit初始化向量,從B0開始進行計算,計算結果作為下一輪的向量,重複至Bm
        ![](https://i.imgur.com/FcYLn6p.png =500x300)
        
- **M**essage **A**uthentication **C**odes (**MACs**)
    - 用來驗證messge真實性
        - server會把一個只有server知道的secret跟需要驗證的messge進行字串串接,再用MD5或是SHA1等Merkle damgård結構的hash function取其hash值
    - 當攻擊者知道hash(secret||msg)以及secret長度時,就可以輕鬆推算出hash(secret||msg||padding||append||new_padding)
        > padding包含填充為448bit所需以及代表長度的64bit
        > append是攻擊者想要知道其hash值的new_msg
        > ||: 字串串接
        - 自己寫一個將hash(secret||msg||padding)作為hash(==new_msg||new_padding)的初始化向量的Hash運算,就可以得到在原本系統上secret||msg||padding||new_msg||new_padding的hash值囉
        ```python
        # old init vector(fixed)
        A = 0x67452301
        B = 0xefcdab89
        C = 0x98badcfe
        D = 0x10325476
        
        ## for example (from CTF problem) ##
        md5(secret + msg + padding) = '571580b26c65f306376d4f64e53cb5c7'
        
        # new init vector #
        # (manage by the md5 value previous) 
        A = 0xb2801557
        B = 0x06f3656c
        C = 0x644f6d37
        D = 0xc7b53ce5
        
        new_md5(new_msg + padding) = '79717e731df8699d1476fab654763560' 
        # new_md5(new_msg + padding) is as same as 
        # md5(secret + msg + padding + new_msg + new_padding)
        ```
- 好用的工具
    - [HashPump](https://github.com/bwall/HashPump)

## Block Cipher
- input固定長度: 把原始data切成好幾個固定長度的block進行加密,如果最後切完不足block size就補padding上去
- output固定長度: 把每個block各自用private key加密,得到的output也是固定長度
- private key常見為16byte,但也有24/32
- 
### AES
- 依加密方式區分成好幾種模式
    1. ECB: 最簡單的方式,各個block各自做加密,彼此間不相干擾
        `問題: 同樣的明文會產生同樣的密文`
        ![](https://i.imgur.com/Ay30QY4.png =300x200)

    2. CBC: 
        - 加密: 產生一個init vector(iv),從第一個block(b0)開始,先將iv跟b0做XOR再跟private key進行加密,產生的結果作為下一個block(b1)的iv
        ![](https://i.imgur.com/ZxeSy3F.png =300x200)
        - 解密: 從最後一個block的密文開始,先解密之後再跟前一個block的密文做XOR得到原始data
            ```
            Ci = Ek(Pi XOR Ci-1)
            Pi = Dk(Ci) XOR Ci-1
            ```
            - 好處: 解密可所有block平行計算


    3. CTR
        - 加密:初始化一個counter,第一個block將counter加密之後跟明文做xor, 第二個block將counter+1加密之後跟明文做xor,以此類推
            `init counter: F0F1F2F3 F4F5F6F7 F8F9FAFB FCFDFEFF`
            - 好處:可以平行計算,因為block之間無關,且解決ECB相同明文會產生相同密文的問題
            - 缺點: 如果攻擊者竄改密文,使用者解密之後無法察覺是否被竄改,依然無法解決完整性校驗的問題
    4. GCM (GMAC + CTR Mode)
        1. GMAC
        利用Galois Field(GF)乘法來計算msg的MAC,當private key為128bit時,密文以128bit為單位分組
            > Mh代表對密文做GF乘法乘上H
            > XOR相當於GF(2^128)中的加法
            ```
            MAC = ((c0*H + c1) *H + c2) *H ...
                = c0*H^n + c1*H^n-1 + ... + cn-1*H mod2^128
            ```
            ![](https://i.imgur.com/Nabq970.png)
        
        2. GCM
        先執行CTR之後再進行GMAC已產生一個tag(MAC值)
        ![](https://i.imgur.com/sgL0DlW.png)
            > h = Ek(ctr+0)
            > A = 附加消息
            > C = c0 + c1 + c2 + ... + cn-3
            > L = A的bit length + C的bit length
            > h = Ek(Jo)
                
            ```
            MAC(Tag) = (((A*H +c0) *H + c1) *H +... + L) *H + h (mod 2^128)
                    = A*H^n + c0*H^n-1 + c1*H^n-2 + ... + cn-3*H^2 + L*H + h (mod 2^128)
            ```
            
### 相關的攻擊方式
#### Cut & Paste 攻擊 (ECB)
#### Prepend Oracle 攻擊 (ECB)
#### Bit Flipping(位元翻轉)攻擊 (CBC)
#### Padding Oracle 攻擊 (CBC)
- 使用CBC模式 + PKCS#7(一種padding格式)
    - Recall CBC
        ```
        Ci = Ek(Pi XOR Ci-1)
        Pi = Dk(Ci) XOR Ci-1
        ```
        由bit flipping攻擊,只要動了某個block的密文的第#個byte,就會影響到其他block的第#個byte 
        
    - PKCS#7 = PKCS#5:
        - 如果需要padding 1byte就用0x01填充
          如果需要padding 2byte就用0x02填充
          以此類推...
          > EX 以一個block 16byte為例
          > 明文 : ABCDEFGHIJ --> 10byte
          > + padding : ABCDEFGHIJ + '\x06' * 6

- 原理: 
    解密之後看最後一個byte就可以知道有#byte的padding,往回推#byte就能推算明文最後一個byte在哪 
    - 但如果解密之後padding格式錯誤,通常sever會用某種方式告訴使用者padding錯誤
    > Ex
    > 解密之後的明文 : ...\x03\x05\x03
    > 最後一個byte為0x03,此時往前3byte比對發現有不是0x03的byte
    > 噴padding錯誤
- 攻擊者只知道密文&解密之後是否padding正確,就可以暴力破解明文,**因為padding錯誤的機率其實很高**,因此只要能夠找到符合的基本上就是正確答案
    ```
    recall
        Ci = Ek(Pi XOR Ci-1)
        Pi = Dk(Ci) XOR Ci-1
    ```
    :::info
    1. 假設明文的最後一個byte是padding 0x01
    2. 由`Pi = Dk(Ci) ^ Ci-1`可知`Dk(Ci) ^ Ci-1 == 0x01`
       雖然Dk(Ci)未知
       但可以藉由偽造`Ci-1`的最後一個byte( = 0-256)來讓server告訴我們
       當最後一byte = 0x01時, `Ci-1'`有沒有解
    3. 假設存在一個`Ci-1`,使得`Dk(Ci) = 0x01 ^ Ci-1'`,則
        ```
        Pi = Dk(Ci) ^ Ci-1(原本的密文)
           = (0x01 ^ Ci-1') ^ Ci-1
        ```
    4. 如果不存在,則假設明文最後1 byte是0x02
    :::
    
- 因此攻擊流程大概是
    1. 算明文的最後一個byte(可能為0x1-0xf = Pi')
        > 偽造Ci-1的最後一個byte為Ci-1',爆破找出可能的Pi'值
        > 如果是可能的值就會padding正確
        > 則Pi = Pi' ^ Ci-1 ^ Ci-1'
        ```python=
        # 算last byte
        for j in range(16):
            for k in range(256):
                c = cipher[(b_num-1) * 16:b_num * 16] #target block
                c_prev = cipher[(b_num - 2) * 16 : (b_num - 1) * 16] #input vector
                if (k != c_prev[-1]):
                    my_c = iv + cipher[:15] + bytes([k]) + c
                    if (oracle(my_c)): #check padding correction
                        print(f'ans : {k}')
        ```